@velajs/cloudflare 1.10.0 → 2.0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +14 -3
  2. package/LICENSE +21 -0
  3. package/README.md +197 -322
  4. package/dist/durable-objects.d.ts +28 -0
  5. package/dist/durable-objects.js +494 -0
  6. package/dist/durable-objects.js.map +1 -0
  7. package/dist/index.d.ts +622 -41
  8. package/dist/index.js +1023 -39
  9. package/dist/index.js.map +1 -0
  10. package/dist/nonce-validation-LE0vFk-7.js +445 -0
  11. package/dist/nonce-validation-LE0vFk-7.js.map +1 -0
  12. package/dist/nonce.durable-object-Df3_42Sy.d.ts +141 -0
  13. package/package.json +66 -44
  14. package/dist/binding-ref.d.ts +0 -8
  15. package/dist/binding-ref.js +0 -19
  16. package/dist/cloudflare-application.d.ts +0 -125
  17. package/dist/cloudflare-application.js +0 -186
  18. package/dist/cloudflare-factory.d.ts +0 -88
  19. package/dist/cloudflare-factory.js +0 -107
  20. package/dist/decorators/env.d.ts +0 -16
  21. package/dist/decorators/env.js +0 -22
  22. package/dist/decorators/queue-consumer.d.ts +0 -23
  23. package/dist/decorators/queue-consumer.js +0 -40
  24. package/dist/decorators/scheduled.d.ts +0 -20
  25. package/dist/decorators/scheduled.js +0 -36
  26. package/dist/env-ref.d.ts +0 -10
  27. package/dist/env-ref.js +0 -13
  28. package/dist/modules/ai.module.d.ts +0 -1
  29. package/dist/modules/ai.module.js +0 -8
  30. package/dist/modules/create-binding-module.d.ts +0 -14
  31. package/dist/modules/create-binding-module.js +0 -42
  32. package/dist/modules/d1.module.d.ts +0 -1
  33. package/dist/modules/d1.module.js +0 -8
  34. package/dist/modules/durable-object.module.d.ts +0 -1
  35. package/dist/modules/durable-object.module.js +0 -8
  36. package/dist/modules/env.module.d.ts +0 -17
  37. package/dist/modules/env.module.js +0 -35
  38. package/dist/modules/hyperdrive.module.d.ts +0 -1
  39. package/dist/modules/hyperdrive.module.js +0 -8
  40. package/dist/modules/kv.module.d.ts +0 -1
  41. package/dist/modules/kv.module.js +0 -8
  42. package/dist/modules/queue.module.d.ts +0 -1
  43. package/dist/modules/queue.module.js +0 -8
  44. package/dist/modules/r2.module.d.ts +0 -1
  45. package/dist/modules/r2.module.js +0 -8
  46. package/dist/modules/vectorize.module.d.ts +0 -1
  47. package/dist/modules/vectorize.module.js +0 -8
  48. package/dist/services/ai.service.d.ts +0 -6
  49. package/dist/services/ai.service.js +0 -33
  50. package/dist/services/d1.service.d.ts +0 -6
  51. package/dist/services/d1.service.js +0 -33
  52. package/dist/services/durable-object.service.d.ts +0 -6
  53. package/dist/services/durable-object.service.js +0 -33
  54. package/dist/services/env.service.d.ts +0 -31
  55. package/dist/services/env.service.js +0 -36
  56. package/dist/services/flagship-flag.driver.d.ts +0 -57
  57. package/dist/services/flagship-flag.driver.js +0 -49
  58. package/dist/services/hyperdrive.service.d.ts +0 -12
  59. package/dist/services/hyperdrive.service.js +0 -51
  60. package/dist/services/kv-cache.store.d.ts +0 -19
  61. package/dist/services/kv-cache.store.js +0 -58
  62. package/dist/services/kv-flag.driver.d.ts +0 -48
  63. package/dist/services/kv-flag.driver.js +0 -62
  64. package/dist/services/kv.service.d.ts +0 -11
  65. package/dist/services/kv.service.js +0 -33
  66. package/dist/services/queue.service.d.ts +0 -6
  67. package/dist/services/queue.service.js +0 -33
  68. package/dist/services/r2.service.d.ts +0 -6
  69. package/dist/services/r2.service.js +0 -33
  70. package/dist/services/vectorize.service.d.ts +0 -6
  71. package/dist/services/vectorize.service.js +0 -33
  72. package/dist/storage/index.d.ts +0 -7
  73. package/dist/storage/index.js +0 -6
  74. package/dist/storage/r2-storage.driver.d.ts +0 -19
  75. package/dist/storage/r2-storage.driver.js +0 -61
  76. package/dist/storage/storage-manager.service.d.ts +0 -16
  77. package/dist/storage/storage-manager.service.js +0 -56
  78. package/dist/storage/storage.controller.d.ts +0 -16
  79. package/dist/storage/storage.controller.js +0 -88
  80. package/dist/storage/storage.module.d.ts +0 -7
  81. package/dist/storage/storage.module.js +0 -37
  82. package/dist/storage/storage.service.d.ts +0 -20
  83. package/dist/storage/storage.service.js +0 -78
  84. package/dist/storage/storage.tokens.d.ts +0 -3
  85. package/dist/storage/storage.tokens.js +0 -2
  86. package/dist/storage/storage.types.d.ts +0 -17
  87. package/dist/storage/storage.types.js +0 -1
  88. package/dist/tokens.d.ts +0 -12
  89. package/dist/tokens.js +0 -12
  90. package/dist/types.d.ts +0 -13
  91. package/dist/types.js +0 -1
  92. package/dist/websocket/broadcast.d.ts +0 -15
  93. package/dist/websocket/broadcast.js +0 -26
  94. package/dist/websocket/cf-room-registry.d.ts +0 -23
  95. package/dist/websocket/cf-room-registry.js +0 -65
  96. package/dist/websocket/cf-ws-client.d.ts +0 -28
  97. package/dist/websocket/cf-ws-client.js +0 -83
  98. package/dist/websocket/cloudflare-websocket.module.d.ts +0 -11
  99. package/dist/websocket/cloudflare-websocket.module.js +0 -27
  100. package/dist/websocket/do-bootstrap.d.ts +0 -24
  101. package/dist/websocket/do-bootstrap.js +0 -55
  102. package/dist/websocket/do-live.d.ts +0 -81
  103. package/dist/websocket/do-live.js +0 -197
  104. package/dist/websocket/do-state.d.ts +0 -35
  105. package/dist/websocket/do-state.js +0 -4
  106. package/dist/websocket/do-websocket-host.d.ts +0 -28
  107. package/dist/websocket/do-websocket-host.js +0 -69
  108. package/dist/websocket/index.d.ts +0 -15
  109. package/dist/websocket/index.js +0 -15
  110. package/dist/websocket/room-id.d.ts +0 -6
  111. package/dist/websocket/room-id.js +0 -12
  112. package/dist/websocket/websocket-routing.d.ts +0 -14
  113. package/dist/websocket/websocket-routing.js +0 -45
  114. package/dist/websocket/websocket.durable-object.d.ts +0 -15
  115. package/dist/websocket/websocket.durable-object.js +0 -83
  116. package/dist/websocket/ws-server-holder.d.ts +0 -16
  117. package/dist/websocket/ws-server-holder.js +0 -34
