feat(federation): two-gateway test harness scaffold (FED-M3-02) #505

Merged
jason.woltje merged 3 commits from feat/federation-m3-harness into main 2026-04-24 03:01:26 +00:00
3 changed files with 32 additions and 13 deletions
Showing only changes of commit 4cf9362e75 - Show all commits

View File

@@ -30,6 +30,7 @@ export default tseslint.config(
'apps/gateway/vitest.config.ts', 'apps/gateway/vitest.config.ts',
'packages/storage/vitest.config.ts', 'packages/storage/vitest.config.ts',
'packages/mosaic/__tests__/*.ts', 'packages/mosaic/__tests__/*.ts',
'tools/federation-harness/*.ts',
], ],
}, },
}, },

View File

@@ -215,17 +215,28 @@ update the digest in `docker-compose.two-gateways.yml` and in this file.
## Known Limitations ## Known Limitations
### BETTER_AUTH_URL enrollment URL bug (production code — not fixed here) ### BETTER_AUTH_URL enrollment URL bug (upstream production code — not yet fixed)
`apps/gateway/src/federation/federation.controller.ts:145` constructs the `apps/gateway/src/federation/federation.controller.ts:145` constructs the
enrollment URL using `process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242'`. enrollment URL using `process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242'`.
In non-harness deployments (where `BETTER_AUTH_URL` is not set or points to the This is an upstream bug: `BETTER_AUTH_URL` is the Better Auth origin (typically
web origin rather than the gateway's own base URL) this produces an incorrect the web app), not the gateway's own base URL. In non-harness deployments this
enrollment URL that points to the wrong host or port. produces an enrollment URL pointing to the wrong host or port.
The harness works around this by explicitly setting **How the harness handles this:**
`BETTER_AUTH_URL: 'http://gateway-b:3000'` in the compose file so the enrollment
URL correctly references gateway-b's internal Docker hostname. 1. **In-cluster calls (container-to-container):** The compose file sets
`BETTER_AUTH_URL: 'http://gateway-b:3000'` so the enrollment URL returned by
the gateway uses the Docker internal hostname. This lets other containers in the
`fed-test-net` network resolve and reach Server B's enrollment endpoint.
2. **Host-side URL rewrite (seed script):** The `seed.ts` script runs on the host
machine where `gateway-b` is not a resolvable hostname. Before calling
`fetch(enrollmentUrl, ...)`, the seed script rewrites the URL: it extracts only
the token path segment from `enrollmentUrl` and reassembles the URL using the
host-accessible `serverBUrl` (default: `http://localhost:14002`). This lets the
seed script redeem enrollment tokens from the host without being affected by the
in-cluster hostname in the returned URL.
**TODO:** Fix `federation.controller.ts` to derive the enrollment URL from its own **TODO:** Fix `federation.controller.ts` to derive the enrollment URL from its own
listening address (e.g. `GATEWAY_BASE_URL` env var or a dedicated listening address (e.g. `GATEWAY_BASE_URL` env var or a dedicated

View File

@@ -195,9 +195,7 @@ async function bootstrapAdmin(
// 1. Check status // 1. Check status
const statusRes = await fetch(`${baseUrl}/api/bootstrap/status`); const statusRes = await fetch(`${baseUrl}/api/bootstrap/status`);
if (!statusRes.ok) { if (!statusRes.ok) {
throw new Error( throw new Error(`[seed] GET ${baseUrl}/api/bootstrap/status → ${statusRes.status.toString()}`);
`[seed] GET ${baseUrl}/api/bootstrap/status → ${statusRes.status.toString()}`,
);
} }
const status = (await statusRes.json()) as { needsSetup: boolean }; const status = (await statusRes.json()) as { needsSetup: boolean };
@@ -214,7 +212,7 @@ async function bootstrapAdmin(
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
name: `Harness Admin (${label})`, name: `Harness Admin (${label})`,
email: `harness-admin-${label.toLowerCase()}@example.invalid`, email: `harness-admin-${label.toLowerCase().replace(/\s+/g, '-')}@example.invalid`,
password, password,
}), }),
}); });
@@ -344,9 +342,18 @@ async function enrollGrant(opts: {
}); });
console.log(`[seed] Created peer on A: ${peerA.peerId}`); console.log(`[seed] Created peer on A: ${peerA.peerId}`);
// 5. Redeem token at Server B's enrollment endpoint with A's CSR // 5. Redeem token at Server B's enrollment endpoint with A's CSR.
// The enrollment endpoint is not admin-guarded — the one-time token IS the credential. // The enrollment endpoint is not admin-guarded — the one-time token IS the credential.
const redeemUrl = tokenResult.enrollmentUrl; //
// The enrollmentUrl returned by the gateway is built using BETTER_AUTH_URL which
// resolves to the in-cluster Docker hostname (gateway-b:3000). That URL is only
// reachable from other containers, not from the host machine running this script.
// We rewrite the host portion to use the host-accessible serverBUrl so the
// seed script can reach the endpoint from the host.
const parsedEnrollment = new URL(tokenResult.enrollmentUrl);
const tokenSegment = parsedEnrollment.pathname.split('/').pop()!;
const redeemUrl = `${serverBUrl}/api/federation/enrollment/${tokenSegment}`;
console.log(`[seed] Rewritten redeem URL (host-accessible): ${redeemUrl}`);
const redeemRes = await fetch(redeemUrl, { const redeemRes = await fetch(redeemUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },