@velajs/cloudflare 1.10.1 → 1.22.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 +25 -0
- package/LICENSE +21 -0
- package/README.md +197 -322
- package/dist/durable-objects.d.ts +28 -0
- package/dist/durable-objects.js +494 -0
- package/dist/durable-objects.js.map +1 -0
- package/dist/index.d.ts +193 -376
- package/dist/index.js +412 -1034
- package/dist/index.js.map +1 -1
- package/dist/nonce-validation-LE0vFk-7.js +445 -0
- package/dist/nonce-validation-LE0vFk-7.js.map +1 -0
- package/dist/nonce.durable-object-Df3_42Sy.d.ts +141 -0
- package/package.json +34 -26
package/dist/index.js
CHANGED
|
@@ -1,48 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as roomToDurableId, _ as resolveCloudflareRoot, a as durableObjectLive, c as liveInvalidateToRoom, d as WsServerHolder, f as DoPitrUnavailableError, g as __decorate, h as readDoPitrBookmark, i as durableObjectCursorLog, m as isDoPitrUnavailable, n as isValidExpiry, p as armDoPitr, r as DoCursorLog, t as isCanonicalBoundedText, v as assertCloudflareEnvironment, x as durableObjectRoomName, y as registerCloudflareEnvironment } from "./nonce-validation-LE0vFk-7.js";
|
|
2
|
+
import { getConnInfo } from "hono/cloudflare-workers";
|
|
3
|
+
import { CRON_METADATA, ConfigurableModuleBuilder, Controller, Get, Inject, Injectable, InjectionToken, Module, PipelineRunner, Req, VelaFactory, buildEntrypointExecutionContext, createParamDecorator, defineMetadata, defineProvider, getMetadata, getTrustedRequestIdentity, registerEntrypointKind, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela";
|
|
2
4
|
import { ComponentManager } from "@velajs/vela/internal";
|
|
3
|
-
import { ConnectedSocket, MessageBody, SubscribeMessage, WS_GATEWAY_METADATA, WS_SERVER, WebSocketGateway, WebSocketServer, WsDispatcher, WsException,
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_value;
|
|
11
|
-
constructor(bindingName) {
|
|
12
|
-
this.bindingName = bindingName;
|
|
13
|
-
}
|
|
14
|
-
get value() {
|
|
15
|
-
if (this._value === void 0) throw new Error(`Cloudflare binding '${this.bindingName}' not initialized. Ensure createCloudflareApp() is used and a request has been made.`);
|
|
16
|
-
return this._value;
|
|
17
|
-
}
|
|
18
|
-
/** @internal — called by createCloudflareApp middleware */
|
|
19
|
-
_initialize(value) {
|
|
20
|
-
this._value = value;
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/websocket/room-id.ts
|
|
25
|
-
/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */
|
|
26
|
-
function roomTag(roomId) {
|
|
27
|
-
return `room:${roomId}`;
|
|
5
|
+
import { ConnectedSocket, DEFAULT_WS_MAX_FRAME_BYTES, MessageBody, SubscribeMessage, WS_GATEWAY_METADATA, WS_SERVER, WebSocketGateway, WebSocketServer, WsDispatcher, WsException, assertBroadcastCommandFits, authenticateWebSocketUpgrade, resolveGatewayRoomId, resolveGatewayRoomParam, resolveMaxFrameBytes } from "@velajs/vela/websocket";
|
|
6
|
+
import { STORAGE_SIGNED_URL_PURPOSE, joinStoragePath, signUrl, verifySignedUrl } from "@velajs/vela/storage";
|
|
7
|
+
//#region src/websocket/websocket-routing.ts
|
|
8
|
+
const MAX_IDENTITY_FIELD_BYTES = 2048;
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
function isIdentityField(value) {
|
|
11
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\r") && !value.includes("\n") && encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES;
|
|
28
12
|
}
|
|
29
|
-
/**
|
|
30
|
-
function
|
|
31
|
-
|
|
13
|
+
/** `null` means an identity was present but violated the transport contract. */
|
|
14
|
+
function accessIdentity(c) {
|
|
15
|
+
const value = getTrustedRequestIdentity(c.req.raw);
|
|
16
|
+
if (!value) return void 0;
|
|
17
|
+
const { principal, tenantId, expiresAtMs } = value;
|
|
18
|
+
if (!isIdentityField(principal.issuer) || !isIdentityField(principal.subject) || !isIdentityField(tenantId) || typeof expiresAtMs !== "number" || !Number.isSafeInteger(expiresAtMs) || expiresAtMs <= 0) return null;
|
|
19
|
+
return {
|
|
20
|
+
principal,
|
|
21
|
+
tenantId,
|
|
22
|
+
expiresAtMs
|
|
23
|
+
};
|
|
32
24
|
}
|
|
33
|
-
/**
|
|
34
|
-
function
|
|
35
|
-
|
|
25
|
+
/** `null` means two independently verified identities disagree. */
|
|
26
|
+
function combineIdentities(requestIdentity, upgradeIdentity) {
|
|
27
|
+
if (!requestIdentity && !upgradeIdentity) return void 0;
|
|
28
|
+
if (!requestIdentity) return upgradeIdentity;
|
|
29
|
+
if (!upgradeIdentity) return requestIdentity;
|
|
30
|
+
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;
|
|
31
|
+
return {
|
|
32
|
+
principal: { ...upgradeIdentity.principal },
|
|
33
|
+
tenantId: upgradeIdentity.tenantId,
|
|
34
|
+
expiresAtMs: Math.min(requestIdentity.expiresAtMs, upgradeIdentity.expiresAtMs)
|
|
35
|
+
};
|
|
36
36
|
}
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/websocket/websocket-routing.ts
|
|
39
37
|
/** Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted gateways only). */
|
|
40
38
|
function collectWsGatewayRoutes(instance) {
|
|
41
39
|
const options = getMetadata(WS_GATEWAY_METADATA, instance.constructor);
|
|
42
40
|
if (!options?.path || !options?.binding) return [];
|
|
41
|
+
resolveGatewayRoomParam(options);
|
|
42
|
+
resolveMaxFrameBytes(options);
|
|
43
43
|
return [{
|
|
44
44
|
path: options.path,
|
|
45
|
-
binding: options.binding
|
|
45
|
+
binding: options.binding,
|
|
46
|
+
options: { ...options }
|
|
46
47
|
}];
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
@@ -54,21 +55,65 @@ function collectWsGatewayRoutes(instance) {
|
|
|
54
55
|
function registerWebSocketRoutes(hono, routes) {
|
|
55
56
|
for (const route of routes) hono.get(route.path, async (c) => {
|
|
56
57
|
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return c.text("Expected WebSocket upgrade", 426);
|
|
57
|
-
const ns = c.env[route.binding];
|
|
58
|
-
if (!ns) return c.text(`Durable Object binding '${route.binding}' is not configured`, 500);
|
|
59
|
-
const roomId = c.req.param("id") ?? route.path;
|
|
60
|
-
const stub = ns.get(roomToDurableId(ns, roomId));
|
|
61
58
|
const headers = new Headers(c.req.raw.headers);
|
|
62
59
|
headers.delete("x-vela-room");
|
|
63
60
|
headers.delete("x-vela-path");
|
|
64
61
|
headers.delete("x-vela-user");
|
|
65
|
-
headers.
|
|
66
|
-
headers.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
headers.delete("x-vela-expires-at");
|
|
63
|
+
headers.delete("x-vela-expires-at-ms");
|
|
64
|
+
headers.delete("x-vela-issuer");
|
|
65
|
+
headers.delete("x-vela-subject");
|
|
66
|
+
headers.delete("x-vela-principal-type");
|
|
67
|
+
headers.delete("x-vela-tenant");
|
|
68
|
+
const sanitizedRequest = new Request(c.req.raw, { headers });
|
|
69
|
+
let roomId;
|
|
70
|
+
try {
|
|
71
|
+
roomId = resolveGatewayRoomId(route.options, (name) => c.req.param(name));
|
|
72
|
+
} catch {
|
|
73
|
+
return c.text("Invalid WebSocket room", 400);
|
|
74
|
+
}
|
|
75
|
+
const upgrade = await authenticateWebSocketUpgrade(route.options, sanitizedRequest, roomId);
|
|
76
|
+
if (upgrade === false) return c.text("WebSocket upgrade forbidden", 403);
|
|
77
|
+
const requestIdentity = accessIdentity(c);
|
|
78
|
+
if (requestIdentity === null) return c.text("Invalid WebSocket identity", 403);
|
|
79
|
+
const identity = combineIdentities(requestIdentity, upgrade.identity);
|
|
80
|
+
if (identity === null) return c.text("Conflicting WebSocket identities", 403);
|
|
81
|
+
if (identity && identity.expiresAtMs <= Date.now()) return c.text("WebSocket identity expired", 403);
|
|
82
|
+
const forwardHeaders = new Headers(upgrade.request.headers);
|
|
83
|
+
forwardHeaders.set("x-vela-room", roomId);
|
|
84
|
+
forwardHeaders.set("x-vela-path", route.path);
|
|
85
|
+
if (identity) {
|
|
86
|
+
forwardHeaders.set("x-vela-user", identity.principal.subject);
|
|
87
|
+
forwardHeaders.set("x-vela-issuer", identity.principal.issuer);
|
|
88
|
+
forwardHeaders.set("x-vela-subject", identity.principal.subject);
|
|
89
|
+
forwardHeaders.set("x-vela-principal-type", identity.principal.principalType);
|
|
90
|
+
forwardHeaders.set("x-vela-tenant", identity.tenantId);
|
|
91
|
+
forwardHeaders.set("x-vela-expires-at-ms", String(identity.expiresAtMs));
|
|
92
|
+
}
|
|
93
|
+
return forwardToRoom(c.env, route.binding, route.path, roomId, new Request(upgrade.request, { headers: forwardHeaders }));
|
|
70
94
|
});
|
|
71
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Gateway metadata contains a runtime binding name, so the native type is
|
|
98
|
+
* erased. Validate only the operations consumed here and their observable
|
|
99
|
+
* results; never assert that an arbitrary value implements a native namespace.
|
|
100
|
+
*/
|
|
101
|
+
async function forwardToRoom(env, binding, path, room, request) {
|
|
102
|
+
if (typeof env !== "object" || env === null) throw new Error("Worker environment is missing");
|
|
103
|
+
const namespace = Reflect.get(env, binding);
|
|
104
|
+
if (typeof namespace !== "object" || namespace === null) return new Response(`Durable Object binding '${binding}' is not configured`, { status: 500 });
|
|
105
|
+
const idFromName = Reflect.get(namespace, "idFromName");
|
|
106
|
+
const get = Reflect.get(namespace, "get");
|
|
107
|
+
if (typeof idFromName !== "function" || typeof get !== "function") throw new Error("Invalid Durable Object namespace");
|
|
108
|
+
const id = Reflect.apply(idFromName, namespace, [durableObjectRoomName(path, room)]);
|
|
109
|
+
const stub = Reflect.apply(get, namespace, [id]);
|
|
110
|
+
if (typeof stub !== "object" || stub === null) throw new Error("Invalid Durable Object stub");
|
|
111
|
+
const fetch = Reflect.get(stub, "fetch");
|
|
112
|
+
if (typeof fetch !== "function") throw new Error("Durable Object stub has no fetch operation");
|
|
113
|
+
const response = await Reflect.apply(fetch, stub, [request]);
|
|
114
|
+
if (!(response instanceof Response)) throw new Error("Durable Object returned an invalid response");
|
|
115
|
+
return response;
|
|
116
|
+
}
|
|
72
117
|
//#endregion
|
|
73
118
|
//#region src/cloudflare-application.ts
|
|
74
119
|
registerEntrypointKind({
|
|
@@ -77,9 +122,15 @@ registerEntrypointKind({
|
|
|
77
122
|
level: "method"
|
|
78
123
|
});
|
|
79
124
|
function invoke(instance, methodName, args) {
|
|
80
|
-
const method = instance
|
|
125
|
+
const method = Reflect.get(instance, methodName);
|
|
81
126
|
if (typeof method !== "function") throw new Error(`Method '${methodName}' is not a function on ${instance.constructor.name}`);
|
|
82
|
-
return
|
|
127
|
+
return Reflect.apply(method, instance, args);
|
|
128
|
+
}
|
|
129
|
+
function entrypointString(meta, property) {
|
|
130
|
+
if (typeof meta !== "object" || meta === null) throw new Error("Invalid entrypoint metadata.");
|
|
131
|
+
const value = Reflect.get(meta, property);
|
|
132
|
+
if (typeof value !== "string") throw new Error(`Invalid entrypoint metadata: ${property} must be a string.`);
|
|
133
|
+
return value;
|
|
83
134
|
}
|
|
84
135
|
/**
|
|
85
136
|
* Wraps VelaApplication with Cloudflare-specific handlers:
|
|
@@ -92,7 +143,7 @@ function invoke(instance, methodName, args) {
|
|
|
92
143
|
*
|
|
93
144
|
* @example
|
|
94
145
|
* ```ts
|
|
95
|
-
* const app = await createCloudflareApp(AppModule);
|
|
146
|
+
* const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
|
|
96
147
|
* export default {
|
|
97
148
|
* fetch: app.fetch,
|
|
98
149
|
* scheduled: app.scheduled.bind(app),
|
|
@@ -103,7 +154,7 @@ function invoke(instance, methodName, args) {
|
|
|
103
154
|
* @example
|
|
104
155
|
* ```ts
|
|
105
156
|
* // Serve OpenAPI docs alongside your routes
|
|
106
|
-
* const app = await createCloudflareApp(AppModule);
|
|
157
|
+
* const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
|
|
107
158
|
* const document = createOpenApiDocument(AppModule);
|
|
108
159
|
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
109
160
|
* // GET /openapi.json -> JSON document
|
|
@@ -112,30 +163,35 @@ function invoke(instance, methodName, args) {
|
|
|
112
163
|
*/
|
|
113
164
|
var CloudflareApplication = class {
|
|
114
165
|
app;
|
|
166
|
+
env;
|
|
115
167
|
wsGatewayRoutes = [];
|
|
116
|
-
constructor(app) {
|
|
168
|
+
constructor(app, env) {
|
|
117
169
|
this.app = app;
|
|
170
|
+
this.env = env;
|
|
171
|
+
this.get = app.get.bind(app);
|
|
118
172
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
173
|
+
fetch = async (request, env, ctx) => {
|
|
174
|
+
assertCloudflareEnvironment(this.env, env);
|
|
175
|
+
return this.app.fetch(request, env, ctx);
|
|
176
|
+
};
|
|
122
177
|
getHonoApp() {
|
|
123
178
|
return this.app.getHonoApp();
|
|
124
179
|
}
|
|
125
180
|
/**
|
|
126
181
|
* Resolve a provider from the application's DI container (delegates to
|
|
127
182
|
* `VelaApplication.get`). Handy for grabbing a service — e.g. an auth service —
|
|
128
|
-
* to use inside `createCloudflareApp({ middleware: [...] })` request middleware,
|
|
183
|
+
* to use inside `createCloudflareApp({ middleware: env => [...] })` request middleware,
|
|
129
184
|
* which runs outside the DI request pipeline.
|
|
130
185
|
*
|
|
131
186
|
* @example
|
|
132
187
|
* ```ts
|
|
133
|
-
* const app = await createCloudflareApp(AppModule);
|
|
134
|
-
* const auth = app.get
|
|
188
|
+
* const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
|
|
189
|
+
* const auth = app.get(BetterAuthService);
|
|
135
190
|
* ```
|
|
136
191
|
*/
|
|
137
|
-
get
|
|
138
|
-
|
|
192
|
+
get;
|
|
193
|
+
get entrypoints() {
|
|
194
|
+
return this.app.entrypoints;
|
|
139
195
|
}
|
|
140
196
|
/**
|
|
141
197
|
* Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
|
|
@@ -148,7 +204,7 @@ var CloudflareApplication = class {
|
|
|
148
204
|
* ```ts
|
|
149
205
|
* import { createOpenApiDocument } from '@velajs/vela';
|
|
150
206
|
*
|
|
151
|
-
* const app = await createCloudflareApp(AppModule);
|
|
207
|
+
* const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
|
|
152
208
|
* const document = createOpenApiDocument(AppModule, {
|
|
153
209
|
* info: { title: 'My API', version: '1.0.0' },
|
|
154
210
|
* });
|
|
@@ -184,12 +240,13 @@ var CloudflareApplication = class {
|
|
|
184
240
|
* request-scoped child (request-scoped providers rebuild per tick).
|
|
185
241
|
*/
|
|
186
242
|
async scheduled(event, env, ctx) {
|
|
243
|
+
assertCloudflareEnvironment(this.env, env);
|
|
187
244
|
const handlers = [...this.app.entrypoints.ofKind("cf:scheduled").map((ep) => ({
|
|
188
245
|
ep,
|
|
189
|
-
cron: ep.meta
|
|
246
|
+
cron: entrypointString(ep.meta, "cron")
|
|
190
247
|
})), ...this.app.entrypoints.ofKind("cf:vela-cron").map((ep) => ({
|
|
191
248
|
ep,
|
|
192
|
-
cron: ep.meta
|
|
249
|
+
cron: entrypointString(ep.meta, "expression")
|
|
193
250
|
}))].filter((h) => h.cron === event.cron);
|
|
194
251
|
await Promise.all(handlers.map(({ ep }) => this.dispatchEntrypoint(ep, [
|
|
195
252
|
event,
|
|
@@ -207,10 +264,12 @@ var CloudflareApplication = class {
|
|
|
207
264
|
*/
|
|
208
265
|
async dispatchEntrypoint(ep, args) {
|
|
209
266
|
const targetClass = ep.token;
|
|
267
|
+
if (typeof targetClass !== "function") throw new Error("Entrypoint token must be a class.");
|
|
210
268
|
const methodName = String(ep.methodName);
|
|
211
269
|
const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, args[0]);
|
|
212
270
|
await runInEntrypointScope(this.app.getContainer(), async (scope) => {
|
|
213
271
|
const instance = scope.resolve(ep.token);
|
|
272
|
+
if (typeof instance !== "object" || instance === null) throw new Error("Entrypoint must resolve to an object.");
|
|
214
273
|
const guards = ComponentManager.resolveGuards(ComponentManager.getScopedComponents("guard", targetClass, methodName), scope);
|
|
215
274
|
const interceptors = ComponentManager.resolveInterceptors(ComponentManager.getScopedComponents("interceptor", targetClass, methodName), scope);
|
|
216
275
|
const filters = ComponentManager.resolveFilters([...ComponentManager.getScopedComponents("filter", targetClass, methodName)].reverse(), scope);
|
|
@@ -238,7 +297,8 @@ var CloudflareApplication = class {
|
|
|
238
297
|
* child (request-scoped providers rebuild per batch — no boot-time captives).
|
|
239
298
|
*/
|
|
240
299
|
async queue(batch, env, ctx) {
|
|
241
|
-
|
|
300
|
+
assertCloudflareEnvironment(this.env, env);
|
|
301
|
+
const handlers = this.app.entrypoints.ofKind("cf:queue").filter((ep) => entrypointString(ep.meta, "queueName") === batch.queue);
|
|
242
302
|
await Promise.all(handlers.map((ep) => this.dispatchEntrypoint(ep, [
|
|
243
303
|
batch,
|
|
244
304
|
env,
|
|
@@ -250,668 +310,144 @@ var CloudflareApplication = class {
|
|
|
250
310
|
}
|
|
251
311
|
};
|
|
252
312
|
//#endregion
|
|
253
|
-
//#region src/env-ref.ts
|
|
254
|
-
const ENV_SENTINEL = "__cf_env__";
|
|
255
|
-
/**
|
|
256
|
-
* Holds the entire Cloudflare Worker `env` record (bindings + vars/secrets),
|
|
257
|
-
* not a single binding. Subclasses {@link BindingRef} so createCloudflareApp's
|
|
258
|
-
* binding-init middleware collects and initializes it through the same path;
|
|
259
|
-
* the factory special-cases it to pass the full `env` instead of one binding.
|
|
260
|
-
*/
|
|
261
|
-
var EnvRef = class extends BindingRef {
|
|
262
|
-
constructor() {
|
|
263
|
-
super(ENV_SENTINEL);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/websocket/cf-ws-client.ts
|
|
268
|
-
const MAX_ATTACHMENT_BYTES = 16384;
|
|
269
|
-
const encoder = new TextEncoder();
|
|
270
|
-
const EMPTY = {
|
|
271
|
-
connId: "",
|
|
272
|
-
path: "",
|
|
273
|
-
rooms: [],
|
|
274
|
-
data: {}
|
|
275
|
-
};
|
|
276
|
-
/**
|
|
277
|
-
* Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.
|
|
278
|
-
* Per-connection state lives in the hibernation attachment (survives eviction),
|
|
279
|
-
* so a fresh `CfWsClient` is reconstructed per message with no in-memory state.
|
|
280
|
-
*/
|
|
281
|
-
var CfWsClient = class {
|
|
282
|
-
ctx;
|
|
283
|
-
ws;
|
|
284
|
-
attachment;
|
|
285
|
-
constructor(ctx, ws) {
|
|
286
|
-
this.ctx = ctx;
|
|
287
|
-
this.ws = ws;
|
|
288
|
-
const raw = ws.deserializeAttachment();
|
|
289
|
-
this.attachment = raw ?? {
|
|
290
|
-
...EMPTY,
|
|
291
|
-
data: {}
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
get id() {
|
|
295
|
-
return this.attachment.connId;
|
|
296
|
-
}
|
|
297
|
-
/** The gateway route path this socket connected on (used to route messages). */
|
|
298
|
-
get path() {
|
|
299
|
-
return this.attachment.path;
|
|
300
|
-
}
|
|
301
|
-
get rooms() {
|
|
302
|
-
return new Set(this.attachment.rooms);
|
|
303
|
-
}
|
|
304
|
-
get data() {
|
|
305
|
-
return this.attachment.data;
|
|
306
|
-
}
|
|
307
|
-
set data(value) {
|
|
308
|
-
this.attachment.data = value;
|
|
309
|
-
}
|
|
310
|
-
get raw() {
|
|
311
|
-
return this.ws;
|
|
312
|
-
}
|
|
313
|
-
send(event, data, id) {
|
|
314
|
-
this.ws.send(JSON.stringify(id !== void 0 ? {
|
|
315
|
-
id,
|
|
316
|
-
event,
|
|
317
|
-
data
|
|
318
|
-
} : {
|
|
319
|
-
event,
|
|
320
|
-
data
|
|
321
|
-
}));
|
|
322
|
-
}
|
|
323
|
-
sendRaw(payload) {
|
|
324
|
-
this.ws.send(payload);
|
|
325
|
-
}
|
|
326
|
-
join(room) {
|
|
327
|
-
if (!this.attachment.rooms.includes(room)) {
|
|
328
|
-
this.attachment.rooms.push(room);
|
|
329
|
-
this.persist();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
leave(room) {
|
|
333
|
-
const next = this.attachment.rooms.filter((r) => r !== room);
|
|
334
|
-
if (next.length !== this.attachment.rooms.length) {
|
|
335
|
-
this.attachment.rooms = next;
|
|
336
|
-
this.persist();
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
/** Persist `data`/room mutations to the hibernation attachment. */
|
|
340
|
-
commit() {
|
|
341
|
-
this.persist();
|
|
342
|
-
}
|
|
343
|
-
close(code, reason) {
|
|
344
|
-
this.ws.close(code, reason);
|
|
345
|
-
}
|
|
346
|
-
persist() {
|
|
347
|
-
const serialized = JSON.stringify(this.attachment);
|
|
348
|
-
if (encoder.encode(serialized).length > MAX_ATTACHMENT_BYTES) throw new Error("WebSocket attachment exceeds the 16 KiB Cloudflare limit. Store large per-connection state in Durable Object storage keyed by connId instead.");
|
|
349
|
-
this.ws.serializeAttachment(this.attachment);
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
//#endregion
|
|
353
|
-
//#region src/websocket/do-live.ts
|
|
354
|
-
const DEFAULT_ROOM = "default";
|
|
355
|
-
const DEFAULT_MAX_LOG_ROWS = 4096;
|
|
356
|
-
/**
|
|
357
|
-
* The durable `CursorLog`: an append-only tag-invalidation log in the DO's
|
|
358
|
-
* SQLite (`__vela_live_log`, AUTOINCREMENT seq = cursor) plus an epoch UUID in
|
|
359
|
-
* `__vela_live_meta`. Because the cursor survives hibernation AND trims (it is
|
|
360
|
-
* read from `sqlite_sequence`, lunora's `ctx-db-cdc.ts` trick), a reconnecting
|
|
361
|
-
* client whose gap the log still covers gets a tiny `resume` instead of a
|
|
362
|
-
* re-run — the real-resume half of the live protocol.
|
|
363
|
-
*
|
|
364
|
-
* Constructed un-initialized at module-composition time (the same app module
|
|
365
|
-
* bootstraps in the Worker AND in each DO); `initDoLive` wires the SQLite
|
|
366
|
-
* handle inside the DO. In the Worker isolate it stays un-initialized — and is
|
|
367
|
-
* never consulted there, because `durableObjectLive()` routes every
|
|
368
|
-
* invalidation to the room DO's log (one log scope per room, exactly the
|
|
369
|
-
* protocol's model).
|
|
370
|
-
*/
|
|
371
|
-
var DoCursorLog = class {
|
|
372
|
-
maxRows;
|
|
373
|
-
sql;
|
|
374
|
-
epoch;
|
|
375
|
-
constructor(maxRows = DEFAULT_MAX_LOG_ROWS) {
|
|
376
|
-
this.maxRows = maxRows;
|
|
377
|
-
}
|
|
378
|
-
/** @internal — called by `initDoLive` with the DO's `ctx.storage.sql`. */
|
|
379
|
-
_initialize(sql) {
|
|
380
|
-
this.sql = sql;
|
|
381
|
-
sql.exec("CREATE TABLE IF NOT EXISTS __vela_live_log (seq INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, tags TEXT NOT NULL)");
|
|
382
|
-
sql.exec("CREATE TABLE IF NOT EXISTS __vela_live_meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)");
|
|
383
|
-
const row = sql.exec("SELECT v FROM __vela_live_meta WHERE k = 'epoch'").toArray()[0];
|
|
384
|
-
if (row && typeof row.v === "string") this.epoch = row.v;
|
|
385
|
-
else {
|
|
386
|
-
this.epoch = crypto.randomUUID();
|
|
387
|
-
sql.exec("INSERT INTO __vela_live_meta (k, v) VALUES ('epoch', ?)", this.epoch);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
append(tags) {
|
|
391
|
-
const sql = this.assertReady();
|
|
392
|
-
sql.exec("INSERT INTO __vela_live_log (ts, tags) VALUES (?, ?)", Date.now(), JSON.stringify(tags));
|
|
393
|
-
const stamp = this.current();
|
|
394
|
-
if (stamp.cursor > this.maxRows) sql.exec("DELETE FROM __vela_live_log WHERE seq <= ?", stamp.cursor - this.maxRows);
|
|
395
|
-
return stamp;
|
|
396
|
-
}
|
|
397
|
-
current() {
|
|
398
|
-
const sql = this.assertReady();
|
|
399
|
-
let cursor = 0;
|
|
400
|
-
try {
|
|
401
|
-
const row = sql.exec("SELECT seq FROM sqlite_sequence WHERE name = '__vela_live_log'").toArray()[0];
|
|
402
|
-
cursor = typeof row?.seq === "number" ? row.seq : Number(row?.seq ?? 0);
|
|
403
|
-
} catch {
|
|
404
|
-
cursor = 0;
|
|
405
|
-
}
|
|
406
|
-
return {
|
|
407
|
-
cursor,
|
|
408
|
-
epoch: this.epoch
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
evaluateResume(sinceCursor, sinceEpoch, subscriptionTags) {
|
|
412
|
-
const sql = this.assertReady();
|
|
413
|
-
const { cursor, epoch } = this.current();
|
|
414
|
-
if (sinceEpoch !== epoch) return "snapshot";
|
|
415
|
-
if (sinceCursor > cursor) return "snapshot";
|
|
416
|
-
if (sinceCursor === cursor) return "resume";
|
|
417
|
-
const minRow = sql.exec("SELECT MIN(seq) AS m FROM __vela_live_log").toArray()[0];
|
|
418
|
-
const min = minRow?.m == null ? void 0 : Number(minRow.m);
|
|
419
|
-
if (min === void 0 || min > sinceCursor + 1) return "snapshot";
|
|
420
|
-
const subTags = new Set(subscriptionTags);
|
|
421
|
-
for (const row of sql.exec("SELECT tags FROM __vela_live_log WHERE seq > ?", sinceCursor).toArray()) {
|
|
422
|
-
let tags;
|
|
423
|
-
try {
|
|
424
|
-
tags = JSON.parse(String(row.tags));
|
|
425
|
-
} catch {
|
|
426
|
-
return "snapshot";
|
|
427
|
-
}
|
|
428
|
-
if (Array.isArray(tags) && tags.some((tag) => subTags.has(tag))) return "rerun";
|
|
429
|
-
}
|
|
430
|
-
return "resume";
|
|
431
|
-
}
|
|
432
|
-
assertReady() {
|
|
433
|
-
if (!this.sql) throw new Error("DoCursorLog is not initialized. It only runs inside a SQLite-backed Durable Object (wrangler: new_sqlite_classes) — Worker-side invalidations must go through durableObjectLive(), which routes them to the room DO's log.");
|
|
434
|
-
return this.sql;
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
/**
|
|
438
|
-
* The Cloudflare `LiveDriver`. Dual-mode, because the SAME app module
|
|
439
|
-
* bootstraps in both isolates:
|
|
440
|
-
*
|
|
441
|
-
* - **Worker** (HTTP mutations, queue consumers, crons): route the command to
|
|
442
|
-
* the room's Durable Object over the `invalidate` RPC — the same canonical
|
|
443
|
-
* `roomToDurableId` mapping the upgrade route and `broadcastToRoom` use —
|
|
444
|
-
* and return THAT log scope's commit stamp (what `Vela-Commit-Cursor`
|
|
445
|
-
* must carry).
|
|
446
|
-
* - **DO** (writes issued from inside the object): apply to the local engine.
|
|
447
|
-
*/
|
|
448
|
-
function durableObjectLive(options) {
|
|
449
|
-
let sink;
|
|
450
|
-
let env;
|
|
451
|
-
let localMode = false;
|
|
452
|
-
return {
|
|
453
|
-
kind: "durable-object",
|
|
454
|
-
bind(boundSink) {
|
|
455
|
-
sink = boundSink;
|
|
456
|
-
},
|
|
457
|
-
_initializeEnv(capturedEnv) {
|
|
458
|
-
env = capturedEnv;
|
|
459
|
-
},
|
|
460
|
-
_setLocalMode() {
|
|
461
|
-
localMode = true;
|
|
462
|
-
},
|
|
463
|
-
dispatch(cmd) {
|
|
464
|
-
if (localMode) return sink?.applyInvalidation(cmd);
|
|
465
|
-
const namespace = env?.[options.binding];
|
|
466
|
-
if (!namespace) throw new Error(`durableObjectLive: binding '${options.binding}' is not available. In a Worker, createCloudflareApp() captures env on the first request; check the wrangler binding name.`);
|
|
467
|
-
const room = cmd.room ?? options.defaultRoom ?? DEFAULT_ROOM;
|
|
468
|
-
return namespace.get(roomToDurableId(namespace, room)).invalidate({
|
|
469
|
-
...cmd,
|
|
470
|
-
room
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
const isCfLiveDriver = (value) => typeof value === "object" && value !== null && typeof value._setLocalMode === "function" && typeof value._initializeEnv === "function";
|
|
476
|
-
/** Worker-side wiring, called from `cloudflareAdapter`'s first-request middleware. */
|
|
477
|
-
function initializeWorkerLive(container, env) {
|
|
478
|
-
let driver;
|
|
479
|
-
try {
|
|
480
|
-
driver = container.resolve(LIVE_DRIVER);
|
|
481
|
-
} catch {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (isCfLiveDriver(driver)) driver._initializeEnv(env);
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* DO-side wiring, called from `buildDoRuntime`: initialize the SQLite cursor
|
|
488
|
-
* log, flip the driver to local mode, and replay every hibernation-persisted
|
|
489
|
-
* subscription into the (fresh) engine so an eviction is invisible to
|
|
490
|
-
* subscribers. Returns the engine for the `invalidate` RPC, or undefined when
|
|
491
|
-
* the app doesn't use LiveModule.
|
|
492
|
-
*/
|
|
493
|
-
function initDoLive(app, container, ctx) {
|
|
494
|
-
const entry = app.entrypoints.ofKind("live")[0];
|
|
495
|
-
if (!entry) return void 0;
|
|
496
|
-
const engine = entry.meta.engine;
|
|
497
|
-
try {
|
|
498
|
-
const log = container.resolve(LIVE_CURSOR_LOG);
|
|
499
|
-
if (log instanceof DoCursorLog) {
|
|
500
|
-
const sql = ctx.storage?.sql;
|
|
501
|
-
if (!sql) throw new Error("DoCursorLog requires a SQLite-backed Durable Object: add this class to wrangler's `migrations[].new_sqlite_classes`. Falling back is not possible — either enable SQLite or drop the `log: durableObjectCursorLog()` option (snapshot-on-reconnect semantics).");
|
|
502
|
-
log._initialize(sql);
|
|
503
|
-
}
|
|
504
|
-
} catch (err) {
|
|
505
|
-
if (err instanceof Error && err.message.includes("new_sqlite_classes")) throw err;
|
|
506
|
-
}
|
|
507
|
-
try {
|
|
508
|
-
const driver = container.resolve(LIVE_DRIVER);
|
|
509
|
-
if (isCfLiveDriver(driver)) driver._setLocalMode();
|
|
510
|
-
} catch {}
|
|
511
|
-
for (const ws of ctx.getWebSockets()) {
|
|
512
|
-
const client = new CfWsClient(ctx, ws);
|
|
513
|
-
for (const record of readPersistedLiveSubscriptions(client)) engine.restoreSubscription(client.path, client, record);
|
|
514
|
-
}
|
|
515
|
-
return engine;
|
|
516
|
-
}
|
|
517
|
-
/** Ergonomic alias: the log option for `LiveModule.forRoot` on Cloudflare. */
|
|
518
|
-
function durableObjectCursorLog(maxRows) {
|
|
519
|
-
return new DoCursorLog(maxRows);
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Invalidate live tags in a room from a Worker (controller / cron / queue
|
|
523
|
-
* consumer) — the live sibling of `broadcastToRoom`. Returns the room log
|
|
524
|
-
* scope's commit stamp for `Vela-Commit-Cursor` stamping.
|
|
525
|
-
*/
|
|
526
|
-
async function liveInvalidateToRoom(ns, room, tags) {
|
|
527
|
-
return ns.get(roomToDurableId(ns, room)).invalidate({
|
|
528
|
-
room,
|
|
529
|
-
tags
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
//#endregion
|
|
533
313
|
//#region src/cloudflare-factory.ts
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
for (const value of container.getUseValues()) if (value instanceof BindingRef) refs.push(value);
|
|
537
|
-
return refs;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Create a Cloudflare Workers application.
|
|
541
|
-
*
|
|
542
|
-
* Sets up a one-time Hono middleware that captures `c.env` on the first
|
|
543
|
-
* request and initializes all configured binding refs. Optional
|
|
544
|
-
* {@link CreateCloudflareAppOptions} are forwarded to the underlying
|
|
545
|
-
* `VelaFactory.create` so consumers don't have to wrap the resulting
|
|
546
|
-
* application in an outer Hono just to set a `globalPrefix` or attach
|
|
547
|
-
* extra request middleware.
|
|
548
|
-
*
|
|
549
|
-
* @example
|
|
550
|
-
* ```ts
|
|
551
|
-
* // Minimal — backwards compatible
|
|
552
|
-
* const app = await createCloudflareApp(AppModule);
|
|
553
|
-
* export default app; // has .fetch, .scheduled, .queue
|
|
554
|
-
* ```
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* ```ts
|
|
558
|
-
* // With a global prefix and outer middleware
|
|
559
|
-
* const app = await createCloudflareApp(AppModule, {
|
|
560
|
-
* globalPrefix: '/v1',
|
|
561
|
-
* middleware: [
|
|
562
|
-
* async (c, next) => {
|
|
563
|
-
* c.set('tenantId', c.req.header('x-tenant-id') ?? 'public');
|
|
564
|
-
* await next();
|
|
565
|
-
* },
|
|
566
|
-
* ],
|
|
567
|
-
* });
|
|
568
|
-
* ```
|
|
569
|
-
*/
|
|
570
|
-
/**
|
|
571
|
-
* The Cloudflare platform binding as a vela {@link RuntimeAdapter}: a one-time
|
|
572
|
-
* request middleware captures `c.env` on the first request and initializes
|
|
573
|
-
* every `BindingRef`/`EnvRef` (collected at `onBootstrap`, before any request
|
|
574
|
-
* can arrive). Adapter `requestMiddleware` is prepended to the global chain by
|
|
575
|
-
* `VelaFactory.create`, so user middleware can safely read binding refs.
|
|
576
|
-
*
|
|
577
|
-
* Exposed so consumers composing `VelaFactory.create` themselves can opt in:
|
|
578
|
-
*
|
|
579
|
-
* ```ts
|
|
580
|
-
* const app = await VelaFactory.create(AppModule, { adapters: [cloudflareAdapter()] });
|
|
581
|
-
* ```
|
|
582
|
-
*/
|
|
583
|
-
function cloudflareAdapter() {
|
|
584
|
-
let initialized = false;
|
|
585
|
-
let refs = [];
|
|
586
|
-
let liveContainer;
|
|
314
|
+
/** Bind an application to one environment before provider factories and lifecycle hooks. */
|
|
315
|
+
function cloudflareAdapter(options) {
|
|
587
316
|
return {
|
|
588
317
|
name: "cloudflare",
|
|
589
|
-
requestMiddleware: [async (
|
|
590
|
-
|
|
591
|
-
initialized = true;
|
|
592
|
-
const env = c.env ?? {};
|
|
593
|
-
for (const ref of refs) if (ref instanceof EnvRef) ref._initialize(env);
|
|
594
|
-
else ref._initialize(env[ref.bindingName]);
|
|
595
|
-
if (liveContainer) initializeWorkerLive(liveContainer, env);
|
|
596
|
-
}
|
|
318
|
+
requestMiddleware: [async (context, next) => {
|
|
319
|
+
assertCloudflareEnvironment(options.env, context.env);
|
|
597
320
|
await next();
|
|
598
321
|
}],
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
322
|
+
invocationTransport: ({ app }) => (request) => Promise.resolve(app.fetch(request, options.env)),
|
|
323
|
+
getClientIp: (c) => getConnInfo(c).remote.address ?? null,
|
|
324
|
+
configureContainer: (container) => {
|
|
325
|
+
registerCloudflareEnvironment(container, {
|
|
326
|
+
token: options.envToken,
|
|
327
|
+
env: options.env
|
|
328
|
+
});
|
|
602
329
|
}
|
|
603
330
|
};
|
|
604
331
|
}
|
|
605
|
-
|
|
606
|
-
|
|
332
|
+
/** Build an application for one native Workers environment. Call inside a platform event. */
|
|
333
|
+
async function createCloudflareApp(rootModule, options) {
|
|
334
|
+
const velaApp = await VelaFactory.create(resolveCloudflareRoot(rootModule, options.env), {
|
|
607
335
|
globalPrefix: options.globalPrefix,
|
|
608
|
-
|
|
609
|
-
|
|
336
|
+
security: options.security,
|
|
337
|
+
middleware: options.middleware?.(options.env),
|
|
338
|
+
adapters: [cloudflareAdapter(options)]
|
|
610
339
|
});
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
registerWebSocketRoutes(
|
|
614
|
-
return
|
|
615
|
-
}
|
|
616
|
-
//#endregion
|
|
617
|
-
//#region src/tokens.ts
|
|
618
|
-
const KV_BINDING_REF = new InjectionToken("CF_KV_BINDING_REF");
|
|
619
|
-
const D1_BINDING_REF = new InjectionToken("CF_D1_BINDING_REF");
|
|
620
|
-
const R2_BINDING_REF = new InjectionToken("CF_R2_BINDING_REF");
|
|
621
|
-
const QUEUE_BINDING_REF = new InjectionToken("CF_QUEUE_BINDING_REF");
|
|
622
|
-
const DO_BINDING_REF = new InjectionToken("CF_DO_BINDING_REF");
|
|
623
|
-
const AI_BINDING_REF = new InjectionToken("CF_AI_BINDING_REF");
|
|
624
|
-
const VECTORIZE_BINDING_REF = new InjectionToken("CF_VECTORIZE_BINDING_REF");
|
|
625
|
-
const HYPERDRIVE_BINDING_REF = new InjectionToken("CF_HYPERDRIVE_BINDING_REF");
|
|
626
|
-
const ENV_REF = new InjectionToken("CF_ENV_REF");
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateMetadata.js
|
|
629
|
-
function __decorateMetadata(k, v) {
|
|
630
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
340
|
+
const app = new CloudflareApplication(velaApp, options.env);
|
|
341
|
+
app.scanInstances(velaApp.getInstances());
|
|
342
|
+
registerWebSocketRoutes(app.getHonoApp(), app.getWsGatewayRoutes());
|
|
343
|
+
return app;
|
|
631
344
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Worker entrypoint with one bootstrap per environment identity. Weak keys let
|
|
347
|
+
* obsolete environments and secrets be collected. Concurrent cold events share
|
|
348
|
+
* construction; failed construction is evicted so the next event can retry.
|
|
349
|
+
*/
|
|
350
|
+
function createCloudflareWorker(rootModule, options) {
|
|
351
|
+
const applications = /* @__PURE__ */ new WeakMap();
|
|
352
|
+
const application = (env) => {
|
|
353
|
+
const existing = applications.get(env);
|
|
354
|
+
if (existing) return existing;
|
|
355
|
+
const pending = createCloudflareApp(rootModule, {
|
|
356
|
+
...options,
|
|
357
|
+
env
|
|
358
|
+
});
|
|
359
|
+
applications.set(env, pending);
|
|
360
|
+
pending.catch(() => {
|
|
361
|
+
if (applications.get(env) === pending) applications.delete(env);
|
|
362
|
+
});
|
|
363
|
+
return pending;
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
async fetch(request, env, ctx) {
|
|
367
|
+
return (await application(env)).fetch(request, env, ctx);
|
|
368
|
+
},
|
|
369
|
+
async scheduled(event, env, ctx) {
|
|
370
|
+
return (await application(env)).scheduled(event, env, ctx);
|
|
371
|
+
},
|
|
372
|
+
async queue(batch, env, ctx) {
|
|
373
|
+
return (await application(env)).queue(batch, env, ctx);
|
|
374
|
+
}
|
|
637
375
|
};
|
|
638
376
|
}
|
|
639
377
|
//#endregion
|
|
640
|
-
//#region
|
|
641
|
-
|
|
642
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
643
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
644
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
645
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
646
|
-
}
|
|
378
|
+
//#region src/storage/storage.tokens.ts
|
|
379
|
+
const STORAGE_OPTIONS = new InjectionToken("STORAGE_OPTIONS");
|
|
647
380
|
//#endregion
|
|
648
|
-
//#region src/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
381
|
+
//#region src/storage/storage-key-claim.ts
|
|
382
|
+
/** R2-compatible object-key claim bound. Keeps decode work predictably small. */
|
|
383
|
+
const MAX_STORAGE_KEY_BYTES = 1024;
|
|
384
|
+
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
385
|
+
const ROOT_TOKEN_PATTERNS = {
|
|
386
|
+
date: "\\d{4}-\\d{2}-\\d{2}",
|
|
387
|
+
year: "\\d{4}",
|
|
388
|
+
month: "(?:0[1-9]|1[0-2])",
|
|
389
|
+
day: "(?:0[1-9]|[12]\\d|3[01])",
|
|
390
|
+
uuid: "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"
|
|
657
391
|
};
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
function createBindingModule(opts) {
|
|
666
|
-
const className = `${opts.name}Module`;
|
|
667
|
-
const moduleClass = { [className]: class {} }[className];
|
|
668
|
-
return defineConfigurableModule({
|
|
669
|
-
module: moduleClass,
|
|
670
|
-
methodName: "forRoot",
|
|
671
|
-
keyFrom: ({ binding }) => binding,
|
|
672
|
-
providers: ({ binding }) => [{
|
|
673
|
-
provide: opts.bindingRefToken,
|
|
674
|
-
useValue: new BindingRef(binding)
|
|
675
|
-
}, opts.serviceClass],
|
|
676
|
-
exports: [opts.serviceClass, opts.bindingRefToken]
|
|
677
|
-
});
|
|
392
|
+
/** Encode an object key as an opaque, canonical base64url query claim. */
|
|
393
|
+
function encodeStorageKeyClaim(key) {
|
|
394
|
+
const bytes = new TextEncoder().encode(key);
|
|
395
|
+
if (bytes.byteLength === 0 || bytes.byteLength > 1024) throw new Error(`Storage key must be 1–${MAX_STORAGE_KEY_BYTES} UTF-8 bytes`);
|
|
396
|
+
let binary = "";
|
|
397
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
398
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
678
399
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
};
|
|
697
|
-
D1Service = __decorate([
|
|
698
|
-
Injectable(),
|
|
699
|
-
__decorateParam(0, Inject(D1_BINDING_REF)),
|
|
700
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
701
|
-
], D1Service);
|
|
702
|
-
//#endregion
|
|
703
|
-
//#region src/modules/d1.module.ts
|
|
704
|
-
const D1Module = createBindingModule({
|
|
705
|
-
name: "D1",
|
|
706
|
-
serviceClass: D1Service,
|
|
707
|
-
bindingRefToken: D1_BINDING_REF
|
|
708
|
-
});
|
|
709
|
-
//#endregion
|
|
710
|
-
//#region src/services/r2.service.ts
|
|
711
|
-
let R2Service = class R2Service {
|
|
712
|
-
ref;
|
|
713
|
-
constructor(ref) {
|
|
714
|
-
this.ref = ref;
|
|
715
|
-
}
|
|
716
|
-
get bucket() {
|
|
717
|
-
return this.ref.value;
|
|
718
|
-
}
|
|
719
|
-
};
|
|
720
|
-
R2Service = __decorate([
|
|
721
|
-
Injectable(),
|
|
722
|
-
__decorateParam(0, Inject(R2_BINDING_REF)),
|
|
723
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
724
|
-
], R2Service);
|
|
725
|
-
//#endregion
|
|
726
|
-
//#region src/modules/r2.module.ts
|
|
727
|
-
const R2Module = createBindingModule({
|
|
728
|
-
name: "R2",
|
|
729
|
-
serviceClass: R2Service,
|
|
730
|
-
bindingRefToken: R2_BINDING_REF
|
|
731
|
-
});
|
|
732
|
-
//#endregion
|
|
733
|
-
//#region src/services/queue.service.ts
|
|
734
|
-
let QueueService = class QueueService {
|
|
735
|
-
ref;
|
|
736
|
-
constructor(ref) {
|
|
737
|
-
this.ref = ref;
|
|
738
|
-
}
|
|
739
|
-
get queue() {
|
|
740
|
-
return this.ref.value;
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
QueueService = __decorate([
|
|
744
|
-
Injectable(),
|
|
745
|
-
__decorateParam(0, Inject(QUEUE_BINDING_REF)),
|
|
746
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
747
|
-
], QueueService);
|
|
748
|
-
//#endregion
|
|
749
|
-
//#region src/modules/queue.module.ts
|
|
750
|
-
const QueueModule = createBindingModule({
|
|
751
|
-
name: "Queue",
|
|
752
|
-
serviceClass: QueueService,
|
|
753
|
-
bindingRefToken: QUEUE_BINDING_REF
|
|
754
|
-
});
|
|
755
|
-
//#endregion
|
|
756
|
-
//#region src/services/durable-object.service.ts
|
|
757
|
-
let DurableObjectService = class DurableObjectService {
|
|
758
|
-
ref;
|
|
759
|
-
constructor(ref) {
|
|
760
|
-
this.ref = ref;
|
|
761
|
-
}
|
|
762
|
-
get namespace() {
|
|
763
|
-
return this.ref.value;
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
DurableObjectService = __decorate([
|
|
767
|
-
Injectable(),
|
|
768
|
-
__decorateParam(0, Inject(DO_BINDING_REF)),
|
|
769
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
770
|
-
], DurableObjectService);
|
|
771
|
-
//#endregion
|
|
772
|
-
//#region src/modules/durable-object.module.ts
|
|
773
|
-
const DurableObjectModule = createBindingModule({
|
|
774
|
-
name: "DurableObject",
|
|
775
|
-
serviceClass: DurableObjectService,
|
|
776
|
-
bindingRefToken: DO_BINDING_REF
|
|
777
|
-
});
|
|
778
|
-
//#endregion
|
|
779
|
-
//#region src/services/ai.service.ts
|
|
780
|
-
let AIService = class AIService {
|
|
781
|
-
ref;
|
|
782
|
-
constructor(ref) {
|
|
783
|
-
this.ref = ref;
|
|
784
|
-
}
|
|
785
|
-
get binding() {
|
|
786
|
-
return this.ref.value;
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
AIService = __decorate([
|
|
790
|
-
Injectable(),
|
|
791
|
-
__decorateParam(0, Inject(AI_BINDING_REF)),
|
|
792
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
793
|
-
], AIService);
|
|
794
|
-
//#endregion
|
|
795
|
-
//#region src/modules/ai.module.ts
|
|
796
|
-
const AIModule = createBindingModule({
|
|
797
|
-
name: "AI",
|
|
798
|
-
serviceClass: AIService,
|
|
799
|
-
bindingRefToken: AI_BINDING_REF
|
|
800
|
-
});
|
|
801
|
-
//#endregion
|
|
802
|
-
//#region src/services/vectorize.service.ts
|
|
803
|
-
let VectorizeService = class VectorizeService {
|
|
804
|
-
ref;
|
|
805
|
-
constructor(ref) {
|
|
806
|
-
this.ref = ref;
|
|
807
|
-
}
|
|
808
|
-
get index() {
|
|
809
|
-
return this.ref.value;
|
|
810
|
-
}
|
|
811
|
-
};
|
|
812
|
-
VectorizeService = __decorate([
|
|
813
|
-
Injectable(),
|
|
814
|
-
__decorateParam(0, Inject(VECTORIZE_BINDING_REF)),
|
|
815
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
816
|
-
], VectorizeService);
|
|
817
|
-
//#endregion
|
|
818
|
-
//#region src/modules/vectorize.module.ts
|
|
819
|
-
const VectorizeModule = createBindingModule({
|
|
820
|
-
name: "Vectorize",
|
|
821
|
-
serviceClass: VectorizeService,
|
|
822
|
-
bindingRefToken: VECTORIZE_BINDING_REF
|
|
823
|
-
});
|
|
824
|
-
//#endregion
|
|
825
|
-
//#region src/services/hyperdrive.service.ts
|
|
826
|
-
let HyperdriveService = class HyperdriveService {
|
|
827
|
-
ref;
|
|
828
|
-
constructor(ref) {
|
|
829
|
-
this.ref = ref;
|
|
830
|
-
}
|
|
831
|
-
get binding() {
|
|
832
|
-
return this.ref.value;
|
|
833
|
-
}
|
|
834
|
-
get connectionString() {
|
|
835
|
-
return this.binding.connectionString;
|
|
836
|
-
}
|
|
837
|
-
get host() {
|
|
838
|
-
return this.binding.host;
|
|
839
|
-
}
|
|
840
|
-
get port() {
|
|
841
|
-
return this.binding.port;
|
|
842
|
-
}
|
|
843
|
-
get user() {
|
|
844
|
-
return this.binding.user;
|
|
845
|
-
}
|
|
846
|
-
get password() {
|
|
847
|
-
return this.binding.password;
|
|
848
|
-
}
|
|
849
|
-
get database() {
|
|
850
|
-
return this.binding.database;
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
HyperdriveService = __decorate([
|
|
854
|
-
Injectable(),
|
|
855
|
-
__decorateParam(0, Inject(HYPERDRIVE_BINDING_REF)),
|
|
856
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
857
|
-
], HyperdriveService);
|
|
858
|
-
//#endregion
|
|
859
|
-
//#region src/modules/hyperdrive.module.ts
|
|
860
|
-
const HyperdriveModule = createBindingModule({
|
|
861
|
-
name: "Hyperdrive",
|
|
862
|
-
serviceClass: HyperdriveService,
|
|
863
|
-
bindingRefToken: HYPERDRIVE_BINDING_REF
|
|
864
|
-
});
|
|
865
|
-
//#endregion
|
|
866
|
-
//#region src/services/env.service.ts
|
|
867
|
-
let EnvService = class EnvService {
|
|
868
|
-
ref;
|
|
869
|
-
constructor(ref) {
|
|
870
|
-
this.ref = ref;
|
|
871
|
-
}
|
|
872
|
-
/** The full env record. Throws if read before the first request. */
|
|
873
|
-
get env() {
|
|
874
|
-
return this.ref.value;
|
|
875
|
-
}
|
|
876
|
-
/** Read a single env entry (binding, var, or secret) by name. */
|
|
877
|
-
get(key) {
|
|
878
|
-
return this.ref.value[key];
|
|
400
|
+
/** Decode exactly one canonical base64url layer. Malformed/non-UTF-8 claims return undefined. */
|
|
401
|
+
function decodeStorageKeyClaim(claim) {
|
|
402
|
+
try {
|
|
403
|
+
if (claim.length === 0 || claim.length > Math.ceil(4096 / 3) || !BASE64URL_RE.test(claim) || claim.length % 4 === 1) return;
|
|
404
|
+
const base64 = claim.replace(/-/g, "+").replace(/_/g, "/");
|
|
405
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
406
|
+
const binary = atob(padded);
|
|
407
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
408
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
409
|
+
if (bytes.byteLength === 0 || bytes.byteLength > 1024) return void 0;
|
|
410
|
+
const decoded = new TextDecoder("utf-8", {
|
|
411
|
+
fatal: true,
|
|
412
|
+
ignoreBOM: false
|
|
413
|
+
}).decode(bytes);
|
|
414
|
+
return encodeStorageKeyClaim(decoded) === claim ? decoded : void 0;
|
|
415
|
+
} catch {
|
|
416
|
+
return;
|
|
879
417
|
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
* ```ts
|
|
893
|
-
* @Module({ imports: [EnvModule.forRoot(), AuthModule.forRootAsync({ ... })] })
|
|
894
|
-
* class AppModule {}
|
|
895
|
-
* ```
|
|
896
|
-
*
|
|
897
|
-
* The {@link EnvRef} it provides is collected and initialized by
|
|
898
|
-
* createCloudflareApp's binding-init middleware on the first request (the
|
|
899
|
-
* factory passes it the full `env`, not a single binding).
|
|
900
|
-
*/
|
|
901
|
-
var EnvModule = class EnvModule {
|
|
902
|
-
static forRoot() {
|
|
903
|
-
const ref = new EnvRef();
|
|
904
|
-
return {
|
|
905
|
-
module: EnvModule,
|
|
906
|
-
global: true,
|
|
907
|
-
providers: [{
|
|
908
|
-
provide: ENV_REF,
|
|
909
|
-
useValue: ref
|
|
910
|
-
}, EnvService],
|
|
911
|
-
exports: [EnvService, ENV_REF]
|
|
912
|
-
};
|
|
418
|
+
}
|
|
419
|
+
function isDotSegment(segment) {
|
|
420
|
+
let decoded = segment;
|
|
421
|
+
for (let i = 0; i < 2; i++) {
|
|
422
|
+
if (decoded === "" || decoded === "." || decoded === "..") return true;
|
|
423
|
+
try {
|
|
424
|
+
const next = decodeURIComponent(decoded);
|
|
425
|
+
if (next === decoded) break;
|
|
426
|
+
decoded = next;
|
|
427
|
+
} catch {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
913
430
|
}
|
|
914
|
-
|
|
431
|
+
return decoded === "" || decoded === "." || decoded === "..";
|
|
432
|
+
}
|
|
433
|
+
function rootSegmentPattern(segment) {
|
|
434
|
+
let pattern = "";
|
|
435
|
+
let cursor = 0;
|
|
436
|
+
for (const match of segment.matchAll(/\{(date|year|month|day|uuid)\}/g)) {
|
|
437
|
+
pattern += segment.slice(cursor, match.index).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
438
|
+
pattern += ROOT_TOKEN_PATTERNS[match[1]];
|
|
439
|
+
cursor = match.index + match[0].length;
|
|
440
|
+
}
|
|
441
|
+
pattern += segment.slice(cursor).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
442
|
+
return pattern;
|
|
443
|
+
}
|
|
444
|
+
/** Assert a canonical key is beneath a static or templated configured root. */
|
|
445
|
+
function isStorageKeyWithinRoot(key, root) {
|
|
446
|
+
const segments = (root ?? "").split(/[/\\]+/).filter((segment) => !isDotSegment(segment));
|
|
447
|
+
if (segments.length === 0) return key.length > 0;
|
|
448
|
+
const rootPattern = segments.map(rootSegmentPattern).join("/");
|
|
449
|
+
return new RegExp(`^(?:${rootPattern})(?:/|$)`).test(key);
|
|
450
|
+
}
|
|
915
451
|
//#endregion
|
|
916
452
|
//#region src/storage/r2-storage.driver.ts
|
|
917
453
|
/** Base path of the StorageController presign-proxy route. */
|
|
@@ -955,9 +491,19 @@ var R2StorageDriver = class {
|
|
|
955
491
|
}
|
|
956
492
|
async getPresignedUrl(path, method, expiresIn) {
|
|
957
493
|
if (!this.config.secret) throw new Error("A signing secret is required for presigned URLs (set APP_SECRET).");
|
|
958
|
-
if (!Number.
|
|
494
|
+
if (!Number.isSafeInteger(expiresIn) || expiresIn <= 0) throw new Error(`Invalid presigned URL expiry: ${expiresIn}s (must be a positive safe integer).`);
|
|
495
|
+
const claim = encodeStorageKeyClaim(path);
|
|
496
|
+
const routePath = `${STORAGE_ROUTE_BASE}/${encodeURIComponent(this.config.disk)}`;
|
|
497
|
+
const query = new URLSearchParams({
|
|
498
|
+
key: claim,
|
|
499
|
+
method
|
|
500
|
+
});
|
|
959
501
|
return {
|
|
960
|
-
url: await signUrl(`${
|
|
502
|
+
url: await signUrl(`${routePath}?${query}`, this.config.secret, {
|
|
503
|
+
expiresIn,
|
|
504
|
+
method,
|
|
505
|
+
purpose: STORAGE_SIGNED_URL_PURPOSE
|
|
506
|
+
}),
|
|
961
507
|
method,
|
|
962
508
|
expiresIn,
|
|
963
509
|
expiresAt: new Date(Date.now() + expiresIn * 1e3)
|
|
@@ -965,16 +511,23 @@ var R2StorageDriver = class {
|
|
|
965
511
|
}
|
|
966
512
|
};
|
|
967
513
|
//#endregion
|
|
968
|
-
//#region
|
|
969
|
-
|
|
514
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
|
|
515
|
+
function __decorateMetadata(k, v) {
|
|
516
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
|
|
520
|
+
function __decorateParam(paramIndex, decorator) {
|
|
521
|
+
return function(target, key) {
|
|
522
|
+
decorator(target, key, paramIndex);
|
|
523
|
+
};
|
|
524
|
+
}
|
|
970
525
|
//#endregion
|
|
971
526
|
//#region src/storage/storage-manager.service.ts
|
|
972
527
|
let StorageManagerService = class StorageManagerService {
|
|
973
528
|
options;
|
|
974
|
-
|
|
975
|
-
constructor(options, env) {
|
|
529
|
+
constructor(options) {
|
|
976
530
|
this.options = options;
|
|
977
|
-
this.env = env;
|
|
978
531
|
}
|
|
979
532
|
hasDisk(disk) {
|
|
980
533
|
return this.options.disks.some((d) => d.disk === disk);
|
|
@@ -985,53 +538,59 @@ let StorageManagerService = class StorageManagerService {
|
|
|
985
538
|
return config;
|
|
986
539
|
}
|
|
987
540
|
getDriver(disk) {
|
|
988
|
-
const config = this.getDiskConfig(disk);
|
|
989
|
-
const bucket = this.env.get(config.binding);
|
|
990
|
-
if (!bucket) throw new Error(`R2 binding "${config.binding}" for disk "${disk}" was not found in env.`);
|
|
991
541
|
return new R2StorageDriver({
|
|
992
542
|
disk,
|
|
993
|
-
bucket,
|
|
994
|
-
secret: this.
|
|
543
|
+
bucket: this.getDiskConfig(disk).bucket,
|
|
544
|
+
secret: this.options.secret
|
|
995
545
|
});
|
|
996
546
|
}
|
|
997
547
|
};
|
|
998
548
|
StorageManagerService = __decorate([
|
|
999
549
|
Injectable(),
|
|
1000
550
|
__decorateParam(0, Inject(STORAGE_OPTIONS)),
|
|
1001
|
-
|
|
1002
|
-
__decorateMetadata("design:paramtypes", [Object, typeof EnvService === "undefined" ? Object : EnvService])
|
|
551
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
1003
552
|
], StorageManagerService);
|
|
1004
553
|
//#endregion
|
|
1005
554
|
//#region src/storage/storage.controller.ts
|
|
1006
555
|
let StorageController = class StorageController {
|
|
1007
556
|
manager;
|
|
1008
|
-
|
|
1009
|
-
constructor(manager,
|
|
557
|
+
options;
|
|
558
|
+
constructor(manager, options) {
|
|
1010
559
|
this.manager = manager;
|
|
1011
|
-
this.
|
|
560
|
+
this.options = options;
|
|
1012
561
|
}
|
|
1013
562
|
async download(c) {
|
|
1014
|
-
const secret = this.
|
|
563
|
+
const secret = this.options.secret;
|
|
1015
564
|
if (!secret) return new Response("Storage signing is not configured", { status: 500 });
|
|
565
|
+
if (!await verifySignedUrl(c.req.url, secret, {
|
|
566
|
+
method: c.req.method,
|
|
567
|
+
purpose: STORAGE_SIGNED_URL_PURPOSE
|
|
568
|
+
})) return new Response("Invalid or expired URL", { status: 403 });
|
|
569
|
+
const url = new URL(c.req.url);
|
|
570
|
+
if (url.searchParams.get("method") !== "GET") return new Response("URL is not scoped for reads", { status: 403 });
|
|
1016
571
|
const disk = c.req.param("disk");
|
|
1017
572
|
if (!disk || !this.manager.hasDisk(disk)) return new Response("Unknown disk", { status: 404 });
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
if (
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const fullPath = idx >= 0 ? decodeURIComponent(pathname.slice(idx + prefix.length)) : "";
|
|
573
|
+
const claim = url.searchParams.get("key");
|
|
574
|
+
const decoded = claim ? decodeStorageKeyClaim(claim) : void 0;
|
|
575
|
+
if (!decoded) return new Response("Malformed storage key claim", { status: 400 });
|
|
576
|
+
const fullPath = joinStoragePath(void 0, decoded);
|
|
577
|
+
if (fullPath !== decoded) return new Response("Invalid storage key", { status: 403 });
|
|
578
|
+
if (!isStorageKeyWithinRoot(fullPath, this.manager.getDiskConfig(disk).root)) return new Response("Storage key is outside the configured root", { status: 403 });
|
|
1025
579
|
try {
|
|
1026
580
|
const result = await this.manager.getDriver(disk).download(fullPath);
|
|
1027
|
-
|
|
581
|
+
const filename = fullPath.split("/").at(-1) || "download";
|
|
582
|
+
return new Response(result.toStream(), { headers: {
|
|
583
|
+
"content-type": result.contentType || "application/octet-stream",
|
|
584
|
+
"content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
|
585
|
+
"x-content-type-options": "nosniff"
|
|
586
|
+
} });
|
|
1028
587
|
} catch {
|
|
1029
588
|
return new Response("Not found", { status: 404 });
|
|
1030
589
|
}
|
|
1031
590
|
}
|
|
1032
591
|
};
|
|
1033
592
|
__decorate([
|
|
1034
|
-
Get("/:disk
|
|
593
|
+
Get("/:disk"),
|
|
1035
594
|
__decorateParam(0, Req()),
|
|
1036
595
|
__decorateMetadata("design:type", Function),
|
|
1037
596
|
__decorateMetadata("design:paramtypes", [Object]),
|
|
@@ -1040,8 +599,8 @@ __decorate([
|
|
|
1040
599
|
StorageController = __decorate([
|
|
1041
600
|
Controller("storage"),
|
|
1042
601
|
__decorateParam(0, Inject(StorageManagerService)),
|
|
1043
|
-
__decorateParam(1, Inject(
|
|
1044
|
-
__decorateMetadata("design:paramtypes", [typeof StorageManagerService === "undefined" ? Object : StorageManagerService,
|
|
602
|
+
__decorateParam(1, Inject(STORAGE_OPTIONS)),
|
|
603
|
+
__decorateMetadata("design:paramtypes", [typeof StorageManagerService === "undefined" ? Object : StorageManagerService, Object])
|
|
1045
604
|
], StorageController);
|
|
1046
605
|
//#endregion
|
|
1047
606
|
//#region src/storage/storage.service.ts
|
|
@@ -1087,7 +646,7 @@ let StorageService = class StorageService {
|
|
|
1087
646
|
validateExpiry(expiresIn) {
|
|
1088
647
|
const cfg = this.options.presignedUrl ?? DEFAULT_PRESIGN;
|
|
1089
648
|
const value = expiresIn ?? cfg.defaultExpiry;
|
|
1090
|
-
if (!Number.
|
|
649
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > cfg.maxExpiry) throw new Error(`Presigned URL expiry ${value}s is out of range (1–${cfg.maxExpiry}s).`);
|
|
1091
650
|
return value;
|
|
1092
651
|
}
|
|
1093
652
|
};
|
|
@@ -1105,7 +664,6 @@ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
|
|
|
1105
664
|
}).build();
|
|
1106
665
|
let StorageModule = class StorageModule extends ConfigurableModuleClass {};
|
|
1107
666
|
StorageModule = __decorate([Module({
|
|
1108
|
-
imports: [EnvModule.forRoot()],
|
|
1109
667
|
providers: [StorageManagerService, StorageService],
|
|
1110
668
|
controllers: [StorageController],
|
|
1111
669
|
exports: [
|
|
@@ -1116,13 +674,19 @@ StorageModule = __decorate([Module({
|
|
|
1116
674
|
})], StorageModule);
|
|
1117
675
|
//#endregion
|
|
1118
676
|
//#region src/services/kv-cache.store.ts
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
677
|
+
/**
|
|
678
|
+
* Cloudflare KV-backed {@link CacheStore}. Values are JSON-encoded. Intended as
|
|
679
|
+
* the slow tier under a `TieredCacheStore` (memory L1 → KV L2), but usable
|
|
680
|
+
* standalone from a typed environment factory: `new KVCacheStore(env.CACHE)`.
|
|
681
|
+
* Reads return unknown JSON; validate values at the consuming boundary.
|
|
682
|
+
*
|
|
683
|
+
* Note: Cloudflare KV requires `expirationTtl >= 60s`, so sub-minute TTLs are
|
|
684
|
+
* clamped up. Keep short TTLs on the memory tier; use KV for longer-lived entries.
|
|
685
|
+
*/
|
|
686
|
+
var KVCacheStore = class {
|
|
687
|
+
ns;
|
|
688
|
+
constructor(ns) {
|
|
689
|
+
this.ns = ns;
|
|
1126
690
|
}
|
|
1127
691
|
async get(key) {
|
|
1128
692
|
const value = await this.ns.get(key, "json");
|
|
@@ -1144,11 +708,6 @@ let KVCacheStore = class KVCacheStore {
|
|
|
1144
708
|
} while (cursor);
|
|
1145
709
|
}
|
|
1146
710
|
};
|
|
1147
|
-
KVCacheStore = __decorate([
|
|
1148
|
-
Injectable(),
|
|
1149
|
-
__decorateParam(0, Inject(KVService)),
|
|
1150
|
-
__decorateMetadata("design:paramtypes", [typeof KVService === "undefined" ? Object : KVService])
|
|
1151
|
-
], KVCacheStore);
|
|
1152
711
|
//#endregion
|
|
1153
712
|
//#region src/services/flagship-flag.driver.ts
|
|
1154
713
|
/**
|
|
@@ -1161,16 +720,12 @@ KVCacheStore = __decorate([
|
|
|
1161
720
|
* dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
|
|
1162
721
|
* the never-throw guarantee.
|
|
1163
722
|
*
|
|
1164
|
-
*
|
|
1165
|
-
* wiring from a bootstrap factory (mirroring how {@link EnvService} reads are
|
|
1166
|
-
* deferred); a resolved binding may be passed directly in tests.
|
|
723
|
+
* Build the driver inside a provider factory with the native environment:
|
|
1167
724
|
*
|
|
1168
725
|
* ```ts
|
|
1169
726
|
* FeatureFlagsModule.forRootAsync({
|
|
1170
|
-
* inject: [
|
|
1171
|
-
* useFactory: (env:
|
|
1172
|
-
* drivers: [flagshipFlagDriver(() => env.get<FlagshipBinding>('FLAGS')!)],
|
|
1173
|
-
* }),
|
|
727
|
+
* inject: [ENV],
|
|
728
|
+
* useFactory: (env: WorkerEnv) => ({ drivers: [flagshipFlagDriver(env.FLAGS)] }),
|
|
1174
729
|
* });
|
|
1175
730
|
* ```
|
|
1176
731
|
*
|
|
@@ -1204,18 +759,36 @@ function flagshipFlagDriver(binding, options) {
|
|
|
1204
759
|
}
|
|
1205
760
|
//#endregion
|
|
1206
761
|
//#region src/services/kv-flag.driver.ts
|
|
1207
|
-
|
|
1208
|
-
|
|
762
|
+
/**
|
|
763
|
+
* Cloudflare KV-backed {@link FeatureFlagDriver}. Flags are stored as JSON
|
|
764
|
+
* values under an optional key prefix and read with `get(key, 'json')`. Reads
|
|
765
|
+
* are type-checked against the requested type: a missing key or a value of the
|
|
766
|
+
* wrong JSON type returns the caller's `fallback`. KV has no targeting, so the
|
|
767
|
+
* evaluation context is ignored.
|
|
768
|
+
*
|
|
769
|
+
* The driver stays honest — it does **not** swallow errors. A KV failure (or a
|
|
770
|
+
* `SyntaxError` from a malformed stored value) propagates; the never-throw
|
|
771
|
+
* guarantee lives in `@velajs/feature-flags`'s service layer.
|
|
772
|
+
*
|
|
773
|
+
* Placed like {@link KVCacheStore}: construct it in a wiring factory over a
|
|
774
|
+
* resolved {@link KVNamespace}.
|
|
775
|
+
*
|
|
776
|
+
* ```ts
|
|
777
|
+
* FeatureFlagsModule.forRootAsync({
|
|
778
|
+
* inject: [ENV],
|
|
779
|
+
* useFactory: (env: WorkerEnv) => ({ drivers: [new KvFlagDriver(env.CACHE, { prefix: 'flag:' })] }),
|
|
780
|
+
* });
|
|
781
|
+
* ```
|
|
782
|
+
*/
|
|
783
|
+
var KvFlagDriver = class {
|
|
784
|
+
ns;
|
|
1209
785
|
name;
|
|
1210
786
|
prefix;
|
|
1211
|
-
constructor(
|
|
1212
|
-
this.
|
|
787
|
+
constructor(ns, options = {}) {
|
|
788
|
+
this.ns = ns;
|
|
1213
789
|
this.name = options.name ?? "kv";
|
|
1214
790
|
this.prefix = options.prefix ?? "";
|
|
1215
791
|
}
|
|
1216
|
-
get ns() {
|
|
1217
|
-
return this.kv.namespace;
|
|
1218
|
-
}
|
|
1219
792
|
getBoolean(key, fallback, _ctx) {
|
|
1220
793
|
return this.read(key, fallback, (v) => typeof v === "boolean");
|
|
1221
794
|
}
|
|
@@ -1239,11 +812,6 @@ let KvFlagDriver = class KvFlagDriver {
|
|
|
1239
812
|
return matches(value) ? value : fallback;
|
|
1240
813
|
}
|
|
1241
814
|
};
|
|
1242
|
-
KvFlagDriver = __decorate([
|
|
1243
|
-
Injectable(),
|
|
1244
|
-
__decorateParam(0, Inject(KVService)),
|
|
1245
|
-
__decorateMetadata("design:paramtypes", [typeof KVService === "undefined" ? Object : KVService, Object])
|
|
1246
|
-
], KvFlagDriver);
|
|
1247
815
|
/** Convenience factory for {@link KvFlagDriver}. */
|
|
1248
816
|
function kvFlagDriver(kv, options) {
|
|
1249
817
|
return new KvFlagDriver(kv, options);
|
|
@@ -1259,7 +827,7 @@ function kvFlagDriver(kv, options) {
|
|
|
1259
827
|
* @example
|
|
1260
828
|
* ```ts
|
|
1261
829
|
* @Get()
|
|
1262
|
-
* handle(@Env() env:
|
|
830
|
+
* handle(@Env() env: WorkerEnv) { ... }
|
|
1263
831
|
*
|
|
1264
832
|
* @Get()
|
|
1265
833
|
* handle(@Env('MY_KV') kv: KVNamespace) { ... }
|
|
@@ -1267,8 +835,8 @@ function kvFlagDriver(kv, options) {
|
|
|
1267
835
|
*/
|
|
1268
836
|
const Env = createParamDecorator((bindingName, ctx) => {
|
|
1269
837
|
const env = ctx.getContext().env;
|
|
1270
|
-
if (
|
|
1271
|
-
return bindingName ? env
|
|
838
|
+
if (typeof env !== "object" || env === null) return void 0;
|
|
839
|
+
return bindingName ? Reflect.get(env, bindingName) : env;
|
|
1272
840
|
});
|
|
1273
841
|
//#endregion
|
|
1274
842
|
//#region src/decorators/scheduled.ts
|
|
@@ -1338,262 +906,6 @@ function QueueConsumer(queueName) {
|
|
|
1338
906
|
};
|
|
1339
907
|
}
|
|
1340
908
|
//#endregion
|
|
1341
|
-
//#region src/websocket/cf-room-registry.ts
|
|
1342
|
-
/**
|
|
1343
|
-
* Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room
|
|
1344
|
-
* membership lives in tags (hub room, set at accept) + the attachment (dynamic
|
|
1345
|
-
* `join()`), never in DO instance fields — so it survives hibernation with zero
|
|
1346
|
-
* rehydration.
|
|
1347
|
-
*/
|
|
1348
|
-
var CfRoomRegistry = class {
|
|
1349
|
-
ctx;
|
|
1350
|
-
constructor(ctx) {
|
|
1351
|
-
this.ctx = ctx;
|
|
1352
|
-
}
|
|
1353
|
-
register(_client) {}
|
|
1354
|
-
leaveAll(_client) {}
|
|
1355
|
-
join(client, room) {
|
|
1356
|
-
return client.join(room);
|
|
1357
|
-
}
|
|
1358
|
-
leave(client, room) {
|
|
1359
|
-
return client.leave(room);
|
|
1360
|
-
}
|
|
1361
|
-
localIdsInRoom(room) {
|
|
1362
|
-
return this.socketsForRoom(room).map((ws) => this.attachmentOf(ws).connId);
|
|
1363
|
-
}
|
|
1364
|
-
deliverLocal(cmd) {
|
|
1365
|
-
const excludeIds = new Set(cmd.exceptIds ?? []);
|
|
1366
|
-
const excludeRooms = cmd.exceptRooms ?? [];
|
|
1367
|
-
const targets = cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);
|
|
1368
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1369
|
-
for (const ws of targets) {
|
|
1370
|
-
const att = this.attachmentOf(ws);
|
|
1371
|
-
if (seen.has(att.connId)) continue;
|
|
1372
|
-
seen.add(att.connId);
|
|
1373
|
-
if (excludeIds.has(att.connId)) continue;
|
|
1374
|
-
if (excludeRooms.some((r) => att.rooms.includes(r))) continue;
|
|
1375
|
-
ws.send(cmd.frame);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
collectRooms(rooms) {
|
|
1379
|
-
const set = /* @__PURE__ */ new Set();
|
|
1380
|
-
for (const room of rooms) for (const ws of this.socketsForRoom(room)) set.add(ws);
|
|
1381
|
-
return [...set];
|
|
1382
|
-
}
|
|
1383
|
-
socketsForRoom(room) {
|
|
1384
|
-
return this.ctx.getWebSockets().filter((ws) => this.attachmentOf(ws).rooms.includes(room));
|
|
1385
|
-
}
|
|
1386
|
-
attachmentOf(ws) {
|
|
1387
|
-
return ws.deserializeAttachment() ?? {
|
|
1388
|
-
connId: "",
|
|
1389
|
-
path: "",
|
|
1390
|
-
rooms: [],
|
|
1391
|
-
data: {}
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
/** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */
|
|
1395
|
-
clientFor(ws) {
|
|
1396
|
-
return new CfWsClient(this.ctx, ws);
|
|
1397
|
-
}
|
|
1398
|
-
};
|
|
1399
|
-
//#endregion
|
|
1400
|
-
//#region src/websocket/ws-server-holder.ts
|
|
1401
|
-
let WsServerHolder = class WsServerHolder {
|
|
1402
|
-
target;
|
|
1403
|
-
setTarget(server) {
|
|
1404
|
-
this.target = server;
|
|
1405
|
-
}
|
|
1406
|
-
get resolved() {
|
|
1407
|
-
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, room, ...).");
|
|
1408
|
-
return this.target;
|
|
1409
|
-
}
|
|
1410
|
-
emit(event, data) {
|
|
1411
|
-
return this.resolved.emit(event, data);
|
|
1412
|
-
}
|
|
1413
|
-
to(room) {
|
|
1414
|
-
return this.resolved.to(room);
|
|
1415
|
-
}
|
|
1416
|
-
in(room) {
|
|
1417
|
-
return this.resolved.in(room);
|
|
1418
|
-
}
|
|
1419
|
-
except(room) {
|
|
1420
|
-
return this.resolved.except(room);
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
|
-
WsServerHolder = __decorate([Injectable()], WsServerHolder);
|
|
1424
|
-
//#endregion
|
|
1425
|
-
//#region src/websocket/do-bootstrap.ts
|
|
1426
|
-
/**
|
|
1427
|
-
* Slim DI bootstrap for the Durable Object isolate: wires the container and runs
|
|
1428
|
-
* `OnModuleInit`/`OnApplicationBootstrap` (so `WsDispatcher` discovers gateways)
|
|
1429
|
-
* WITHOUT building the Hono app/routes the DO never serves. Cloudflare binding
|
|
1430
|
-
* refs are initialized straight from the DO's `env`, and the ctx-backed server
|
|
1431
|
-
* is bound before bootstrap lifecycle so gateway `afterInit`/handlers see it.
|
|
1432
|
-
*/
|
|
1433
|
-
async function buildDoRuntime(rootModule, ctx, env) {
|
|
1434
|
-
const { container, routeManager, loader } = await bootstrap(rootModule);
|
|
1435
|
-
for (const value of container.getUseValues()) if (value instanceof EnvRef) value._initialize(env);
|
|
1436
|
-
else if (value instanceof BindingRef) value._initialize(env[value.bindingName]);
|
|
1437
|
-
const registry = new CfRoomRegistry(ctx);
|
|
1438
|
-
const driver = local();
|
|
1439
|
-
driver.bind(registry);
|
|
1440
|
-
const server = new WsServerImpl(driver);
|
|
1441
|
-
try {
|
|
1442
|
-
const holder = container.resolve(WS_SERVER);
|
|
1443
|
-
if (holder instanceof WsServerHolder) holder.setTarget(server);
|
|
1444
|
-
} catch {}
|
|
1445
|
-
const app = new VelaApplication(container, routeManager);
|
|
1446
|
-
app.setInstances(await loader.resolveAllInstances());
|
|
1447
|
-
await app.callOnModuleInit();
|
|
1448
|
-
await app.callOnApplicationBootstrap();
|
|
1449
|
-
const wsEntrypoints = app.entrypoints.ofKind("websocket");
|
|
1450
|
-
const live = initDoLive(app, container, ctx);
|
|
1451
|
-
return {
|
|
1452
|
-
dispatcher: wsEntrypoints[0]?.meta.dispatcher ?? app.get(WsDispatcher),
|
|
1453
|
-
registry,
|
|
1454
|
-
server,
|
|
1455
|
-
gatewayPaths: wsEntrypoints.map((ep) => ep.meta.path),
|
|
1456
|
-
live,
|
|
1457
|
-
close: (signal) => app.close(signal)
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
//#endregion
|
|
1461
|
-
//#region src/websocket/do-websocket-host.ts
|
|
1462
|
-
/**
|
|
1463
|
-
* The socket lifecycle inside a WebSocket Durable Object, decoupled from the
|
|
1464
|
-
* `cloudflare:workers` base class so it is unit-testable with fakes. The thin
|
|
1465
|
-
* `VelaWebSocketDurableObject` shell forwards its hibernation callbacks here.
|
|
1466
|
-
*/
|
|
1467
|
-
var DoWebSocketHost = class {
|
|
1468
|
-
ctx;
|
|
1469
|
-
dispatcher;
|
|
1470
|
-
registry;
|
|
1471
|
-
gatewayPaths;
|
|
1472
|
-
constructor(ctx, dispatcher, registry, gatewayPaths = []) {
|
|
1473
|
-
this.ctx = ctx;
|
|
1474
|
-
this.dispatcher = dispatcher;
|
|
1475
|
-
this.registry = registry;
|
|
1476
|
-
this.gatewayPaths = gatewayPaths;
|
|
1477
|
-
}
|
|
1478
|
-
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */
|
|
1479
|
-
defaultPath() {
|
|
1480
|
-
return this.gatewayPaths[0];
|
|
1481
|
-
}
|
|
1482
|
-
/**
|
|
1483
|
-
* Accept a hibernatable socket: tag it with its hub room + connection id,
|
|
1484
|
-
* persist the attachment, then fire `OnGatewayConnection` WITHOUT blocking the
|
|
1485
|
-
* 101 response (the caller returns it immediately).
|
|
1486
|
-
*/
|
|
1487
|
-
accept(ws, path, roomId, userId) {
|
|
1488
|
-
const connId = crypto.randomUUID();
|
|
1489
|
-
this.ctx.acceptWebSocket(ws, [roomTag(roomId), connTag(connId)]);
|
|
1490
|
-
const attachment = {
|
|
1491
|
-
connId,
|
|
1492
|
-
userId,
|
|
1493
|
-
path,
|
|
1494
|
-
rooms: [roomId],
|
|
1495
|
-
data: userId ? { userId } : {}
|
|
1496
|
-
};
|
|
1497
|
-
ws.serializeAttachment(attachment);
|
|
1498
|
-
Promise.resolve(this.dispatcher.handleOpen(path, new CfWsClient(this.ctx, ws))).catch((err) => console.warn("[vela] websocket handleConnection failed:", err));
|
|
1499
|
-
}
|
|
1500
|
-
async onMessage(ws, message) {
|
|
1501
|
-
const client = new CfWsClient(this.ctx, ws);
|
|
1502
|
-
await this.dispatcher.dispatchMessage(client.path, client, message);
|
|
1503
|
-
}
|
|
1504
|
-
async onClose(ws, code, reason) {
|
|
1505
|
-
const client = new CfWsClient(this.ctx, ws);
|
|
1506
|
-
await this.dispatcher.handleClose(client.path, client, code, reason);
|
|
1507
|
-
try {
|
|
1508
|
-
if (code === 1e3 || code >= 3e3 && code <= 4999) ws.close(code, reason);
|
|
1509
|
-
else ws.close();
|
|
1510
|
-
} catch {}
|
|
1511
|
-
}
|
|
1512
|
-
async onError(ws, err) {
|
|
1513
|
-
const client = new CfWsClient(this.ctx, ws);
|
|
1514
|
-
await this.dispatcher.handleError(client.path, client, err);
|
|
1515
|
-
}
|
|
1516
|
-
/** Deliver a broadcast command to this DO's local sockets (RPC entry point). */
|
|
1517
|
-
broadcast(cmd) {
|
|
1518
|
-
this.registry.deliverLocal(cmd);
|
|
1519
|
-
}
|
|
1520
|
-
};
|
|
1521
|
-
//#endregion
|
|
1522
|
-
//#region src/websocket/websocket.durable-object.ts
|
|
1523
|
-
const PING = "{\"event\":\"ping\"}";
|
|
1524
|
-
const PONG = "{\"event\":\"pong\"}";
|
|
1525
|
-
/**
|
|
1526
|
-
* Base class for the WebSocket Durable Object. The user exports a named subclass
|
|
1527
|
-
* (matching their `wrangler.toml` `class_name`) built from their `AppModule`:
|
|
1528
|
-
*
|
|
1529
|
-
* ```ts
|
|
1530
|
-
* export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
|
|
1531
|
-
* ```
|
|
1532
|
-
*
|
|
1533
|
-
* It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
|
|
1534
|
-
* bridge DO hibernation) and forwards every event into the runtime-agnostic
|
|
1535
|
-
* `WsDispatcher` via {@link DoWebSocketHost}.
|
|
1536
|
-
*/
|
|
1537
|
-
function VelaWebSocketDurableObject(rootModule) {
|
|
1538
|
-
return class VelaWsDurableObject extends DurableObject {
|
|
1539
|
-
host;
|
|
1540
|
-
liveEngine;
|
|
1541
|
-
ready;
|
|
1542
|
-
constructor(ctx, env) {
|
|
1543
|
-
super(ctx, env);
|
|
1544
|
-
try {
|
|
1545
|
-
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));
|
|
1546
|
-
} catch {}
|
|
1547
|
-
this.ready = ctx.blockConcurrencyWhile(async () => {
|
|
1548
|
-
const runtime = await buildDoRuntime(rootModule, ctx, env);
|
|
1549
|
-
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
|
|
1550
|
-
this.liveEngine = runtime.live;
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
async fetch(request) {
|
|
1554
|
-
await this.ready;
|
|
1555
|
-
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
1556
|
-
const { 0: client, 1: server } = new WebSocketPair();
|
|
1557
|
-
const url = new URL(request.url);
|
|
1558
|
-
const roomId = request.headers.get("x-vela-room") ?? this.ctx.id.name ?? url.pathname;
|
|
1559
|
-
const path = request.headers.get("x-vela-path") ?? this.host.defaultPath() ?? url.pathname;
|
|
1560
|
-
const userId = request.headers.get("x-vela-user") || void 0;
|
|
1561
|
-
this.host.accept(server, path, roomId, userId);
|
|
1562
|
-
return new Response(null, {
|
|
1563
|
-
status: 101,
|
|
1564
|
-
webSocket: client
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
async webSocketMessage(ws, message) {
|
|
1568
|
-
await this.ready;
|
|
1569
|
-
await this.host.onMessage(ws, message);
|
|
1570
|
-
}
|
|
1571
|
-
async webSocketClose(ws, code, reason) {
|
|
1572
|
-
await this.ready;
|
|
1573
|
-
await this.host.onClose(ws, code, reason);
|
|
1574
|
-
}
|
|
1575
|
-
async webSocketError(ws, error) {
|
|
1576
|
-
await this.ready;
|
|
1577
|
-
await this.host.onError(ws, error);
|
|
1578
|
-
}
|
|
1579
|
-
/** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */
|
|
1580
|
-
async broadcast(cmd) {
|
|
1581
|
-
await this.ready;
|
|
1582
|
-
this.host.broadcast(cmd);
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /
|
|
1586
|
-
* liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns
|
|
1587
|
-
* the commit stamp (what `Vela-Commit-Cursor` carries); the subscription
|
|
1588
|
-
* refreshes fan out asynchronously.
|
|
1589
|
-
*/
|
|
1590
|
-
async invalidate(cmd) {
|
|
1591
|
-
await this.ready;
|
|
1592
|
-
return this.liveEngine?.applyInvalidation(cmd);
|
|
1593
|
-
}
|
|
1594
|
-
};
|
|
1595
|
-
}
|
|
1596
|
-
//#endregion
|
|
1597
909
|
//#region src/websocket/cloudflare-websocket.module.ts
|
|
1598
910
|
/**
|
|
1599
911
|
* Cloudflare counterpart to the core `WebSocketModule.forRoot()`. Import this in
|
|
@@ -1604,12 +916,10 @@ function VelaWebSocketDurableObject(rootModule) {
|
|
|
1604
916
|
*/
|
|
1605
917
|
var CloudflareWebSocketModule = class CloudflareWebSocketModule {
|
|
1606
918
|
static forRoot() {
|
|
919
|
+
const providers = [defineProvider(WS_SERVER, { useClass: WsServerHolder }), WsDispatcher];
|
|
1607
920
|
return {
|
|
1608
921
|
module: CloudflareWebSocketModule,
|
|
1609
|
-
providers
|
|
1610
|
-
provide: WS_SERVER,
|
|
1611
|
-
useClass: WsServerHolder
|
|
1612
|
-
}, WsDispatcher],
|
|
922
|
+
providers,
|
|
1613
923
|
exports: [WS_SERVER, WsDispatcher]
|
|
1614
924
|
};
|
|
1615
925
|
}
|
|
@@ -1624,11 +934,11 @@ var CloudflareWebSocketModule = class CloudflareWebSocketModule {
|
|
|
1624
934
|
*
|
|
1625
935
|
* @example
|
|
1626
936
|
* ```ts
|
|
1627
|
-
* // In a controller — ns from
|
|
1628
|
-
* await broadcastToRoom(ns, `org:${id}`, 'order.created', order);
|
|
937
|
+
* // In a controller — ns from the typed Worker environment
|
|
938
|
+
* await broadcastToRoom(ns, '/orgs/:orgId/ws', `org:${id}`, 'order.created', order);
|
|
1629
939
|
* ```
|
|
1630
940
|
*/
|
|
1631
|
-
async function broadcastToRoom(ns, room, event, data, options) {
|
|
941
|
+
async function broadcastToRoom(ns, gatewayPath, room, event, data, options) {
|
|
1632
942
|
const cmd = {
|
|
1633
943
|
rooms: [room],
|
|
1634
944
|
exceptIds: options?.exceptIds,
|
|
@@ -1637,9 +947,77 @@ async function broadcastToRoom(ns, room, event, data, options) {
|
|
|
1637
947
|
data
|
|
1638
948
|
})
|
|
1639
949
|
};
|
|
1640
|
-
|
|
950
|
+
const maxFrameBytes = resolveMaxFrameBytes({ maxFrameBytes: options?.maxFrameBytes ?? DEFAULT_WS_MAX_FRAME_BYTES });
|
|
951
|
+
assertBroadcastCommandFits(cmd, maxFrameBytes);
|
|
952
|
+
await ns.get(roomToDurableId(ns, gatewayPath, room)).broadcast(cmd);
|
|
953
|
+
}
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/rate-limit/cloudflare-rate-limit.store.ts
|
|
956
|
+
/**
|
|
957
|
+
* Adapt a Cloudflare Workers Rate Limiting binding to Vela's throttler store.
|
|
958
|
+
*
|
|
959
|
+
* The platform binding makes the allow/deny decision. It does not expose exact
|
|
960
|
+
* counters or reset timestamps, so this adapter intentionally omits `remaining`.
|
|
961
|
+
*/
|
|
962
|
+
function cloudflareRateLimitStore(binding, options) {
|
|
963
|
+
if (!binding || typeof binding !== "function" && typeof binding.limit !== "function") throw new TypeError("A Cloudflare Rate Limiting binding is required");
|
|
964
|
+
if (!Number.isSafeInteger(options.limit) || options.limit <= 0 || options.limit >= Number.MAX_SAFE_INTEGER) throw new RangeError("Rate limit must be a positive safe integer");
|
|
965
|
+
if (options.periodSeconds !== 10 && options.periodSeconds !== 60) throw new RangeError("Cloudflare rate-limit periods must be 10 or 60 seconds");
|
|
966
|
+
const maxKeyBytes = options.maxKeyBytes ?? 1024;
|
|
967
|
+
if (!Number.isSafeInteger(maxKeyBytes) || maxKeyBytes <= 0 || maxKeyBytes > 4096) throw new RangeError("maxKeyBytes must be between 1 and 4096");
|
|
968
|
+
const ttlMs = options.periodSeconds * 1e3;
|
|
969
|
+
const encoder = new TextEncoder();
|
|
970
|
+
const resolveBinding = typeof binding === "function" ? binding : () => binding;
|
|
971
|
+
return {
|
|
972
|
+
async increment(key, requestedTtlMs) {
|
|
973
|
+
if (requestedTtlMs !== ttlMs) throw new Error(`Cloudflare binding period mismatch: expected ${ttlMs}ms, received ${requestedTtlMs}ms`);
|
|
974
|
+
if (typeof key !== "string" || key.length === 0 || /[\u0000-\u001f\u007f]/.test(key) || encoder.encode(key).byteLength > maxKeyBytes) throw new Error("Refusing an invalid or oversized rate-limit key");
|
|
975
|
+
const currentBinding = resolveBinding();
|
|
976
|
+
if (!currentBinding || typeof currentBinding.limit !== "function") throw new Error("Cloudflare Rate Limiting binding is unavailable");
|
|
977
|
+
const decision = await currentBinding.limit({ key });
|
|
978
|
+
if (!decision || typeof decision.success !== "boolean") throw new Error("Cloudflare rate-limit binding returned an invalid decision");
|
|
979
|
+
return {
|
|
980
|
+
count: decision.success ? 0 : options.limit + 1,
|
|
981
|
+
ttlMs,
|
|
982
|
+
allowed: decision.success,
|
|
983
|
+
enforcedLimit: options.limit
|
|
984
|
+
};
|
|
985
|
+
},
|
|
986
|
+
reset() {
|
|
987
|
+
throw new Error("Cloudflare Rate Limiting bindings do not support counter reset");
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/nonce/durable-object-nonce.store.ts
|
|
993
|
+
const APP_NAMESPACE_PREFIX = "vela:nonce:v1:";
|
|
994
|
+
const MAX_APP_NAMESPACE_BYTES = 128;
|
|
995
|
+
/**
|
|
996
|
+
* Strict, cross-isolate {@link NonceStore} backed by one SQLite Durable Object
|
|
997
|
+
* per explicit application namespace.
|
|
998
|
+
*
|
|
999
|
+
* Invalid input, an unavailable/malformed binding, RPC failure, or a malformed
|
|
1000
|
+
* RPC result all deny the claim (`false`). Only the literal boolean `true` from
|
|
1001
|
+
* the Durable Object is accepted.
|
|
1002
|
+
*/
|
|
1003
|
+
function durableObjectNonceStore(options) {
|
|
1004
|
+
if (!options || typeof options !== "object") throw new TypeError("Durable Object nonce-store options are required");
|
|
1005
|
+
if (!isCanonicalBoundedText(options.appNamespace, MAX_APP_NAMESPACE_BYTES)) throw new TypeError(`appNamespace must be canonical, non-empty, and at most ${MAX_APP_NAMESPACE_BYTES} UTF-8 bytes`);
|
|
1006
|
+
if (typeof options.binding !== "function") throw new TypeError("A lazy Durable Object namespace binding resolver is required");
|
|
1007
|
+
const objectName = `${APP_NAMESPACE_PREFIX}${options.appNamespace}`;
|
|
1008
|
+
return { async claim(nonce, expEpochSeconds) {
|
|
1009
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1010
|
+
if (!isCanonicalBoundedText(nonce, 512) || !isValidExpiry(expEpochSeconds, now)) return false;
|
|
1011
|
+
try {
|
|
1012
|
+
const namespace = await options.binding();
|
|
1013
|
+
const id = namespace.idFromName(objectName);
|
|
1014
|
+
return await namespace.get(id).claim(nonce, expEpochSeconds) === true;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
} };
|
|
1641
1019
|
}
|
|
1642
1020
|
//#endregion
|
|
1643
|
-
export {
|
|
1021
|
+
export { CloudflareApplication, CloudflareWebSocketModule, ConnectedSocket, DoCursorLog, DoPitrUnavailableError, Env, FlagshipFlagDriver, KVCacheStore, KvFlagDriver, MessageBody, QueueConsumer, R2StorageDriver, STORAGE_OPTIONS, Scheduled, StorageController, StorageManagerService, StorageModule, StorageService, SubscribeMessage, WebSocketGateway, WebSocketServer, WsException, armDoPitr, broadcastToRoom, cloudflareAdapter, cloudflareRateLimitStore, createCloudflareApp, createCloudflareWorker, durableObjectCursorLog, durableObjectLive, durableObjectNonceStore, durableObjectRoomName, flagshipFlagDriver, isDoPitrUnavailable, kvFlagDriver, liveInvalidateToRoom, readDoPitrBookmark };
|
|
1644
1022
|
|
|
1645
1023
|
//# sourceMappingURL=index.js.map
|