Skip to content

Commit 1a03bd8

Browse files
committed
devhook auth wip
1 parent 669788a commit 1a03bd8

5 files changed

Lines changed: 615 additions & 253 deletions

File tree

internal/api/src/routes/devhook.server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type { APIServer } from "../server";
44
import { createWebhookURL } from "../server-helper";
55

66
export default function mountDevhook(server: APIServer) {
7+
// this endpoint is used by packages/server/src/server.ts
8+
// to authorize the listen request. it must use the exact same auth
9+
// method as the one required to listen on the matching devhook URL.
710
server.get("/:devhook/url", withDevhookAuth, async (c) => {
811
const id = c.req.param("devhook");
912
if (!validate(id)) {
@@ -21,6 +24,9 @@ export default function mountDevhook(server: APIServer) {
2124
return c.json({ url });
2225
});
2326

27+
// this endpoint is somewhat misleading. in self-hosted mode,
28+
// it's not used during the flow to listen on the devhook URL.
29+
// websocket upgrade logic is handled in packages/server/src/server.ts
2430
server.get("/:devhook", withDevhookAuth, async (c) => {
2531
const id = c.req.param("devhook");
2632
if (!validate(id)) {

internal/api/src/routes/devhook.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ test.each([
8787
},
8888
});
8989

90+
// Ensure connection works.
9091
await connectPromise;
9192

93+
// Test wildcard hostname routing.
94+
// We need to make the request go through the test server with the correct Host header.
9295
const devhookURL = bindings.createRequestURL!(id);
9396
const response = await fetch(url, { headers: { Host: devhookURL.host } });
9497
expect(response.status).toBe(200);

packages/server/src/devhook.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2-
import { serve } from "@blink.so/api/test";
2+
import Client from "@blink.so/api";
33
import { createDevhookSupport } from "./devhook";
4+
import { serve } from "./test";
45

56
describe("devhook integration tests", () => {
67
let server: Awaited<ReturnType<typeof serve>>;
@@ -18,8 +19,8 @@ describe("devhook integration tests", () => {
1819
});
1920
});
2021

21-
afterAll(() => {
22-
server.stop();
22+
afterAll(async () => {
23+
await server[Symbol.asyncDispose]();
2324
});
2425

2526
describe("UUID validation", () => {
@@ -82,6 +83,54 @@ describe("devhook integration tests", () => {
8283
});
8384
});
8485

86+
describe("authentication", () => {
87+
test("requires auth for devhook listen", async () => {
88+
const id = crypto.randomUUID();
89+
const client = new Client({ baseURL: server.url.toString() });
90+
let connected = false;
91+
let errorEvent: unknown;
92+
93+
const outcome = await new Promise<"error" | "disconnect">(
94+
(resolve, reject) => {
95+
const timer = setTimeout(() => {
96+
reject(new Error("Timed out waiting for devhook auth failure"));
97+
}, 5000);
98+
99+
let disposable: { dispose: () => void } | undefined;
100+
disposable = client.devhook.listen({
101+
id,
102+
onRequest: async () => new Response("ok"),
103+
onConnect: () => {
104+
connected = true;
105+
},
106+
onDisconnect: () => {
107+
clearTimeout(timer);
108+
disposable?.dispose();
109+
resolve("disconnect");
110+
},
111+
onError: (err) => {
112+
errorEvent = err;
113+
clearTimeout(timer);
114+
disposable?.dispose();
115+
resolve("error");
116+
},
117+
});
118+
}
119+
);
120+
121+
expect(outcome).toBe("error");
122+
expect(connected).toBe(false);
123+
expect(errorEvent).toBeDefined();
124+
125+
// Assert the actual auth error message via HTTP since WebSocket errors
126+
// do not expose the handshake response body.
127+
const response = await client.request("GET", `/api/devhook/${id}/url`);
128+
expect(response.status).toBe(401);
129+
const body = await response.json();
130+
expect(body.message).toBe("Unauthorized");
131+
});
132+
});
133+
85134
describe("devhook proxy flow", () => {
86135
test("proxies requests through connected devhook", async () => {
87136
const { helpers, bindings, url } = server;

0 commit comments

Comments
 (0)