fix(hub): bind socket transport to Hub authority

This commit is contained in:
Mohamed Boudra
2026-07-13 15:39:41 +02:00
parent 64df1896d0
commit f62aec1a03
2 changed files with 30 additions and 1 deletions
@@ -91,6 +91,24 @@ test("enrollment rejects a transport URL that cannot open a WebSocket", async ()
).rejects.toThrow("Hub WebSocket URL must use ws or wss");
});
test("enrollment rejects a WebSocket outside the enrolled Hub authority", async () => {
const hubOrigin = await startEnrollmentHub("ws://other-hub.test/daemon");
const remote = new DirectHubRelationshipRemote();
await expect(
remote.enroll({
relationshipId: "relationship-1",
idempotencyKey: "ceremony-1",
hubOrigin,
token: "token",
serverId: "server-1",
daemonPublicKey: "public-key",
credentialVerifier: "verifier",
scopes: ["hub.*"],
}),
).rejects.toThrow("Hub WebSocket URL must match the Hub origin");
});
test.each(["enrollment", "revocation"])("%s HTTP calls are bounded", async (operation) => {
const hubOrigin = await startStalledHub();
const remote = new DirectHubRelationshipRemote({ requestTimeoutMs: 25 });
@@ -66,6 +66,15 @@ const EnrollmentResultSchema = z.object({
}),
});
function ensureWebSocketMatchesHubOrigin(hubOrigin: string, webSocketUrl: string): void {
const hub = new URL(hubOrigin);
const socket = new URL(webSocketUrl);
const expectedProtocol = hub.protocol === "https:" ? "wss:" : "ws:";
if (socket.protocol !== expectedProtocol || socket.host !== hub.host) {
throw new Error("Hub WebSocket URL must match the Hub origin");
}
}
export class DirectHubRelationshipRemote implements HubRelationshipRemote {
private readonly requestTimeoutMs: number;
@@ -97,7 +106,9 @@ export class DirectHubRelationshipRemote implements HubRelationshipRemote {
}
throw new Error(`Hub enrollment failed (${response.status})`);
}
return EnrollmentResultSchema.parse(await response.json());
const enrollment = EnrollmentResultSchema.parse(await response.json());
ensureWebSocketMatchesHubOrigin(input.hubOrigin, enrollment.webSocketUrl);
return enrollment;
});
}