dynamodb-reactive 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ import { SubscribeRequest, SnapshotResponse, UnsubscribeRequest, JsonPatch } from './cluster.js';
2
+ import { HubSocket, HubPaths } from './hub.js';
3
+
4
+ /**
5
+ * Cloudflare Durable Object shell for the reactive hub. It is structural: it
6
+ * takes the Durable Object's `ctx` instead of extending `DurableObject`, so this
7
+ * entry imports nothing from `cloudflare:workers` and builds like every other
8
+ * entry. Your Durable Object class owns its name and migrations and delegates
9
+ * `fetch`, `webSocketMessage`, `webSocketClose` and `webSocketError` to it.
10
+ */
11
+ /** Set by the route handler on requests it forwards to the hub; never trusted from clients. */
12
+ declare const HUB_PRINCIPAL_HEADER = "x-reactive-principal";
13
+ /** A hibernatable Workers WebSocket, as far as the hub needs it. */
14
+ interface DurableObjectWebSocket extends HubSocket {
15
+ close(code?: number, reason?: string): void;
16
+ }
17
+ /** The parts of `DurableObjectState` the hub uses. */
18
+ interface DurableObjectStateLike {
19
+ acceptWebSocket(socket: DurableObjectWebSocket, tags?: string[]): void;
20
+ getWebSockets(tag?: string): DurableObjectWebSocket[];
21
+ setWebSocketAutoResponse?(pair?: unknown): void;
22
+ storage: {
23
+ get<T>(key: string): Promise<T | undefined>;
24
+ put<T>(key: string, value: T): Promise<void>;
25
+ };
26
+ }
27
+ /** Anything with a Durable Object stub's `fetch`. */
28
+ interface HubStub {
29
+ fetch(request: Request): Promise<Response>;
30
+ }
31
+ type WebSocketPairConstructor = new () => {
32
+ 0: DurableObjectWebSocket;
33
+ 1: DurableObjectWebSocket;
34
+ };
35
+ interface DurableObjectHubOptions {
36
+ paths: HubPaths;
37
+ /** Storage key for a path's version. Defaults to `version:<path>`. */
38
+ versionKey?: (path: string) => string;
39
+ maxAttachmentBytes?: number;
40
+ /**
41
+ * Answers the client's `ping` with `pong` without waking the Durable Object.
42
+ * Defaults to true.
43
+ */
44
+ autoPong?: boolean;
45
+ /** Workers' `WebSocketPair`; injectable for tests. */
46
+ WebSocketPair?: WebSocketPairConstructor;
47
+ /** Creates a connection id; injectable for tests. */
48
+ randomId?: () => string;
49
+ }
50
+ /** Wires the hub core to a Durable Object's hibernatable WebSockets and storage. */
51
+ declare function createDurableObjectHub(ctx: DurableObjectStateLike, options: DurableObjectHubOptions): {
52
+ hub: {
53
+ message(socket: HubSocket, raw: string): void;
54
+ subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse>;
55
+ unsubscribe(request: UnsubscribeRequest, principal: string): void;
56
+ notify(path: string): Promise<void>;
57
+ publish(path: string, patches: JsonPatch[]): Promise<void>;
58
+ };
59
+ /** The Durable Object's `fetch`: WebSocket upgrades and actions from `hubClient`. */
60
+ fetch(request: Request): Promise<Response>;
61
+ webSocketMessage(socket: DurableObjectWebSocket, message: string | ArrayBuffer): void;
62
+ webSocketClose(socket: DurableObjectWebSocket, code: number): void;
63
+ webSocketError(socket: DurableObjectWebSocket): void;
64
+ };
65
+ type DurableObjectHub = ReturnType<typeof createDurableObjectHub>;
66
+ /** Calls a hub Durable Object from the Worker. */
67
+ declare function hubClient(stub: HubStub): {
68
+ /** Forwards a reactive HTTP request and returns the hub's response unchanged. */
69
+ request: (body: SubscribeRequest | UnsubscribeRequest, principal: string) => Promise<Response>;
70
+ notify: (path: string) => Promise<void>;
71
+ publish: (path: string, patches: JsonPatch[]) => Promise<void>;
72
+ };
73
+ interface ReactiveRoutes {
74
+ /** `POST` exchanges a credential for a signed ticket. */
75
+ ticket: string;
76
+ /** WebSocket upgrade, authenticated by the ticket in the `token` query parameter. */
77
+ socket: string;
78
+ /** Reactive HTTP subscribe and unsubscribe, authenticated by the ticket as a bearer token. */
79
+ reactive: string;
80
+ }
81
+ interface ReactiveRoutesOptions {
82
+ routes: ReactiveRoutes;
83
+ /** Ticket signing secret. Requests get 503 when it is not configured. */
84
+ secret: string | undefined;
85
+ /** The hub Durable Object stub that serves these routes. */
86
+ hub: () => HubStub;
87
+ /** Returns the principal for a ticket request, or undefined to answer 401. */
88
+ authenticate(request: Request): string | undefined | Promise<string | undefined>;
89
+ ticketLifetimeSeconds?: number;
90
+ }
91
+ /** Serves the ticket, socket and reactive HTTP routes; `undefined` for any other path. */
92
+ declare function handleReactiveRoutes(request: Request, options: ReactiveRoutesOptions): Promise<Response | undefined>;
93
+
94
+ export { HUB_PRINCIPAL_HEADER, createDurableObjectHub, handleReactiveRoutes, hubClient };
95
+ export type { DurableObjectHub, DurableObjectHubOptions, DurableObjectStateLike, DurableObjectWebSocket, HubStub, ReactiveRoutes, ReactiveRoutesOptions };
@@ -0,0 +1,374 @@
1
+ // ../client/src/cluster.ts
2
+ var DEFAULT_TICKET_LIFETIME_SECONDS = 60;
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null;
5
+ }
6
+ function parseClusterTicket(value) {
7
+ if (!isRecord(value) || typeof value.token !== "string" || value.token.length === 0 || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt) || typeof value.url !== "string") {
8
+ throw new Error("Cluster ticket response is invalid");
9
+ }
10
+ const url = new URL(value.url);
11
+ if (url.protocol !== "wss:") throw new Error("Cluster ticket URL must use WSS");
12
+ return { token: value.token, expiresAt: value.expiresAt, url: url.toString() };
13
+ }
14
+ var encoder = new TextEncoder();
15
+ function base64UrlEncode(bytes) {
16
+ let binary = "";
17
+ for (const byte of bytes) binary += String.fromCharCode(byte);
18
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
19
+ }
20
+ function base64UrlDecode(value) {
21
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/");
22
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
23
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
24
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
25
+ return bytes;
26
+ }
27
+ function hmacKey(secret, usage) {
28
+ return crypto.subtle.importKey(
29
+ "raw",
30
+ encoder.encode(secret),
31
+ { name: "HMAC", hash: "SHA-256" },
32
+ false,
33
+ [usage]
34
+ );
35
+ }
36
+ async function createSignedClusterTicket(options) {
37
+ const expiresAt = Math.floor((options.now ?? Date.now()) / 1e3) + (options.lifetimeSeconds ?? DEFAULT_TICKET_LIFETIME_SECONDS);
38
+ const payload = base64UrlEncode(
39
+ encoder.encode(JSON.stringify({ p: options.principal, exp: expiresAt }))
40
+ );
41
+ const signature = new Uint8Array(
42
+ await crypto.subtle.sign("HMAC", await hmacKey(options.secret, "sign"), encoder.encode(payload))
43
+ );
44
+ return parseClusterTicket({
45
+ token: `${payload}.${base64UrlEncode(signature)}`,
46
+ expiresAt,
47
+ url: options.url
48
+ });
49
+ }
50
+ async function verifySignedClusterTicket(token, secret, now = Date.now()) {
51
+ const [payload, signature, extra] = token.split(".");
52
+ if (!payload || !signature || extra !== void 0) return void 0;
53
+ try {
54
+ const valid = await crypto.subtle.verify(
55
+ "HMAC",
56
+ await hmacKey(secret, "verify"),
57
+ base64UrlDecode(signature),
58
+ encoder.encode(payload)
59
+ );
60
+ if (!valid) return void 0;
61
+ const claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(payload)));
62
+ if (!isRecord(claims) || typeof claims.p !== "string" || typeof claims.exp !== "number" || claims.exp <= Math.floor(now / 1e3)) {
63
+ return void 0;
64
+ }
65
+ return claims.p;
66
+ } catch {
67
+ return void 0;
68
+ }
69
+ }
70
+
71
+ // ../client/src/hub.ts
72
+ var DEFAULT_MAX_ATTACHMENT_BYTES = 2048;
73
+ var ReactiveHubError = class extends Error {
74
+ constructor(message) {
75
+ super(message);
76
+ this.name = "ReactiveHubError";
77
+ }
78
+ };
79
+ function isRecord2(value) {
80
+ return typeof value === "object" && value !== null;
81
+ }
82
+ function isHubRequest(value) {
83
+ if (!isRecord2(value)) return false;
84
+ return (value.type === "subscribe" || value.type === "unsubscribe") && typeof value.connectionId === "string" && typeof value.subscriptionId === "string" && typeof value.generation === "number" && (value.type === "unsubscribe" || typeof value.path === "string");
85
+ }
86
+ function bearerToken(request) {
87
+ const match = /^Bearer ([^\s]+)$/u.exec(request.headers.get("authorization") ?? "");
88
+ return match?.[1] ?? "";
89
+ }
90
+ function createHubSocketState(connectionId, principal) {
91
+ return { connectionId, principal, subscriptions: [] };
92
+ }
93
+ function socketState(socket) {
94
+ const state = socket.deserializeAttachment();
95
+ return { ...state, subscriptions: state.subscriptions.filter((entry) => entry.path) };
96
+ }
97
+ function send(socket, event) {
98
+ try {
99
+ socket.send(JSON.stringify(event));
100
+ } catch {
101
+ }
102
+ }
103
+ function createReactiveHub(options) {
104
+ const { sockets, versions } = options;
105
+ const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
106
+ const resolve = (path) => typeof options.paths === "function" ? options.paths(path) : Object.hasOwn(options.paths, path) ? options.paths[path] : void 0;
107
+ const ownedSocket = (connectionId, principal) => {
108
+ const socket = sockets.byConnectionId(connectionId);
109
+ return socket && socketState(socket).principal === principal ? socket : void 0;
110
+ };
111
+ const subscribersOf = (path) => sockets.all().flatMap(
112
+ (socket) => socketState(socket).subscriptions.filter((subscription) => subscription.path === path).map((subscription) => ({ socket, subscription }))
113
+ );
114
+ const bump = async (path) => {
115
+ const fromVersion = await versions.get(path);
116
+ const toVersion = fromVersion + 1;
117
+ await versions.set(path, toVersion);
118
+ return { fromVersion, toVersion };
119
+ };
120
+ return {
121
+ /** Answers the client's `init` and `ping` frames. */
122
+ message(socket, raw) {
123
+ let frame;
124
+ try {
125
+ frame = JSON.parse(raw);
126
+ } catch {
127
+ return;
128
+ }
129
+ const type = isRecord2(frame) ? frame.type : void 0;
130
+ if (type === "init") {
131
+ send(socket, { type: "connected", connectionId: socketState(socket).connectionId });
132
+ } else if (type === "ping") {
133
+ send(socket, { type: "pong" });
134
+ }
135
+ },
136
+ async subscribe(request, principal) {
137
+ const handler = resolve(request.path);
138
+ if (!handler) throw new ReactiveHubError("Unknown subscription path");
139
+ if (!ownedSocket(request.connectionId, principal)) {
140
+ throw new ReactiveHubError("Unknown connection");
141
+ }
142
+ const context = { path: request.path, principal, input: request.input };
143
+ if (handler.authorize && !await handler.authorize(context)) {
144
+ throw new ReactiveHubError("Subscription not allowed");
145
+ }
146
+ const socket = ownedSocket(request.connectionId, principal);
147
+ if (!socket) throw new ReactiveHubError("Unknown connection");
148
+ const state = socketState(socket);
149
+ const next = {
150
+ ...state,
151
+ subscriptions: [
152
+ ...state.subscriptions.filter(
153
+ (entry) => entry.subscriptionId !== request.subscriptionId
154
+ ),
155
+ {
156
+ subscriptionId: request.subscriptionId,
157
+ generation: request.generation,
158
+ path: request.path
159
+ }
160
+ ]
161
+ };
162
+ if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {
163
+ throw new ReactiveHubError("Too many subscriptions on this connection");
164
+ }
165
+ socket.serializeAttachment(next);
166
+ const version = await versions.get(request.path);
167
+ const data = await handler.snapshot(context);
168
+ return {
169
+ type: "snapshot",
170
+ subscriptionId: request.subscriptionId,
171
+ generation: request.generation,
172
+ version,
173
+ data
174
+ };
175
+ },
176
+ unsubscribe(request, principal) {
177
+ const socket = ownedSocket(request.connectionId, principal);
178
+ if (!socket) return;
179
+ const state = socketState(socket);
180
+ socket.serializeAttachment({
181
+ ...state,
182
+ subscriptions: state.subscriptions.filter(
183
+ (entry) => entry.subscriptionId !== request.subscriptionId
184
+ )
185
+ });
186
+ },
187
+ /** Bumps the path's version and asks each of its subscribers to re-fetch. */
188
+ async notify(path) {
189
+ const { fromVersion, toVersion } = await bump(path);
190
+ for (const { socket, subscription } of subscribersOf(path)) {
191
+ send(socket, {
192
+ type: "resync",
193
+ subscriptionId: subscription.subscriptionId,
194
+ generation: subscription.generation,
195
+ fromVersion,
196
+ toVersion,
197
+ messageId: crypto.randomUUID()
198
+ });
199
+ }
200
+ },
201
+ /**
202
+ * Bumps the path's version and sends the same JSON patches to each subscriber.
203
+ * Use it only when every subscriber of the path sees the same data.
204
+ */
205
+ async publish(path, patches) {
206
+ const { fromVersion, toVersion } = await bump(path);
207
+ for (const { socket, subscription } of subscribersOf(path)) {
208
+ send(socket, {
209
+ type: "patch",
210
+ subscriptionId: subscription.subscriptionId,
211
+ generation: subscription.generation,
212
+ fromVersion,
213
+ toVersion,
214
+ messageId: crypto.randomUUID(),
215
+ patches
216
+ });
217
+ }
218
+ }
219
+ };
220
+ }
221
+
222
+ // ../client/src/cloudflare.ts
223
+ var HUB_PRINCIPAL_HEADER = "x-reactive-principal";
224
+ var HUB_ACTION_URL = "https://reactive-hub/";
225
+ function json(value, status = 200) {
226
+ return Response.json(value, { status, headers: { "cache-control": "no-store" } });
227
+ }
228
+ function error(message, status) {
229
+ return json({ type: "error", message }, status);
230
+ }
231
+ function workersGlobal(name) {
232
+ return globalThis[name];
233
+ }
234
+ function createDurableObjectHub(ctx, options) {
235
+ const versionKey = options.versionKey ?? ((path) => `version:${path}`);
236
+ const randomId = options.randomId ?? (() => crypto.randomUUID());
237
+ const hub = createReactiveHub({
238
+ sockets: {
239
+ all: () => ctx.getWebSockets(),
240
+ // Tagging by connection id lets HTTP subscribes find the socket after hibernation.
241
+ byConnectionId: (connectionId) => ctx.getWebSockets(connectionId)[0]
242
+ },
243
+ versions: {
244
+ get: async (path) => await ctx.storage.get(versionKey(path)) ?? 0,
245
+ set: (path, version) => ctx.storage.put(versionKey(path), version)
246
+ },
247
+ paths: options.paths,
248
+ ...options.maxAttachmentBytes === void 0 ? {} : { maxAttachmentBytes: options.maxAttachmentBytes }
249
+ });
250
+ if (options.autoPong !== false) {
251
+ const Pair = workersGlobal("WebSocketRequestResponsePair");
252
+ if (Pair && ctx.setWebSocketAutoResponse) {
253
+ ctx.setWebSocketAutoResponse(
254
+ new Pair(JSON.stringify({ type: "ping" }), JSON.stringify({ type: "pong" }))
255
+ );
256
+ }
257
+ }
258
+ const accept = (request) => {
259
+ const principal = request.headers.get(HUB_PRINCIPAL_HEADER);
260
+ if (!principal) return error("Unauthorized", 401);
261
+ const Pair = options.WebSocketPair ?? workersGlobal("WebSocketPair");
262
+ if (!Pair) throw new Error("WebSocketPair is not available in this runtime");
263
+ const pair = new Pair();
264
+ const connectionId = randomId();
265
+ ctx.acceptWebSocket(pair[1], [connectionId]);
266
+ pair[1].serializeAttachment(createHubSocketState(connectionId, principal));
267
+ return new Response(null, { status: 101, webSocket: pair[0] });
268
+ };
269
+ const act = async (action) => {
270
+ try {
271
+ switch (action.action) {
272
+ case "subscribe":
273
+ return json(await hub.subscribe(action.body, action.principal));
274
+ case "unsubscribe":
275
+ hub.unsubscribe(action.body, action.principal);
276
+ return json({ type: "ok" });
277
+ case "notify":
278
+ await hub.notify(action.path);
279
+ return json({ type: "ok" });
280
+ case "publish":
281
+ await hub.publish(action.path, action.patches);
282
+ return json({ type: "ok" });
283
+ default:
284
+ return error("Unknown hub action", 400);
285
+ }
286
+ } catch (cause) {
287
+ if (cause instanceof ReactiveHubError) return error(cause.message, 400);
288
+ throw cause;
289
+ }
290
+ };
291
+ return {
292
+ hub,
293
+ /** The Durable Object's `fetch`: WebSocket upgrades and actions from `hubClient`. */
294
+ async fetch(request) {
295
+ if (request.headers.get("upgrade") === "websocket") return accept(request);
296
+ return act(await request.json());
297
+ },
298
+ webSocketMessage(socket, message) {
299
+ if (typeof message === "string") hub.message(socket, message);
300
+ },
301
+ webSocketClose(socket, code) {
302
+ try {
303
+ socket.close(code === 1005 ? 1e3 : code, "closed");
304
+ } catch {
305
+ }
306
+ },
307
+ webSocketError(socket) {
308
+ try {
309
+ socket.close(1011, "error");
310
+ } catch {
311
+ }
312
+ }
313
+ };
314
+ }
315
+ function hubClient(stub) {
316
+ const post = (action) => stub.fetch(new Request(HUB_ACTION_URL, { method: "POST", body: JSON.stringify(action) }));
317
+ const ok = async (response) => {
318
+ if (!response.ok) throw new Error(`Hub request failed: HTTP ${response.status}`);
319
+ };
320
+ return {
321
+ /** Forwards a reactive HTTP request and returns the hub's response unchanged. */
322
+ request: (body, principal) => post(
323
+ body.type === "subscribe" ? { action: "subscribe", principal, body } : { action: "unsubscribe", principal, body }
324
+ ),
325
+ notify: async (path) => ok(await post({ action: "notify", path })),
326
+ publish: async (path, patches) => ok(await post({ action: "publish", path, patches }))
327
+ };
328
+ }
329
+ async function handleReactiveRoutes(request, options) {
330
+ const url = new URL(request.url);
331
+ const { routes, secret } = options;
332
+ if (![routes.ticket, routes.socket, routes.reactive].includes(url.pathname)) return void 0;
333
+ if (!secret) return error("Reactive hub is not configured", 503);
334
+ if (url.pathname === routes.ticket) {
335
+ if (request.method !== "POST") return error("Method not allowed", 405);
336
+ const principal2 = await options.authenticate(request);
337
+ if (!principal2) return error("Unauthorized", 401);
338
+ const socketUrl = new URL(routes.socket, url);
339
+ socketUrl.protocol = "wss:";
340
+ return json(
341
+ await createSignedClusterTicket({
342
+ secret,
343
+ principal: principal2,
344
+ url: socketUrl.toString(),
345
+ ...options.ticketLifetimeSeconds === void 0 ? {} : { lifetimeSeconds: options.ticketLifetimeSeconds }
346
+ })
347
+ );
348
+ }
349
+ if (url.pathname === routes.socket) {
350
+ if (request.headers.get("upgrade") !== "websocket") {
351
+ return error("Expected a WebSocket upgrade", 426);
352
+ }
353
+ const principal2 = await verifySignedClusterTicket(url.searchParams.get("token") ?? "", secret);
354
+ if (!principal2) return new Response("Unauthorized", { status: 401 });
355
+ const forwarded = new Request(request);
356
+ forwarded.headers.set(HUB_PRINCIPAL_HEADER, principal2);
357
+ return options.hub().fetch(forwarded);
358
+ }
359
+ if (request.method !== "POST") return error("Method not allowed", 405);
360
+ const principal = await verifySignedClusterTicket(bearerToken(request), secret);
361
+ if (!principal) return error("Unauthorized", 401);
362
+ let body;
363
+ try {
364
+ body = await request.json();
365
+ } catch {
366
+ return error("Invalid JSON", 400);
367
+ }
368
+ if (!isHubRequest(body)) return error("Unsupported reactive request", 400);
369
+ return hubClient(options.hub()).request(body, principal);
370
+ }
371
+
372
+ export { HUB_PRINCIPAL_HEADER, createDurableObjectHub, handleReactiveRoutes, hubClient };
373
+ //# sourceMappingURL=cloudflare.js.map
374
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../client/src/cluster.ts","../../client/src/hub.ts","../../client/src/cloudflare.ts"],"names":["isRecord","principal"],"mappings":"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","file":"cloudflare.js","sourcesContent":["import { ReactiveClient } from './client.js';\nimport type { ReactiveClientConfig } from './types.js';\n\n/**\n * Cluster workers run on machines with no inbound access. They authenticate\n * outbound with a long-lived runner token, exchange it for a short-lived ticket,\n * and hold a reactive WebSocket that pushes work to them. These helpers use only\n * Web standards (fetch, WebSocket, Web Crypto), so the same code runs in Node and\n * in Workers runtimes, whichever backend serves the reactive protocol.\n */\n\nexport const DEFAULT_CLUSTER_TICKET_PATH = '/api/cluster/token';\nexport const DEFAULT_CLUSTER_HTTP_PATH = '/api/reactive';\nconst DEFAULT_RETRY_DELAY_MS = 10_000;\nconst DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\nconst DEFAULT_TICKET_LIFETIME_SECONDS = 60;\n\n/** Short-lived credential returned by the ticket endpoint. */\nexport interface ClusterTicket {\n /** Sent as the WebSocket `token` query parameter and the HTTP bearer token. */\n token: string;\n /** Expiry in epoch seconds. */\n expiresAt: number;\n /** `wss:` URL of the reactive WebSocket. */\n url: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Validates a ticket endpoint response; the socket URL must use `wss:`. */\nexport function parseClusterTicket(value: unknown): ClusterTicket {\n if (\n !isRecord(value) ||\n typeof value.token !== 'string' ||\n value.token.length === 0 ||\n typeof value.expiresAt !== 'number' ||\n !Number.isFinite(value.expiresAt) ||\n typeof value.url !== 'string'\n ) {\n throw new Error('Cluster ticket response is invalid');\n }\n const url = new URL(value.url);\n if (url.protocol !== 'wss:') throw new Error('Cluster ticket URL must use WSS');\n return { token: value.token, expiresAt: value.expiresAt, url: url.toString() };\n}\n\nexport interface ClusterWorkerConfig {\n /** Origin of the application that serves the ticket and reactive HTTP endpoints. */\n baseUrl: string;\n /** Long-lived runner credential exchanged for tickets. */\n runnerToken: string | (() => string | Promise<string>);\n /** Ticket endpoint path. Defaults to `/api/cluster/token`. */\n ticketPath?: string;\n /** Reactive HTTP endpoint path. Defaults to `/api/reactive`. */\n httpPath?: string;\n /** Delay between ticket, connect, and reconnect attempts. Defaults to 10 seconds. */\n retryDelay?: number;\n /** Ticket request timeout. Defaults to 10 seconds. */\n requestTimeout?: number;\n /** Stops retrying when aborted. */\n signal?: AbortSignal;\n /** Called with each failed ticket or connect attempt before the next retry. */\n onRetry?: (error: Error) => void;\n /** Extra client options; the URL, HTTP URL, and auth are managed here. */\n client?: Omit<ReactiveClientConfig, 'url' | 'httpUrl' | 'auth'>;\n /** Fetch implementation for ticket requests. Defaults to the global fetch. */\n fetch?: typeof fetch;\n}\n\nfunction toError(cause: unknown): Error {\n return cause instanceof Error ? cause : new Error(String(cause));\n}\n\nfunction abortError(signal: AbortSignal): Error {\n return toError(signal.reason ?? new Error('Cluster worker connection aborted'));\n}\n\nfunction wait(milliseconds: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal));\n return;\n }\n const onAbort = () => {\n clearTimeout(timer);\n reject(abortError(signal!));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, milliseconds);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** Requests a ticket from the application using the runner token. */\nexport async function fetchClusterTicket(\n config: Pick<\n ClusterWorkerConfig,\n 'baseUrl' | 'runnerToken' | 'ticketPath' | 'requestTimeout' | 'fetch'\n >,\n): Promise<ClusterTicket> {\n const runnerToken =\n typeof config.runnerToken === 'function'\n ? await config.runnerToken()\n : config.runnerToken;\n const endpoint = new URL(\n config.ticketPath ?? DEFAULT_CLUSTER_TICKET_PATH,\n config.baseUrl,\n );\n const response = await (config.fetch ?? fetch)(endpoint, {\n method: 'POST',\n headers: { authorization: `Bearer ${runnerToken}` },\n signal: AbortSignal.timeout(config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS),\n });\n if (!response.ok) {\n throw new Error(`Cluster ticket request failed: HTTP ${response.status}`);\n }\n return parseClusterTicket(await response.json());\n}\n\n/**\n * Connects a cluster worker, retrying ticket and connect failures until it\n * succeeds or `signal` aborts. Each (re)connect fetches a fresh ticket. The\n * returned client keeps reconnecting on its own; subscribe to work with\n * `client.subscribe(...)` and stop with `client.disconnect()`.\n */\nexport async function connectClusterWorker(\n config: ClusterWorkerConfig,\n): Promise<ReactiveClient> {\n const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY_MS;\n const httpUrl = new URL(\n config.httpPath ?? DEFAULT_CLUSTER_HTTP_PATH,\n config.baseUrl,\n ).toString();\n for (;;) {\n if (config.signal?.aborted) throw abortError(config.signal);\n let client: ReactiveClient | undefined;\n try {\n // The first ticket only supplies the socket URL; the client fetches its own\n // ticket for every connection so reconnects never reuse an expired one.\n // oxlint-disable-next-line no-await-in-loop -- Attempts retry serially.\n const ticket = await fetchClusterTicket(config);\n client = new ReactiveClient({\n reconnectDelay: retryDelay,\n maxReconnectAttempts: Number.MAX_SAFE_INTEGER,\n ...config.client,\n url: ticket.url,\n httpUrl,\n auth: async () => (await fetchClusterTicket(config)).token,\n });\n // oxlint-disable-next-line no-await-in-loop -- Attempts retry serially.\n await client.connect();\n return client;\n } catch (cause) {\n // connect() keeps reconnecting after it rejects; stop it before retrying.\n client?.disconnect();\n if (config.signal?.aborted) throw abortError(config.signal);\n config.onRetry?.(toError(cause));\n // oxlint-disable-next-line no-await-in-loop -- Backoff belongs to the retry loop.\n await wait(retryDelay, config.signal);\n }\n }\n}\n\nconst encoder = new TextEncoder();\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');\n}\n\nfunction base64UrlDecode(value: string): Uint8Array<ArrayBuffer> {\n const padded = value.replaceAll('-', '+').replaceAll('_', '/');\n const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));\n const bytes = new Uint8Array(new ArrayBuffer(binary.length));\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\nfunction hmacKey(secret: string, usage: 'sign' | 'verify'): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n [usage],\n );\n}\n\nexport interface SignedClusterTicketOptions {\n /** HMAC secret shared only by the servers that mint and verify tickets. */\n secret: string;\n /** Service principal the ticket authenticates as. */\n principal: string;\n /** `wss:` URL returned to the worker. */\n url: string;\n /** Ticket lifetime. Defaults to 60 seconds. */\n lifetimeSeconds?: number;\n /** Current time in milliseconds, for tests. */\n now?: number;\n}\n\n/**\n * Mints a stateless HMAC-signed ticket. Verifying it needs no storage read,\n * which keeps reconnect storms off the database.\n */\nexport async function createSignedClusterTicket(\n options: SignedClusterTicketOptions,\n): Promise<ClusterTicket> {\n const expiresAt =\n Math.floor((options.now ?? Date.now()) / 1000) +\n (options.lifetimeSeconds ?? DEFAULT_TICKET_LIFETIME_SECONDS);\n const payload = base64UrlEncode(\n encoder.encode(JSON.stringify({ p: options.principal, exp: expiresAt })),\n );\n const signature = new Uint8Array(\n await crypto.subtle.sign('HMAC', await hmacKey(options.secret, 'sign'), encoder.encode(payload)),\n );\n return parseClusterTicket({\n token: `${payload}.${base64UrlEncode(signature)}`,\n expiresAt,\n url: options.url,\n });\n}\n\n/** Returns the ticket's principal, or `undefined` when it is forged, malformed, or expired. */\nexport async function verifySignedClusterTicket(\n token: string,\n secret: string,\n now = Date.now(),\n): Promise<string | undefined> {\n const [payload, signature, extra] = token.split('.');\n if (!payload || !signature || extra !== undefined) return undefined;\n try {\n // crypto.subtle.verify compares in constant time.\n const valid = await crypto.subtle.verify(\n 'HMAC',\n await hmacKey(secret, 'verify'),\n base64UrlDecode(signature),\n encoder.encode(payload),\n );\n if (!valid) return undefined;\n const claims: unknown = JSON.parse(new TextDecoder().decode(base64UrlDecode(payload)));\n if (\n !isRecord(claims) ||\n typeof claims.p !== 'string' ||\n typeof claims.exp !== 'number' ||\n claims.exp <= Math.floor(now / 1000)\n ) {\n return undefined;\n }\n return claims.p;\n } catch {\n return undefined;\n }\n}\n","import type {\n JsonPatch,\n SnapshotResponse,\n SubscribeRequest,\n UnsubscribeRequest,\n WebSocketEvent,\n} from './protocol.js';\n\n/**\n * Protocol core for a reactive hub: the server half of the wire protocol for\n * backends that hold WebSockets themselves (for example a Cloudflare Durable\n * Object). It keeps every piece of per-socket state on the socket's attachment so\n * the state survives hibernation, and it has no runtime imports, so it runs and\n * tests in Node.\n */\n\n/** Hibernation limits a socket attachment to 2,048 bytes of serialized JSON. */\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 2_048;\n\n/** One subscription held by a socket. */\nexport interface HubSubscription {\n subscriptionId: string;\n generation: number;\n path: string;\n}\n\n/** State kept on each socket's attachment. */\nexport interface HubSocketState {\n connectionId: string;\n principal: string;\n subscriptions: HubSubscription[];\n}\n\n/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */\nexport interface HubSocket {\n send(message: string): void;\n deserializeAttachment(): unknown;\n serializeAttachment(value: unknown): void;\n}\n\nexport interface HubSockets {\n all(): HubSocket[];\n byConnectionId(connectionId: string): HubSocket | undefined;\n}\n\n/** Monotonic per-path versions. Versions only need to grow while sockets stay open. */\nexport interface HubVersions {\n get(path: string): Promise<number>;\n set(path: string, version: number): Promise<void>;\n}\n\nexport interface HubPathContext {\n path: string;\n principal: string;\n input: unknown;\n}\n\nexport interface HubPath {\n /**\n * Runs before the subscription is stored. Return false to reject it. Every\n * subscriber of a path receives its pushes, so authorise here, not in `snapshot`.\n */\n authorize?(context: HubPathContext): boolean | Promise<boolean>;\n /** Current data for the path, sent as the subscription snapshot. */\n snapshot(context: HubPathContext): unknown;\n}\n\n/** Fixed paths by name, or a resolver for parameterised paths such as `drafts.<id>`. */\nexport type HubPaths = Record<string, HubPath> | ((path: string) => HubPath | undefined);\n\nexport interface ReactiveHubOptions {\n sockets: HubSockets;\n versions: HubVersions;\n paths: HubPaths;\n /** Rejects subscribes that would grow an attachment past this size. */\n maxAttachmentBytes?: number;\n}\n\n/** Thrown for requests the caller is not allowed to make. */\nexport class ReactiveHubError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ReactiveHubError';\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Validates a reactive HTTP body for the subscribe and unsubscribe requests a hub serves. */\nexport function isHubRequest(value: unknown): value is SubscribeRequest | UnsubscribeRequest {\n if (!isRecord(value)) return false;\n return (\n (value.type === 'subscribe' || value.type === 'unsubscribe') &&\n typeof value.connectionId === 'string' &&\n typeof value.subscriptionId === 'string' &&\n typeof value.generation === 'number' &&\n (value.type === 'unsubscribe' || typeof value.path === 'string')\n );\n}\n\n/** Returns the bearer token from an `Authorization` header, or an empty string. */\nexport function bearerToken(request: Request): string {\n const match = /^Bearer ([^\\s]+)$/u.exec(request.headers.get('authorization') ?? '');\n return match?.[1] ?? '';\n}\n\n/** Initial attachment for a newly accepted socket. */\nexport function createHubSocketState(connectionId: string, principal: string): HubSocketState {\n return { connectionId, principal, subscriptions: [] };\n}\n\nfunction socketState(socket: HubSocket): HubSocketState {\n const state = socket.deserializeAttachment() as HubSocketState;\n // Attachments written before subscriptions carried a path have no path; they\n // are dropped rather than fanned out to every notification.\n return { ...state, subscriptions: state.subscriptions.filter((entry) => entry.path) };\n}\n\nfunction send(socket: HubSocket, event: WebSocketEvent): void {\n try {\n socket.send(JSON.stringify(event));\n } catch {\n // A socket closing mid-broadcast is cleaned up by its close handler.\n }\n}\n\n/** Creates the protocol core for one hub instance. */\nexport function createReactiveHub(options: ReactiveHubOptions) {\n const { sockets, versions } = options;\n const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;\n const resolve = (path: string): HubPath | undefined =>\n typeof options.paths === 'function'\n ? options.paths(path)\n : Object.hasOwn(options.paths, path)\n ? options.paths[path]\n : undefined;\n\n const ownedSocket = (connectionId: string, principal: string): HubSocket | undefined => {\n const socket = sockets.byConnectionId(connectionId);\n // The HTTP caller must own the WebSocket it registers against.\n return socket && socketState(socket).principal === principal ? socket : undefined;\n };\n\n const subscribersOf = (path: string) =>\n sockets.all().flatMap((socket) =>\n socketState(socket)\n .subscriptions.filter((subscription) => subscription.path === path)\n .map((subscription) => ({ socket, subscription })),\n );\n\n const bump = async (path: string) => {\n const fromVersion = await versions.get(path);\n const toVersion = fromVersion + 1;\n await versions.set(path, toVersion);\n return { fromVersion, toVersion };\n };\n\n return {\n /** Answers the client's `init` and `ping` frames. */\n message(socket: HubSocket, raw: string): void {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return;\n }\n const type = isRecord(frame) ? frame.type : undefined;\n if (type === 'init') {\n send(socket, { type: 'connected', connectionId: socketState(socket).connectionId });\n } else if (type === 'ping') {\n send(socket, { type: 'pong' });\n }\n },\n\n async subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse> {\n const handler = resolve(request.path);\n if (!handler) throw new ReactiveHubError('Unknown subscription path');\n if (!ownedSocket(request.connectionId, principal)) {\n throw new ReactiveHubError('Unknown connection');\n }\n const context: HubPathContext = { path: request.path, principal, input: request.input };\n if (handler.authorize && !(await handler.authorize(context))) {\n throw new ReactiveHubError('Subscription not allowed');\n }\n // Re-read after the awaits so a concurrent subscribe on the same socket is kept.\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) throw new ReactiveHubError('Unknown connection');\n const state = socketState(socket);\n const next: HubSocketState = {\n ...state,\n subscriptions: [\n ...state.subscriptions.filter(\n (entry) => entry.subscriptionId !== request.subscriptionId,\n ),\n {\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n path: request.path,\n },\n ],\n };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Too many subscriptions on this connection');\n }\n socket.serializeAttachment(next);\n // Read the version before the data so a concurrent notify can only make the\n // client re-fetch, never miss a change.\n const version = await versions.get(request.path);\n const data = await handler.snapshot(context);\n return {\n type: 'snapshot',\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n version,\n data,\n };\n },\n\n unsubscribe(request: UnsubscribeRequest, principal: string): void {\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) return;\n const state = socketState(socket);\n socket.serializeAttachment({\n ...state,\n subscriptions: state.subscriptions.filter(\n (entry) => entry.subscriptionId !== request.subscriptionId,\n ),\n } satisfies HubSocketState);\n },\n\n /** Bumps the path's version and asks each of its subscribers to re-fetch. */\n async notify(path: string): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'resync',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n });\n }\n },\n\n /**\n * Bumps the path's version and sends the same JSON patches to each subscriber.\n * Use it only when every subscriber of the path sees the same data.\n */\n async publish(path: string, patches: JsonPatch[]): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'patch',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n patches,\n });\n }\n },\n };\n}\n\nexport type ReactiveHub = ReturnType<typeof createReactiveHub>;\n","import { createSignedClusterTicket, verifySignedClusterTicket } from './cluster.js';\nimport {\n bearerToken,\n createHubSocketState,\n createReactiveHub,\n type HubPaths,\n type HubSocket,\n isHubRequest,\n ReactiveHubError,\n} from './hub.js';\nimport type { JsonPatch, SubscribeRequest, UnsubscribeRequest } from './protocol.js';\n\n/**\n * Cloudflare Durable Object shell for the reactive hub. It is structural: it\n * takes the Durable Object's `ctx` instead of extending `DurableObject`, so this\n * entry imports nothing from `cloudflare:workers` and builds like every other\n * entry. Your Durable Object class owns its name and migrations and delegates\n * `fetch`, `webSocketMessage`, `webSocketClose` and `webSocketError` to it.\n */\n\n/** Set by the route handler on requests it forwards to the hub; never trusted from clients. */\nexport const HUB_PRINCIPAL_HEADER = 'x-reactive-principal';\nconst HUB_ACTION_URL = 'https://reactive-hub/';\n\n/** A hibernatable Workers WebSocket, as far as the hub needs it. */\nexport interface DurableObjectWebSocket extends HubSocket {\n close(code?: number, reason?: string): void;\n}\n\n/** The parts of `DurableObjectState` the hub uses. */\nexport interface DurableObjectStateLike {\n acceptWebSocket(socket: DurableObjectWebSocket, tags?: string[]): void;\n getWebSockets(tag?: string): DurableObjectWebSocket[];\n setWebSocketAutoResponse?(pair?: unknown): void;\n storage: {\n get<T>(key: string): Promise<T | undefined>;\n put<T>(key: string, value: T): Promise<void>;\n };\n}\n\n/** Anything with a Durable Object stub's `fetch`. */\nexport interface HubStub {\n fetch(request: Request): Promise<Response>;\n}\n\ntype WebSocketPairConstructor = new () => {\n 0: DurableObjectWebSocket;\n 1: DurableObjectWebSocket;\n};\ntype RequestResponsePairConstructor = new (request: string, response: string) => unknown;\n\nexport interface DurableObjectHubOptions {\n paths: HubPaths;\n /** Storage key for a path's version. Defaults to `version:<path>`. */\n versionKey?: (path: string) => string;\n maxAttachmentBytes?: number;\n /**\n * Answers the client's `ping` with `pong` without waking the Durable Object.\n * Defaults to true.\n */\n autoPong?: boolean;\n /** Workers' `WebSocketPair`; injectable for tests. */\n WebSocketPair?: WebSocketPairConstructor;\n /** Creates a connection id; injectable for tests. */\n randomId?: () => string;\n}\n\ntype HubAction =\n | { action: 'subscribe'; principal: string; body: SubscribeRequest }\n | { action: 'unsubscribe'; principal: string; body: UnsubscribeRequest }\n | { action: 'notify'; path: string }\n | { action: 'publish'; path: string; patches: JsonPatch[] };\n\nfunction json(value: unknown, status = 200): Response {\n return Response.json(value, { status, headers: { 'cache-control': 'no-store' } });\n}\n\nfunction error(message: string, status: number): Response {\n return json({ type: 'error', message }, status);\n}\n\nfunction workersGlobal<T>(name: string): T | undefined {\n return (globalThis as Record<string, unknown>)[name] as T | undefined;\n}\n\n/** Wires the hub core to a Durable Object's hibernatable WebSockets and storage. */\nexport function createDurableObjectHub(\n ctx: DurableObjectStateLike,\n options: DurableObjectHubOptions,\n) {\n const versionKey = options.versionKey ?? ((path: string) => `version:${path}`);\n const randomId = options.randomId ?? (() => crypto.randomUUID());\n const hub = createReactiveHub({\n sockets: {\n all: () => ctx.getWebSockets(),\n // Tagging by connection id lets HTTP subscribes find the socket after hibernation.\n byConnectionId: (connectionId) => ctx.getWebSockets(connectionId)[0],\n },\n versions: {\n get: async (path) => (await ctx.storage.get<number>(versionKey(path))) ?? 0,\n set: (path, version) => ctx.storage.put(versionKey(path), version),\n },\n paths: options.paths,\n ...(options.maxAttachmentBytes === undefined\n ? {}\n : { maxAttachmentBytes: options.maxAttachmentBytes }),\n });\n\n if (options.autoPong !== false) {\n const Pair = workersGlobal<RequestResponsePairConstructor>('WebSocketRequestResponsePair');\n if (Pair && ctx.setWebSocketAutoResponse) {\n ctx.setWebSocketAutoResponse(\n new Pair(JSON.stringify({ type: 'ping' }), JSON.stringify({ type: 'pong' })),\n );\n }\n }\n\n const accept = (request: Request): Response => {\n const principal = request.headers.get(HUB_PRINCIPAL_HEADER);\n if (!principal) return error('Unauthorized', 401);\n const Pair = options.WebSocketPair ?? workersGlobal<WebSocketPairConstructor>('WebSocketPair');\n if (!Pair) throw new Error('WebSocketPair is not available in this runtime');\n const pair = new Pair();\n const connectionId = randomId();\n ctx.acceptWebSocket(pair[1], [connectionId]);\n pair[1].serializeAttachment(createHubSocketState(connectionId, principal));\n return new Response(null, { status: 101, webSocket: pair[0] } as ResponseInit);\n };\n\n const act = async (action: HubAction): Promise<Response> => {\n try {\n switch (action.action) {\n case 'subscribe':\n return json(await hub.subscribe(action.body, action.principal));\n case 'unsubscribe':\n hub.unsubscribe(action.body, action.principal);\n return json({ type: 'ok' });\n case 'notify':\n await hub.notify(action.path);\n return json({ type: 'ok' });\n case 'publish':\n await hub.publish(action.path, action.patches);\n return json({ type: 'ok' });\n default:\n return error('Unknown hub action', 400);\n }\n } catch (cause) {\n if (cause instanceof ReactiveHubError) return error(cause.message, 400);\n throw cause;\n }\n };\n\n return {\n hub,\n\n /** The Durable Object's `fetch`: WebSocket upgrades and actions from `hubClient`. */\n async fetch(request: Request): Promise<Response> {\n if (request.headers.get('upgrade') === 'websocket') return accept(request);\n return act((await request.json()) as HubAction);\n },\n\n webSocketMessage(socket: DurableObjectWebSocket, message: string | ArrayBuffer): void {\n if (typeof message === 'string') hub.message(socket, message);\n },\n\n webSocketClose(socket: DurableObjectWebSocket, code: number): void {\n try {\n socket.close(code === 1005 ? 1000 : code, 'closed');\n } catch {\n // Already closed, or the runtime replied to the close frame itself.\n }\n },\n\n webSocketError(socket: DurableObjectWebSocket): void {\n try {\n socket.close(1011, 'error');\n } catch {\n // Already closed.\n }\n },\n };\n}\n\nexport type DurableObjectHub = ReturnType<typeof createDurableObjectHub>;\n\n/** Calls a hub Durable Object from the Worker. */\nexport function hubClient(stub: HubStub) {\n const post = (action: HubAction) =>\n stub.fetch(new Request(HUB_ACTION_URL, { method: 'POST', body: JSON.stringify(action) }));\n const ok = async (response: Response) => {\n if (!response.ok) throw new Error(`Hub request failed: HTTP ${response.status}`);\n };\n return {\n /** Forwards a reactive HTTP request and returns the hub's response unchanged. */\n request: (body: SubscribeRequest | UnsubscribeRequest, principal: string) =>\n post(\n body.type === 'subscribe'\n ? { action: 'subscribe', principal, body }\n : { action: 'unsubscribe', principal, body },\n ),\n notify: async (path: string) => ok(await post({ action: 'notify', path })),\n publish: async (path: string, patches: JsonPatch[]) =>\n ok(await post({ action: 'publish', path, patches })),\n };\n}\n\nexport interface ReactiveRoutes {\n /** `POST` exchanges a credential for a signed ticket. */\n ticket: string;\n /** WebSocket upgrade, authenticated by the ticket in the `token` query parameter. */\n socket: string;\n /** Reactive HTTP subscribe and unsubscribe, authenticated by the ticket as a bearer token. */\n reactive: string;\n}\n\nexport interface ReactiveRoutesOptions {\n routes: ReactiveRoutes;\n /** Ticket signing secret. Requests get 503 when it is not configured. */\n secret: string | undefined;\n /** The hub Durable Object stub that serves these routes. */\n hub: () => HubStub;\n /** Returns the principal for a ticket request, or undefined to answer 401. */\n authenticate(request: Request): string | undefined | Promise<string | undefined>;\n ticketLifetimeSeconds?: number;\n}\n\n/** Serves the ticket, socket and reactive HTTP routes; `undefined` for any other path. */\nexport async function handleReactiveRoutes(\n request: Request,\n options: ReactiveRoutesOptions,\n): Promise<Response | undefined> {\n const url = new URL(request.url);\n const { routes, secret } = options;\n if (![routes.ticket, routes.socket, routes.reactive].includes(url.pathname)) return undefined;\n if (!secret) return error('Reactive hub is not configured', 503);\n\n if (url.pathname === routes.ticket) {\n if (request.method !== 'POST') return error('Method not allowed', 405);\n const principal = await options.authenticate(request);\n if (!principal) return error('Unauthorized', 401);\n const socketUrl = new URL(routes.socket, url);\n socketUrl.protocol = 'wss:';\n return json(\n await createSignedClusterTicket({\n secret,\n principal,\n url: socketUrl.toString(),\n ...(options.ticketLifetimeSeconds === undefined\n ? {}\n : { lifetimeSeconds: options.ticketLifetimeSeconds }),\n }),\n );\n }\n\n if (url.pathname === routes.socket) {\n if (request.headers.get('upgrade') !== 'websocket') {\n return error('Expected a WebSocket upgrade', 426);\n }\n const principal = await verifySignedClusterTicket(url.searchParams.get('token') ?? '', secret);\n if (!principal) return new Response('Unauthorized', { status: 401 });\n const forwarded = new Request(request);\n forwarded.headers.set(HUB_PRINCIPAL_HEADER, principal);\n return options.hub().fetch(forwarded);\n }\n\n if (request.method !== 'POST') return error('Method not allowed', 405);\n const principal = await verifySignedClusterTicket(bearerToken(request), secret);\n if (!principal) return error('Unauthorized', 401);\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return error('Invalid JSON', 400);\n }\n if (!isHubRequest(body)) return error('Unsupported reactive request', 400);\n return hubClient(options.hub()).request(body, principal);\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { ReactiveClientConfig, ReactiveClient } from './client.cjs';
2
+ export { C as CallRequest, a as ConnectedEvent, E as ErrorResponse, H as HttpRequest, J as JsonPatch, P as PatchEvent, b as PongEvent, R as ResyncEvent, S as SnapshotResponse, c as SubscribeRequest, U as UnsubscribeRequest, W as WebSocketClientMessage, d as WebSocketEvent } from './protocol-C4Syt_Bl.js';
2
3
 
3
4
  /**
4
5
  * Cluster workers run on machines with no inbound access. They authenticate
@@ -71,82 +72,5 @@ declare function createSignedClusterTicket(options: SignedClusterTicketOptions):
71
72
  /** Returns the ticket's principal, or `undefined` when it is forged, malformed, or expired. */
72
73
  declare function verifySignedClusterTicket(token: string, secret: string, now?: number): Promise<string | undefined>;
73
74
 
74
- interface JsonPatch {
75
- op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
76
- path: string;
77
- value?: unknown;
78
- from?: string;
79
- }
80
- interface SnapshotResponse {
81
- type: 'snapshot';
82
- subscriptionId: string;
83
- generation: number;
84
- version: number;
85
- data: unknown;
86
- }
87
- interface ErrorResponse {
88
- type: 'error';
89
- message: string;
90
- }
91
- interface PatchEvent {
92
- type: 'patch';
93
- subscriptionId: string;
94
- generation: number;
95
- fromVersion: number;
96
- toVersion: number;
97
- messageId: string;
98
- patches: JsonPatch[];
99
- }
100
- interface ResyncEvent {
101
- type: 'resync';
102
- subscriptionId: string;
103
- generation: number;
104
- fromVersion: number;
105
- toVersion: number;
106
- messageId: string;
107
- }
108
- interface ConnectedEvent {
109
- type: 'connected';
110
- connectionId: string;
111
- }
112
- interface PongEvent {
113
- type: 'pong';
114
- }
115
- type WebSocketEvent = ConnectedEvent | PongEvent | PatchEvent | ResyncEvent;
116
- /** Frames the client sends on the WebSocket: `init` after opening, then periodic `ping`. */
117
- type WebSocketClientMessage = {
118
- type: 'init';
119
- } | {
120
- type: 'ping';
121
- };
122
- /**
123
- * HTTP request bodies the client POSTs to `httpUrl`. A resync re-sends `subscribe`
124
- * with the same subscription and a new registration; servers answer with a snapshot
125
- * whose `version` must exceed the last one the client accepted.
126
- */
127
- interface SubscribeRequest {
128
- type: 'subscribe';
129
- connectionId: string;
130
- subscriptionId: string;
131
- generation: number;
132
- registrationId: string;
133
- path: string;
134
- input: unknown;
135
- }
136
- interface UnsubscribeRequest {
137
- type: 'unsubscribe';
138
- connectionId: string;
139
- subscriptionId: string;
140
- generation: number;
141
- registrationId: string;
142
- }
143
- interface CallRequest {
144
- type: 'call';
145
- connectionId: string;
146
- path: string;
147
- input: unknown;
148
- }
149
- type HttpRequest = SubscribeRequest | UnsubscribeRequest | CallRequest;
150
-
151
75
  export { DEFAULT_CLUSTER_HTTP_PATH, DEFAULT_CLUSTER_TICKET_PATH, connectClusterWorker, createSignedClusterTicket, fetchClusterTicket, parseClusterTicket, verifySignedClusterTicket };
152
- export type { CallRequest, ClusterTicket, ClusterWorkerConfig, ConnectedEvent, ErrorResponse, HttpRequest, JsonPatch, PatchEvent, PongEvent, ResyncEvent, SignedClusterTicketOptions, SnapshotResponse, SubscribeRequest, UnsubscribeRequest, WebSocketClientMessage, WebSocketEvent };
76
+ export type { ClusterTicket, ClusterWorkerConfig, SignedClusterTicketOptions };