From d2073efdd723386123828cdca6837ae1579915bf Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:08:53 +0200 Subject: [PATCH] feat(federation): add in-memory nonce store with TTL eviction (FED-008) --- packages/server/src/routes/federation.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index b5439a0a..a0a1c549 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -121,6 +121,24 @@ function isRelayRateLimited(peerOrigin: string): boolean { return false; } +// ─── In-memory nonce store for replay protection (per-peer) ────────────────── +// Maps peerOrigin → (nonce → insertion timestamp). Nonces are evicted after +// NONCE_MAX_AGE_MS (15 min) to match the HMAC timestamp window. +const NONCE_MAX_AGE_MS = 15 * 60 * 1000; +const nonceStore = new Map>(); + +/** Returns true if the nonce is a duplicate (already seen for this peer). */ +function isNonceDuplicate(peerOrigin: string, nonce: string): boolean { + let peerNonces = nonceStore.get(peerOrigin); + if (!peerNonces) { + peerNonces = new Map(); + nonceStore.set(peerOrigin, peerNonces); + } + if (peerNonces.has(nonce)) return true; + peerNonces.set(nonce, Date.now()); + return false; +} + // Periodically clean stale buckets to prevent unbounded memory growth setInterval(() => { const cutoff = Date.now() - ACCEPT_RATE_WINDOW_MS; @@ -141,6 +159,14 @@ setInterval(() => { relayRateBuckets.delete(origin); } } + // Evict expired nonces + const nonceCutoff = Date.now() - NONCE_MAX_AGE_MS; + for (const [origin, nonces] of nonceStore) { + for (const [nonce, ts] of nonces) { + if (ts < nonceCutoff) nonces.delete(nonce); + } + if (nonces.size === 0) nonceStore.delete(origin); + } }, ACCEPT_RATE_WINDOW_MS).unref(); export async function federationRoutes(app: FastifyInstance): Promise {