diff --git a/eslint.config.mjs b/eslint.config.mjs index e7e631a..eb57f77 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -30,6 +30,7 @@ export default tseslint.config( 'apps/gateway/vitest.config.ts', 'packages/storage/vitest.config.ts', 'packages/mosaic/__tests__/*.ts', + 'tools/federation-harness/*.ts', ], }, }, diff --git a/tools/federation-harness/README.md b/tools/federation-harness/README.md index 4dcd2dd..d403169 100644 --- a/tools/federation-harness/README.md +++ b/tools/federation-harness/README.md @@ -215,17 +215,28 @@ update the digest in `docker-compose.two-gateways.yml` and in this file. ## 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 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 -web origin rather than the gateway's own base URL) this produces an incorrect -enrollment URL that points to the wrong host or port. +This is an upstream bug: `BETTER_AUTH_URL` is the Better Auth origin (typically +the web app), not the gateway's own base URL. In non-harness deployments this +produces an enrollment URL pointing to the wrong host or port. -The harness works around this by explicitly setting -`BETTER_AUTH_URL: 'http://gateway-b:3000'` in the compose file so the enrollment -URL correctly references gateway-b's internal Docker hostname. +**How the harness handles this:** + +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 listening address (e.g. `GATEWAY_BASE_URL` env var or a dedicated diff --git a/tools/federation-harness/seed.ts b/tools/federation-harness/seed.ts index 4efb9bd..6287f49 100644 --- a/tools/federation-harness/seed.ts +++ b/tools/federation-harness/seed.ts @@ -195,9 +195,7 @@ async function bootstrapAdmin( // 1. Check status const statusRes = await fetch(`${baseUrl}/api/bootstrap/status`); if (!statusRes.ok) { - throw new Error( - `[seed] GET ${baseUrl}/api/bootstrap/status → ${statusRes.status.toString()}`, - ); + throw new Error(`[seed] GET ${baseUrl}/api/bootstrap/status → ${statusRes.status.toString()}`); } const status = (await statusRes.json()) as { needsSetup: boolean }; @@ -214,7 +212,7 @@ async function bootstrapAdmin( headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: `Harness Admin (${label})`, - email: `harness-admin-${label.toLowerCase()}@example.invalid`, + email: `harness-admin-${label.toLowerCase().replace(/\s+/g, '-')}@example.invalid`, password, }), }); @@ -344,9 +342,18 @@ async function enrollGrant(opts: { }); 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. - 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, { method: 'POST', headers: { 'Content-Type': 'application/json' },