fix(federation): harness round-2 — email validation + host-side URL rewrite
- 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:
@@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user