package/dist/index.js CHANGED
@@ -1,39 +1,1023 @@
1
- // Factory & Application
2
- export { cloudflareAdapter, createCloudflareApp } from "./cloudflare-factory.js";
3
- export { CloudflareApplication } from "./cloudflare-application.js";
4
- // Modules
5
- export { KVModule } from "./modules/kv.module.js";
6
- export { D1Module } from "./modules/d1.module.js";
7
- export { R2Module } from "./modules/r2.module.js";
8
- export { QueueModule } from "./modules/queue.module.js";
9
- export { DurableObjectModule } from "./modules/durable-object.module.js";
10
- export { AIModule } from "./modules/ai.module.js";
11
- export { VectorizeModule } from "./modules/vectorize.module.js";
12
- export { HyperdriveModule } from "./modules/hyperdrive.module.js";
13
- export { EnvModule } from "./modules/env.module.js";
14
- // Storage (multi-disk over R2 + presign proxy)
15
- export { StorageModule, StorageService, StorageManagerService, StorageController, R2StorageDriver, STORAGE_OPTIONS } from "./storage/index.js";
16
- // Services
17
- export { KVService } from "./services/kv.service.js";
18
- export { KVCacheStore } from "./services/kv-cache.store.js";
19
- export { D1Service } from "./services/d1.service.js";
20
- export { R2Service } from "./services/r2.service.js";
21
- export { QueueService } from "./services/queue.service.js";
22
- export { DurableObjectService } from "./services/durable-object.service.js";
23
- export { AIService } from "./services/ai.service.js";
24
- export { VectorizeService } from "./services/vectorize.service.js";
25
- export { HyperdriveService } from "./services/hyperdrive.service.js";
26
- export { EnvService } from "./services/env.service.js";
27
- // Feature-flag drivers (implement @velajs/feature-flags' FeatureFlagDriver contract)
28
- export { FlagshipFlagDriver, flagshipFlagDriver } from "./services/flagship-flag.driver.js";
29
- export { KvFlagDriver, kvFlagDriver } from "./services/kv-flag.driver.js";
30
- // Decorators
31
- export { Env } from "./decorators/env.js";
32
- export { Scheduled } from "./decorators/scheduled.js";
33
- export { QueueConsumer } from "./decorators/queue-consumer.js";
34
- // WebSocket (Durable Object transport for the Vela WebSocketModule)
35
- export { VelaWebSocketDurableObject, CloudflareWebSocketModule, broadcastToRoom } from "./websocket/index.js";
36
- // Live queries (Durable Object transport for @velajs/vela/live)
37
- export { DoCursorLog, durableObjectCursorLog, durableObjectLive, liveInvalidateToRoom } from "./websocket/index.js";
38
- // Re-export the core gateway API so a Cloudflare app can import it from one place.
39
- export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException } from "@velajs/vela/websocket";
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";
4
+ import { ComponentManager } from "@velajs/vela/internal";
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;
12
+ }
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
+ };
24
+ }
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
+ }
37
+ /** Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted gateways only). */
38
+ function collectWsGatewayRoutes(instance) {
39
+ const options = getMetadata(WS_GATEWAY_METADATA, instance.constructor);
40
+ if (!options?.path || !options?.binding) return [];
41
+ resolveGatewayRoomParam(options);
42
+ resolveMaxFrameBytes(options);
43
+ return [{
44
+ path: options.path,
45
+ binding: options.binding,
46
+ options: { ...options }
47
+ }];
48
+ }
49
+ /**
50
+ * Registers the upgrade routes on the Worker's Hono app. Each route validates
51
+ * the `Upgrade` header, resolves the room's Durable Object, and forwards the raw
52
+ * request — injecting spoof-safe `x-vela-*` headers the DO reads. The DO returns
53
+ * the `101` with the client socket.
54
+ */
55
+ function registerWebSocketRoutes(hono, routes) {
56
+ for (const route of routes) hono.get(route.path, async (c) => {
57
+ if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return c.text("Expected WebSocket upgrade", 426);
58
+ const headers = new Headers(c.req.raw.headers);
59
+ headers.delete("x-vela-room");
60
+ headers.delete("x-vela-path");
61
+ headers.delete("x-vela-user");
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 }));
94
+ });
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
+ }
117
+ //#endregion
118
+ //#region src/cloudflare-application.ts
119
+ registerEntrypointKind({
120
+ kind: "cf:vela-cron",
121
+ metaKey: CRON_METADATA,
122
+ level: "method"
123
+ });
124
+ function invoke(instance, methodName, args) {
125
+ const method = Reflect.get(instance, methodName);
126
+ if (typeof method !== "function") throw new Error(`Method '${methodName}' is not a function on ${instance.constructor.name}`);
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;
134
+ }
135
+ /**
136
+ * Wraps VelaApplication with Cloudflare-specific handlers:
137
+ * - `fetch` — HTTP request handler (from Hono)
138
+ * - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
139
+ * AND vela's own `@Cron()` jobs)
140
+ * - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
141
+ * - `mountOpenApi` — Serve an OpenAPI document (and optional Scalar UI) on
142
+ * the underlying Hono app
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
147
+ * export default {
148
+ * fetch: app.fetch,
149
+ * scheduled: app.scheduled.bind(app),
150
+ * queue: app.queue.bind(app),
151
+ * };
152
+ * ```
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * // Serve OpenAPI docs alongside your routes
157
+ * const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
158
+ * const document = createOpenApiDocument(AppModule);
159
+ * app.mountOpenApi({ document, ui: 'scalar' });
160
+ * // GET /openapi.json -> JSON document
161
+ * // GET /scalar -> Scalar UI (loads from CDN)
162
+ * ```
163
+ */
164
+ var CloudflareApplication = class {
165
+ app;
166
+ env;
167
+ wsGatewayRoutes = [];
168
+ constructor(app, env) {
169
+ this.app = app;
170
+ this.env = env;
171
+ this.get = app.get.bind(app);
172
+ }
173
+ fetch = async (request, env, ctx) => {
174
+ assertCloudflareEnvironment(this.env, env);
175
+ return this.app.fetch(request, env, ctx);
176
+ };
177
+ getHonoApp() {
178
+ return this.app.getHonoApp();
179
+ }
180
+ /**
181
+ * Resolve a provider from the application's DI container (delegates to
182
+ * `VelaApplication.get`). Handy for grabbing a service — e.g. an auth service —
183
+ * to use inside `createCloudflareApp({ middleware: env => [...] })` request middleware,
184
+ * which runs outside the DI request pipeline.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
189
+ * const auth = app.get(BetterAuthService);
190
+ * ```
191
+ */
192
+ get;
193
+ get entrypoints() {
194
+ return this.app.entrypoints;
195
+ }
196
+ /**
197
+ * Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
198
+ * underlying Hono app. Delegates verbatim to `VelaApplication.mountOpenApi`,
199
+ * so the JSON endpoint defaults to `/openapi.json` and the Scalar UI (when
200
+ * opted in) defaults to `/scalar`. Edge-safe — the UI HTML loads Scalar from
201
+ * a CDN at runtime, nothing is bundled server-side.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * import { createOpenApiDocument } from '@velajs/vela';
206
+ *
207
+ * const app = await createCloudflareApp(AppModule, { env, envToken: ENV });
208
+ * const document = createOpenApiDocument(AppModule, {
209
+ * info: { title: 'My API', version: '1.0.0' },
210
+ * });
211
+ * app.mountOpenApi({ document, ui: 'scalar' });
212
+ * // GET /openapi.json -> { openapi: '3.1.0', ... }
213
+ * // GET /scalar -> Scalar UI HTML
214
+ * ```
215
+ */
216
+ mountOpenApi(options) {
217
+ this.app.mountOpenApi(options);
218
+ return this;
219
+ }
220
+ /**
221
+ * @internal — scans instances for `@WebSocketGateway({ path, binding })`
222
+ * upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
223
+ * come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
224
+ * kinds) at dispatch time.
225
+ */
226
+ scanInstances(instances) {
227
+ for (const instance of instances) {
228
+ if (!instance || typeof instance !== "object") continue;
229
+ this.wsGatewayRoutes.push(...collectWsGatewayRoutes(instance));
230
+ }
231
+ }
232
+ /** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */
233
+ getWsGatewayRoutes() {
234
+ return this.wsGatewayRoutes;
235
+ }
236
+ /**
237
+ * Handle Cloudflare scheduled (cron) events.
238
+ * Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
239
+ * handlers read from `app.entrypoints`; each handler runs inside a fresh
240
+ * request-scoped child (request-scoped providers rebuild per tick).
241
+ */
242
+ async scheduled(event, env, ctx) {
243
+ assertCloudflareEnvironment(this.env, env);
244
+ const handlers = [...this.app.entrypoints.ofKind("cf:scheduled").map((ep) => ({
245
+ ep,
246
+ cron: entrypointString(ep.meta, "cron")
247
+ })), ...this.app.entrypoints.ofKind("cf:vela-cron").map((ep) => ({
248
+ ep,
249
+ cron: entrypointString(ep.meta, "expression")
250
+ }))].filter((h) => h.cron === event.cron);
251
+ await Promise.all(handlers.map(({ ep }) => this.dispatchEntrypoint(ep, [
252
+ event,
253
+ env,
254
+ ctx
255
+ ])));
256
+ }
257
+ /**
258
+ * Run one entrypoint handler inside a fresh request scope, through the
259
+ * shared guard → interceptor pipeline (components declared with
260
+ * `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
261
+ * method). HTTP-global components deliberately do NOT apply — an HTTP auth
262
+ * guard has no business rejecting a queue batch. Unclaimed errors rethrow
263
+ * so the platform's retry semantics stay intact.
264
+ */
265
+ async dispatchEntrypoint(ep, args) {
266
+ const targetClass = ep.token;
267
+ if (typeof targetClass !== "function") throw new Error("Entrypoint token must be a class.");
268
+ const methodName = String(ep.methodName);
269
+ const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, args[0]);
270
+ await runInEntrypointScope(this.app.getContainer(), async (scope) => {
271
+ const instance = scope.resolve(ep.token);
272
+ if (typeof instance !== "object" || instance === null) throw new Error("Entrypoint must resolve to an object.");
273
+ const guards = ComponentManager.resolveGuards(ComponentManager.getScopedComponents("guard", targetClass, methodName), scope);
274
+ const interceptors = ComponentManager.resolveInterceptors(ComponentManager.getScopedComponents("interceptor", targetClass, methodName), scope);
275
+ const filters = ComponentManager.resolveFilters([...ComponentManager.getScopedComponents("filter", targetClass, methodName)].reverse(), scope);
276
+ try {
277
+ await PipelineRunner.run({
278
+ context,
279
+ guards,
280
+ interceptors,
281
+ resolveArgs: async () => args,
282
+ invoke: async (resolved) => invoke(instance, methodName, resolved)
283
+ });
284
+ } catch (error) {
285
+ for (const filter of filters) if (shouldFilterCatch(filter, error)) {
286
+ await filter.catch(error, context);
287
+ return;
288
+ }
289
+ throw error;
290
+ }
291
+ });
292
+ }
293
+ /**
294
+ * Handle Cloudflare Queue consumer events.
295
+ * Matches the batch queue name to `@QueueConsumer()` handlers read from
296
+ * `app.entrypoints`; each batch is processed inside a fresh request-scoped
297
+ * child (request-scoped providers rebuild per batch — no boot-time captives).
298
+ */
299
+ async queue(batch, env, ctx) {
300
+ assertCloudflareEnvironment(this.env, env);
301
+ const handlers = this.app.entrypoints.ofKind("cf:queue").filter((ep) => entrypointString(ep.meta, "queueName") === batch.queue);
302
+ await Promise.all(handlers.map((ep) => this.dispatchEntrypoint(ep, [
303
+ batch,
304
+ env,
305
+ ctx
306
+ ])));
307
+ }
308
+ async close(signal) {
309
+ return this.app.close(signal);
310
+ }
311
+ };
312
+ //#endregion
313
+ //#region src/cloudflare-factory.ts
314
+ /** Bind an application to one environment before provider factories and lifecycle hooks. */
315
+ function cloudflareAdapter(options) {
316
+ return {
317
+ name: "cloudflare",
318
+ requestMiddleware: [async (context, next) => {
319
+ assertCloudflareEnvironment(options.env, context.env);
320
+ await next();
321
+ }],
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
+ });
329
+ }
330
+ };
331
+ }
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), {
335
+ globalPrefix: options.globalPrefix,
336
+ security: options.security,
337
+ middleware: options.middleware?.(options.env),
338
+ adapters: [cloudflareAdapter(options)]
339
+ });
340
+ const app = new CloudflareApplication(velaApp, options.env);
341
+ app.scanInstances(velaApp.getInstances());
342
+ registerWebSocketRoutes(app.getHonoApp(), app.getWsGatewayRoutes());
343
+ return app;
344
+ }
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
+ }
375
+ };
376
+ }
377
+ //#endregion
378
+ //#region src/storage/storage.tokens.ts
379
+ const STORAGE_OPTIONS = new InjectionToken("STORAGE_OPTIONS");
380
+ //#endregion
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}"
391
+ };
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(/=+$/, "");
399
+ }
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;
417
+ }
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
+ }
430
+ }
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
+ }
451
+ //#endregion
452
+ //#region src/storage/r2-storage.driver.ts
453
+ /** Base path of the StorageController presign-proxy route. */
454
+ const STORAGE_ROUTE_BASE = "/storage";
455
+ /** {@link StorageDriver} over a Cloudflare R2 bucket. */
456
+ var R2StorageDriver = class {
457
+ config;
458
+ constructor(config) {
459
+ this.config = config;
460
+ }
461
+ async upload(body, path, options) {
462
+ await this.config.bucket.put(path, body, {
463
+ httpMetadata: options.mimeType ? { contentType: options.mimeType } : void 0,
464
+ customMetadata: options.metadata
465
+ });
466
+ return {
467
+ path,
468
+ disk: this.config.disk,
469
+ size: options.size,
470
+ mimeType: options.mimeType ?? "application/octet-stream",
471
+ uploadedAt: /* @__PURE__ */ new Date()
472
+ };
473
+ }
474
+ async download(path) {
475
+ const obj = await this.config.bucket.get(path);
476
+ if (!obj) throw new Error(`Storage object not found at "${path}".`);
477
+ return {
478
+ toStream: () => obj.body,
479
+ toArrayBuffer: () => obj.arrayBuffer(),
480
+ toText: () => obj.text(),
481
+ contentType: obj.httpMetadata?.contentType ?? "application/octet-stream",
482
+ size: obj.size,
483
+ metadata: obj.customMetadata
484
+ };
485
+ }
486
+ async delete(path) {
487
+ await this.config.bucket.delete(path);
488
+ }
489
+ async exists(path) {
490
+ return await this.config.bucket.head(path) !== null;
491
+ }
492
+ async getPresignedUrl(path, method, expiresIn) {
493
+ if (!this.config.secret) throw new Error("A signing secret is required for presigned URLs (set APP_SECRET).");
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
+ });
501
+ return {
502
+ url: await signUrl(`${routePath}?${query}`, this.config.secret, {
503
+ expiresIn,
504
+ method,
505
+ purpose: STORAGE_SIGNED_URL_PURPOSE
506
+ }),
507
+ method,
508
+ expiresIn,
509
+ expiresAt: new Date(Date.now() + expiresIn * 1e3)
510
+ };
511
+ }
512
+ };
513
+ //#endregion
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
+ }
525
+ //#endregion
526
+ //#region src/storage/storage-manager.service.ts
527
+ let StorageManagerService = class StorageManagerService {
528
+ options;
529
+ constructor(options) {
530
+ this.options = options;
531
+ }
532
+ hasDisk(disk) {
533
+ return this.options.disks.some((d) => d.disk === disk);
534
+ }
535
+ getDiskConfig(disk) {
536
+ const config = this.options.disks.find((d) => d.disk === disk);
537
+ if (!config) throw new Error(`Storage disk "${disk}" is not configured.`);
538
+ return config;
539
+ }
540
+ getDriver(disk) {
541
+ return new R2StorageDriver({
542
+ disk,
543
+ bucket: this.getDiskConfig(disk).bucket,
544
+ secret: this.options.secret
545
+ });
546
+ }
547
+ };
548
+ StorageManagerService = __decorate([
549
+ Injectable(),
550
+ __decorateParam(0, Inject(STORAGE_OPTIONS)),
551
+ __decorateMetadata("design:paramtypes", [Object])
552
+ ], StorageManagerService);
553
+ //#endregion
554
+ //#region src/storage/storage.controller.ts
555
+ let StorageController = class StorageController {
556
+ manager;
557
+ options;
558
+ constructor(manager, options) {
559
+ this.manager = manager;
560
+ this.options = options;
561
+ }
562
+ async download(c) {
563
+ const secret = this.options.secret;
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 });
571
+ const disk = c.req.param("disk");
572
+ if (!disk || !this.manager.hasDisk(disk)) return new Response("Unknown disk", { status: 404 });
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 });
579
+ try {
580
+ const result = await this.manager.getDriver(disk).download(fullPath);
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
+ } });
587
+ } catch {
588
+ return new Response("Not found", { status: 404 });
589
+ }
590
+ }
591
+ };
592
+ __decorate([
593
+ Get("/:disk"),
594
+ __decorateParam(0, Req()),
595
+ __decorateMetadata("design:type", Function),
596
+ __decorateMetadata("design:paramtypes", [Object]),
597
+ __decorateMetadata("design:returntype", Promise)
598
+ ], StorageController.prototype, "download", null);
599
+ StorageController = __decorate([
600
+ Controller("storage"),
601
+ __decorateParam(0, Inject(StorageManagerService)),
602
+ __decorateParam(1, Inject(STORAGE_OPTIONS)),
603
+ __decorateMetadata("design:paramtypes", [typeof StorageManagerService === "undefined" ? Object : StorageManagerService, Object])
604
+ ], StorageController);
605
+ //#endregion
606
+ //#region src/storage/storage.service.ts
607
+ const DEFAULT_PRESIGN = {
608
+ defaultExpiry: 3600,
609
+ maxExpiry: 86400
610
+ };
611
+ let StorageService = class StorageService {
612
+ options;
613
+ manager;
614
+ constructor(options, manager) {
615
+ this.options = options;
616
+ this.manager = manager;
617
+ }
618
+ put(relativePath, body, options = {}, disk) {
619
+ const name = this.resolveDisk(disk);
620
+ return this.manager.getDriver(name).upload(body, this.fullPath(relativePath, name), options);
621
+ }
622
+ get(relativePath, disk) {
623
+ const name = this.resolveDisk(disk);
624
+ return this.manager.getDriver(name).download(this.fullPath(relativePath, name));
625
+ }
626
+ delete(relativePath, disk) {
627
+ const name = this.resolveDisk(disk);
628
+ return this.manager.getDriver(name).delete(this.fullPath(relativePath, name));
629
+ }
630
+ exists(relativePath, disk) {
631
+ const name = this.resolveDisk(disk);
632
+ return this.manager.getDriver(name).exists(this.fullPath(relativePath, name));
633
+ }
634
+ url(relativePath, method = "GET", expiresIn, disk) {
635
+ const name = this.resolveDisk(disk);
636
+ return this.manager.getDriver(name).getPresignedUrl(this.fullPath(relativePath, name), method, this.validateExpiry(expiresIn));
637
+ }
638
+ resolveDisk(disk) {
639
+ const name = disk ?? this.options.defaultDisk;
640
+ if (!this.manager.hasDisk(name)) throw new Error(`Storage disk "${name}" is not configured.`);
641
+ return name;
642
+ }
643
+ fullPath(relativePath, disk) {
644
+ return joinStoragePath(this.manager.getDiskConfig(disk).root, relativePath);
645
+ }
646
+ validateExpiry(expiresIn) {
647
+ const cfg = this.options.presignedUrl ?? DEFAULT_PRESIGN;
648
+ const value = expiresIn ?? cfg.defaultExpiry;
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).`);
650
+ return value;
651
+ }
652
+ };
653
+ StorageService = __decorate([
654
+ Injectable(),
655
+ __decorateParam(0, Inject(STORAGE_OPTIONS)),
656
+ __decorateParam(1, Inject(StorageManagerService)),
657
+ __decorateMetadata("design:paramtypes", [Object, typeof StorageManagerService === "undefined" ? Object : StorageManagerService])
658
+ ], StorageService);
659
+ //#endregion
660
+ //#region src/storage/storage.module.ts
661
+ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
662
+ moduleName: "Storage",
663
+ optionsInjectionToken: STORAGE_OPTIONS
664
+ }).build();
665
+ let StorageModule = class StorageModule extends ConfigurableModuleClass {};
666
+ StorageModule = __decorate([Module({
667
+ providers: [StorageManagerService, StorageService],
668
+ controllers: [StorageController],
669
+ exports: [
670
+ StorageService,
671
+ StorageManagerService,
672
+ STORAGE_OPTIONS
673
+ ]
674
+ })], StorageModule);
675
+ //#endregion
676
+ //#region src/services/kv-cache.store.ts
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;
690
+ }
691
+ async get(key) {
692
+ const value = await this.ns.get(key, "json");
693
+ return value === null ? void 0 : value;
694
+ }
695
+ async set(key, value, ttl) {
696
+ const options = ttl !== void 0 ? { expirationTtl: Math.max(60, Math.floor(ttl)) } : void 0;
697
+ await this.ns.put(key, JSON.stringify(value), options);
698
+ }
699
+ async del(key) {
700
+ await this.ns.delete(key);
701
+ }
702
+ async clear() {
703
+ let cursor;
704
+ do {
705
+ const list = await this.ns.list(cursor ? { cursor } : void 0);
706
+ await Promise.all(list.keys.map((entry) => this.ns.delete(entry.name)));
707
+ cursor = list.list_complete ? void 0 : list.cursor;
708
+ } while (cursor);
709
+ }
710
+ };
711
+ //#endregion
712
+ //#region src/services/flagship-flag.driver.ts
713
+ /**
714
+ * {@link FeatureFlagDriver} backed by a Cloudflare Flagship binding.
715
+ *
716
+ * A thin, honest wrapper: each contract method maps 1:1 onto the binding's
717
+ * corresponding value method, forwarding the caller's `fallback` (the binding's
718
+ * `defaultValue`) and evaluation context. The binding resolves the fallback on
719
+ * evaluation errors; anything the binding *rejects* with (e.g. a `remote: true`
720
+ * dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
721
+ * the never-throw guarantee.
722
+ *
723
+ * Build the driver inside a provider factory with the native environment:
724
+ *
725
+ * ```ts
726
+ * FeatureFlagsModule.forRootAsync({
727
+ * inject: [ENV],
728
+ * useFactory: (env: WorkerEnv) => ({ drivers: [flagshipFlagDriver(env.FLAGS)] }),
729
+ * });
730
+ * ```
731
+ *
732
+ * Evaluation ergonomics (the 1:1 binding-method mapping) are ported from the
733
+ * Stratal feature-flags service (MIT, © Temitayo Fadojutimi), reshaped as a
734
+ * bare driver.
735
+ */
736
+ var FlagshipFlagDriver = class {
737
+ name;
738
+ resolve;
739
+ constructor(binding, options = {}) {
740
+ this.resolve = typeof binding === "function" ? binding : () => binding;
741
+ this.name = options.name ?? "flagship";
742
+ }
743
+ getBoolean(key, fallback, ctx) {
744
+ return this.resolve().getBooleanValue(key, fallback, ctx);
745
+ }
746
+ getString(key, fallback, ctx) {
747
+ return this.resolve().getStringValue(key, fallback, ctx);
748
+ }
749
+ getNumber(key, fallback, ctx) {
750
+ return this.resolve().getNumberValue(key, fallback, ctx);
751
+ }
752
+ getObject(key, fallback, ctx) {
753
+ return this.resolve().getObjectValue(key, fallback, ctx);
754
+ }
755
+ };
756
+ /** Convenience factory for {@link FlagshipFlagDriver}. */
757
+ function flagshipFlagDriver(binding, options) {
758
+ return new FlagshipFlagDriver(binding, options);
759
+ }
760
+ //#endregion
761
+ //#region src/services/kv-flag.driver.ts
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;
785
+ name;
786
+ prefix;
787
+ constructor(ns, options = {}) {
788
+ this.ns = ns;
789
+ this.name = options.name ?? "kv";
790
+ this.prefix = options.prefix ?? "";
791
+ }
792
+ getBoolean(key, fallback, _ctx) {
793
+ return this.read(key, fallback, (v) => typeof v === "boolean");
794
+ }
795
+ getString(key, fallback, _ctx) {
796
+ return this.read(key, fallback, (v) => typeof v === "string");
797
+ }
798
+ getNumber(key, fallback, _ctx) {
799
+ return this.read(key, fallback, (v) => typeof v === "number");
800
+ }
801
+ getObject(key, fallback, _ctx) {
802
+ return this.read(key, fallback, (v) => typeof v === "object" && v !== null);
803
+ }
804
+ /**
805
+ * Reads and JSON-parses the (prefixed) key, returning the parsed value only
806
+ * when `matches` accepts its type; otherwise the caller's fallback. A missing
807
+ * key reads as `null` → fallback. Read/parse errors are left to propagate.
808
+ */
809
+ async read(key, fallback, matches) {
810
+ const value = await this.ns.get(this.prefix + key, "json");
811
+ if (value === null || value === void 0) return fallback;
812
+ return matches(value) ? value : fallback;
813
+ }
814
+ };
815
+ /** Convenience factory for {@link KvFlagDriver}. */
816
+ function kvFlagDriver(kv, options) {
817
+ return new KvFlagDriver(kv, options);
818
+ }
819
+ //#endregion
820
+ //#region src/decorators/env.ts
821
+ /**
822
+ * Parameter decorator to inject Cloudflare environment bindings.
823
+ *
824
+ * Without arguments, returns the entire `env` object.
825
+ * With a binding name, returns that specific binding.
826
+ *
827
+ * @example
828
+ * ```ts
829
+ * @Get()
830
+ * handle(@Env() env: WorkerEnv) { ... }
831
+ *
832
+ * @Get()
833
+ * handle(@Env('MY_KV') kv: KVNamespace) { ... }
834
+ * ```
835
+ */
836
+ const Env = createParamDecorator((bindingName, ctx) => {
837
+ const env = ctx.getContext().env;
838
+ if (typeof env !== "object" || env === null) return void 0;
839
+ return bindingName ? Reflect.get(env, bindingName) : env;
840
+ });
841
+ //#endregion
842
+ //#region src/decorators/scheduled.ts
843
+ const SCHEDULED_METADATA_KEY = "cloudflare:scheduled";
844
+ registerEntrypointKind({
845
+ kind: "cf:scheduled",
846
+ metaKey: SCHEDULED_METADATA_KEY,
847
+ level: "method"
848
+ });
849
+ /**
850
+ * Marks a method as a scheduled (cron) handler.
851
+ *
852
+ * @example
853
+ * ```ts
854
+ * @Injectable()
855
+ * class WorkerService {
856
+ * @Scheduled('0 * * * *')
857
+ * async hourlyCron() {
858
+ * console.log('Running hourly');
859
+ * }
860
+ * }
861
+ * ```
862
+ */
863
+ function Scheduled(cron) {
864
+ return (target, propertyKey, _descriptor) => {
865
+ const existing = getMetadata(SCHEDULED_METADATA_KEY, target.constructor) ?? [];
866
+ existing.push({
867
+ cron,
868
+ methodName: String(propertyKey)
869
+ });
870
+ defineMetadata(SCHEDULED_METADATA_KEY, existing, target.constructor);
871
+ };
872
+ }
873
+ //#endregion
874
+ //#region src/decorators/queue-consumer.ts
875
+ const QUEUE_CONSUMER_METADATA_KEY = "cloudflare:queue-consumer";
876
+ registerEntrypointKind({
877
+ kind: "cf:queue",
878
+ metaKey: QUEUE_CONSUMER_METADATA_KEY,
879
+ level: "method"
880
+ });
881
+ /**
882
+ * Marks a method as a queue consumer handler.
883
+ *
884
+ * @example
885
+ * ```ts
886
+ * @Injectable()
887
+ * class WorkerService {
888
+ * @QueueConsumer('email-queue')
889
+ * async processEmails(batch: MessageBatch) {
890
+ * for (const msg of batch.messages) {
891
+ * console.log('Processing:', msg.body);
892
+ * msg.ack();
893
+ * }
894
+ * }
895
+ * }
896
+ * ```
897
+ */
898
+ function QueueConsumer(queueName) {
899
+ return (target, propertyKey, _descriptor) => {
900
+ const existing = getMetadata(QUEUE_CONSUMER_METADATA_KEY, target.constructor) ?? [];
901
+ existing.push({
902
+ queueName,
903
+ methodName: String(propertyKey)
904
+ });
905
+ defineMetadata(QUEUE_CONSUMER_METADATA_KEY, existing, target.constructor);
906
+ };
907
+ }
908
+ //#endregion
909
+ //#region src/websocket/cloudflare-websocket.module.ts
910
+ /**
911
+ * Cloudflare counterpart to the core `WebSocketModule.forRoot()`. Import this in
912
+ * your `AppModule` instead: it provides the gateway dispatcher plus a late-bound
913
+ * `WS_SERVER` (`WsServerHolder`) that the WebSocket Durable Object wires to a
914
+ * ctx-backed server per instance. `useClass` ensures a fresh holder per DI
915
+ * container so colocated DO instances never share a server.
916
+ */
917
+ var CloudflareWebSocketModule = class CloudflareWebSocketModule {
918
+ static forRoot() {
919
+ const providers = [defineProvider(WS_SERVER, { useClass: WsServerHolder }), WsDispatcher];
920
+ return {
921
+ module: CloudflareWebSocketModule,
922
+ providers,
923
+ exports: [WS_SERVER, WsDispatcher]
924
+ };
925
+ }
926
+ };
927
+ //#endregion
928
+ //#region src/websocket/broadcast.ts
929
+ /**
930
+ * Push to a room from a Worker HTTP handler / cron / queue consumer (server-
931
+ * initiated emit). Resolves the room's Durable Object and calls its `broadcast`
932
+ * RPC method — the same canonical room→DO mapping the upgrade route uses, so it
933
+ * always reaches the DO holding those sockets.
934
+ *
935
+ * @example
936
+ * ```ts
937
+ * // In a controller — ns from the typed Worker environment
938
+ * await broadcastToRoom(ns, '/orgs/:orgId/ws', `org:${id}`, 'order.created', order);
939
+ * ```
940
+ */
941
+ async function broadcastToRoom(ns, gatewayPath, room, event, data, options) {
942
+ const cmd = {
943
+ rooms: [room],
944
+ exceptIds: options?.exceptIds,
945
+ frame: JSON.stringify({
946
+ event,
947
+ data
948
+ })
949
+ };
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
+ } };
1019
+ }
1020
+ //#endregion
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 };
1022
+
1023
+ //# sourceMappingURL=index.js.map