From 4cf9362e75532b45cb7797f1419f84e2fc5ff944 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 23 Apr 2026 20:50:58 -0500 Subject: [PATCH] =?UTF-8?q?fix(federation):=20harness=20round-2=20?= =?UTF-8?q?=E2=80=94=20email=20validation=20+=20host-side=20URL=20rewrite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug-1: replace whitespace in admin email local-part (was breaking @IsEmail) - Bug-2: rewrite enrollment URL to use host-accessible base in seed.ts (in-cluster URL not resolvable from host) - Bug-3: correct README Known Limitations section - eslint.config.mjs: add tools/federation-harness/*.ts to allowDefaultProject so pre-commit hook can lint harness scripts Co-Authored-By: Claude Sonnet 4.6 --- eslint.config.mjs | 1 + tools/federation-harness/README.md | 25 ++++++++++++++++++------- tools/federation-harness/seed.ts | 19 +++++++++++++------ 3 files changed, 32 insertions(+), 13 deletions(-) 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' },