feat(federation): performHandshake captures + clears approval token

- 202 response: parse approvalToken from body and store on the local
  federation_peers row alongside status='awaiting_approval'. Legacy
  receivers that omit the field result in null stored token, handled
  gracefully by the verification logic landing in subsequent commits.
- 200 response: clear approvalToken in the same UPDATE that sets
  status='active' (single-use consumption per spec §3.2).

Test coverage: 4 tests in federationPeering.approvalToken.test.ts covering
JSON token, missing token (legacy), non-JSON body, and 200 clear after
prior token storage.
This commit is contained in:
Jannis Braun
2026-04-26 11:45:16 +02:00
parent 2d8ecd0131
commit 9e078c44ba
2 changed files with 188 additions and 3 deletions
+16 -3
View File
@@ -166,9 +166,22 @@ async function performHandshake(
});
if (response.status === 202) {
// Request queued for admin approval on the remote side
// Capture the approval token from the 202 body if present. Stored on
// our federation_peers row so a subsequent inbound /peer/accept (from
// the remote's /approve flow) can be cryptographically verified before
// we promote awaiting_approval → active. Spec §3.7.
let approvalToken: string | null = null;
try {
const body = (await response.json()) as { approvalToken?: string };
if (typeof body?.approvalToken === 'string' && body.approvalToken.length > 0) {
approvalToken = body.approvalToken;
}
} catch {
// Non-JSON or empty body — legacy receiver, leave null.
}
db.update(schema.federationPeers)
.set({ status: 'awaiting_approval' })
.set({ status: 'awaiting_approval', approvalToken })
.where(eq(schema.federationPeers.id, peerId))
.run();
const { connectionManager } = await import('../ws/handler.js');
@@ -191,7 +204,7 @@ async function performHandshake(
}
db.update(schema.federationPeers)
.set({ status: 'active', lastSeenAt: Date.now(), instanceName: remoteInstanceName })
.set({ status: 'active', lastSeenAt: Date.now(), instanceName: remoteInstanceName, approvalToken: null })
.where(eq(schema.federationPeers.id, peerId))
.run();
const { connectionManager } = await import('../ws/handler.js');