fix(federation): harness round-2 — email validation + host-side URL rewrite
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

- 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 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-04-23 20:50:58 -05:00
parent cb118a53d9
commit 4cf9362e75
3 changed files with 32 additions and 13 deletions

View File

@@ -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

View File

@@ -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' },