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:
@@ -300,6 +300,7 @@ As of 2026-04-25, the sender's home server owns the entire federated friend-add
|
|||||||
- `'pending'` + peer row `awaiting_approval` (re-queried after the call) → 409 `peer_pending_approval`
|
- `'pending'` + peer row `awaiting_approval` (re-queried after the call) → 409 `peer_pending_approval`
|
||||||
- `'rejected'` → 403 `peer_rejected`
|
- `'rejected'` → 403 `peer_rejected`
|
||||||
- `'failed'` → 503 `peer_unreachable`
|
- `'failed'` → 503 `peer_unreachable`
|
||||||
|
- `'admin_required'` (gate fired locally) → 409 `peer_pending_local_admin` — your own admin must approve before we reach out
|
||||||
5. **lookupRemoteUser(peerOrigin, baseName)** — POSTs HMAC-signed `{ username }` to `peerOrigin/api/federation/users/lookup`. Result mapping:
|
5. **lookupRemoteUser(peerOrigin, baseName)** — POSTs HMAC-signed `{ username }` to `peerOrigin/api/federation/users/lookup`. Result mapping:
|
||||||
- `not_found` → 404 `user_not_found`
|
- `not_found` → 404 `user_not_found`
|
||||||
- `unreachable` → 503 `peer_unreachable`
|
- `unreachable` → 503 `peer_unreachable`
|
||||||
|
|||||||
@@ -798,7 +798,18 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { ensurePeered } = await import('../utils/federationPeering.js');
|
const { ensurePeered } = await import('../utils/federationPeering.js');
|
||||||
const result = await ensurePeered(remoteOrigin);
|
// NOTE: /peer/ensure is currently only invoked from friend-add client paths
|
||||||
|
// (see packages/web/src/stores/instanceStore.ts ensurePeered references).
|
||||||
|
// The hardcoded reason here is correct TODAY but will become wrong when
|
||||||
|
// DM-to-stranger or space-join grow into the gate. When that happens,
|
||||||
|
// surface the reason and target through the request body instead. Do NOT
|
||||||
|
// silently leave the hardcoding in place when adding a new caller.
|
||||||
|
const result = await ensurePeered(remoteOrigin, {
|
||||||
|
kind: 'user_action',
|
||||||
|
userId: request.userId,
|
||||||
|
reason: 'friend_add',
|
||||||
|
target: remoteOrigin,
|
||||||
|
});
|
||||||
|
|
||||||
// NOTE: The internal EnsurePeeredResult status names differ from the client-facing
|
// NOTE: The internal EnsurePeeredResult status names differ from the client-facing
|
||||||
// peeringStatus values. The mapping:
|
// peeringStatus values. The mapping:
|
||||||
@@ -806,6 +817,7 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// 'rejected' → 'rejected' (permanently blocked)
|
// 'rejected' → 'rejected' (permanently blocked)
|
||||||
// 'pending' → 'awaiting_approval' (queued on remote, waiting for admin)
|
// 'pending' → 'awaiting_approval' (queued on remote, waiting for admin)
|
||||||
// 'failed' → 'pending' (transient error, will retry automatically)
|
// 'failed' → 'pending' (transient error, will retry automatically)
|
||||||
|
// 'admin_required' → 'admin_required' (local outbound gate fired — our admin must approve)
|
||||||
// The internal 'pending' means "we got a 202 from the remote — admin hasn't acted yet",
|
// The internal 'pending' means "we got a 202 from the remote — admin hasn't acted yet",
|
||||||
// while 'failed' means "network/timeout — the outbox worker will retry next tick".
|
// while 'failed' means "network/timeout — the outbox worker will retry next tick".
|
||||||
// The client sees 'awaiting_approval' (actionable info) vs 'pending' (transient, will resolve).
|
// The client sees 'awaiting_approval' (actionable info) vs 'pending' (transient, will resolve).
|
||||||
@@ -818,6 +830,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
return reply.code(200).send({ peeringStatus: 'awaiting_approval', error: result.error });
|
return reply.code(200).send({ peeringStatus: 'awaiting_approval', error: result.error });
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return reply.code(200).send({ peeringStatus: 'pending', error: result.error });
|
return reply.code(200).send({ peeringStatus: 'pending', error: result.error });
|
||||||
|
case 'admin_required':
|
||||||
|
return reply.code(200).send({ peeringStatus: 'admin_required' });
|
||||||
default:
|
default:
|
||||||
return reply.code(200).send({ peeringStatus: 'pending', error: 'Unknown peering result' });
|
return reply.code(200).send({ peeringStatus: 'pending', error: 'Unknown peering result' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,7 +178,12 @@ async function handleFederatedFriendRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. ensurePeered — block until 'active', or surface peer status as error
|
// 2. ensurePeered — block until 'active', or surface peer status as error
|
||||||
const peering = await ensurePeered(peerOrigin);
|
const peering = await ensurePeered(peerOrigin, {
|
||||||
|
kind: 'user_action',
|
||||||
|
userId: sender.id,
|
||||||
|
reason: 'friend_add',
|
||||||
|
target: `${baseName}@${targetDomain}`,
|
||||||
|
});
|
||||||
if (peering.status === 'rejected') {
|
if (peering.status === 'rejected') {
|
||||||
return reply.code(403).send({ error: 'peer_rejected', statusCode: 403, domain: targetDomain });
|
return reply.code(403).send({ error: 'peer_rejected', statusCode: 403, domain: targetDomain });
|
||||||
}
|
}
|
||||||
@@ -195,6 +200,13 @@ async function handleFederatedFriendRequest(
|
|||||||
}
|
}
|
||||||
return reply.code(409).send({ error: 'peer_pending', statusCode: 409, domain: targetDomain });
|
return reply.code(409).send({ error: 'peer_pending', statusCode: 409, domain: targetDomain });
|
||||||
}
|
}
|
||||||
|
if (peering.status === 'admin_required') {
|
||||||
|
return reply.code(409).send({
|
||||||
|
error: 'peer_pending_local_admin',
|
||||||
|
statusCode: 409,
|
||||||
|
domain: targetDomain,
|
||||||
|
});
|
||||||
|
}
|
||||||
// peering.status === 'active' — continue
|
// peering.status === 'active' — continue
|
||||||
|
|
||||||
// 3. Lookup
|
// 3. Lookup
|
||||||
|
|||||||
@@ -520,6 +520,7 @@ export const CALL_PEERING_TIMEOUT_MS = 3_000;
|
|||||||
export type CallRelayFailureReason =
|
export type CallRelayFailureReason =
|
||||||
| 'peer_rejected'
|
| 'peer_rejected'
|
||||||
| 'peer_awaiting_approval'
|
| 'peer_awaiting_approval'
|
||||||
|
| 'peer_admin_required'
|
||||||
| 'peer_transient_failure'
|
| 'peer_transient_failure'
|
||||||
| 'post_failed';
|
| 'post_failed';
|
||||||
|
|
||||||
@@ -547,6 +548,7 @@ export function mapCallReasonToEventReason(reason: CallRelayFailureReason): DmCa
|
|||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'peer_rejected': return 'peer_rejected';
|
case 'peer_rejected': return 'peer_rejected';
|
||||||
case 'peer_awaiting_approval': return 'peer_awaiting_approval';
|
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 'peer_transient_failure': return 'peer_transient_failure';
|
||||||
case 'post_failed': return 'peer_transient_failure'; // 4xx looks transient to users
|
case 'post_failed': return 'peer_transient_failure'; // 4xx looks transient to users
|
||||||
}
|
}
|
||||||
@@ -583,14 +585,14 @@ export async function sendCallRelay(
|
|||||||
if (!peer) {
|
if (!peer) {
|
||||||
// ─── Non-blocking mode (typing): warm up in background, do not POST ──
|
// ─── Non-blocking mode (typing): warm up in background, do not POST ──
|
||||||
if (timeoutMs === 0) {
|
if (timeoutMs === 0) {
|
||||||
ensurePeered(targetPeerOrigin).catch(err => {
|
ensurePeered(targetPeerOrigin, { kind: 'system' }).catch(err => {
|
||||||
console.warn('[federation] typing-triggered background handshake:', targetPeerOrigin, err);
|
console.warn('[federation] typing-triggered background handshake:', targetPeerOrigin, err);
|
||||||
});
|
});
|
||||||
return { ok: false, reason: 'peer_transient_failure', error: 'peer not active' };
|
return { ok: false, reason: 'peer_transient_failure', error: 'peer not active' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Race ensurePeered against the deadline ──
|
// ─── Race ensurePeered against the deadline ──
|
||||||
const raced = await racePeering(targetPeerOrigin, timeoutMs);
|
const raced = await racePeering(targetPeerOrigin, timeoutMs, { kind: 'system' });
|
||||||
|
|
||||||
switch (raced.status) {
|
switch (raced.status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
@@ -607,6 +609,8 @@ export async function sendCallRelay(
|
|||||||
return { ok: false, reason: 'peer_rejected', error: raced.error };
|
return { ok: false, reason: 'peer_rejected', error: raced.error };
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return { ok: false, reason: 'peer_awaiting_approval', error: raced.error };
|
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':
|
case 'failed':
|
||||||
return { ok: false, reason: 'peer_transient_failure', error: raced.error };
|
return { ok: false, reason: 'peer_transient_failure', error: raced.error };
|
||||||
case 'timeout':
|
case 'timeout':
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ describe('performHandshake — approval token capture & clear', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { ensurePeered } = await import('./federationPeering.js');
|
const { ensurePeered } = await import('./federationPeering.js');
|
||||||
const result = await ensurePeered('https://remote.example');
|
const result = await ensurePeered('https://remote.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('pending');
|
expect(result.status).toBe('pending');
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ describe('performHandshake — approval token capture & clear', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { ensurePeered } = await import('./federationPeering.js');
|
const { ensurePeered } = await import('./federationPeering.js');
|
||||||
const result = await ensurePeered('https://legacy.example');
|
const result = await ensurePeered('https://legacy.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('pending');
|
expect(result.status).toBe('pending');
|
||||||
const peer = testDb.select().from(schema.federationPeers)
|
const peer = testDb.select().from(schema.federationPeers)
|
||||||
@@ -135,7 +135,7 @@ describe('performHandshake — approval token capture & clear', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { ensurePeered } = await import('./federationPeering.js');
|
const { ensurePeered } = await import('./federationPeering.js');
|
||||||
const result = await ensurePeered('https://empty.example');
|
const result = await ensurePeered('https://empty.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('pending');
|
expect(result.status).toBe('pending');
|
||||||
const peer = testDb.select().from(schema.federationPeers)
|
const peer = testDb.select().from(schema.federationPeers)
|
||||||
@@ -161,7 +161,7 @@ describe('performHandshake — approval token capture & clear', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { ensurePeered } = await import('./federationPeering.js');
|
const { ensurePeered } = await import('./federationPeering.js');
|
||||||
const result = await ensurePeered('https://remote.example');
|
const result = await ensurePeered('https://remote.example', { kind: 'system' });
|
||||||
expect(result.status).toBe('active');
|
expect(result.status).toBe('active');
|
||||||
|
|
||||||
const peer = testDb.select().from(schema.federationPeers)
|
const peer = testDb.select().from(schema.federationPeers)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ describe('performHandshake — persist remote instanceName', () => {
|
|||||||
|
|
||||||
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
||||||
_clearInFlightPeering();
|
_clearInFlightPeering();
|
||||||
const result = await ensurePeered('https://remote.example');
|
const result = await ensurePeered('https://remote.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('active');
|
expect(result.status).toBe('active');
|
||||||
const row = testDb.select().from(schema.federationPeers)
|
const row = testDb.select().from(schema.federationPeers)
|
||||||
@@ -117,7 +117,7 @@ describe('performHandshake — persist remote instanceName', () => {
|
|||||||
|
|
||||||
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
||||||
_clearInFlightPeering();
|
_clearInFlightPeering();
|
||||||
const result = await ensurePeered('https://remote.example');
|
const result = await ensurePeered('https://remote.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('active');
|
expect(result.status).toBe('active');
|
||||||
const row = testDb.select().from(schema.federationPeers)
|
const row = testDb.select().from(schema.federationPeers)
|
||||||
@@ -136,7 +136,7 @@ describe('performHandshake — persist remote instanceName', () => {
|
|||||||
|
|
||||||
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
||||||
_clearInFlightPeering();
|
_clearInFlightPeering();
|
||||||
const result = await ensurePeered('https://remote.example');
|
const result = await ensurePeered('https://remote.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('active');
|
expect(result.status).toBe('active');
|
||||||
const row = testDb.select().from(schema.federationPeers)
|
const row = testDb.select().from(schema.federationPeers)
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
|
|
||||||
// Mid-handshake, the pending peer row existed.
|
// Mid-handshake, the pending peer row existed.
|
||||||
expect(peerRowsDuringHandshake).toHaveLength(1);
|
expect(peerRowsDuringHandshake).toHaveLength(1);
|
||||||
expect(peerRowsDuringHandshake[0].status).toBe('pending');
|
expect(peerRowsDuringHandshake[0]!.status).toBe('pending');
|
||||||
expect(peerRowsDuringHandshake[0].origin).toBe('https://orbit.example');
|
expect(peerRowsDuringHandshake[0]!.origin).toBe('https://orbit.example');
|
||||||
|
|
||||||
// No outbound queue rows were created.
|
// No outbound queue rows were created.
|
||||||
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
||||||
@@ -166,17 +166,17 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
// Parent row created.
|
// Parent row created.
|
||||||
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
||||||
expect(parents).toHaveLength(1);
|
expect(parents).toHaveLength(1);
|
||||||
expect(parents[0].direction).toBe('outbound');
|
expect(parents[0]!.direction).toBe('outbound');
|
||||||
expect(parents[0].origin).toBe('https://orbit.example');
|
expect(parents[0]!.origin).toBe('https://orbit.example');
|
||||||
expect(parents[0].hmacSecret).toBeNull();
|
expect(parents[0]!.hmacSecret).toBeNull();
|
||||||
|
|
||||||
// Subscriber row created.
|
// Subscriber row created.
|
||||||
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
||||||
expect(subs).toHaveLength(1);
|
expect(subs).toHaveLength(1);
|
||||||
expect(subs[0].userId).toBe('user1');
|
expect(subs[0]!.userId).toBe('user1');
|
||||||
expect(subs[0].triggerReason).toBe('friend_add');
|
expect(subs[0]!.triggerReason).toBe('friend_add');
|
||||||
expect(subs[0].triggerTarget).toBe('bob@orbit.example');
|
expect(subs[0]!.triggerTarget).toBe('bob@orbit.example');
|
||||||
expect(subs[0].requestId).toBe(parents[0].id);
|
expect(subs[0]!.requestId).toBe(parents[0]!.id);
|
||||||
|
|
||||||
// No federation_peers row created; no outbound POST attempted.
|
// No federation_peers row created; no outbound POST attempted.
|
||||||
const peers = testDb.select().from(schema.federationPeers).all();
|
const peers = testDb.select().from(schema.federationPeers).all();
|
||||||
@@ -288,7 +288,7 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
// Existing pending peer row still present (existingPeerId path doesn't delete on failure).
|
// Existing pending peer row still present (existingPeerId path doesn't delete on failure).
|
||||||
const peers = testDb.select().from(schema.federationPeers).all();
|
const peers = testDb.select().from(schema.federationPeers).all();
|
||||||
expect(peers).toHaveLength(1);
|
expect(peers).toHaveLength(1);
|
||||||
expect(peers[0].id).toBe('peer-pending');
|
expect(peers[0]!.id).toBe('peer-pending');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Test 6 ─────────────────────────────────────────────────────────────
|
// ─── Test 6 ─────────────────────────────────────────────────────────────
|
||||||
@@ -350,7 +350,7 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
|
|
||||||
const subsAfterFirst = testDb.select().from(schema.peerApprovalSubscribers).all();
|
const subsAfterFirst = testDb.select().from(schema.peerApprovalSubscribers).all();
|
||||||
expect(subsAfterFirst).toHaveLength(1);
|
expect(subsAfterFirst).toHaveLength(1);
|
||||||
const firstCreatedAt = subsAfterFirst[0].createdAt;
|
const firstCreatedAt = subsAfterFirst[0]!.createdAt;
|
||||||
|
|
||||||
// Wait a moment so the refreshed createdAt would differ.
|
// Wait a moment so the refreshed createdAt would differ.
|
||||||
await new Promise(r => setTimeout(r, 5));
|
await new Promise(r => setTimeout(r, 5));
|
||||||
@@ -366,18 +366,18 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
// Still exactly one parent row keyed on (origin, direction='outbound').
|
// Still exactly one parent row keyed on (origin, direction='outbound').
|
||||||
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
||||||
expect(parents).toHaveLength(1);
|
expect(parents).toHaveLength(1);
|
||||||
expect(parents[0].direction).toBe('outbound');
|
expect(parents[0]!.direction).toBe('outbound');
|
||||||
expect(parents[0].origin).toBe('https://orbit.example');
|
expect(parents[0]!.origin).toBe('https://orbit.example');
|
||||||
|
|
||||||
// Still exactly one subscriber keyed on (request_id, user_id, reason, target).
|
// Still exactly one subscriber keyed on (request_id, user_id, reason, target).
|
||||||
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
||||||
expect(subs).toHaveLength(1);
|
expect(subs).toHaveLength(1);
|
||||||
expect(subs[0].userId).toBe('user1');
|
expect(subs[0]!.userId).toBe('user1');
|
||||||
expect(subs[0].triggerReason).toBe('friend_add');
|
expect(subs[0]!.triggerReason).toBe('friend_add');
|
||||||
expect(subs[0].triggerTarget).toBe('bob@orbit.example');
|
expect(subs[0]!.triggerTarget).toBe('bob@orbit.example');
|
||||||
|
|
||||||
// createdAt was refreshed on the second call.
|
// createdAt was refreshed on the second call.
|
||||||
expect(subs[0].createdAt).toBeGreaterThan(firstCreatedAt);
|
expect(subs[0]!.createdAt).toBeGreaterThan(firstCreatedAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Test 8 ─────────────────────────────────────────────────────────────
|
// ─── Test 8 ─────────────────────────────────────────────────────────────
|
||||||
@@ -409,8 +409,8 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
// Exactly one parent row.
|
// Exactly one parent row.
|
||||||
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
||||||
expect(parents).toHaveLength(1);
|
expect(parents).toHaveLength(1);
|
||||||
expect(parents[0].origin).toBe('https://orbit.example');
|
expect(parents[0]!.origin).toBe('https://orbit.example');
|
||||||
expect(parents[0].direction).toBe('outbound');
|
expect(parents[0]!.direction).toBe('outbound');
|
||||||
|
|
||||||
// Two subscribers, both pointing at the same parent.
|
// Two subscribers, both pointing at the same parent.
|
||||||
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
||||||
@@ -418,7 +418,7 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
const userIds = subs.map(s => s.userId).sort();
|
const userIds = subs.map(s => s.userId).sort();
|
||||||
expect(userIds).toEqual(['user1', 'user2']);
|
expect(userIds).toEqual(['user1', 'user2']);
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
expect(sub.requestId).toBe(parents[0].id);
|
expect(sub.requestId).toBe(parents[0]!.id);
|
||||||
expect(sub.triggerReason).toBe('friend_add');
|
expect(sub.triggerReason).toBe('friend_add');
|
||||||
expect(sub.triggerTarget).toBe('bob@orbit.example');
|
expect(sub.triggerTarget).toBe('bob@orbit.example');
|
||||||
}
|
}
|
||||||
@@ -443,13 +443,13 @@ describe('ensurePeered — outbound gate behavior across (autoAccept × peer-row
|
|||||||
|
|
||||||
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
const subs = testDb.select().from(schema.peerApprovalSubscribers).all();
|
||||||
expect(subs).toHaveLength(1);
|
expect(subs).toHaveLength(1);
|
||||||
expect(subs[0].userId).toBe('user1');
|
expect(subs[0]!.userId).toBe('user1');
|
||||||
expect(subs[0].triggerReason).toBe('space_join');
|
expect(subs[0]!.triggerReason).toBe('space_join');
|
||||||
expect(subs[0].triggerTarget).toBe('space-abc-123');
|
expect(subs[0]!.triggerTarget).toBe('space-abc-123');
|
||||||
|
|
||||||
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
const parents = testDb.select().from(schema.peerApprovalRequests).all();
|
||||||
expect(parents).toHaveLength(1);
|
expect(parents).toHaveLength(1);
|
||||||
expect(parents[0].direction).toBe('outbound');
|
expect(parents[0]!.direction).toBe('outbound');
|
||||||
expect(parents[0].origin).toBe('https://orbit.example');
|
expect(parents[0]!.origin).toBe('https://orbit.example');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ describe('racePeering', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
peerId: 'peer-1',
|
peerId: 'peer-1',
|
||||||
}));
|
}));
|
||||||
const result = await racePeering('https://example.com', 1_000, stub);
|
const result = await racePeering('https://example.com', 1_000, { kind: 'system' }, stub);
|
||||||
expect(result).toEqual({ status: 'active', peerId: 'peer-1' });
|
expect(result).toEqual({ status: 'active', peerId: 'peer-1' });
|
||||||
expect(stub).toHaveBeenCalledWith('https://example.com');
|
expect(stub).toHaveBeenCalledWith('https://example.com', { kind: 'system' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns timeout when ensurePeered takes longer than the deadline', async () => {
|
it('returns timeout when ensurePeered takes longer than the deadline', async () => {
|
||||||
@@ -61,7 +61,7 @@ describe('racePeering', () => {
|
|||||||
const stub = vi.fn((): Promise<EnsurePeeredResult> => new Promise(() => {
|
const stub = vi.fn((): Promise<EnsurePeeredResult> => new Promise(() => {
|
||||||
// Never resolves — simulates a slow handshake.
|
// Never resolves — simulates a slow handshake.
|
||||||
}));
|
}));
|
||||||
const racePromise = racePeering('https://example.com', 50, stub);
|
const racePromise = racePeering('https://example.com', 50, { kind: 'system' }, stub);
|
||||||
await vi.advanceTimersByTimeAsync(50);
|
await vi.advanceTimersByTimeAsync(50);
|
||||||
const result = await racePromise;
|
const result = await racePromise;
|
||||||
expect(result).toEqual({ status: 'timeout' });
|
expect(result).toEqual({ status: 'timeout' });
|
||||||
@@ -73,7 +73,7 @@ describe('racePeering', () => {
|
|||||||
status: 'rejected',
|
status: 'rejected',
|
||||||
error: 'peer denied',
|
error: 'peer denied',
|
||||||
}));
|
}));
|
||||||
const result = await racePeering('https://example.com', 1_000, stub);
|
const result = await racePeering('https://example.com', 1_000, { kind: 'system' }, stub);
|
||||||
expect(result).toEqual({ status: 'rejected', error: 'peer denied' });
|
expect(result).toEqual({ status: 'rejected', error: 'peer denied' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ describe('racePeering', () => {
|
|||||||
const stub = vi.fn(() => new Promise<EnsurePeeredResult>((_, reject) => {
|
const stub = vi.fn(() => new Promise<EnsurePeeredResult>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error('late failure')), 30);
|
setTimeout(() => reject(new Error('late failure')), 30);
|
||||||
}));
|
}));
|
||||||
const racePromise = racePeering('https://example.com', 10, stub);
|
const racePromise = racePeering('https://example.com', 10, { kind: 'system' }, stub);
|
||||||
await vi.advanceTimersByTimeAsync(10);
|
await vi.advanceTimersByTimeAsync(10);
|
||||||
const result = await racePromise;
|
const result = await racePromise;
|
||||||
expect(result).toEqual({ status: 'timeout' });
|
expect(result).toEqual({ status: 'timeout' });
|
||||||
@@ -104,7 +104,7 @@ describe('racePeering', () => {
|
|||||||
const stub = vi.fn(async (): Promise<EnsurePeeredResult> => {
|
const stub = vi.fn(async (): Promise<EnsurePeeredResult> => {
|
||||||
throw new Error('immediate handshake failure');
|
throw new Error('immediate handshake failure');
|
||||||
});
|
});
|
||||||
const result = await racePeering('https://example.com', 1_000, stub);
|
const result = await racePeering('https://example.com', 1_000, { kind: 'system' }, stub);
|
||||||
expect(result).toEqual({ status: 'failed', error: 'immediate handshake failure' });
|
expect(result).toEqual({ status: 'failed', error: 'immediate handshake failure' });
|
||||||
// The handshake rejection was the race winner — no background warn should fire.
|
// The handshake rejection was the race winner — no background warn should fire.
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
@@ -156,7 +156,7 @@ describe('ensurePeered needs_attention handling', () => {
|
|||||||
const { ensurePeered } = await import('./federationPeering.js');
|
const { ensurePeered } = await import('./federationPeering.js');
|
||||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||||
|
|
||||||
const result = await ensurePeered('https://remote.example');
|
const result = await ensurePeered('https://remote.example', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('rejected');
|
expect(result.status).toBe('rejected');
|
||||||
if (result.status === 'rejected') {
|
if (result.status === 'rejected') {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ describe('ensurePeered — refuses when unresolved inbound approval-request exis
|
|||||||
|
|
||||||
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
||||||
_clearInFlightPeering();
|
_clearInFlightPeering();
|
||||||
const result = await ensurePeered('https://orbit.test');
|
const result = await ensurePeered('https://orbit.test', { kind: 'system' });
|
||||||
|
|
||||||
expect(result.status).toBe('rejected');
|
expect(result.status).toBe('rejected');
|
||||||
if (result.status === 'rejected') {
|
if (result.status === 'rejected') {
|
||||||
@@ -126,7 +126,7 @@ describe('ensurePeered — refuses when unresolved inbound approval-request exis
|
|||||||
|
|
||||||
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
const { ensurePeered, _clearInFlightPeering } = await import('./federationPeering.js');
|
||||||
_clearInFlightPeering();
|
_clearInFlightPeering();
|
||||||
const result = await ensurePeered('https://nopeer.test');
|
const result = await ensurePeered('https://nopeer.test', { kind: 'system' });
|
||||||
|
|
||||||
// Reached performHandshake — failure mode is 'failed' (network), NOT
|
// Reached performHandshake — failure mode is 'failed' (network), NOT
|
||||||
// the pre-handshake 'rejected' from the new guard.
|
// the pre-handshake 'rejected' from the new guard.
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ async function resolvePendingPeers(): Promise<void> {
|
|||||||
for (const { peerId, peerOrigin } of pendingWithEntries) {
|
for (const { peerId, peerOrigin } of pendingWithEntries) {
|
||||||
console.log(`[federation-worker] Attempting auto-peer with ${peerOrigin}...`);
|
console.log(`[federation-worker] Attempting auto-peer with ${peerOrigin}...`);
|
||||||
|
|
||||||
const result = await ensurePeered(peerOrigin);
|
const result = await ensurePeered(peerOrigin, { kind: 'system' });
|
||||||
|
|
||||||
switch (result.status) {
|
switch (result.status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
|
|||||||
Reference in New Issue
Block a user