Skip to content

Commit ef2fce0

Browse files
committed
fix(webhooks): relay token carries the c_ prefix on the wire and in the inbox URL
Live-relay verified: play.svix.com returns 400 'Invalid token' for unprefixed tokens, and the relay only registers an inbox when the start frame carries the same c_ token. With c_ in both, a POST to the inbox round-trips through the WebSocket and the reply frame is accepted — proven end-to-end against the real relay with no PLAPI involvement. Reverses spec change #12 (recorded as spec change #27).
1 parent 4eaa629 commit ef2fce0

4 files changed

Lines changed: 12 additions & 9 deletions

File tree

packages/cli-core/src/commands/webhooks/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ clerk webhooks listen [--forward-to <url>] [--events <list>] [--skip-verify] [--
246246
247247
Behavior notes:
248248
249-
- **Relay token**: 10 random base62 chars, raw on the wire (no `c_` prefix), persisted per instance in the CLI config (`relay.<instanceId>.token`). Close code 1008 = token collision → new token generated, persisted, redialed, and the endpoint URL re-pointed.
249+
- **Relay token**: `c_` + 10 random base62 charsthe same token goes in the start frame, the inbox URL, and the per-instance CLI config (`relay.<instanceId>.token`). Live-relay verified: `play.svix.com` rejects unprefixed tokens, and the relay only registers an inbox when the start frame carries the `c_` token. Close code 1008 = token collision → new token generated, persisted, redialed, and the endpoint URL re-pointed.
250250
- **Keepalive**: the relay server pings ~every 21s, but Bun's client WebSocket auto-pongs below the JS API (no ping events). After 30s of silence the client sends an active `ws.ping()` probe — writes to a dead link surface as close/error, which redials with the same token. Reconnects never change the relay URL.
251251
- **Per-delivery output**: human mode prints `time --> event_type msg_…` then `<-- status method path ms` via `log.ui` (bypasses the stderr throttle). Diagnostics: 401 → `clerkMiddleware` public-route hint; 400 → raw-body/`verifyWebhook()` order hint; 5xx → response body inline plus the exact `clerk webhooks replay <msg_id>` line; unreachable handler → synthetic **502** framed back to the relay.
252252
- **Verification**: deliveries failing HMAC are warned about and still forwarded (the mismatch means the relay secret diverged, not that the local handler should silently miss events).

packages/cli-core/src/commands/webhooks/listen.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe("webhooks listen", () => {
161161
expect(mockResolveAppContext).not.toHaveBeenCalled();
162162
});
163163

164-
test("first run generates and persists a 10-char base62 token, then creates the endpoint", async () => {
164+
test("first run generates and persists a c_-prefixed base62 token, then creates the endpoint", async () => {
165165
mockGetRelayEntry.mockResolvedValue(undefined);
166166

167167
await startListen({}, captured);
@@ -172,7 +172,7 @@ describe("webhooks listen", () => {
172172
];
173173
const persistedToken = firstEntry.token;
174174
expect(firstInstanceId).toBe("ins_1");
175-
expect(persistedToken).toMatch(/^[0-9A-Za-z]{10}$/);
175+
expect(persistedToken).toMatch(/^c_[0-9A-Za-z]{10}$/);
176176

177177
expect(mockCreateWebhookEndpoint).toHaveBeenCalledWith("app_1", "ins_1", {
178178
url: `https://play.svix.com/in/${persistedToken}/`,

packages/cli-core/src/commands/webhooks/relay-protocol.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import {
99
} from "./relay-protocol.ts";
1010

1111
describe("generateRelayToken", () => {
12-
test("produces 10 base62 chars with no prefix", () => {
12+
test("produces c_ + 10 base62 chars (live-relay wire format)", () => {
1313
const token = generateRelayToken();
14-
expect(token).toMatch(/^[0-9A-Za-z]{10}$/);
15-
expect(token.startsWith("c_")).toBe(false);
14+
expect(token).toMatch(/^c_[0-9A-Za-z]{10}$/);
1615
});
1716

1817
test("produces distinct tokens across calls", () => {
@@ -22,7 +21,7 @@ describe("generateRelayToken", () => {
2221
});
2322

2423
describe("relayReceiveUrl", () => {
25-
test("builds the play.svix.com URL with the raw token", () => {
24+
test("builds the play.svix.com URL with the token verbatim", () => {
2625
expect(relayReceiveUrl("Ab12Cd34Ef")).toBe("https://play.svix.com/in/Ab12Cd34Ef/");
2726
});
2827
});

packages/cli-core/src/commands/webhooks/relay-protocol.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2323
const TOKEN_LENGTH = 10;
2424
// Largest multiple of 62 below 256; bytes at or above it would bias the modulo.
2525
const UNBIASED_BYTE_LIMIT = 248;
26+
// Live-relay verified (2026-06-10): play.svix.com rejects unprefixed tokens
27+
// ("Invalid token"), and the relay only registers an inbox when the start
28+
// frame carries the same c_ token. The prefix is wire format, not cosmetics.
29+
const TOKEN_PREFIX = "c_";
2630

27-
/** 10 random base62 chars, raw — no `c_` prefix on the wire or in the URL. */
31+
/** `c_` + 10 random base62 chars — the same token goes in the start frame, the inbox URL, and config. */
2832
export function generateRelayToken(): string {
2933
let token = "";
3034
while (token.length < TOKEN_LENGTH) {
@@ -36,7 +40,7 @@ export function generateRelayToken(): string {
3640
if (token.length === TOKEN_LENGTH) break;
3741
}
3842
}
39-
return token;
43+
return TOKEN_PREFIX + token;
4044
}
4145

4246
export function relayReceiveUrl(token: string): string {

0 commit comments

Comments
 (0)