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',
|
'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',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user