feat(federation): pass explicit intent at every ensurePeered call site

- social.ts friend-add: user_action, with 409 peer_pending_local_admin
  when gate fires
- /peer/ensure: user_action, surfaces peeringStatus: 'admin_required'
- sendCallRelay (typing warm-up + call relay): system intent
- federationWorker resolvePendingPeers: system intent (defensive — gate
  is unreachable from here since pending rows already exist)
- CallRelayFailureReason: peer_admin_required added (mapped to
  peer_transient_failure on the user-facing event surface, since system
  intent should never legitimately surface admin_required)
- Test files: thread intent arg through racePeering and ensurePeered
  calls (positional shift from racePeering signature change)
- outboundGate.test.ts: tighten noUncheckedIndexedAccess access via
  non-null assertions after toHaveLength()
- docs/systems/social.md: peer_pending_local_admin error code documented
This commit is contained in:
Jannis Braun
2026-04-26 21:37:19 +02:00
parent ef80e3b416
commit 4d4dc383d7
10 changed files with 81 additions and 50 deletions
@@ -520,6 +520,7 @@ export const CALL_PEERING_TIMEOUT_MS = 3_000;
export type CallRelayFailureReason =
| 'peer_rejected'
| 'peer_awaiting_approval'
| 'peer_admin_required'
| 'peer_transient_failure'
| 'post_failed';
@@ -547,6 +548,7 @@ export function mapCallReasonToEventReason(reason: CallRelayFailureReason): DmCa
switch (reason) {
case 'peer_rejected': return 'peer_rejected';
case 'peer_awaiting_approval': return 'peer_awaiting_approval';
case 'peer_admin_required': return 'peer_transient_failure'; // gate-unreachable from system intent; defensive map
case 'peer_transient_failure': return 'peer_transient_failure';
case 'post_failed': return 'peer_transient_failure'; // 4xx looks transient to users
}
@@ -583,14 +585,14 @@ export async function sendCallRelay(
if (!peer) {
// ─── Non-blocking mode (typing): warm up in background, do not POST ──
if (timeoutMs === 0) {
ensurePeered(targetPeerOrigin).catch(err => {
ensurePeered(targetPeerOrigin, { kind: 'system' }).catch(err => {
console.warn('[federation] typing-triggered background handshake:', targetPeerOrigin, err);
});
return { ok: false, reason: 'peer_transient_failure', error: 'peer not active' };
}
// ─── Race ensurePeered against the deadline ──
const raced = await racePeering(targetPeerOrigin, timeoutMs);
const raced = await racePeering(targetPeerOrigin, timeoutMs, { kind: 'system' });
switch (raced.status) {
case 'active':
@@ -607,6 +609,8 @@ export async function sendCallRelay(
return { ok: false, reason: 'peer_rejected', error: raced.error };
case 'pending':
return { ok: false, reason: 'peer_awaiting_approval', error: raced.error };
case 'admin_required':
return { ok: false, reason: 'peer_admin_required', error: raced.error };
case 'failed':
return { ok: false, reason: 'peer_transient_failure', error: raced.error };
case 'timeout':