@velajs/cloudflare 1.29.0 → 1.30.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.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js +8 -0
- package/dist/cloudflare-application.js +309 -0
- package/dist/cloudflare-application.js.map +1 -0
- package/dist/cloudflare-factory.js +109 -0
- package/dist/cloudflare-factory.js.map +1 -0
- package/dist/decorators/queue-consumer.js +39 -0
- package/dist/decorators/queue-consumer.js.map +1 -0
- package/dist/durable-objects.d.ts +3 -4
- package/dist/durable-objects.js +2 -524
- package/dist/environment.js +19 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +21 -1276
- package/dist/nonce/durable-object-nonce.store.js +33 -0
- package/dist/nonce/durable-object-nonce.store.js.map +1 -0
- package/dist/nonce/nonce-validation.js +18 -0
- package/dist/nonce/nonce-validation.js.map +1 -0
- package/dist/nonce/nonce.durable-object.js +54 -0
- package/dist/nonce/nonce.durable-object.js.map +1 -0
- package/dist/{nonce.durable-object-Df4CZi-0.d.ts → nonce.durable-object-Ci9AGssY.d.ts} +2 -3
- package/dist/queue/cloudflare-queues.js +215 -0
- package/dist/queue/cloudflare-queues.js.map +1 -0
- package/dist/queues.d.ts +1 -2
- package/dist/queues.js +1 -214
- package/dist/rate-limit/cloudflare-rate-limit.store.js +40 -0
- package/dist/rate-limit/cloudflare-rate-limit.store.js.map +1 -0
- package/dist/schedule-diagnostics.js +51 -0
- package/dist/schedule-diagnostics.js.map +1 -0
- package/dist/scheduled-event.js +68 -0
- package/dist/scheduled-event.js.map +1 -0
- package/dist/services/flagship-flag.driver.js +52 -0
- package/dist/services/flagship-flag.driver.js.map +1 -0
- package/dist/services/kv-cache.store.js +82 -0
- package/dist/services/kv-cache.store.js.map +1 -0
- package/dist/services/kv-flag.driver.js +62 -0
- package/dist/services/kv-flag.driver.js.map +1 -0
- package/dist/storage/r2-storage.driver.js +67 -0
- package/dist/storage/r2-storage.driver.js.map +1 -0
- package/dist/storage/storage-key-claim.js +74 -0
- package/dist/storage/storage-key-claim.js.map +1 -0
- package/dist/storage/storage-manager.service.js +38 -0
- package/dist/storage/storage-manager.service.js.map +1 -0
- package/dist/storage/storage.controller.js +64 -0
- package/dist/storage/storage.controller.js.map +1 -0
- package/dist/storage/storage.module.js +25 -0
- package/dist/storage/storage.module.js.map +1 -0
- package/dist/storage/storage.service.js +64 -0
- package/dist/storage/storage.service.js.map +1 -0
- package/dist/storage/storage.tokens.js +7 -0
- package/dist/storage/storage.tokens.js.map +1 -0
- package/dist/vela-env-DFvyoNT3.d.ts +1 -2
- package/dist/websocket/broadcast.js +32 -0
- package/dist/websocket/broadcast.js.map +1 -0
- package/dist/websocket/cf-room-registry.js +137 -0
- package/dist/websocket/cf-room-registry.js.map +1 -0
- package/dist/websocket/cf-ws-client.js +94 -0
- package/dist/websocket/cf-ws-client.js.map +1 -0
- package/dist/websocket/cloudflare-websocket.module.js +25 -0
- package/dist/websocket/cloudflare-websocket.module.js.map +1 -0
- package/dist/websocket/do-bootstrap.js +56 -0
- package/dist/websocket/do-bootstrap.js.map +1 -0
- package/dist/websocket/do-live.js +184 -0
- package/dist/websocket/do-live.js.map +1 -0
- package/dist/websocket/do-pitr.js +81 -0
- package/dist/websocket/do-pitr.js.map +1 -0
- package/dist/websocket/do-state.js +7 -0
- package/dist/websocket/do-state.js.map +1 -0
- package/dist/websocket/do-websocket-host.js +169 -0
- package/dist/websocket/do-websocket-host.js.map +1 -0
- package/dist/websocket/room-id.js +27 -0
- package/dist/websocket/room-id.js.map +1 -0
- package/dist/websocket/websocket-routing.js +133 -0
- package/dist/websocket/websocket-routing.js.map +1 -0
- package/dist/websocket/websocket.durable-object.js +138 -0
- package/dist/websocket/websocket.durable-object.js.map +1 -0
- package/dist/websocket/ws-attachment.js +79 -0
- package/dist/websocket/ws-attachment.js.map +1 -0
- package/dist/websocket/ws-server-holder.js +38 -0
- package/dist/websocket/ws-server-holder.js.map +1 -0
- package/package.json +9 -7
- package/dist/durable-objects.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/nonce-validation-Dy8z05A9.js +0 -522
- package/dist/nonce-validation-Dy8z05A9.js.map +0 -1
- package/dist/queues.js.map +0 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { durableObjectRoomName } from "./room-id.js";
|
|
2
|
+
import { getMetadata, getTrustedRequestIdentity } from "@velajs/vela/module-kit";
|
|
3
|
+
import { WS_GATEWAY_METADATA, createWebSocketUpgradeGate, resolveGatewayRoomId, resolveGatewayRoomParam, resolveMaxFrameBytes } from "@velajs/vela/websocket";
|
|
4
|
+
//#region src/websocket/websocket-routing.ts
|
|
5
|
+
const MAX_IDENTITY_FIELD_BYTES = 2048;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
function isIdentityField(value) {
|
|
8
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\r") && !value.includes("\n") && encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES;
|
|
9
|
+
}
|
|
10
|
+
/** `null` means an identity was present but violated the transport contract. */
|
|
11
|
+
function accessIdentity(c) {
|
|
12
|
+
const value = getTrustedRequestIdentity(c.req.raw);
|
|
13
|
+
if (!value) return void 0;
|
|
14
|
+
const { principal, tenantId, expiresAtMs } = value;
|
|
15
|
+
if (!isIdentityField(principal.issuer) || !isIdentityField(principal.subject) || !isIdentityField(tenantId) || typeof expiresAtMs !== "number" || !Number.isSafeInteger(expiresAtMs) || expiresAtMs <= 0) return null;
|
|
16
|
+
return {
|
|
17
|
+
principal,
|
|
18
|
+
tenantId,
|
|
19
|
+
expiresAtMs
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** `null` means two independently verified identities disagree. */
|
|
23
|
+
function combineIdentities(requestIdentity, upgradeIdentity) {
|
|
24
|
+
if (!requestIdentity && !upgradeIdentity) return void 0;
|
|
25
|
+
if (!requestIdentity) return upgradeIdentity;
|
|
26
|
+
if (!upgradeIdentity) return requestIdentity;
|
|
27
|
+
if (requestIdentity.principal.issuer !== upgradeIdentity.principal.issuer || requestIdentity.principal.subject !== upgradeIdentity.principal.subject || requestIdentity.principal.principalType !== upgradeIdentity.principal.principalType || requestIdentity.tenantId !== upgradeIdentity.tenantId) return null;
|
|
28
|
+
return {
|
|
29
|
+
principal: { ...upgradeIdentity.principal },
|
|
30
|
+
tenantId: upgradeIdentity.tenantId,
|
|
31
|
+
expiresAtMs: Math.min(requestIdentity.expiresAtMs, upgradeIdentity.expiresAtMs)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** An instance's constructor is the class token its module registered. */
|
|
35
|
+
function isClass(value) {
|
|
36
|
+
return typeof value === "function";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted
|
|
40
|
+
* gateways only). The application container names the module that declares the
|
|
41
|
+
* gateway when exactly one module registers it.
|
|
42
|
+
*/
|
|
43
|
+
function collectWsGatewayRoutes(instance, container) {
|
|
44
|
+
const options = getMetadata(WS_GATEWAY_METADATA, instance.constructor);
|
|
45
|
+
if (!options?.path || !options?.binding) return [];
|
|
46
|
+
resolveGatewayRoomParam(options);
|
|
47
|
+
resolveMaxFrameBytes(options);
|
|
48
|
+
const gateway = instance.constructor;
|
|
49
|
+
const owners = isClass(gateway) ? container.getOwnerModuleIds(gateway) : [];
|
|
50
|
+
return [{
|
|
51
|
+
path: options.path,
|
|
52
|
+
binding: options.binding,
|
|
53
|
+
options: { ...options },
|
|
54
|
+
...owners.length === 1 ? { moduleId: owners[0] } : {}
|
|
55
|
+
}];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Registers the upgrade routes on the Worker's Hono app. Each route validates
|
|
59
|
+
* the `Upgrade` header, authenticates through the gateway's authenticator
|
|
60
|
+
* (resolved once from `container`, the application's DI container), resolves
|
|
61
|
+
* the room's Durable Object, and forwards the raw request — injecting
|
|
62
|
+
* spoof-safe `x-vela-*` headers the DO reads. The DO returns the `101` with the
|
|
63
|
+
* client socket.
|
|
64
|
+
*/
|
|
65
|
+
function registerWebSocketRoutes(hono, routes, container) {
|
|
66
|
+
for (const route of routes) {
|
|
67
|
+
const authenticate = createWebSocketUpgradeGate(container, route);
|
|
68
|
+
hono.get(route.path, async (c) => {
|
|
69
|
+
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return c.text("Expected WebSocket upgrade", 426);
|
|
70
|
+
const headers = new Headers(c.req.raw.headers);
|
|
71
|
+
headers.delete("x-vela-room");
|
|
72
|
+
headers.delete("x-vela-path");
|
|
73
|
+
headers.delete("x-vela-user");
|
|
74
|
+
headers.delete("x-vela-expires-at");
|
|
75
|
+
headers.delete("x-vela-expires-at-ms");
|
|
76
|
+
headers.delete("x-vela-issuer");
|
|
77
|
+
headers.delete("x-vela-subject");
|
|
78
|
+
headers.delete("x-vela-principal-type");
|
|
79
|
+
headers.delete("x-vela-tenant");
|
|
80
|
+
const sanitizedRequest = new Request(c.req.raw, { headers });
|
|
81
|
+
let roomId;
|
|
82
|
+
try {
|
|
83
|
+
roomId = resolveGatewayRoomId(route.options, (name) => c.req.param(name));
|
|
84
|
+
} catch {
|
|
85
|
+
return c.text("Invalid WebSocket room", 400);
|
|
86
|
+
}
|
|
87
|
+
const upgrade = await authenticate(sanitizedRequest, roomId);
|
|
88
|
+
if (upgrade === false) return c.text("WebSocket upgrade forbidden", 403);
|
|
89
|
+
const requestIdentity = accessIdentity(c);
|
|
90
|
+
if (requestIdentity === null) return c.text("Invalid WebSocket identity", 403);
|
|
91
|
+
const identity = combineIdentities(requestIdentity, upgrade.identity);
|
|
92
|
+
if (identity === null) return c.text("Conflicting WebSocket identities", 403);
|
|
93
|
+
if (identity && identity.expiresAtMs <= Date.now()) return c.text("WebSocket identity expired", 403);
|
|
94
|
+
const forwardHeaders = new Headers(upgrade.request.headers);
|
|
95
|
+
forwardHeaders.set("x-vela-room", roomId);
|
|
96
|
+
forwardHeaders.set("x-vela-path", route.path);
|
|
97
|
+
if (identity) {
|
|
98
|
+
forwardHeaders.set("x-vela-user", identity.principal.subject);
|
|
99
|
+
forwardHeaders.set("x-vela-issuer", identity.principal.issuer);
|
|
100
|
+
forwardHeaders.set("x-vela-subject", identity.principal.subject);
|
|
101
|
+
forwardHeaders.set("x-vela-principal-type", identity.principal.principalType);
|
|
102
|
+
forwardHeaders.set("x-vela-tenant", identity.tenantId);
|
|
103
|
+
forwardHeaders.set("x-vela-expires-at-ms", String(identity.expiresAtMs));
|
|
104
|
+
}
|
|
105
|
+
return forwardToRoom(c.env, route.binding, route.path, roomId, new Request(upgrade.request, { headers: forwardHeaders }));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Gateway metadata contains a runtime binding name, so the native type is
|
|
111
|
+
* erased. Validate only the operations consumed here and their observable
|
|
112
|
+
* results; never assert that an arbitrary value implements a native namespace.
|
|
113
|
+
*/
|
|
114
|
+
async function forwardToRoom(env, binding, path, room, request) {
|
|
115
|
+
if (typeof env !== "object" || env === null) throw new Error("Worker environment is missing");
|
|
116
|
+
const namespace = Reflect.get(env, binding);
|
|
117
|
+
if (typeof namespace !== "object" || namespace === null) return new Response(`Durable Object binding '${binding}' is not configured`, { status: 500 });
|
|
118
|
+
const idFromName = Reflect.get(namespace, "idFromName");
|
|
119
|
+
const get = Reflect.get(namespace, "get");
|
|
120
|
+
if (typeof idFromName !== "function" || typeof get !== "function") throw new Error("Invalid Durable Object namespace");
|
|
121
|
+
const id = Reflect.apply(idFromName, namespace, [durableObjectRoomName(path, room)]);
|
|
122
|
+
const stub = Reflect.apply(get, namespace, [id]);
|
|
123
|
+
if (typeof stub !== "object" || stub === null) throw new Error("Invalid Durable Object stub");
|
|
124
|
+
const fetch = Reflect.get(stub, "fetch");
|
|
125
|
+
if (typeof fetch !== "function") throw new Error("Durable Object stub has no fetch operation");
|
|
126
|
+
const response = await Reflect.apply(fetch, stub, [request]);
|
|
127
|
+
if (!(response instanceof Response)) throw new Error("Durable Object returned an invalid response");
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { collectWsGatewayRoutes, registerWebSocketRoutes };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=websocket-routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-routing.js","names":[],"sources":["../../src/websocket/websocket-routing.ts"],"sourcesContent":["import type { Type, VelaContext as Context, VelaHono as Hono } from '@velajs/vela';\nimport type { Container } from '@velajs/vela/module-kit';\nimport { getMetadata, getTrustedRequestIdentity } from '@velajs/vela/module-kit';\nimport {\n createWebSocketUpgradeGate,\n resolveGatewayRoomId,\n resolveGatewayRoomParam,\n resolveMaxFrameBytes,\n WS_GATEWAY_METADATA,\n type WebSocketGatewayOptions,\n type WebSocketUpgradeIdentity,\n} from '@velajs/vela/websocket';\nimport { durableObjectRoomName } from './room-id';\n\nexport interface WsGatewayRoute {\n path: string;\n binding: string;\n options: WebSocketGatewayOptions;\n /** Container module that declares the gateway; its authenticator resolves from here. */\n moduleId?: string;\n}\n\nconst MAX_IDENTITY_FIELD_BYTES = 2048;\nconst encoder = new TextEncoder();\n\ntype PrincipalType = 'user' | 'service';\n\ninterface ForwardedIdentity extends WebSocketUpgradeIdentity {\n principal: {\n issuer: string;\n subject: string;\n principalType: PrincipalType;\n };\n tenantId: string;\n expiresAtMs: number;\n}\n\nfunction isIdentityField(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n !value.includes('\\r') &&\n !value.includes('\\n') &&\n encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES\n );\n}\n\n/** `null` means an identity was present but violated the transport contract. */\nfunction accessIdentity(c: Context): ForwardedIdentity | null | undefined {\n const value = getTrustedRequestIdentity(c.req.raw);\n if (!value) return undefined;\n const { principal, tenantId, expiresAtMs } = value;\n if (\n !isIdentityField(principal.issuer) ||\n !isIdentityField(principal.subject) ||\n !isIdentityField(tenantId) ||\n typeof expiresAtMs !== 'number' ||\n !Number.isSafeInteger(expiresAtMs) ||\n expiresAtMs <= 0\n )\n return null;\n return { principal, tenantId, expiresAtMs };\n}\n\n/** `null` means two independently verified identities disagree. */\nfunction combineIdentities(\n requestIdentity: ForwardedIdentity | undefined,\n upgradeIdentity: WebSocketUpgradeIdentity | undefined,\n): ForwardedIdentity | null | undefined {\n if (!requestIdentity && !upgradeIdentity) return undefined;\n if (!requestIdentity) return upgradeIdentity;\n if (!upgradeIdentity) return requestIdentity;\n if (\n requestIdentity.principal.issuer !== upgradeIdentity.principal.issuer ||\n requestIdentity.principal.subject !== upgradeIdentity.principal.subject ||\n requestIdentity.principal.principalType !== upgradeIdentity.principal.principalType ||\n requestIdentity.tenantId !== upgradeIdentity.tenantId\n ) {\n return null;\n }\n return {\n principal: { ...upgradeIdentity.principal },\n tenantId: upgradeIdentity.tenantId,\n expiresAtMs: Math.min(requestIdentity.expiresAtMs, upgradeIdentity.expiresAtMs),\n };\n}\n\n/** An instance's constructor is the class token its module registered. */\nfunction isClass(value: unknown): value is Type {\n return typeof value === 'function';\n}\n\n/**\n * Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted\n * gateways only). The application container names the module that declares the\n * gateway when exactly one module registers it.\n */\nexport function collectWsGatewayRoutes(instance: object, container: Container): WsGatewayRoute[] {\n // Decorator metadata is the framework's explicit reflection boundary.\n const options = getMetadata<WebSocketGatewayOptions>(WS_GATEWAY_METADATA, instance.constructor);\n if (!options?.path || !options?.binding) return [];\n resolveGatewayRoomParam(options);\n resolveMaxFrameBytes(options);\n const gateway: unknown = instance.constructor;\n const owners = isClass(gateway) ? container.getOwnerModuleIds(gateway) : [];\n return [\n {\n path: options.path,\n binding: options.binding,\n options: { ...options },\n ...(owners.length === 1 ? { moduleId: owners[0] } : {}),\n },\n ];\n}\n\n/**\n * Registers the upgrade routes on the Worker's Hono app. Each route validates\n * the `Upgrade` header, authenticates through the gateway's authenticator\n * (resolved once from `container`, the application's DI container), resolves\n * the room's Durable Object, and forwards the raw request — injecting\n * spoof-safe `x-vela-*` headers the DO reads. The DO returns the `101` with the\n * client socket.\n */\nexport function registerWebSocketRoutes(\n hono: Hono,\n routes: WsGatewayRoute[],\n container: Container,\n): void {\n for (const route of routes) {\n const authenticate = createWebSocketUpgradeGate(container, route);\n hono.get(route.path, async (c: Context) => {\n if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {\n return c.text('Expected WebSocket upgrade', 426);\n }\n\n // Internal transport headers are never application credentials. Remove\n // client-supplied values before even the pre-allocation authorization\n // hook sees the request, then populate trusted values below.\n const headers = new Headers(c.req.raw.headers);\n headers.delete('x-vela-room');\n headers.delete('x-vela-path');\n headers.delete('x-vela-user');\n headers.delete('x-vela-expires-at');\n headers.delete('x-vela-expires-at-ms');\n headers.delete('x-vela-issuer');\n headers.delete('x-vela-subject');\n headers.delete('x-vela-principal-type');\n headers.delete('x-vela-tenant');\n const sanitizedRequest = new Request(c.req.raw, { headers });\n\n let roomId: string;\n try {\n roomId = resolveGatewayRoomId(route.options, (name) => c.req.param(name));\n } catch {\n return c.text('Invalid WebSocket room', 400);\n }\n\n // Origin, application authorization, and ticket/cookie authentication\n // all complete before the gateway Durable Object id is resolved.\n const upgrade = await authenticate(sanitizedRequest, roomId);\n if (upgrade === false) return c.text('WebSocket upgrade forbidden', 403);\n\n const requestIdentity = accessIdentity(c);\n if (requestIdentity === null) return c.text('Invalid WebSocket identity', 403);\n const identity = combineIdentities(requestIdentity, upgrade.identity);\n if (identity === null) return c.text('Conflicting WebSocket identities', 403);\n if (identity && identity.expiresAtMs <= Date.now()) {\n return c.text('WebSocket identity expired', 403);\n }\n\n // Populate the ticket-free forwarding request with trusted server values.\n const forwardHeaders = new Headers(upgrade.request.headers);\n forwardHeaders.set('x-vela-room', roomId);\n forwardHeaders.set('x-vela-path', route.path);\n if (identity) {\n forwardHeaders.set('x-vela-user', identity.principal.subject);\n forwardHeaders.set('x-vela-issuer', identity.principal.issuer);\n forwardHeaders.set('x-vela-subject', identity.principal.subject);\n forwardHeaders.set('x-vela-principal-type', identity.principal.principalType);\n forwardHeaders.set('x-vela-tenant', identity.tenantId);\n forwardHeaders.set('x-vela-expires-at-ms', String(identity.expiresAtMs));\n }\n\n return forwardToRoom(\n c.env,\n route.binding,\n route.path,\n roomId,\n new Request(upgrade.request, { headers: forwardHeaders }),\n );\n });\n }\n}\n\n/**\n * Gateway metadata contains a runtime binding name, so the native type is\n * erased. Validate only the operations consumed here and their observable\n * results; never assert that an arbitrary value implements a native namespace.\n */\nasync function forwardToRoom(\n env: unknown,\n binding: string,\n path: string,\n room: string,\n request: Request,\n): Promise<Response> {\n if (typeof env !== 'object' || env === null) throw new Error('Worker environment is missing');\n const namespace: unknown = Reflect.get(env, binding);\n if (typeof namespace !== 'object' || namespace === null) {\n return new Response(`Durable Object binding '${binding}' is not configured`, { status: 500 });\n }\n const idFromName: unknown = Reflect.get(namespace, 'idFromName');\n const get: unknown = Reflect.get(namespace, 'get');\n if (typeof idFromName !== 'function' || typeof get !== 'function') {\n throw new Error('Invalid Durable Object namespace');\n }\n const id: unknown = Reflect.apply(idFromName, namespace, [durableObjectRoomName(path, room)]);\n const stub: unknown = Reflect.apply(get, namespace, [id]);\n if (typeof stub !== 'object' || stub === null) throw new Error('Invalid Durable Object stub');\n const fetch: unknown = Reflect.get(stub, 'fetch');\n if (typeof fetch !== 'function') throw new Error('Durable Object stub has no fetch operation');\n const response: unknown = await Reflect.apply(fetch, stub, [request]);\n if (!(response instanceof Response))\n throw new Error('Durable Object returned an invalid response');\n return response;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { armDoPitr, readDoPitrBookmark } from "./do-pitr.js";
|
|
2
|
+
import { DoWebSocketHost } from "./do-websocket-host.js";
|
|
3
|
+
import { buildDoRuntime } from "./do-bootstrap.js";
|
|
4
|
+
import { DurableObject } from "cloudflare:workers";
|
|
5
|
+
//#region src/websocket/websocket.durable-object.ts
|
|
6
|
+
const PING = "{\"event\":\"$ping\"}";
|
|
7
|
+
const PONG = "{\"event\":\"$pong\"}";
|
|
8
|
+
const MAX_IDENTITY_FIELD_BYTES = 2048;
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
function isIdentityField(value) {
|
|
11
|
+
return value !== null && value.length > 0 && encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Base class for the WebSocket Durable Object. The user exports a named subclass
|
|
15
|
+
* (matching their `wrangler.toml` `class_name`) built from their `AppModule`, or
|
|
16
|
+
* from a `DynamicModule` declared at module scope:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
|
|
23
|
+
* bridge DO hibernation) and forwards every event into the runtime-agnostic
|
|
24
|
+
* `WsDispatcher` via {@link DoWebSocketHost}. The DO's `env` is the
|
|
25
|
+
* application's ENV, as in the Worker.
|
|
26
|
+
*/
|
|
27
|
+
function VelaWebSocketDurableObject(rootModule) {
|
|
28
|
+
return class VelaWsDurableObject extends DurableObject {
|
|
29
|
+
host;
|
|
30
|
+
liveEngine;
|
|
31
|
+
ready;
|
|
32
|
+
constructor(ctx, env) {
|
|
33
|
+
super(ctx, env);
|
|
34
|
+
try {
|
|
35
|
+
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));
|
|
36
|
+
} catch {}
|
|
37
|
+
this.ready = ctx.blockConcurrencyWhile(async () => {
|
|
38
|
+
const runtime = await buildDoRuntime(rootModule, ctx, { env });
|
|
39
|
+
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
|
|
40
|
+
this.liveEngine = runtime.live;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Intra-worker RPC only; HTTP fetch never exposes this admin snapshot. */
|
|
44
|
+
async inspectLive() {
|
|
45
|
+
await this.ready;
|
|
46
|
+
return this.liveEngine?.inspect() ?? {
|
|
47
|
+
subscriptions: [],
|
|
48
|
+
rooms: []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async fetch(request) {
|
|
52
|
+
await this.ready;
|
|
53
|
+
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
54
|
+
const url = new URL(request.url);
|
|
55
|
+
const roomId = request.headers.get("x-vela-room") ?? this.ctx.id.name ?? url.pathname;
|
|
56
|
+
const path = request.headers.get("x-vela-path") ?? this.host.defaultPath() ?? url.pathname;
|
|
57
|
+
const userId = request.headers.get("x-vela-user") || void 0;
|
|
58
|
+
const rawExpiresAtMs = request.headers.get("x-vela-expires-at-ms");
|
|
59
|
+
const parsedExpiresAtMs = rawExpiresAtMs === null ? void 0 : Number(rawExpiresAtMs);
|
|
60
|
+
if (parsedExpiresAtMs !== void 0 && (!Number.isSafeInteger(parsedExpiresAtMs) || parsedExpiresAtMs <= 0)) return new Response("Invalid WebSocket identity expiry", { status: 403 });
|
|
61
|
+
const issuer = request.headers.get("x-vela-issuer");
|
|
62
|
+
const subject = request.headers.get("x-vela-subject");
|
|
63
|
+
const principalType = request.headers.get("x-vela-principal-type");
|
|
64
|
+
const tenantId = request.headers.get("x-vela-tenant");
|
|
65
|
+
const hasPrincipalHeader = issuer !== null || subject !== null || principalType !== null || tenantId !== null;
|
|
66
|
+
let principal;
|
|
67
|
+
if (hasPrincipalHeader) {
|
|
68
|
+
if (!isIdentityField(issuer) || !isIdentityField(subject) || principalType !== "user" && principalType !== "service" || !isIdentityField(tenantId) || parsedExpiresAtMs === void 0 || userId !== void 0 && userId !== subject) return new Response("Invalid WebSocket principal", { status: 403 });
|
|
69
|
+
principal = {
|
|
70
|
+
issuer,
|
|
71
|
+
subject,
|
|
72
|
+
principalType,
|
|
73
|
+
tenantId
|
|
74
|
+
};
|
|
75
|
+
} else if (parsedExpiresAtMs !== void 0) return new Response("WebSocket identity tuple is missing", { status: 403 });
|
|
76
|
+
if (parsedExpiresAtMs !== void 0 && parsedExpiresAtMs <= Date.now()) return new Response("WebSocket identity expired", { status: 403 });
|
|
77
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
78
|
+
if (!await this.host.accept(server, path, roomId, userId, parsedExpiresAtMs, principal)) return new Response("WebSocket connection rejected", { status: 403 });
|
|
79
|
+
return new Response(null, {
|
|
80
|
+
status: 101,
|
|
81
|
+
webSocket: client
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async webSocketMessage(ws, message) {
|
|
85
|
+
await this.ready;
|
|
86
|
+
await this.host.onMessage(ws, message);
|
|
87
|
+
}
|
|
88
|
+
async webSocketClose(ws, code, reason) {
|
|
89
|
+
await this.ready;
|
|
90
|
+
await this.host.onClose(ws, code, reason);
|
|
91
|
+
}
|
|
92
|
+
async webSocketError(ws, error) {
|
|
93
|
+
await this.ready;
|
|
94
|
+
await this.host.onError(ws, error);
|
|
95
|
+
}
|
|
96
|
+
/** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */
|
|
97
|
+
async broadcast(cmd) {
|
|
98
|
+
await this.ready;
|
|
99
|
+
await this.host.broadcast(cmd);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /
|
|
103
|
+
* liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns
|
|
104
|
+
* the commit stamp (what `Vela-Commit-Cursor` carries); the subscription
|
|
105
|
+
* refreshes fan out asynchronously.
|
|
106
|
+
*/
|
|
107
|
+
async invalidate(cmd) {
|
|
108
|
+
await this.ready;
|
|
109
|
+
return this.liveEngine?.applyInvalidation(cmd);
|
|
110
|
+
}
|
|
111
|
+
/** DO RPC — the current PITR bookmark (typed-unavailable on a non-SQLite DO). */
|
|
112
|
+
async pitrCurrentBookmark() {
|
|
113
|
+
await this.ready;
|
|
114
|
+
return readDoPitrBookmark(this.ctx.storage);
|
|
115
|
+
}
|
|
116
|
+
/** DO RPC — the PITR bookmark closest to `time` (+ the current bookmark). */
|
|
117
|
+
async pitrBookmarkForTime(time) {
|
|
118
|
+
await this.ready;
|
|
119
|
+
return readDoPitrBookmark(this.ctx.storage, time);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* DO RPC — arm a PITR restore and return the undo bookmark. When `restart` is
|
|
123
|
+
* requested, `ctx.abort()` is called AFTER the undo bookmark is computed so
|
|
124
|
+
* the recovery applies on the immediately-following session; otherwise the
|
|
125
|
+
* restore applies on the next natural restart (`restarted: false`).
|
|
126
|
+
*/
|
|
127
|
+
async pitrArmRestore(opts) {
|
|
128
|
+
await this.ready;
|
|
129
|
+
const result = await armDoPitr(this.ctx.storage, opts);
|
|
130
|
+
if (opts.restart === true) this.ctx.abort("vela PITR restore");
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { VelaWebSocketDurableObject };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=websocket.durable-object.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.durable-object.js","names":[],"sources":["../../src/websocket/websocket.durable-object.ts"],"sourcesContent":["import { DurableObject } from 'cloudflare:workers';\nimport type { VelaEnv } from '@velajs/vela';\nimport type { BroadcastCommand } from '@velajs/vela/websocket';\nimport type {\n CommitStamp,\n InvalidationCommand,\n LiveEngine,\n LiveInspection,\n} from '@velajs/vela/live';\nimport { buildDoRuntime } from './do-bootstrap';\nimport { DoWebSocketHost, type WsConnectionPrincipal } from './do-websocket-host';\nimport { armDoPitr, readDoPitrBookmark } from './do-pitr';\nimport type { CloudflareRoot } from '../root-module';\nimport type {\n DoPitrArmOptions,\n DoPitrArmResult,\n DoPitrBookmarkRead,\n VelaDoPitrRpc,\n} from './do-pitr';\n\nconst PING = '{\"event\":\"$ping\"}';\nconst PONG = '{\"event\":\"$pong\"}';\nconst MAX_IDENTITY_FIELD_BYTES = 2048;\nconst encoder = new TextEncoder();\n\nfunction isIdentityField(value: string | null): value is string {\n return (\n value !== null &&\n value.length > 0 &&\n encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES\n );\n}\n\n/**\n * Base class for the WebSocket Durable Object. The user exports a named subclass\n * (matching their `wrangler.toml` `class_name`) built from their `AppModule`, or\n * from a `DynamicModule` declared at module scope:\n *\n * ```ts\n * export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}\n * ```\n *\n * It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot\n * bridge DO hibernation) and forwards every event into the runtime-agnostic\n * `WsDispatcher` via {@link DoWebSocketHost}. The DO's `env` is the\n * application's ENV, as in the Worker.\n */\nexport function VelaWebSocketDurableObject(rootModule: CloudflareRoot): new (\n ctx: DurableObjectState,\n env: VelaEnv,\n) => DurableObject<VelaEnv> &\n VelaDoPitrRpc & {\n broadcast(cmd: BroadcastCommand): Promise<void>;\n invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined>;\n inspectLive(): Promise<LiveInspection>;\n } {\n return class VelaWsDurableObject extends DurableObject<VelaEnv> {\n private host!: DoWebSocketHost;\n private liveEngine?: LiveEngine;\n private readonly ready: Promise<void>;\n\n constructor(ctx: DurableObjectState, env: VelaEnv) {\n super(ctx, env);\n // Application-level ping/pong answered WITHOUT waking a hibernated DO.\n try {\n ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));\n } catch {\n // Older runtimes without auto-response — fine, protocol pings still work.\n }\n this.ready = ctx.blockConcurrencyWhile(async () => {\n // The root is static, so constructing another instance declares no new\n // classes in the isolate-global metadata registry.\n const runtime = await buildDoRuntime(rootModule, ctx, { env });\n this.host = new DoWebSocketHost(\n ctx,\n runtime.dispatcher,\n runtime.registry,\n runtime.gatewayPaths,\n );\n this.liveEngine = runtime.live;\n });\n }\n\n /** Intra-worker RPC only; HTTP fetch never exposes this admin snapshot. */\n async inspectLive(): Promise<LiveInspection> {\n await this.ready;\n return this.liveEngine?.inspect() ?? { subscriptions: [], rooms: [] };\n }\n\n override async fetch(request: Request): Promise<Response> {\n await this.ready;\n if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {\n return new Response('Expected WebSocket upgrade', { status: 426 });\n }\n\n const url = new URL(request.url);\n // Headers are primary (carry auth + multi-gateway routing); fall back to the\n // DO's own name (set via idFromName(room)) and the single gateway path so a\n // plain `stub.fetch(request)` forward still works.\n const roomId = request.headers.get('x-vela-room') ?? this.ctx.id.name ?? url.pathname;\n const path = request.headers.get('x-vela-path') ?? this.host.defaultPath() ?? url.pathname;\n const userId = request.headers.get('x-vela-user') || undefined;\n const rawExpiresAtMs = request.headers.get('x-vela-expires-at-ms');\n const parsedExpiresAtMs = rawExpiresAtMs === null ? undefined : Number(rawExpiresAtMs);\n if (\n parsedExpiresAtMs !== undefined &&\n (!Number.isSafeInteger(parsedExpiresAtMs) || parsedExpiresAtMs <= 0)\n ) {\n return new Response('Invalid WebSocket identity expiry', { status: 403 });\n }\n\n const issuer = request.headers.get('x-vela-issuer');\n const subject = request.headers.get('x-vela-subject');\n const principalType = request.headers.get('x-vela-principal-type');\n const tenantId = request.headers.get('x-vela-tenant');\n const hasPrincipalHeader =\n issuer !== null || subject !== null || principalType !== null || tenantId !== null;\n let principal: WsConnectionPrincipal | undefined;\n if (hasPrincipalHeader) {\n if (\n !isIdentityField(issuer) ||\n !isIdentityField(subject) ||\n (principalType !== 'user' && principalType !== 'service') ||\n !isIdentityField(tenantId) ||\n parsedExpiresAtMs === undefined ||\n (userId !== undefined && userId !== subject)\n ) {\n return new Response('Invalid WebSocket principal', { status: 403 });\n }\n principal = { issuer, subject, principalType, tenantId };\n } else if (parsedExpiresAtMs !== undefined) {\n return new Response('WebSocket identity tuple is missing', { status: 403 });\n }\n\n if (parsedExpiresAtMs !== undefined && parsedExpiresAtMs <= Date.now()) {\n return new Response('WebSocket identity expired', { status: 403 });\n }\n\n const { 0: client, 1: server } = new WebSocketPair();\n const accepted = await this.host.accept(\n server,\n path,\n roomId,\n userId,\n parsedExpiresAtMs,\n principal,\n );\n if (!accepted) return new Response('WebSocket connection rejected', { status: 403 });\n return new Response(null, { status: 101, webSocket: client });\n }\n\n override async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {\n await this.ready;\n await this.host.onMessage(ws, message);\n }\n\n override async webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void> {\n await this.ready;\n await this.host.onClose(ws, code, reason);\n }\n\n override async webSocketError(ws: WebSocket, error: unknown): Promise<void> {\n await this.ready;\n await this.host.onError(ws, error);\n }\n\n /** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */\n async broadcast(cmd: BroadcastCommand): Promise<void> {\n await this.ready;\n await this.host.broadcast(cmd);\n }\n\n /**\n * DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /\n * liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns\n * the commit stamp (what `Vela-Commit-Cursor` carries); the subscription\n * refreshes fan out asynchronously.\n */\n async invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined> {\n await this.ready;\n return this.liveEngine?.applyInvalidation(cmd);\n }\n\n // -- Durable Object PITR (point-in-time recovery) RPC ---------------------\n //\n // GUARD / reachability: these are admin-privileged operations (a restore can\n // roll the DO's SQLite state back up to 30 days and, with `restart`, abort\n // the DO to apply it now). They are safe to expose here because a Durable\n // Object's RPC methods are NOT network-reachable: a `DurableObjectStub` is\n // obtainable ONLY from a Worker that binds this DO namespace, and RPC calls\n // travel Cloudflare's internal capability channel — never the public\n // internet. A WebSocket/HTTP client reaches only `fetch()` above, never these\n // methods. The security boundary is therefore the WORKER-SIDE studio admin\n // gate that fronts the `CloudflareDoTimeTravelPort` (master admin token + the\n // 428 confirm challenge) — the identical trust model as `broadcast()` /\n // `invalidate()` on this same DO. See `do-pitr.ts` for the storage wrappers.\n\n /** DO RPC — the current PITR bookmark (typed-unavailable on a non-SQLite DO). */\n async pitrCurrentBookmark(): Promise<DoPitrBookmarkRead> {\n await this.ready;\n return readDoPitrBookmark(this.ctx.storage);\n }\n\n /** DO RPC — the PITR bookmark closest to `time` (+ the current bookmark). */\n async pitrBookmarkForTime(time: number | string): Promise<DoPitrBookmarkRead> {\n await this.ready;\n return readDoPitrBookmark(this.ctx.storage, time);\n }\n\n /**\n * DO RPC — arm a PITR restore and return the undo bookmark. When `restart` is\n * requested, `ctx.abort()` is called AFTER the undo bookmark is computed so\n * the recovery applies on the immediately-following session; otherwise the\n * restore applies on the next natural restart (`restarted: false`).\n */\n async pitrArmRestore(opts: DoPitrArmOptions): Promise<DoPitrArmResult> {\n await this.ready;\n const result = await armDoPitr(this.ctx.storage, opts);\n if (opts.restart === true) this.ctx.abort('vela PITR restore');\n return result;\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DEFAULT_WS_MAX_JOINED_ROOMS, assertWebSocketRoomId } from "@velajs/vela/websocket";
|
|
2
|
+
//#region src/websocket/ws-attachment.ts
|
|
3
|
+
const record = (value) => {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
5
|
+
const prototype = Object.getPrototypeOf(value);
|
|
6
|
+
return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).every((key) => {
|
|
7
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
8
|
+
return property !== void 0 && "value" in property;
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
const positive = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
12
|
+
const text = (value) => typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).byteLength <= 2048;
|
|
13
|
+
/** Versionless 1.x records remain readable; malformed/future versions fail closed. */
|
|
14
|
+
function readWsAttachment(value) {
|
|
15
|
+
if (!record(value) || value.version !== void 0 && value.version !== 1) return void 0;
|
|
16
|
+
if (!text(value.connId) || typeof value.path !== "string" || value.path.length > 8192 || value.state !== "pending" && value.state !== "active" && value.state !== "rejected" || !Array.isArray(value.rooms) || value.rooms.length > DEFAULT_WS_MAX_JOINED_ROOMS || !record(value.data)) return void 0;
|
|
17
|
+
const rooms = [];
|
|
18
|
+
for (let index = 0; index < value.rooms.length; index++) {
|
|
19
|
+
const descriptor = Object.getOwnPropertyDescriptor(value.rooms, index);
|
|
20
|
+
if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string") return void 0;
|
|
21
|
+
const room = descriptor.value;
|
|
22
|
+
try {
|
|
23
|
+
assertWebSocketRoomId(room);
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (rooms.includes(room)) return void 0;
|
|
28
|
+
rooms.push(room);
|
|
29
|
+
}
|
|
30
|
+
if (value.maxFrameBytes !== void 0 && !positive(value.maxFrameBytes)) return void 0;
|
|
31
|
+
if (value.expiresAtMs !== void 0 && !positive(value.expiresAtMs)) return void 0;
|
|
32
|
+
if (value.userId !== void 0 && !text(value.userId)) return void 0;
|
|
33
|
+
if (value.tenantId !== void 0 && !text(value.tenantId)) return void 0;
|
|
34
|
+
let principal;
|
|
35
|
+
if (value.principal !== void 0) {
|
|
36
|
+
const p = value.principal;
|
|
37
|
+
if (!record(p) || !text(p.issuer) || !text(p.subject) || p.principalType !== "user" && p.principalType !== "service") return void 0;
|
|
38
|
+
principal = {
|
|
39
|
+
issuer: p.issuer,
|
|
40
|
+
subject: p.subject,
|
|
41
|
+
principalType: p.principalType
|
|
42
|
+
};
|
|
43
|
+
if (value.userId !== void 0 && value.userId !== p.subject) return void 0;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
version: 1,
|
|
47
|
+
connId: value.connId,
|
|
48
|
+
state: value.state,
|
|
49
|
+
path: value.path,
|
|
50
|
+
rooms,
|
|
51
|
+
data: { ...value.data },
|
|
52
|
+
...principal ? { principal } : {},
|
|
53
|
+
...typeof value.userId === "string" ? { userId: value.userId } : {},
|
|
54
|
+
...typeof value.tenantId === "string" ? { tenantId: value.tenantId } : {},
|
|
55
|
+
...typeof value.expiresAtMs === "number" ? { expiresAtMs: value.expiresAtMs } : {},
|
|
56
|
+
...typeof value.maxFrameBytes === "number" ? { maxFrameBytes: value.maxFrameBytes } : {}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function socketAttachment(ws) {
|
|
60
|
+
try {
|
|
61
|
+
return readWsAttachment(ws.deserializeAttachment());
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function rejectedAttachment() {
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
connId: "",
|
|
70
|
+
state: "rejected",
|
|
71
|
+
path: "",
|
|
72
|
+
rooms: [],
|
|
73
|
+
data: {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { readWsAttachment, rejectedAttachment, socketAttachment };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=ws-attachment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-attachment.js","names":[],"sources":["../../src/websocket/ws-attachment.ts"],"sourcesContent":["import { assertWebSocketRoomId, DEFAULT_WS_MAX_JOINED_ROOMS } from '@velajs/vela/websocket';\nimport type { WsAttachment, WsLike } from './do-state';\n\nconst record = (value: unknown): value is Record<string, unknown> => {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return (\n (prototype === Object.prototype || prototype === null) &&\n Reflect.ownKeys(value).every((key) => {\n const property = Object.getOwnPropertyDescriptor(value, key);\n return property !== undefined && 'value' in property;\n })\n );\n};\nconst positive = (value: unknown): value is number =>\n typeof value === 'number' && Number.isSafeInteger(value) && value > 0;\nconst text = (value: unknown): value is string =>\n typeof value === 'string' &&\n value.length > 0 &&\n new TextEncoder().encode(value).byteLength <= 2048;\n\n/** Versionless 1.x records remain readable; malformed/future versions fail closed. */\nexport function readWsAttachment(value: unknown): WsAttachment | undefined {\n if (!record(value) || (value.version !== undefined && value.version !== 1)) return undefined;\n if (\n !text(value.connId) ||\n typeof value.path !== 'string' ||\n value.path.length > 8192 ||\n (value.state !== 'pending' && value.state !== 'active' && value.state !== 'rejected') ||\n !Array.isArray(value.rooms) ||\n value.rooms.length > DEFAULT_WS_MAX_JOINED_ROOMS ||\n !record(value.data)\n )\n return undefined;\n const rooms: string[] = [];\n for (let index = 0; index < value.rooms.length; index++) {\n const descriptor = Object.getOwnPropertyDescriptor(value.rooms, index);\n if (!descriptor || !('value' in descriptor) || typeof descriptor.value !== 'string')\n return undefined;\n const room: string = descriptor.value;\n try {\n assertWebSocketRoomId(room);\n } catch {\n return undefined;\n }\n if (rooms.includes(room)) return undefined;\n rooms.push(room);\n }\n if (value.maxFrameBytes !== undefined && !positive(value.maxFrameBytes)) return undefined;\n if (value.expiresAtMs !== undefined && !positive(value.expiresAtMs)) return undefined;\n if (value.userId !== undefined && !text(value.userId)) return undefined;\n if (value.tenantId !== undefined && !text(value.tenantId)) return undefined;\n let principal: WsAttachment['principal'];\n if (value.principal !== undefined) {\n const p = value.principal;\n if (\n !record(p) ||\n !text(p.issuer) ||\n !text(p.subject) ||\n (p.principalType !== 'user' && p.principalType !== 'service')\n )\n return undefined;\n principal = { issuer: p.issuer, subject: p.subject, principalType: p.principalType };\n if (value.userId !== undefined && value.userId !== p.subject) return undefined;\n }\n return {\n version: 1,\n connId: value.connId,\n state: value.state,\n path: value.path,\n rooms,\n data: { ...value.data },\n ...(principal ? { principal } : {}),\n ...(typeof value.userId === 'string' ? { userId: value.userId } : {}),\n ...(typeof value.tenantId === 'string' ? { tenantId: value.tenantId } : {}),\n ...(typeof value.expiresAtMs === 'number' ? { expiresAtMs: value.expiresAtMs } : {}),\n ...(typeof value.maxFrameBytes === 'number' ? { maxFrameBytes: value.maxFrameBytes } : {}),\n };\n}\n\nexport function socketAttachment(ws: WsLike): WsAttachment | undefined {\n try {\n return readWsAttachment(ws.deserializeAttachment());\n } catch {\n return undefined;\n }\n}\n\nexport function rejectedAttachment(): WsAttachment {\n return { version: 1, connId: '', state: 'rejected', path: '', rooms: [], data: {} };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import __decorate from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
2
|
+
import { Injectable } from "@velajs/vela";
|
|
3
|
+
import { resolveMaxFrameBytes } from "@velajs/vela/websocket";
|
|
4
|
+
//#region src/websocket/ws-server-holder.ts
|
|
5
|
+
let WsServerHolder = class WsServerHolder {
|
|
6
|
+
target;
|
|
7
|
+
maxFrameBytes;
|
|
8
|
+
setTarget(server) {
|
|
9
|
+
this.target = server;
|
|
10
|
+
if (this.maxFrameBytes !== void 0) server.setOutboundFrameLimit?.(this.maxFrameBytes);
|
|
11
|
+
}
|
|
12
|
+
setOutboundFrameLimit(maxFrameBytes) {
|
|
13
|
+
const resolved = resolveMaxFrameBytes({ maxFrameBytes });
|
|
14
|
+
this.maxFrameBytes = this.maxFrameBytes === void 0 ? resolved : Math.max(this.maxFrameBytes, resolved);
|
|
15
|
+
this.target?.setOutboundFrameLimit?.(this.maxFrameBytes);
|
|
16
|
+
}
|
|
17
|
+
get resolved() {
|
|
18
|
+
if (!this.target) throw new Error("WebSocket server is only available inside a WebSocket Durable Object. To push from a Worker HTTP handler, use broadcastToRoom(namespace, gatewayPath, room, ...).");
|
|
19
|
+
return this.target;
|
|
20
|
+
}
|
|
21
|
+
emit(event, data) {
|
|
22
|
+
return this.resolved.emit(event, data);
|
|
23
|
+
}
|
|
24
|
+
to(room) {
|
|
25
|
+
return this.resolved.to(room);
|
|
26
|
+
}
|
|
27
|
+
in(room) {
|
|
28
|
+
return this.resolved.in(room);
|
|
29
|
+
}
|
|
30
|
+
except(room) {
|
|
31
|
+
return this.resolved.except(room);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
WsServerHolder = __decorate([Injectable()], WsServerHolder);
|
|
35
|
+
//#endregion
|
|
36
|
+
export { WsServerHolder };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=ws-server-holder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-server-holder.js","names":[],"sources":["../../src/websocket/ws-server-holder.ts"],"sourcesContent":["import { Injectable } from '@velajs/vela';\nimport { resolveMaxFrameBytes } from '@velajs/vela/websocket';\nimport type { BroadcastOperator, WsServer } from '@velajs/vela/websocket';\n\n/**\n * Late-bound `WsServer`. Provided as `WS_SERVER` (one per DI container, i.e. per\n * Durable Object instance), then pointed at the ctx-backed server once the DO\n * builds. Gateways inject it via `@WebSocketServer()`; it throws if used before\n * a runtime binds it (e.g. from the stateless Worker isolate).\n */\n@Injectable()\nexport class WsServerHolder implements WsServer {\n private target?: WsServer;\n private maxFrameBytes?: number;\n\n setTarget(server: WsServer): void {\n this.target = server;\n if (this.maxFrameBytes !== undefined) server.setOutboundFrameLimit?.(this.maxFrameBytes);\n }\n\n setOutboundFrameLimit(maxFrameBytes: number): void {\n const resolved = resolveMaxFrameBytes({ maxFrameBytes });\n this.maxFrameBytes =\n this.maxFrameBytes === undefined ? resolved : Math.max(this.maxFrameBytes, resolved);\n this.target?.setOutboundFrameLimit?.(this.maxFrameBytes);\n }\n\n private get resolved(): WsServer {\n if (!this.target) {\n throw new Error(\n 'WebSocket server is only available inside a WebSocket Durable Object. To ' +\n 'push from a Worker HTTP handler, use broadcastToRoom(namespace, gatewayPath, room, ...).',\n );\n }\n return this.target;\n }\n\n emit(event: string, data?: unknown): void | Promise<void> {\n return this.resolved.emit(event, data);\n }\n to(room: string): BroadcastOperator {\n return this.resolved.to(room);\n }\n in(room: string): BroadcastOperator {\n return this.resolved.in(room);\n }\n except(room: string): BroadcastOperator {\n return this.resolved.except(room);\n }\n}\n"],"mappings":";;;;AAWO,IAAM,iBAAN,MAAM,eAAmC;CAC9C;CACA;CAEA,UAAU,QAAwB;EAChC,KAAK,SAAS;EACd,IAAI,KAAK,kBAAkB,KAAA,GAAW,OAAO,wBAAwB,KAAK,aAAa;CACzF;CAEA,sBAAsB,eAA6B;EACjD,MAAM,WAAW,qBAAqB,EAAE,cAAc,CAAC;EACvD,KAAK,gBACH,KAAK,kBAAkB,KAAA,IAAY,WAAW,KAAK,IAAI,KAAK,eAAe,QAAQ;EACrF,KAAK,QAAQ,wBAAwB,KAAK,aAAa;CACzD;CAEA,IAAY,WAAqB;EAC/B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MACR,mKAEF;EAEF,OAAO,KAAK;CACd;CAEA,KAAK,OAAe,MAAsC;EACxD,OAAO,KAAK,SAAS,KAAK,OAAO,IAAI;CACvC;CACA,GAAG,MAAiC;EAClC,OAAO,KAAK,SAAS,GAAG,IAAI;CAC9B;CACA,GAAG,MAAiC;EAClC,OAAO,KAAK,SAAS,GAAG,IAAI;CAC9B;CACA,OAAO,MAAiC;EACtC,OAAO,KAAK,SAAS,OAAO,IAAI;CAClC;AACF;AAvCC,iBAAA,WAAA,CAAA,WAAW,CAAA,GAAA,cAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/cloudflare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Cloudflare Workers integration for Vela framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -67,15 +67,15 @@
|
|
|
67
67
|
"unplugin-swc": "1.5.9",
|
|
68
68
|
"vitest": "4.1.11",
|
|
69
69
|
"zod": "^3.25.76",
|
|
70
|
-
"@velajs/
|
|
71
|
-
"@velajs/
|
|
72
|
-
"@velajs/vela": "1.
|
|
70
|
+
"@velajs/feature-flags": "1.30.0",
|
|
71
|
+
"@velajs/testing": "1.30.0",
|
|
72
|
+
"@velajs/vela": "1.30.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@cloudflare/workers-types": ">=4",
|
|
76
76
|
"hono": ">=4",
|
|
77
|
-
"@velajs/feature-flags": "^1.
|
|
78
|
-
"@velajs/vela": "^1.
|
|
77
|
+
"@velajs/feature-flags": "^1.30.0",
|
|
78
|
+
"@velajs/vela": "^1.30.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"@velajs/feature-flags": {
|
|
@@ -89,12 +89,14 @@
|
|
|
89
89
|
"build": "tsdown",
|
|
90
90
|
"test": "vitest run",
|
|
91
91
|
"test:workers": "vitest run --config vitest.config.workers.ts",
|
|
92
|
+
"worker-size:check": "node ../vela/scripts/check-worker-size.mjs --baseline worker-size.json",
|
|
93
|
+
"worker-size:update": "node ../vela/scripts/check-worker-size.mjs --baseline worker-size.json --update",
|
|
92
94
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.workers-test.json && tsc --noEmit -p tsconfig.package-tests.json",
|
|
93
95
|
"lint": "oxlint .",
|
|
94
96
|
"format": "oxfmt .",
|
|
95
97
|
"format:check": "oxfmt --check .",
|
|
96
98
|
"publint": "publint",
|
|
97
99
|
"attw": "attw --pack . --profile esm-only",
|
|
98
|
-
"verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm test:workers && pnpm publint && pnpm attw"
|
|
100
|
+
"verify": "pnpm lint && pnpm format:check && pnpm build && pnpm worker-size:check && pnpm typecheck && pnpm test && pnpm test:workers && pnpm publint && pnpm attw"
|
|
99
101
|
}
|
|
100
102
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"durable-objects.js","names":[],"sources":["../src/websocket/cf-room-registry.ts","../src/websocket/do-websocket-host.ts","../src/websocket/do-bootstrap.ts","../src/websocket/websocket.durable-object.ts","../src/nonce/nonce.durable-object.ts"],"sourcesContent":["import { WebSocketSendGate, type WebSocketSendPolicy } from '@velajs/vela/websocket';\nimport { socketAttachment, rejectedAttachment } from './ws-attachment';\nimport type { BroadcastCommand, RoomRegistry, WsClient } from '@velajs/vela/websocket';\nimport type { DoStateLike, WsAttachment, WsLike } from './do-state';\nimport { CfWsClient } from './cf-ws-client';\n\n/**\n * Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room\n * membership lives in tags (hub room, set at accept) + the attachment (dynamic\n * `join()`), never in DO instance fields — so it survives hibernation with zero\n * rehydration.\n */\nexport class CfRoomRegistry implements RoomRegistry {\n readonly #sendGates = new WeakMap<WsLike, WebSocketSendGate>();\n // Sockets whose OnGatewayConnection hook is running in this instance. They\n // are still `pending`, so broadcasts skip them; any other pending socket\n // never finished admission and is closed when a broadcast reaches it.\n readonly #admitting = new WeakSet<WsLike>();\n #sendPolicyForPath?: (path: string) => WebSocketSendPolicy | undefined;\n\n setSendPolicyResolver(resolve: (path: string) => WebSocketSendPolicy | undefined): void {\n this.#sendPolicyForPath = resolve;\n }\n\n private deliveryAuthorizer?: (client: WsClient) => boolean | Promise<boolean>;\n private frameLimitForPath?: (path: string) => number | undefined;\n\n constructor(private readonly ctx: DoStateLike) {}\n\n setDeliveryAuthorizer(authorizer: (client: WsClient) => boolean | Promise<boolean>): void {\n this.deliveryAuthorizer = authorizer;\n }\n\n /** Reconcile pre-migration/woken attachments with authoritative gateway metadata. */\n setFrameLimitResolver(resolver: (path: string) => number | undefined): void {\n this.frameLimitForPath = resolver;\n }\n\n /**\n * Run `admit` (the connection hook) with `ws` marked as being admitted, so a\n * broadcast issued meanwhile (for example `server.emit()` announcing the new\n * connection) skips the still-pending socket instead of rejecting it.\n */\n async admitting<T>(ws: WsLike, admit: () => Promise<T>): Promise<T> {\n this.#admitting.add(ws);\n try {\n return await admit();\n } finally {\n this.#admitting.delete(ws);\n }\n }\n\n // CF sockets are registered with the DO by `acceptWebSocket`; nothing to track.\n register(_client: WsClient): void {}\n leaveAll(_client: WsClient): void {}\n\n join(client: WsClient, room: string): void | Promise<void> {\n return client.join(room);\n }\n\n leave(client: WsClient, room: string): void | Promise<void> {\n return client.leave(room);\n }\n\n localIdsInRoom(room: string): string[] {\n return this.socketsForRoom(room).map((ws) => this.attachmentOf(ws).connId);\n }\n\n deliverLocal(cmd: BroadcastCommand): void | Promise<void> {\n const excludeIds = new Set(cmd.exceptIds ?? []);\n const excludeRooms = cmd.exceptRooms ?? [];\n\n // Empty `rooms` => GLOBAL (every socket in this DO). Otherwise the union of\n // targeted rooms, deduped per connection.\n const targets =\n cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);\n\n const seen = new Set<string>();\n const selected: Array<{ ws: WsLike; client: WsClient }> = [];\n for (const ws of targets) {\n const att = this.reconcileFrameLimit(ws, this.attachmentOf(ws));\n // A socket still in its connection hook is not admitted yet: it receives no\n // room frames, and its own accept() settles it. Rejecting it here would\n // close every client whose handleConnection broadcasts to the room. Other\n // pending sockets fall through and are rejected below.\n if (att.state === 'pending' && this.#admitting.has(ws)) continue;\n if (\n att.state !== 'active' ||\n (att.expiresAtMs !== undefined &&\n (!Number.isSafeInteger(att.expiresAtMs) || att.expiresAtMs <= Date.now()))\n ) {\n try {\n att.state = 'rejected';\n ws.serializeAttachment(att);\n ws.close(1008, 'identity expired or connection rejected');\n } catch {\n // already closed or malformed attachment\n }\n continue;\n }\n if (seen.has(att.connId)) continue;\n seen.add(att.connId);\n if (excludeIds.has(att.connId)) continue;\n if (excludeRooms.some((r) => att.rooms.includes(r))) continue;\n selected.push({ ws, client: this.clientFor(ws) });\n }\n\n if (!this.deliveryAuthorizer) {\n for (const { client } of selected) client.sendRaw(cmd.frame);\n return;\n }\n return Promise.all(\n selected.map(async ({ ws, client }) => {\n let allowed = false;\n try {\n allowed = (await this.deliveryAuthorizer!(client)) === true;\n } catch {\n allowed = false;\n }\n if (allowed) client.sendRaw(cmd.frame);\n else this.reject(ws, 'delivery authorization revoked');\n }),\n ).then(() => undefined);\n }\n\n private collectRooms(rooms: string[]): WsLike[] {\n const set = new Set<WsLike>();\n for (const room of rooms) for (const ws of this.socketsForRoom(room)) set.add(ws);\n return [...set];\n }\n\n private socketsForRoom(room: string): WsLike[] {\n // Membership is the attachment's `rooms`, NOT the hibernation tag: tags are\n // immutable after acceptWebSocket, so a socket that left its hub room still\n // carries the tag. Since one DO ≈ one room, scanning all sockets in the DO\n // and filtering by attachment is both correct and cheap.\n return this.ctx.getWebSockets().filter((ws) => this.attachmentOf(ws).rooms.includes(room));\n }\n\n private attachmentOf(ws: WsLike): WsAttachment {\n return socketAttachment(ws) ?? rejectedAttachment();\n }\n\n /** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */\n clientFor(ws: WsLike): CfWsClient {\n this.reconcileFrameLimit(ws, this.attachmentOf(ws));\n let gate = this.#sendGates.get(ws);\n if (!gate) {\n gate = new WebSocketSendGate(this.#sendPolicyForPath?.(this.attachmentOf(ws).path));\n this.#sendGates.set(ws, gate);\n }\n return new CfWsClient(this.ctx, ws, gate);\n }\n\n private reconcileFrameLimit(ws: WsLike, attachment: WsAttachment): WsAttachment {\n const expected = this.frameLimitForPath?.(attachment.path);\n if (expected !== undefined && attachment.maxFrameBytes !== expected) {\n attachment.maxFrameBytes = expected;\n ws.serializeAttachment(attachment);\n }\n return attachment;\n }\n\n private reject(ws: WsLike, reason: string): void {\n try {\n const attachment = this.attachmentOf(ws);\n attachment.state = 'rejected';\n ws.serializeAttachment(attachment);\n ws.close(1008, reason);\n } catch {\n // already closed or malformed attachment\n }\n }\n}\n","import { socketAttachment } from './ws-attachment';\nimport { WsMessageQueue, assertBroadcastCommandFits } from '@velajs/vela/websocket';\nimport type { BroadcastCommand, WsDispatcher } from '@velajs/vela/websocket';\nimport type { CfRoomRegistry } from './cf-room-registry';\nimport {\n MAX_WS_ATTACHMENT_BYTES,\n type DoStateLike,\n type WsAttachment,\n type WsLike,\n} from './do-state';\nimport { connTag, roomTag } from './room-id';\n\nexport interface WsConnectionPrincipal {\n issuer: string;\n subject: string;\n principalType: 'user' | 'service';\n tenantId: string;\n}\n\n/**\n * The socket lifecycle inside a WebSocket Durable Object, decoupled from the\n * `cloudflare:workers` base class so it is unit-testable with fakes. The thin\n * `VelaWebSocketDurableObject` shell forwards its hibernation callbacks here.\n */\nexport class DoWebSocketHost {\n readonly #messages = new WeakMap<WsLike, WsMessageQueue>();\n constructor(\n private readonly ctx: DoStateLike,\n private readonly dispatcher: WsDispatcher,\n private readonly registry: CfRoomRegistry,\n private readonly gatewayPaths: readonly string[] = [],\n ) {}\n\n /** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */\n defaultPath(): string | undefined {\n return this.gatewayPaths[0];\n }\n\n /**\n * Accept a hibernatable socket: tag it with its hub room + connection id,\n * persist the attachment, then await `OnGatewayConnection` before the caller\n * returns 101. A rejected lifecycle hook closes and fails the upgrade.\n */\n async accept(\n ws: WsLike,\n path: string,\n roomId: string,\n userId?: string,\n expiresAtMs?: number,\n principal?: WsConnectionPrincipal,\n ): Promise<boolean> {\n const maxFrameBytes = this.dispatcher.getGatewayMaxFrameBytes(path);\n if (maxFrameBytes === undefined) return false;\n if (\n expiresAtMs !== undefined &&\n (!Number.isSafeInteger(expiresAtMs) || expiresAtMs <= Date.now())\n ) {\n return false;\n }\n if (\n (principal !== undefined &&\n (expiresAtMs === undefined ||\n userId !== principal.subject ||\n !principal.issuer ||\n !principal.tenantId)) ||\n (expiresAtMs !== undefined && principal === undefined)\n ) {\n return false;\n }\n\n const connId = crypto.randomUUID();\n const attachment: WsAttachment = {\n version: 1,\n connId,\n state: 'pending',\n userId,\n principal:\n principal === undefined\n ? undefined\n : {\n issuer: principal.issuer,\n subject: principal.subject,\n principalType: principal.principalType,\n },\n tenantId: principal?.tenantId,\n expiresAtMs,\n path,\n maxFrameBytes,\n rooms: [roomId],\n data: {\n ...(userId ? { userId } : {}),\n ...(principal\n ? {\n principal: {\n issuer: principal.issuer,\n subject: principal.subject,\n principalType: principal.principalType,\n },\n tenantId: principal.tenantId,\n }\n : {}),\n ...(expiresAtMs !== undefined ? { expiresAtMs } : {}),\n },\n };\n if (new TextEncoder().encode(JSON.stringify(attachment)).byteLength > MAX_WS_ATTACHMENT_BYTES) {\n return false;\n }\n try {\n this.ctx.acceptWebSocket(ws, [roomTag(roomId), connTag(connId)]);\n ws.serializeAttachment(attachment);\n } catch {\n try {\n ws.close(1008, 'Connection rejected');\n } catch {\n // already closed\n }\n return false;\n }\n\n const client = this.registry.clientFor(ws);\n try {\n // Broadcasts the hook issues skip this still-pending socket.\n await this.registry.admitting(ws, () => this.dispatcher.handleOpen(path, client));\n // Another callback may have rejected the socket while the asynchronous\n // connection hook was still pending. Never resurrect that terminal state\n // after the hook resolves.\n if (!this.transition(ws, 'active', 'pending')) {\n throw new Error(\n socketAttachment(ws)?.state === 'rejected'\n ? 'WebSocket was rejected while OnGatewayConnection was running: a frame ' +\n 'arrived before the connection hook completed, or the connection closed or failed'\n : 'Unable to persist authorized WebSocket state',\n );\n }\n return true;\n } catch (err) {\n await this.dispatcher.handleError(path, client, err).catch(() => {});\n this.transition(ws, 'rejected');\n try {\n ws.close(1008, 'Connection rejected');\n } catch {\n // already closed\n }\n return false;\n }\n }\n\n async onMessage(ws: WsLike, message: string | ArrayBuffer): Promise<void> {\n if (!this.isActive(ws)) {\n this.reject(ws, 'Connection is not authorized');\n return;\n }\n let queue = this.#messages.get(ws);\n if (!queue) {\n const path = socketAttachment(ws)?.path;\n const options = this.dispatcher.collectEntrypoints().find((entry) => entry.meta.path === path)\n ?.meta.options;\n queue = new WsMessageQueue(\n () => this.reject(ws, 'WebSocket message budget exceeded', 1013),\n options?.maxPendingMessages,\n options?.maxPendingBytes,\n );\n this.#messages.set(ws, queue);\n }\n await queue.run(message, async () => {\n if (!this.isActive(ws)) return;\n const client = this.registry.clientFor(ws);\n await this.dispatcher.dispatchMessage(client.path, client, message);\n });\n }\n\n async onClose(ws: WsLike, code: number, reason: string): Promise<void> {\n this.#messages.get(ws)?.stop();\n if (!this.isActive(ws, false)) {\n this.transition(ws, 'rejected');\n try {\n ws.close();\n } catch {\n // already closed\n }\n return;\n }\n const client = this.registry.clientFor(ws);\n await this.dispatcher.handleClose(client.path, client, code, reason);\n // Complete the closing handshake. Only 1000 and 3000-4999 are valid for\n // close(); reserved/abnormal codes (1005/1006/1015, etc.) throw a RangeError,\n // so fall back to a codeless close on those.\n try {\n if (code === 1000 || (code >= 3000 && code <= 4999)) ws.close(code, reason);\n else ws.close();\n } catch {\n // socket already closed\n }\n }\n\n async onError(ws: WsLike, err: unknown): Promise<void> {\n if (!this.isActive(ws, false)) {\n this.reject(ws, 'Connection failed before authorization');\n return;\n }\n const client = this.registry.clientFor(ws);\n await this.dispatcher.handleError(client.path, client, err);\n }\n\n /** Deliver a broadcast command to this DO's local sockets (RPC entry point). */\n broadcast(cmd: BroadcastCommand): void | Promise<void> {\n assertBroadcastCommandFits(cmd, this.dispatcher.getMaximumGatewayFrameBytes());\n return this.registry.deliverLocal(cmd);\n }\n\n private transition(\n ws: WsLike,\n state: WsAttachment['state'],\n expectedState?: WsAttachment['state'],\n ): boolean {\n try {\n const attachment = socketAttachment(ws);\n if (!attachment) return false;\n if (expectedState !== undefined && attachment.state !== expectedState) return false;\n attachment.state = state;\n ws.serializeAttachment(attachment);\n return true;\n } catch {\n return false;\n }\n }\n\n private isActive(ws: WsLike, checkExpiry = true): boolean {\n try {\n const attachment = socketAttachment(ws);\n if (!attachment || attachment.state !== 'active') return false;\n return (\n !checkExpiry ||\n attachment.expiresAtMs === undefined ||\n (Number.isSafeInteger(attachment.expiresAtMs) && attachment.expiresAtMs > Date.now())\n );\n } catch {\n return false;\n }\n }\n\n private reject(ws: WsLike, reason: string, code = 1008): void {\n this.#messages.get(ws)?.stop();\n this.transition(ws, 'rejected');\n try {\n ws.close(code, reason);\n } catch {\n // already closed\n }\n }\n}\n","import { bootstrap, VelaApplication } from '@velajs/vela';\nimport type { DynamicModule, Type, VelaEnv } from '@velajs/vela';\nimport {\n local,\n readWsEntrypointMeta,\n WsDispatcher,\n WsServerImpl,\n WS_SERVER,\n} from '@velajs/vela/websocket';\nimport type { WsServer } from '@velajs/vela/websocket';\nimport type { LiveEngine } from '@velajs/vela/live';\nimport { registerCloudflareEnvironment } from '../environment';\nimport { CfWsClient } from './cf-ws-client';\nimport { CfRoomRegistry } from './cf-room-registry';\nimport { initDoLive, initializeDoLiveResources } from './do-live';\nimport type { DoStateLike } from './do-state';\nimport { WsServerHolder } from './ws-server-holder';\n\nexport interface DoRuntime {\n dispatcher: WsDispatcher;\n registry: CfRoomRegistry;\n server: WsServer;\n /** Gateway paths from `app.entrypoints.ofKind('websocket')` (discovery order). */\n gatewayPaths: string[];\n /** The live-query engine (undefined when the app doesn't import LiveModule). */\n live?: LiveEngine;\n close(signal?: string): Promise<void>;\n}\n\n/**\n * Slim DI bootstrap for the Durable Object isolate: wires the container and runs\n * `OnModuleInit`/`OnApplicationBootstrap` (so `WsDispatcher` discovers gateways)\n * WITHOUT building the Hono app/routes the DO never serves. The DO's `env` is\n * seeded as the global ENV before providers construct, and the ctx-backed server\n * is bound before bootstrap lifecycle so gateway `afterInit`/handlers see it.\n */\nexport async function buildDoRuntime(\n rootModule: Type | DynamicModule,\n ctx: DoStateLike,\n options: { env: VelaEnv },\n): Promise<DoRuntime> {\n const { container, routeManager, loader } = await bootstrap(rootModule, {\n configureContainer: (container) => {\n registerCloudflareEnvironment(container, options.env);\n },\n });\n\n const registry = new CfRoomRegistry(ctx);\n const driver = local();\n driver.bind(registry);\n const server = new WsServerImpl(driver);\n\n if (container.has(WS_SERVER)) {\n const holder = await container.resolveAsync(WS_SERVER);\n // The core WebSocketModule's server broadcasts through its own sync driver,\n // which never reaches this Durable Object's hibernatable sockets.\n if (!(holder instanceof WsServerHolder)) {\n throw new Error(\n '[vela] The WebSocket Durable Object found a WS_SERVER from the core WebSocketModule, ' +\n 'which cannot reach Durable Object sockets. On Cloudflare, import ' +\n 'CloudflareWebSocketModule.forRoot() instead of WebSocketModule.forRoot().',\n );\n }\n holder.setTarget(server);\n }\n\n const app = new VelaApplication(container, routeManager);\n app.setInstances(await loader.resolveAllInstances());\n initializeDoLiveResources(container, ctx);\n await app.callOnModuleInit();\n await app.callOnApplicationBootstrap();\n\n // The entrypoint registry is the transport contract: one 'websocket' entry\n // per discovered gateway ({ meta: { path, dispatcher } }). Built by\n // callOnApplicationBootstrap(), so this slim no-routes path has it too.\n const wsEntrypoints = app.entrypoints.ofKind('websocket', readWsEntrypointMeta);\n const dispatcher = wsEntrypoints[0]?.meta.dispatcher ?? app.get(WsDispatcher);\n registry.setSendPolicyResolver(\n (path) => wsEntrypoints.find((entry) => entry.meta.path === path)?.meta.options.sendPolicy,\n );\n registry.setFrameLimitResolver((path) => dispatcher.getGatewayMaxFrameBytes(path));\n registry.setDeliveryAuthorizer((client) => {\n const path = client instanceof CfWsClient ? client.path : '';\n return dispatcher.authorizeDelivery(path, client);\n });\n\n // Live queries: wire the SQLite cursor log + local driver mode and replay\n // hibernation-persisted subscriptions into the fresh engine.\n const live = initDoLive(app, ctx, registry);\n\n return {\n // Zero gateways still yields a live dispatcher (module imported, nothing\n // decorated) — fall back to resolving it directly.\n dispatcher,\n registry,\n server,\n gatewayPaths: wsEntrypoints.map((ep) => ep.meta.path),\n live,\n close: (signal?: string) => app.close(signal),\n };\n}\n","import { DurableObject } from 'cloudflare:workers';\nimport type { VelaEnv } from '@velajs/vela';\nimport type { BroadcastCommand } from '@velajs/vela/websocket';\nimport type {\n CommitStamp,\n InvalidationCommand,\n LiveEngine,\n LiveInspection,\n} from '@velajs/vela/live';\nimport { buildDoRuntime } from './do-bootstrap';\nimport { DoWebSocketHost, type WsConnectionPrincipal } from './do-websocket-host';\nimport { armDoPitr, readDoPitrBookmark } from './do-pitr';\nimport type { CloudflareRoot } from '../root-module';\nimport type {\n DoPitrArmOptions,\n DoPitrArmResult,\n DoPitrBookmarkRead,\n VelaDoPitrRpc,\n} from './do-pitr';\n\nconst PING = '{\"event\":\"$ping\"}';\nconst PONG = '{\"event\":\"$pong\"}';\nconst MAX_IDENTITY_FIELD_BYTES = 2048;\nconst encoder = new TextEncoder();\n\nfunction isIdentityField(value: string | null): value is string {\n return (\n value !== null &&\n value.length > 0 &&\n encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES\n );\n}\n\n/**\n * Base class for the WebSocket Durable Object. The user exports a named subclass\n * (matching their `wrangler.toml` `class_name`) built from their `AppModule`, or\n * from a `DynamicModule` declared at module scope:\n *\n * ```ts\n * export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}\n * ```\n *\n * It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot\n * bridge DO hibernation) and forwards every event into the runtime-agnostic\n * `WsDispatcher` via {@link DoWebSocketHost}. The DO's `env` is the\n * application's ENV, as in the Worker.\n */\nexport function VelaWebSocketDurableObject(rootModule: CloudflareRoot): new (\n ctx: DurableObjectState,\n env: VelaEnv,\n) => DurableObject<VelaEnv> &\n VelaDoPitrRpc & {\n broadcast(cmd: BroadcastCommand): Promise<void>;\n invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined>;\n inspectLive(): Promise<LiveInspection>;\n } {\n return class VelaWsDurableObject extends DurableObject<VelaEnv> {\n private host!: DoWebSocketHost;\n private liveEngine?: LiveEngine;\n private readonly ready: Promise<void>;\n\n constructor(ctx: DurableObjectState, env: VelaEnv) {\n super(ctx, env);\n // Application-level ping/pong answered WITHOUT waking a hibernated DO.\n try {\n ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));\n } catch {\n // Older runtimes without auto-response — fine, protocol pings still work.\n }\n this.ready = ctx.blockConcurrencyWhile(async () => {\n // The root is static, so constructing another instance declares no new\n // classes in the isolate-global metadata registry.\n const runtime = await buildDoRuntime(rootModule, ctx, { env });\n this.host = new DoWebSocketHost(\n ctx,\n runtime.dispatcher,\n runtime.registry,\n runtime.gatewayPaths,\n );\n this.liveEngine = runtime.live;\n });\n }\n\n /** Intra-worker RPC only; HTTP fetch never exposes this admin snapshot. */\n async inspectLive(): Promise<LiveInspection> {\n await this.ready;\n return this.liveEngine?.inspect() ?? { subscriptions: [], rooms: [] };\n }\n\n override async fetch(request: Request): Promise<Response> {\n await this.ready;\n if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {\n return new Response('Expected WebSocket upgrade', { status: 426 });\n }\n\n const url = new URL(request.url);\n // Headers are primary (carry auth + multi-gateway routing); fall back to the\n // DO's own name (set via idFromName(room)) and the single gateway path so a\n // plain `stub.fetch(request)` forward still works.\n const roomId = request.headers.get('x-vela-room') ?? this.ctx.id.name ?? url.pathname;\n const path = request.headers.get('x-vela-path') ?? this.host.defaultPath() ?? url.pathname;\n const userId = request.headers.get('x-vela-user') || undefined;\n const rawExpiresAtMs = request.headers.get('x-vela-expires-at-ms');\n const parsedExpiresAtMs = rawExpiresAtMs === null ? undefined : Number(rawExpiresAtMs);\n if (\n parsedExpiresAtMs !== undefined &&\n (!Number.isSafeInteger(parsedExpiresAtMs) || parsedExpiresAtMs <= 0)\n ) {\n return new Response('Invalid WebSocket identity expiry', { status: 403 });\n }\n\n const issuer = request.headers.get('x-vela-issuer');\n const subject = request.headers.get('x-vela-subject');\n const principalType = request.headers.get('x-vela-principal-type');\n const tenantId = request.headers.get('x-vela-tenant');\n const hasPrincipalHeader =\n issuer !== null || subject !== null || principalType !== null || tenantId !== null;\n let principal: WsConnectionPrincipal | undefined;\n if (hasPrincipalHeader) {\n if (\n !isIdentityField(issuer) ||\n !isIdentityField(subject) ||\n (principalType !== 'user' && principalType !== 'service') ||\n !isIdentityField(tenantId) ||\n parsedExpiresAtMs === undefined ||\n (userId !== undefined && userId !== subject)\n ) {\n return new Response('Invalid WebSocket principal', { status: 403 });\n }\n principal = { issuer, subject, principalType, tenantId };\n } else if (parsedExpiresAtMs !== undefined) {\n return new Response('WebSocket identity tuple is missing', { status: 403 });\n }\n\n if (parsedExpiresAtMs !== undefined && parsedExpiresAtMs <= Date.now()) {\n return new Response('WebSocket identity expired', { status: 403 });\n }\n\n const { 0: client, 1: server } = new WebSocketPair();\n const accepted = await this.host.accept(\n server,\n path,\n roomId,\n userId,\n parsedExpiresAtMs,\n principal,\n );\n if (!accepted) return new Response('WebSocket connection rejected', { status: 403 });\n return new Response(null, { status: 101, webSocket: client });\n }\n\n override async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {\n await this.ready;\n await this.host.onMessage(ws, message);\n }\n\n override async webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void> {\n await this.ready;\n await this.host.onClose(ws, code, reason);\n }\n\n override async webSocketError(ws: WebSocket, error: unknown): Promise<void> {\n await this.ready;\n await this.host.onError(ws, error);\n }\n\n /** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */\n async broadcast(cmd: BroadcastCommand): Promise<void> {\n await this.ready;\n await this.host.broadcast(cmd);\n }\n\n /**\n * DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /\n * liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns\n * the commit stamp (what `Vela-Commit-Cursor` carries); the subscription\n * refreshes fan out asynchronously.\n */\n async invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined> {\n await this.ready;\n return this.liveEngine?.applyInvalidation(cmd);\n }\n\n // -- Durable Object PITR (point-in-time recovery) RPC ---------------------\n //\n // GUARD / reachability: these are admin-privileged operations (a restore can\n // roll the DO's SQLite state back up to 30 days and, with `restart`, abort\n // the DO to apply it now). They are safe to expose here because a Durable\n // Object's RPC methods are NOT network-reachable: a `DurableObjectStub` is\n // obtainable ONLY from a Worker that binds this DO namespace, and RPC calls\n // travel Cloudflare's internal capability channel — never the public\n // internet. A WebSocket/HTTP client reaches only `fetch()` above, never these\n // methods. The security boundary is therefore the WORKER-SIDE studio admin\n // gate that fronts the `CloudflareDoTimeTravelPort` (master admin token + the\n // 428 confirm challenge) — the identical trust model as `broadcast()` /\n // `invalidate()` on this same DO. See `do-pitr.ts` for the storage wrappers.\n\n /** DO RPC — the current PITR bookmark (typed-unavailable on a non-SQLite DO). */\n async pitrCurrentBookmark(): Promise<DoPitrBookmarkRead> {\n await this.ready;\n return readDoPitrBookmark(this.ctx.storage);\n }\n\n /** DO RPC — the PITR bookmark closest to `time` (+ the current bookmark). */\n async pitrBookmarkForTime(time: number | string): Promise<DoPitrBookmarkRead> {\n await this.ready;\n return readDoPitrBookmark(this.ctx.storage, time);\n }\n\n /**\n * DO RPC — arm a PITR restore and return the undo bookmark. When `restart` is\n * requested, `ctx.abort()` is called AFTER the undo bookmark is computed so\n * the recovery applies on the immediately-following session; otherwise the\n * restore applies on the next natural restart (`restarted: false`).\n */\n async pitrArmRestore(opts: DoPitrArmOptions): Promise<DoPitrArmResult> {\n await this.ready;\n const result = await armDoPitr(this.ctx.storage, opts);\n if (opts.restart === true) this.ctx.abort('vela PITR restore');\n return result;\n }\n };\n}\n","import { DurableObject } from 'cloudflare:workers';\nimport { MAX_NONCE_BYTES, isCanonicalBoundedText, isValidExpiry } from './nonce-validation';\nconst EXPIRED_DELETE_BATCH = 1_024;\n\nconst CREATE_TABLE =\n 'CREATE TABLE IF NOT EXISTS __vela_nonce_claims (' +\n 'nonce TEXT PRIMARY KEY NOT NULL, ' +\n 'expires_at INTEGER NOT NULL CHECK (expires_at > 0)' +\n ') WITHOUT ROWID';\nconst CREATE_EXPIRY_INDEX =\n 'CREATE INDEX IF NOT EXISTS __vela_nonce_claims_expiry ON __vela_nonce_claims (expires_at)';\nconst DELETE_EXPIRED =\n 'DELETE FROM __vela_nonce_claims WHERE nonce IN (' +\n 'SELECT nonce FROM __vela_nonce_claims ' +\n 'WHERE expires_at < ? ORDER BY expires_at LIMIT ?' +\n ')';\nconst INSERT_CLAIM =\n 'INSERT INTO __vela_nonce_claims (nonce, expires_at) VALUES (?, ?) ' +\n 'ON CONFLICT(nonce) DO NOTHING RETURNING nonce, expires_at';\n\ninterface NonceSqlCursor {\n toArray(): Record<string, unknown>[];\n}\n\ninterface NonceSqlStorage {\n exec(query: string, ...bindings: unknown[]): NonceSqlCursor;\n}\n\n/**\n * SQLite Durable Object that atomically consumes nonces.\n *\n * Export this class from the Worker entry and register it through a Wrangler\n * `new_sqlite_classes` migration. `INSERT ... ON CONFLICT DO NOTHING RETURNING`\n * is the single-use decision; all SQL runs synchronously before the RPC method\n * yields, and the nonce primary key is the final concurrency boundary.\n */\nexport class VelaNonceDurableObject extends DurableObject<Record<string, unknown>> {\n private sql?: NonceSqlStorage;\n\n constructor(ctx: DurableObjectState, env: Record<string, unknown>) {\n super(ctx, env);\n try {\n const sql = ctx.storage?.sql;\n if (!sql || typeof sql.exec !== 'function') return;\n sql.exec(CREATE_TABLE);\n sql.exec(CREATE_EXPIRY_INDEX);\n this.sql = sql;\n } catch {\n // A non-SQLite/misconfigured class stays fail-closed: every claim denies.\n this.sql = undefined;\n }\n }\n\n async claim(nonce: string, expEpochSeconds: number): Promise<boolean> {\n const sql = this.sql;\n if (!sql) return false;\n\n const now = Math.floor(Date.now() / 1_000);\n if (!isCanonicalBoundedText(nonce, MAX_NONCE_BYTES) || !isValidExpiry(expEpochSeconds, now)) {\n return false;\n }\n\n try {\n // Keep cleanup bounded so one claim cannot monopolize the DO. Entries at\n // exactly `now` remain: Vela's invocation verifier accepts `now === exp`.\n sql.exec(DELETE_EXPIRED, now, EXPIRED_DELETE_BATCH);\n\n const cursor = sql.exec(INSERT_CLAIM, nonce, expEpochSeconds);\n if (!cursor || typeof cursor.toArray !== 'function') return false;\n const rows: unknown = cursor.toArray();\n if (!Array.isArray(rows) || rows.length !== 1) return false;\n\n const row: unknown = rows[0];\n if (typeof row !== 'object' || row === null) return false;\n if (\n !Object.hasOwn(row, 'nonce') ||\n !Object.hasOwn(row, 'expires_at') ||\n !('nonce' in row) ||\n !('expires_at' in row)\n ) {\n return false;\n }\n return row.nonce === nonce && row.expires_at === expEpochSeconds;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
|