routes/federation.ts had grown to 7.6k lines, spanning HTTP route registration, federated identity resolution, ~30 inbound relay event processors, DM reconciliation, and rate-limiting internals — too large to review or hold in context, and awkward to change safely. Split the implementation into 18 focused modules under routes/federation/ (helpers, events/, handlers/) and keep routes/federation.ts as a thin barrel that re-exports the public API and composes the HTTP registrars into federationRoutes(). No import paths change anywhere else. Pure move, no behavior change: - 61/61 named functions byte-identical; only deltas are 2 dynamic-import paths adjusted for the new directory depth - public export surface unchanged (barrel re-exports all 22 symbols) - all 30 endpoints preserved (identical verb+path set) - typecheck, build, and full server suite (790 tests) green Docs: update federation.md source-file map; add split design doc.
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { schema } from '../../db/index.js';
|
|
import { getOurOrigin } from '../../utils/federationAuth.js';
|
|
import { or } from 'drizzle-orm';
|
|
|
|
/** Fields safe to expose to admin callers (everything except hmacSecret). */
|
|
export interface SanitizedPeer {
|
|
id: string;
|
|
origin: string;
|
|
instanceName: string | null;
|
|
status: string;
|
|
lastSeenAt: number | null;
|
|
lastFailureAt: number | null;
|
|
consecutiveFailures: number;
|
|
lastSyncedAt: number | null;
|
|
createdAt: number;
|
|
rotationInProgress: boolean;
|
|
secretRotatedAt: number | null;
|
|
autoRotateIntervalDays: number;
|
|
needsAttentionReason: 'auth_failures' | 'peer_reset_detected' | 'repeer_incomplete' | null;
|
|
}
|
|
|
|
|
|
export function sanitizePeer(row: typeof schema.federationPeers.$inferSelect): SanitizedPeer {
|
|
return {
|
|
id: row.id,
|
|
origin: row.origin,
|
|
instanceName: row.instanceName,
|
|
status: row.status,
|
|
lastSeenAt: row.lastSeenAt,
|
|
lastFailureAt: row.lastFailureAt,
|
|
consecutiveFailures: row.consecutiveFailures,
|
|
lastSyncedAt: row.lastSyncedAt,
|
|
createdAt: row.createdAt,
|
|
rotationInProgress: row.pendingHmacSecret !== null,
|
|
secretRotatedAt: row.secretRotatedAt,
|
|
autoRotateIntervalDays: row.autoRotateIntervalDays,
|
|
needsAttentionReason: row.needsAttentionReason as SanitizedPeer['needsAttentionReason'],
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine this instance's public origin for the peering handshake.
|
|
*
|
|
* Delegates to `getOurOrigin()` so the handshake `sourceOrigin` is IDENTICAL to
|
|
* the `X-Federation-Origin` value used for authenticated S2S requests. This
|
|
* honors `PUBLIC_ORIGIN` (getOurOrigin's precedence: PUBLIC_ORIGIN →
|
|
* `https://${DOMAIN}` → `http://localhost:${PORT}`). Using DOMAIN directly here
|
|
* previously desynced the responder's peer-row key from the auth origin,
|
|
* causing permanent `403 Not peered` whenever PUBLIC_ORIGIN != https://DOMAIN.
|
|
*/
|
|
export function resolveLocalOrigin(): string {
|
|
return getOurOrigin();
|
|
}
|
|
|
|
|
|
/**
|
|
* Validate that a string is a well-formed HTTP(S) URL origin.
|
|
* Returns the normalized origin (no trailing slash) or null if invalid.
|
|
*/
|
|
export function validateOrigin(raw: string): string | null {
|
|
try {
|
|
const url = new URL(raw);
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
if (url.protocol === 'http:' && !['localhost', '127.0.0.1'].includes(url.hostname)) {
|
|
return null;
|
|
}
|
|
return url.origin;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|