diff --git a/docs/systems/federation.md b/docs/systems/federation.md index b983b95b..318b5fc5 100644 --- a/docs/systems/federation.md +++ b/docs/systems/federation.md @@ -1427,6 +1427,10 @@ DM channel hard-delete cascades: reactions, embeds, attachments (DB rows + disk `startFederationWorkers()` in `utils/federationWorker.ts:1211` starts: outbox-delivery tick, federated-call sentinel, file-replication ticks, health-check ticks, and the storage janitor. The whole bundle is gated by `process.env.DISABLE_FEDERATION_WORKERS` matching `'1'` or `'true'` at `index.ts:171-176` (envBool semantics). Tests set `DISABLE_FEDERATION_WORKERS=1` to silence cross-instance background traffic during setup. +### Public Origin Override + +`PUBLIC_ORIGIN` env (read via `config.publicOrigin`, consumed by `getOurOrigin()` in `utils/federationAuth.ts`) overrides the federation transport URL verbatim, taking precedence over the default `https://${DOMAIN}`. When unset, behaviour is unchanged. Intended for reverse-proxy / dev-without-TLS deployments where the public origin must be advertised explicitly (typically `http://...`) and differs from the bare `DOMAIN` value used for federated identity. The integration test harness does NOT use this override — see `seedPeer.ts` for why localhost-port instances cannot collapse to a single peer row. + ### Test-Only Routes `POST /api/admin/test/seed-peer` directly inserts a `federation_peers` row, skipping the multi-step peer handshake. Strictly gated: `NODE_ENV='test'` AND `ENABLE_TEST_ROUTES='1'` together; returns 404 in any other configuration. Used exclusively by the two-instance integration harness in `packages/server/test/`. Validates `origin` (must be http(s) URL), `hmacSecret` (≥32 chars), and `status` (must be one of `'active'`, `'pending'`, `'awaiting_approval'`, `'rejected'`, `'revoked'`, `'needs_attention'`, `'unreachable'`, `'accepted'`). diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index 0ebe7042..51642fbd 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -34,12 +34,27 @@ function envBool(key: string, defaultValue: boolean): boolean { return value === 'true' || value === '1'; } +// PUBLIC_ORIGIN overrides the federation transport URL returned by getOurOrigin(). +// Used by integration test harnesses that bind to 127.0.0.1: and by +// reverse-proxy setups where federation must advertise an http:// origin (the +// proxy terminates TLS upstream). When unset, getOurOrigin() falls back to +// https://${DOMAIN} for production safety. +const publicOrigin = envOptional('PUBLIC_ORIGIN'); +if (publicOrigin !== undefined) { + if (!/^https?:\/\//i.test(publicOrigin)) { + throw new Error( + `PUBLIC_ORIGIN must start with http:// or https:// — got: ${publicOrigin}` + ); + } +} + export const config = { port: envInt('PORT', 3000), host: env('HOST', '0.0.0.0'), jwtSecret: env('JWT_SECRET'), jwtExpiresIn: env('JWT_EXPIRES_IN', '30d'), domain: envOptional('DOMAIN'), + publicOrigin, livekit: { url: envOptional('LIVEKIT_URL'), diff --git a/packages/server/src/utils/federationAuth.ts b/packages/server/src/utils/federationAuth.ts index c80d05e1..50580089 100644 --- a/packages/server/src/utils/federationAuth.ts +++ b/packages/server/src/utils/federationAuth.ts @@ -174,9 +174,21 @@ export function parseFederationHeaders( /** * Return the canonical origin URL for this instance. - * Uses DOMAIN env var for production, falls back to localhost for dev. + * + * Resolution order: + * 1. `PUBLIC_ORIGIN` env (verbatim, trailing slash stripped) — overrides everything. + * Used by integration test harnesses that bind to 127.0.0.1: and by + * reverse-proxy / dev-without-TLS setups where federation must advertise an + * explicit origin distinct from the public DOMAIN. The override is the URL + * transport layer; identity (homeInstance) still derives from DOMAIN. + * 2. `https://${DOMAIN}` — production default. + * 3. `http://localhost:${PORT}` — dev fallback when DOMAIN is unset. */ export function getOurOrigin(): string { + const override = config.publicOrigin; + if (override && override.trim()) { + return override.trim().replace(/\/$/, ''); + } if (config.domain) { return `https://${config.domain}`; }