diff --git a/packages/server/src/server/hub/relationship-remote.test.ts b/packages/server/src/server/hub/relationship-remote.test.ts index 28faab7f12..d95f46f70d 100644 --- a/packages/server/src/server/hub/relationship-remote.test.ts +++ b/packages/server/src/server/hub/relationship-remote.test.ts @@ -73,6 +73,24 @@ test.each([408, 429])("transient enrollment status %s remains retryable", async expect((error as Error).message).toBe(`Hub enrollment failed (${status})`); }); +test("enrollment rejects a transport URL that cannot open a WebSocket", async () => { + const hubOrigin = await startEnrollmentHub("ftp://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 use ws or wss"); +}); + test.each(["enrollment", "revocation"])("%s HTTP calls are bounded", async (operation) => { const hubOrigin = await startStalledHub(); const remote = new DirectHubRelationshipRemote({ requestTimeoutMs: 25 }); @@ -212,6 +230,28 @@ async function startHubReturning(status: number): Promise { return `http://127.0.0.1:${address.port}`; } +async function startEnrollmentHub(webSocketUrl: string): Promise { + const server = createServer(async (request, response) => { + let body = ""; + for await (const chunk of request) body += chunk; + const enrollment = JSON.parse(body) as { relationshipId: string }; + response.writeHead(200, { "content-type": "application/json" }).end( + JSON.stringify({ + relationshipId: enrollment.relationshipId, + scopes: ["hub.*"], + webSocketUrl, + }), + ); + }); + openServers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; +} + async function startStalledHub(): Promise { const server = createServer(() => undefined); openServers.push(server); diff --git a/packages/server/src/server/hub/relationship-remote.ts b/packages/server/src/server/hub/relationship-remote.ts index 59f6a6d330..1408f7e2f7 100644 --- a/packages/server/src/server/hub/relationship-remote.ts +++ b/packages/server/src/server/hub/relationship-remote.ts @@ -58,7 +58,12 @@ export class HubEnrollmentRejectedError extends Error { const EnrollmentResultSchema = z.object({ relationshipId: z.string(), scopes: z.array(z.string()), - webSocketUrl: z.string().url(), + webSocketUrl: z + .string() + .url() + .refine((value) => ["ws:", "wss:"].includes(new URL(value).protocol), { + message: "Hub WebSocket URL must use ws or wss", + }), }); export class DirectHubRelationshipRemote implements HubRelationshipRemote {