@velajs/cloudflare 1.29.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  3. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/cloudflare-application.js +309 -0
  7. package/dist/cloudflare-application.js.map +1 -0
  8. package/dist/cloudflare-factory.js +109 -0
  9. package/dist/cloudflare-factory.js.map +1 -0
  10. package/dist/decorators/queue-consumer.js +39 -0
  11. package/dist/decorators/queue-consumer.js.map +1 -0
  12. package/dist/durable-objects.d.ts +3 -4
  13. package/dist/durable-objects.js +2 -524
  14. package/dist/environment.js +19 -0
  15. package/dist/environment.js.map +1 -0
  16. package/dist/index.d.ts +11 -9
  17. package/dist/index.js +21 -1276
  18. package/dist/nonce/durable-object-nonce.store.js +33 -0
  19. package/dist/nonce/durable-object-nonce.store.js.map +1 -0
  20. package/dist/nonce/nonce-validation.js +18 -0
  21. package/dist/nonce/nonce-validation.js.map +1 -0
  22. package/dist/nonce/nonce.durable-object.js +54 -0
  23. package/dist/nonce/nonce.durable-object.js.map +1 -0
  24. package/dist/{nonce.durable-object-Df4CZi-0.d.ts → nonce.durable-object-Ci9AGssY.d.ts} +2 -3
  25. package/dist/queue/cloudflare-queues.js +215 -0
  26. package/dist/queue/cloudflare-queues.js.map +1 -0
  27. package/dist/queues.d.ts +1 -2
  28. package/dist/queues.js +1 -214
  29. package/dist/rate-limit/cloudflare-rate-limit.store.js +40 -0
  30. package/dist/rate-limit/cloudflare-rate-limit.store.js.map +1 -0
  31. package/dist/schedule-diagnostics.js +51 -0
  32. package/dist/schedule-diagnostics.js.map +1 -0
  33. package/dist/scheduled-event.js +68 -0
  34. package/dist/scheduled-event.js.map +1 -0
  35. package/dist/services/flagship-flag.driver.js +52 -0
  36. package/dist/services/flagship-flag.driver.js.map +1 -0
  37. package/dist/services/kv-cache.store.js +82 -0
  38. package/dist/services/kv-cache.store.js.map +1 -0
  39. package/dist/services/kv-flag.driver.js +62 -0
  40. package/dist/services/kv-flag.driver.js.map +1 -0
  41. package/dist/storage/r2-storage.driver.js +67 -0
  42. package/dist/storage/r2-storage.driver.js.map +1 -0
  43. package/dist/storage/storage-key-claim.js +74 -0
  44. package/dist/storage/storage-key-claim.js.map +1 -0
  45. package/dist/storage/storage-manager.service.js +38 -0
  46. package/dist/storage/storage-manager.service.js.map +1 -0
  47. package/dist/storage/storage.controller.js +64 -0
  48. package/dist/storage/storage.controller.js.map +1 -0
  49. package/dist/storage/storage.module.js +25 -0
  50. package/dist/storage/storage.module.js.map +1 -0
  51. package/dist/storage/storage.service.js +64 -0
  52. package/dist/storage/storage.service.js.map +1 -0
  53. package/dist/storage/storage.tokens.js +7 -0
  54. package/dist/storage/storage.tokens.js.map +1 -0
  55. package/dist/vela-env-DFvyoNT3.d.ts +1 -2
  56. package/dist/websocket/broadcast.js +32 -0
  57. package/dist/websocket/broadcast.js.map +1 -0
  58. package/dist/websocket/cf-room-registry.js +137 -0
  59. package/dist/websocket/cf-room-registry.js.map +1 -0
  60. package/dist/websocket/cf-ws-client.js +94 -0
  61. package/dist/websocket/cf-ws-client.js.map +1 -0
  62. package/dist/websocket/cloudflare-websocket.module.js +25 -0
  63. package/dist/websocket/cloudflare-websocket.module.js.map +1 -0
  64. package/dist/websocket/do-bootstrap.js +56 -0
  65. package/dist/websocket/do-bootstrap.js.map +1 -0
  66. package/dist/websocket/do-live.js +184 -0
  67. package/dist/websocket/do-live.js.map +1 -0
  68. package/dist/websocket/do-pitr.js +81 -0
  69. package/dist/websocket/do-pitr.js.map +1 -0
  70. package/dist/websocket/do-state.js +7 -0
  71. package/dist/websocket/do-state.js.map +1 -0
  72. package/dist/websocket/do-websocket-host.js +169 -0
  73. package/dist/websocket/do-websocket-host.js.map +1 -0
  74. package/dist/websocket/room-id.js +27 -0
  75. package/dist/websocket/room-id.js.map +1 -0
  76. package/dist/websocket/websocket-routing.js +133 -0
  77. package/dist/websocket/websocket-routing.js.map +1 -0
  78. package/dist/websocket/websocket.durable-object.js +138 -0
  79. package/dist/websocket/websocket.durable-object.js.map +1 -0
  80. package/dist/websocket/ws-attachment.js +79 -0
  81. package/dist/websocket/ws-attachment.js.map +1 -0
  82. package/dist/websocket/ws-server-holder.js +38 -0
  83. package/dist/websocket/ws-server-holder.js.map +1 -0
  84. package/package.json +9 -7
  85. package/dist/durable-objects.js.map +0 -1
  86. package/dist/index.js.map +0 -1
  87. package/dist/nonce-validation-Dy8z05A9.js +0 -522
  88. package/dist/nonce-validation-Dy8z05A9.js.map +0 -1
  89. package/dist/queues.js.map +0 -1
@@ -0,0 +1,38 @@
1
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
2
+ import { R2StorageDriver } from "./r2-storage.driver.js";
3
+ import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
4
+ import __decorateParam from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
5
+ import __decorate from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
6
+ import { Inject, Injectable } from "@velajs/vela";
7
+ //#region src/storage/storage-manager.service.ts
8
+ let StorageManagerService = class StorageManagerService {
9
+ options;
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ hasDisk(disk) {
14
+ return this.options.disks.some((d) => d.disk === disk);
15
+ }
16
+ getDiskConfig(disk) {
17
+ const config = this.options.disks.find((d) => d.disk === disk);
18
+ if (!config) throw new Error(`Storage disk "${disk}" is not configured.`);
19
+ return config;
20
+ }
21
+ getDriver(disk) {
22
+ const config = this.getDiskConfig(disk);
23
+ return new R2StorageDriver({
24
+ disk,
25
+ bucket: config.bucket,
26
+ secret: this.options.secret
27
+ });
28
+ }
29
+ };
30
+ StorageManagerService = __decorate([
31
+ Injectable(),
32
+ __decorateParam(0, Inject(STORAGE_OPTIONS)),
33
+ __decorateMetadata("design:paramtypes", [Object])
34
+ ], StorageManagerService);
35
+ //#endregion
36
+ export { StorageManagerService };
37
+
38
+ //# sourceMappingURL=storage-manager.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-manager.service.js","names":[],"sources":["../../src/storage/storage-manager.service.ts"],"sourcesContent":["import { Inject, Injectable } from '@velajs/vela';\nimport { R2StorageDriver } from './r2-storage.driver';\nimport { STORAGE_OPTIONS } from './storage.tokens';\nimport type { DiskConfig, StorageModuleOptions } from './storage.types';\n\n@Injectable()\nexport class StorageManagerService {\n constructor(@Inject(STORAGE_OPTIONS) private readonly options: StorageModuleOptions) {}\n\n hasDisk(disk: string): boolean {\n return this.options.disks.some((d) => d.disk === disk);\n }\n\n getDiskConfig(disk: string): DiskConfig {\n const config = this.options.disks.find((d) => d.disk === disk);\n if (!config) throw new Error(`Storage disk \"${disk}\" is not configured.`);\n return config;\n }\n\n getDriver(disk: string): R2StorageDriver {\n const config = this.getDiskConfig(disk);\n return new R2StorageDriver({ disk, bucket: config.bucket, secret: this.options.secret });\n }\n}\n"],"mappings":";;;;;;;AAMO,IAAM,wBAAN,MAAM,sBAAsB;CACqB;CAAtD,YAAY,SAAyE;EAA/B,KAAA,UAAA;CAAgC;CAEtF,QAAQ,MAAuB;EAC7B,OAAO,KAAK,QAAQ,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;CACvD;CAEA,cAAc,MAA0B;EACtC,MAAM,SAAS,KAAK,QAAQ,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;EAC7D,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,iBAAiB,KAAK,qBAAqB;EACxE,OAAO;CACT;CAEA,UAAU,MAA+B;EACvC,MAAM,SAAS,KAAK,cAAc,IAAI;EACtC,OAAO,IAAI,gBAAgB;GAAE;GAAM,QAAQ,OAAO;GAAQ,QAAQ,KAAK,QAAQ;EAAO,CAAC;CACzF;AACF;;CAlBC,WAAW;CAEG,gBAAA,GAAA,OAAO,eAAe,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
2
+ import { decodeStorageKeyClaim, isStorageKeyWithinRoot } from "./storage-key-claim.js";
3
+ import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
4
+ import __decorateParam from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
5
+ import __decorate from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
6
+ import { StorageManagerService } from "./storage-manager.service.js";
7
+ import { Controller, Get, Inject, Req } from "@velajs/vela";
8
+ import { joinStoragePath } from "@velajs/vela/storage";
9
+ import { STORAGE_SIGNED_URL_PURPOSE, verifySignedUrl } from "@velajs/vela/security";
10
+ //#region src/storage/storage.controller.ts
11
+ let StorageController = class StorageController {
12
+ manager;
13
+ options;
14
+ constructor(manager, options) {
15
+ this.manager = manager;
16
+ this.options = options;
17
+ }
18
+ async download(c) {
19
+ const secret = this.options.secret;
20
+ if (!secret) return new Response("Storage signing is not configured", { status: 500 });
21
+ if (!await verifySignedUrl(c.req.url, secret, {
22
+ method: c.req.method,
23
+ purpose: STORAGE_SIGNED_URL_PURPOSE
24
+ })) return new Response("Invalid or expired URL", { status: 403 });
25
+ const url = new URL(c.req.url);
26
+ if (url.searchParams.get("method") !== "GET") return new Response("URL is not scoped for reads", { status: 403 });
27
+ const disk = c.req.param("disk");
28
+ if (!disk || !this.manager.hasDisk(disk)) return new Response("Unknown disk", { status: 404 });
29
+ const claim = url.searchParams.get("key");
30
+ const decoded = claim ? decodeStorageKeyClaim(claim) : void 0;
31
+ if (!decoded) return new Response("Malformed storage key claim", { status: 400 });
32
+ const fullPath = joinStoragePath(void 0, decoded);
33
+ if (fullPath !== decoded) return new Response("Invalid storage key", { status: 403 });
34
+ if (!isStorageKeyWithinRoot(fullPath, this.manager.getDiskConfig(disk).root)) return new Response("Storage key is outside the configured root", { status: 403 });
35
+ try {
36
+ const result = await this.manager.getDriver(disk).download(fullPath);
37
+ const filename = fullPath.split("/").at(-1) || "download";
38
+ return new Response(result.toStream(), { headers: {
39
+ "content-type": result.contentType || "application/octet-stream",
40
+ "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,
41
+ "x-content-type-options": "nosniff"
42
+ } });
43
+ } catch {
44
+ return new Response("Not found", { status: 404 });
45
+ }
46
+ }
47
+ };
48
+ __decorate([
49
+ Get("/:disk"),
50
+ __decorateParam(0, Req()),
51
+ __decorateMetadata("design:type", Function),
52
+ __decorateMetadata("design:paramtypes", [Object]),
53
+ __decorateMetadata("design:returntype", Promise)
54
+ ], StorageController.prototype, "download", null);
55
+ StorageController = __decorate([
56
+ Controller("storage"),
57
+ __decorateParam(0, Inject(StorageManagerService)),
58
+ __decorateParam(1, Inject(STORAGE_OPTIONS)),
59
+ __decorateMetadata("design:paramtypes", [typeof StorageManagerService === "undefined" ? Object : StorageManagerService, Object])
60
+ ], StorageController);
61
+ //#endregion
62
+ export { StorageController };
63
+
64
+ //# sourceMappingURL=storage.controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.controller.js","names":[],"sources":["../../src/storage/storage.controller.ts"],"sourcesContent":["import { Controller, Get, Inject, Req } from '@velajs/vela';\nimport { joinStoragePath } from '@velajs/vela/storage';\nimport { STORAGE_SIGNED_URL_PURPOSE, verifySignedUrl } from '@velajs/vela/security';\nimport type { Context } from 'hono';\nimport { STORAGE_OPTIONS } from './storage.tokens';\nimport type { StorageModuleOptions } from './storage.types';\nimport { StorageManagerService } from './storage-manager.service';\nimport { decodeStorageKeyClaim, isStorageKeyWithinRoot } from './storage-key-claim';\n\n/**\n * Presign-proxy: serves objects for HMAC-signed URLs produced by\n * `StorageService.url()`. R2 has no native presign, so a signed URL points here;\n * this route verifies the signature (+expiry) before streaming the object.\n * The FULL object key (root already applied at sign time) is carried as an\n * opaque base64url query claim. The signature is verified before the claim is\n * decoded exactly once and checked against the configured disk root.\n */\n@Controller('storage')\nexport class StorageController {\n constructor(\n @Inject(StorageManagerService) private readonly manager: StorageManagerService,\n @Inject(STORAGE_OPTIONS) private readonly options: StorageModuleOptions,\n ) {}\n\n @Get('/:disk')\n async download(@Req() c: Context): Promise<Response> {\n const secret = this.options.secret;\n if (!secret) return new Response('Storage signing is not configured', { status: 500 });\n\n // Verify the complete path/query capability before inspecting the disk or\n // decoding attacker-controlled claims.\n if (\n !(await verifySignedUrl(c.req.url, secret, {\n method: c.req.method,\n purpose: STORAGE_SIGNED_URL_PURPOSE,\n }))\n ) {\n return new Response('Invalid or expired URL', { status: 403 });\n }\n\n // Enforce the signed `method` scope: this proxy only serves reads, so a URL\n // scoped to PUT/DELETE/HEAD must NOT be honored as a GET (it would over-grant\n // read access relative to the token's intended scope). The param is part of\n // the signed payload, so it is trustworthy once the signature verifies.\n const url = new URL(c.req.url);\n if (url.searchParams.get('method') !== 'GET') {\n return new Response('URL is not scoped for reads', { status: 403 });\n }\n\n const disk = c.req.param('disk');\n if (!disk || !this.manager.hasDisk(disk)) return new Response('Unknown disk', { status: 404 });\n\n const claim = url.searchParams.get('key');\n const decoded = claim ? decodeStorageKeyClaim(claim) : undefined;\n if (!decoded) return new Response('Malformed storage key claim', { status: 400 });\n\n // Canonicalize after the one decode. Signed direct-driver callers cannot\n // smuggle dot segments or non-canonical aliases into the proxy contract.\n const fullPath = joinStoragePath(undefined, decoded);\n if (fullPath !== decoded) return new Response('Invalid storage key', { status: 403 });\n\n if (!isStorageKeyWithinRoot(fullPath, this.manager.getDiskConfig(disk).root)) {\n return new Response('Storage key is outside the configured root', { status: 403 });\n }\n\n try {\n const result = await this.manager.getDriver(disk).download(fullPath);\n const filename = fullPath.split('/').at(-1) || 'download';\n return new Response(result.toStream(), {\n headers: {\n 'content-type': result.contentType || 'application/octet-stream',\n // Objects are untrusted user content. The authenticated API origin\n // never renders them inline (especially HTML/SVG), even when the\n // stored Content-Type is attacker controlled.\n 'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,\n 'x-content-type-options': 'nosniff',\n },\n });\n } catch {\n return new Response('Not found', { status: 404 });\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAkBO,IAAM,oBAAN,MAAM,kBAAkB;CAEqB;CACN;CAF5C,YACE,SACA,SACA;EAFgD,KAAA,UAAA;EACN,KAAA,UAAA;CACzC;CAEH,MACM,SAAS,GAAsC;EACnD,MAAM,SAAS,KAAK,QAAQ;EAC5B,IAAI,CAAC,QAAQ,OAAO,IAAI,SAAS,qCAAqC,EAAE,QAAQ,IAAI,CAAC;EAIrF,IACE,CAAE,MAAM,gBAAgB,EAAE,IAAI,KAAK,QAAQ;GACzC,QAAQ,EAAE,IAAI;GACd,SAAS;EACX,CAAC,GAED,OAAO,IAAI,SAAS,0BAA0B,EAAE,QAAQ,IAAI,CAAC;EAO/D,MAAM,MAAM,IAAI,IAAI,EAAE,IAAI,GAAG;EAC7B,IAAI,IAAI,aAAa,IAAI,QAAQ,MAAM,OACrC,OAAO,IAAI,SAAS,+BAA+B,EAAE,QAAQ,IAAI,CAAC;EAGpE,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,IAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ,QAAQ,IAAI,GAAG,OAAO,IAAI,SAAS,gBAAgB,EAAE,QAAQ,IAAI,CAAC;EAE7F,MAAM,QAAQ,IAAI,aAAa,IAAI,KAAK;EACxC,MAAM,UAAU,QAAQ,sBAAsB,KAAK,IAAI,KAAA;EACvD,IAAI,CAAC,SAAS,OAAO,IAAI,SAAS,+BAA+B,EAAE,QAAQ,IAAI,CAAC;EAIhF,MAAM,WAAW,gBAAgB,KAAA,GAAW,OAAO;EACnD,IAAI,aAAa,SAAS,OAAO,IAAI,SAAS,uBAAuB,EAAE,QAAQ,IAAI,CAAC;EAEpF,IAAI,CAAC,uBAAuB,UAAU,KAAK,QAAQ,cAAc,IAAI,CAAC,CAAC,IAAI,GACzE,OAAO,IAAI,SAAS,8CAA8C,EAAE,QAAQ,IAAI,CAAC;EAGnF,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,QAAQ,UAAU,IAAI,CAAC,CAAC,SAAS,QAAQ;GACnE,MAAM,WAAW,SAAS,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK;GAC/C,OAAO,IAAI,SAAS,OAAO,SAAS,GAAG,EACrC,SAAS;IACP,gBAAgB,OAAO,eAAe;IAItC,uBAAuB,gCAAgC,mBAAmB,QAAQ;IAClF,0BAA0B;GAC5B,EACF,CAAC;EACH,QAAQ;GACN,OAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;EAClD;CACF;AACF;;CA1DG,IAAI,QAAQ;CACG,gBAAA,GAAA,IAAI,CAAA;;;;;;CARrB,WAAW,SAAS;CAGhB,gBAAA,GAAA,OAAO,qBAAqB,CAAA;CAC5B,gBAAA,GAAA,OAAO,eAAe,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
2
+ import __decorate from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
3
+ import { StorageManagerService } from "./storage-manager.service.js";
4
+ import { StorageController } from "./storage.controller.js";
5
+ import { StorageService } from "./storage.service.js";
6
+ import { ConfigurableModuleBuilder, Module } from "@velajs/vela";
7
+ //#region src/storage/storage.module.ts
8
+ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
9
+ moduleName: "Storage",
10
+ optionsInjectionToken: STORAGE_OPTIONS
11
+ }).build();
12
+ let StorageModule = class StorageModule extends ConfigurableModuleClass {};
13
+ StorageModule = __decorate([Module({
14
+ providers: [StorageManagerService, StorageService],
15
+ controllers: [StorageController],
16
+ exports: [
17
+ StorageService,
18
+ StorageManagerService,
19
+ STORAGE_OPTIONS
20
+ ]
21
+ })], StorageModule);
22
+ //#endregion
23
+ export { StorageModule };
24
+
25
+ //# sourceMappingURL=storage.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.module.js","names":[],"sources":["../../src/storage/storage.module.ts"],"sourcesContent":["import { ConfigurableModuleBuilder, Module } from '@velajs/vela';\nimport { StorageController } from './storage.controller';\nimport { StorageManagerService } from './storage-manager.service';\nimport { StorageService } from './storage.service';\nimport { STORAGE_OPTIONS } from './storage.tokens';\nimport type { StorageModuleOptions } from './storage.types';\n\nconst { ConfigurableModuleClass } = new ConfigurableModuleBuilder<StorageModuleOptions>({\n moduleName: 'Storage',\n optionsInjectionToken: STORAGE_OPTIONS,\n}).build();\n\n@Module({\n providers: [StorageManagerService, StorageService],\n controllers: [StorageController],\n exports: [StorageService, StorageManagerService, STORAGE_OPTIONS],\n})\nexport class StorageModule extends ConfigurableModuleClass {}\n"],"mappings":";;;;;;;AAOA,MAAM,EAAE,4BAA4B,IAAI,0BAAgD;CACtF,YAAY;CACZ,uBAAuB;AACzB,CAAC,CAAC,CAAC,MAAM;AAOF,IAAM,gBAAN,MAAM,sBAAsB,wBAAwB,CAAC;AAL3D,gBAAA,WAAA,CAAA,OAAO;CACN,WAAW,CAAC,uBAAuB,cAAc;CACjD,aAAa,CAAC,iBAAiB;CAC/B,SAAS;EAAC;EAAgB;EAAuB;CAAe;AAClE,CAAC,CAAA,GAAA,aAAA"}
@@ -0,0 +1,64 @@
1
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
2
+ import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
3
+ import __decorateParam from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
4
+ import __decorate from "../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
5
+ import { StorageManagerService } from "./storage-manager.service.js";
6
+ import { Inject, Injectable } from "@velajs/vela";
7
+ import { joinStoragePath } from "@velajs/vela/storage";
8
+ //#region src/storage/storage.service.ts
9
+ const DEFAULT_PRESIGN = {
10
+ defaultExpiry: 3600,
11
+ maxExpiry: 86400
12
+ };
13
+ let StorageService = class StorageService {
14
+ options;
15
+ manager;
16
+ constructor(options, manager) {
17
+ this.options = options;
18
+ this.manager = manager;
19
+ }
20
+ put(relativePath, body, options = {}, disk) {
21
+ const name = this.resolveDisk(disk);
22
+ return this.manager.getDriver(name).upload(body, this.fullPath(relativePath, name), options);
23
+ }
24
+ get(relativePath, disk) {
25
+ const name = this.resolveDisk(disk);
26
+ return this.manager.getDriver(name).download(this.fullPath(relativePath, name));
27
+ }
28
+ delete(relativePath, disk) {
29
+ const name = this.resolveDisk(disk);
30
+ return this.manager.getDriver(name).delete(this.fullPath(relativePath, name));
31
+ }
32
+ exists(relativePath, disk) {
33
+ const name = this.resolveDisk(disk);
34
+ return this.manager.getDriver(name).exists(this.fullPath(relativePath, name));
35
+ }
36
+ url(relativePath, method = "GET", expiresIn, disk) {
37
+ const name = this.resolveDisk(disk);
38
+ return this.manager.getDriver(name).getPresignedUrl(this.fullPath(relativePath, name), method, this.validateExpiry(expiresIn));
39
+ }
40
+ resolveDisk(disk) {
41
+ const name = disk ?? this.options.defaultDisk;
42
+ if (!this.manager.hasDisk(name)) throw new Error(`Storage disk "${name}" is not configured.`);
43
+ return name;
44
+ }
45
+ fullPath(relativePath, disk) {
46
+ return joinStoragePath(this.manager.getDiskConfig(disk).root, relativePath);
47
+ }
48
+ validateExpiry(expiresIn) {
49
+ const cfg = this.options.presignedUrl ?? DEFAULT_PRESIGN;
50
+ const value = expiresIn ?? cfg.defaultExpiry;
51
+ 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).`);
52
+ return value;
53
+ }
54
+ };
55
+ StorageService = __decorate([
56
+ Injectable(),
57
+ __decorateParam(0, Inject(STORAGE_OPTIONS)),
58
+ __decorateParam(1, Inject(StorageManagerService)),
59
+ __decorateMetadata("design:paramtypes", [Object, typeof StorageManagerService === "undefined" ? Object : StorageManagerService])
60
+ ], StorageService);
61
+ //#endregion
62
+ export { StorageService };
63
+
64
+ //# sourceMappingURL=storage.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.service.js","names":[],"sources":["../../src/storage/storage.service.ts"],"sourcesContent":["import { Inject, Injectable } from '@velajs/vela';\nimport {\n joinStoragePath,\n type DownloadResult,\n type PresignedUrlResult,\n type PresignMethod,\n type StorageBody,\n type UploadOptions,\n type UploadResult,\n} from '@velajs/vela/storage';\nimport { StorageManagerService } from './storage-manager.service';\nimport { STORAGE_OPTIONS } from './storage.tokens';\nimport type { StorageModuleOptions } from './storage.types';\n\nconst DEFAULT_PRESIGN = { defaultExpiry: 3600, maxExpiry: 86400 };\n\n/**\n * Multi-disk storage facade. Applies each disk's (templated) root, resolves the\n * driver, and validates presign expiry. Injectable anywhere via `StorageService`.\n */\n@Injectable()\nexport class StorageService {\n constructor(\n @Inject(STORAGE_OPTIONS) private readonly options: StorageModuleOptions,\n @Inject(StorageManagerService) private readonly manager: StorageManagerService,\n ) {}\n\n put(\n relativePath: string,\n body: StorageBody,\n options: UploadOptions = {},\n disk?: string,\n ): Promise<UploadResult> {\n const name = this.resolveDisk(disk);\n return this.manager.getDriver(name).upload(body, this.fullPath(relativePath, name), options);\n }\n\n get(relativePath: string, disk?: string): Promise<DownloadResult> {\n const name = this.resolveDisk(disk);\n return this.manager.getDriver(name).download(this.fullPath(relativePath, name));\n }\n\n delete(relativePath: string, disk?: string): Promise<void> {\n const name = this.resolveDisk(disk);\n return this.manager.getDriver(name).delete(this.fullPath(relativePath, name));\n }\n\n exists(relativePath: string, disk?: string): Promise<boolean> {\n const name = this.resolveDisk(disk);\n return this.manager.getDriver(name).exists(this.fullPath(relativePath, name));\n }\n\n url(\n relativePath: string,\n method: PresignMethod = 'GET',\n expiresIn?: number,\n disk?: string,\n ): Promise<PresignedUrlResult> {\n const name = this.resolveDisk(disk);\n return this.manager\n .getDriver(name)\n .getPresignedUrl(this.fullPath(relativePath, name), method, this.validateExpiry(expiresIn));\n }\n\n private resolveDisk(disk?: string): string {\n const name = disk ?? this.options.defaultDisk;\n if (!this.manager.hasDisk(name)) throw new Error(`Storage disk \"${name}\" is not configured.`);\n return name;\n }\n\n private fullPath(relativePath: string, disk: string): string {\n return joinStoragePath(this.manager.getDiskConfig(disk).root, relativePath);\n }\n\n private validateExpiry(expiresIn?: number): number {\n const cfg = this.options.presignedUrl ?? DEFAULT_PRESIGN;\n const value = expiresIn ?? cfg.defaultExpiry;\n // `Number.isSafeInteger` rejects NaN/fractional/infinite values — otherwise\n // `NaN < 1 || NaN > max` is false,\n // NaN slips through, signUrl omits `expires`, and the URL never expires.\n if (!Number.isSafeInteger(value) || value < 1 || value > cfg.maxExpiry) {\n throw new Error(`Presigned URL expiry ${value}s is out of range (1–${cfg.maxExpiry}s).`);\n }\n return value;\n }\n}\n"],"mappings":";;;;;;;;AAcA,MAAM,kBAAkB;CAAE,eAAe;CAAM,WAAW;AAAM;AAOzD,IAAM,iBAAN,MAAM,eAAe;CAEkB;CACM;CAFlD,YACE,SACA,SACA;EAF0C,KAAA,UAAA;EACM,KAAA,UAAA;CAC/C;CAEH,IACE,cACA,MACA,UAAyB,CAAC,GAC1B,MACuB;EACvB,MAAM,OAAO,KAAK,YAAY,IAAI;EAClC,OAAO,KAAK,QAAQ,UAAU,IAAI,CAAC,CAAC,OAAO,MAAM,KAAK,SAAS,cAAc,IAAI,GAAG,OAAO;CAC7F;CAEA,IAAI,cAAsB,MAAwC;EAChE,MAAM,OAAO,KAAK,YAAY,IAAI;EAClC,OAAO,KAAK,QAAQ,UAAU,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,cAAc,IAAI,CAAC;CAChF;CAEA,OAAO,cAAsB,MAA8B;EACzD,MAAM,OAAO,KAAK,YAAY,IAAI;EAClC,OAAO,KAAK,QAAQ,UAAU,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,cAAc,IAAI,CAAC;CAC9E;CAEA,OAAO,cAAsB,MAAiC;EAC5D,MAAM,OAAO,KAAK,YAAY,IAAI;EAClC,OAAO,KAAK,QAAQ,UAAU,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,cAAc,IAAI,CAAC;CAC9E;CAEA,IACE,cACA,SAAwB,OACxB,WACA,MAC6B;EAC7B,MAAM,OAAO,KAAK,YAAY,IAAI;EAClC,OAAO,KAAK,QACT,UAAU,IAAI,CAAC,CACf,gBAAgB,KAAK,SAAS,cAAc,IAAI,GAAG,QAAQ,KAAK,eAAe,SAAS,CAAC;CAC9F;CAEA,YAAoB,MAAuB;EACzC,MAAM,OAAO,QAAQ,KAAK,QAAQ;EAClC,IAAI,CAAC,KAAK,QAAQ,QAAQ,IAAI,GAAG,MAAM,IAAI,MAAM,iBAAiB,KAAK,qBAAqB;EAC5F,OAAO;CACT;CAEA,SAAiB,cAAsB,MAAsB;EAC3D,OAAO,gBAAgB,KAAK,QAAQ,cAAc,IAAI,CAAC,CAAC,MAAM,YAAY;CAC5E;CAEA,eAAuB,WAA4B;EACjD,MAAM,MAAM,KAAK,QAAQ,gBAAgB;EACzC,MAAM,QAAQ,aAAa,IAAI;EAI/B,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,KAAK,QAAQ,IAAI,WAC3D,MAAM,IAAI,MAAM,wBAAwB,MAAM,uBAAuB,IAAI,UAAU,IAAI;EAEzF,OAAO;CACT;AACF;;CAjEC,WAAW;CAGP,gBAAA,GAAA,OAAO,eAAe,CAAA;CACtB,gBAAA,GAAA,OAAO,qBAAqB,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { InjectionToken } from "@velajs/vela";
2
+ //#region src/storage/storage.tokens.ts
3
+ const STORAGE_OPTIONS = new InjectionToken("STORAGE_OPTIONS");
4
+ //#endregion
5
+ export { STORAGE_OPTIONS };
6
+
7
+ //# sourceMappingURL=storage.tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.tokens.js","names":[],"sources":["../../src/storage/storage.tokens.ts"],"sourcesContent":["import { InjectionToken } from '@velajs/vela';\nimport type { StorageModuleOptions } from './storage.types';\n\nexport const STORAGE_OPTIONS = new InjectionToken<StorageModuleOptions>('STORAGE_OPTIONS');\n"],"mappings":";;AAGA,MAAa,kBAAkB,IAAI,eAAqC,iBAAiB"}
@@ -6,5 +6,4 @@ declare module '@velajs/vela' {
6
6
  */
7
7
  interface VelaEnv extends Cloudflare.Env {}
8
8
  }
9
- export {};
10
- //# sourceMappingURL=vela-env-DFvyoNT3.d.ts.map
9
+ export {};
@@ -0,0 +1,32 @@
1
+ import { roomToDurableId } from "./room-id.js";
2
+ import { DEFAULT_WS_MAX_FRAME_BYTES, assertBroadcastCommandFits, resolveMaxFrameBytes } from "@velajs/vela/websocket";
3
+ //#region src/websocket/broadcast.ts
4
+ /**
5
+ * Push to a room from a Worker HTTP handler / cron / queue consumer (server-
6
+ * initiated emit). Resolves the room's Durable Object and calls its `broadcast`
7
+ * RPC method — the same canonical room→DO mapping the upgrade route uses, so it
8
+ * always reaches the DO holding those sockets.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // In a controller — ns from the typed Worker environment
13
+ * await broadcastToRoom(ns, '/orgs/:orgId/ws', `org:${id}`, 'order.created', order);
14
+ * ```
15
+ */
16
+ async function broadcastToRoom(ns, gatewayPath, room, event, data, options) {
17
+ const cmd = {
18
+ rooms: [room],
19
+ exceptIds: options?.exceptIds,
20
+ frame: JSON.stringify({
21
+ event,
22
+ data
23
+ })
24
+ };
25
+ const maxFrameBytes = resolveMaxFrameBytes({ maxFrameBytes: options?.maxFrameBytes ?? DEFAULT_WS_MAX_FRAME_BYTES });
26
+ assertBroadcastCommandFits(cmd, maxFrameBytes);
27
+ await ns.get(roomToDurableId(ns, gatewayPath, room)).broadcast(cmd);
28
+ }
29
+ //#endregion
30
+ export { broadcastToRoom };
31
+
32
+ //# sourceMappingURL=broadcast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.js","names":[],"sources":["../../src/websocket/broadcast.ts"],"sourcesContent":["import {\n assertBroadcastCommandFits,\n DEFAULT_WS_MAX_FRAME_BYTES,\n resolveMaxFrameBytes,\n} from '@velajs/vela/websocket';\nimport type { BroadcastCommand } from '@velajs/vela/websocket';\nimport { roomToDurableId } from './room-id';\n\nexport interface WsBroadcastStub {\n broadcast(cmd: BroadcastCommand): Promise<void>;\n}\n\nexport interface BroadcastNamespace {\n idFromName(name: string): DurableObjectId;\n get(id: DurableObjectId): WsBroadcastStub;\n}\n\n/**\n * Push to a room from a Worker HTTP handler / cron / queue consumer (server-\n * initiated emit). Resolves the room's Durable Object and calls its `broadcast`\n * RPC method — the same canonical room→DO mapping the upgrade route uses, so it\n * always reaches the DO holding those sockets.\n *\n * @example\n * ```ts\n * // In a controller — ns from the typed Worker environment\n * await broadcastToRoom(ns, '/orgs/:orgId/ws', `org:${id}`, 'order.created', order);\n * ```\n */\nexport async function broadcastToRoom(\n ns: BroadcastNamespace,\n gatewayPath: string,\n room: string,\n event: string,\n data?: unknown,\n options?: { exceptIds?: string[]; maxFrameBytes?: number },\n): Promise<void> {\n const cmd: BroadcastCommand = {\n rooms: [room],\n exceptIds: options?.exceptIds,\n frame: JSON.stringify({ event, data }),\n };\n const maxFrameBytes = resolveMaxFrameBytes({\n maxFrameBytes: options?.maxFrameBytes ?? DEFAULT_WS_MAX_FRAME_BYTES,\n });\n assertBroadcastCommandFits(cmd, maxFrameBytes);\n const stub = ns.get(roomToDurableId(ns, gatewayPath, room));\n await stub.broadcast(cmd);\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6BA,eAAsB,gBACpB,IACA,aACA,MACA,OACA,MACA,SACe;CACf,MAAM,MAAwB;EAC5B,OAAO,CAAC,IAAI;EACZ,WAAW,SAAS;EACpB,OAAO,KAAK,UAAU;GAAE;GAAO;EAAK,CAAC;CACvC;CACA,MAAM,gBAAgB,qBAAqB,EACzC,eAAe,SAAS,iBAAiB,2BAC3C,CAAC;CACD,2BAA2B,KAAK,aAAa;CAE7C,MADa,GAAG,IAAI,gBAAgB,IAAI,aAAa,IAAI,CAChD,CAAC,CAAC,UAAU,GAAG;AAC1B"}
@@ -0,0 +1,137 @@
1
+ import { rejectedAttachment, socketAttachment } from "./ws-attachment.js";
2
+ import { CfWsClient } from "./cf-ws-client.js";
3
+ import { WebSocketSendGate } from "@velajs/vela/websocket";
4
+ //#region src/websocket/cf-room-registry.ts
5
+ /**
6
+ * Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room
7
+ * membership lives in tags (hub room, set at accept) + the attachment (dynamic
8
+ * `join()`), never in DO instance fields — so it survives hibernation with zero
9
+ * rehydration.
10
+ */
11
+ var CfRoomRegistry = class {
12
+ ctx;
13
+ #sendGates = /* @__PURE__ */ new WeakMap();
14
+ #admitting = /* @__PURE__ */ new WeakSet();
15
+ #sendPolicyForPath;
16
+ setSendPolicyResolver(resolve) {
17
+ this.#sendPolicyForPath = resolve;
18
+ }
19
+ deliveryAuthorizer;
20
+ frameLimitForPath;
21
+ constructor(ctx) {
22
+ this.ctx = ctx;
23
+ }
24
+ setDeliveryAuthorizer(authorizer) {
25
+ this.deliveryAuthorizer = authorizer;
26
+ }
27
+ /** Reconcile pre-migration/woken attachments with authoritative gateway metadata. */
28
+ setFrameLimitResolver(resolver) {
29
+ this.frameLimitForPath = resolver;
30
+ }
31
+ /**
32
+ * Run `admit` (the connection hook) with `ws` marked as being admitted, so a
33
+ * broadcast issued meanwhile (for example `server.emit()` announcing the new
34
+ * connection) skips the still-pending socket instead of rejecting it.
35
+ */
36
+ async admitting(ws, admit) {
37
+ this.#admitting.add(ws);
38
+ try {
39
+ return await admit();
40
+ } finally {
41
+ this.#admitting.delete(ws);
42
+ }
43
+ }
44
+ register(_client) {}
45
+ leaveAll(_client) {}
46
+ join(client, room) {
47
+ return client.join(room);
48
+ }
49
+ leave(client, room) {
50
+ return client.leave(room);
51
+ }
52
+ localIdsInRoom(room) {
53
+ return this.socketsForRoom(room).map((ws) => this.attachmentOf(ws).connId);
54
+ }
55
+ deliverLocal(cmd) {
56
+ const excludeIds = new Set(cmd.exceptIds ?? []);
57
+ const excludeRooms = cmd.exceptRooms ?? [];
58
+ const targets = cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);
59
+ const seen = /* @__PURE__ */ new Set();
60
+ const selected = [];
61
+ for (const ws of targets) {
62
+ const att = this.reconcileFrameLimit(ws, this.attachmentOf(ws));
63
+ if (att.state === "pending" && this.#admitting.has(ws)) continue;
64
+ if (att.state !== "active" || att.expiresAtMs !== void 0 && (!Number.isSafeInteger(att.expiresAtMs) || att.expiresAtMs <= Date.now())) {
65
+ try {
66
+ att.state = "rejected";
67
+ ws.serializeAttachment(att);
68
+ ws.close(1008, "identity expired or connection rejected");
69
+ } catch {}
70
+ continue;
71
+ }
72
+ if (seen.has(att.connId)) continue;
73
+ seen.add(att.connId);
74
+ if (excludeIds.has(att.connId)) continue;
75
+ if (excludeRooms.some((r) => att.rooms.includes(r))) continue;
76
+ selected.push({
77
+ ws,
78
+ client: this.clientFor(ws)
79
+ });
80
+ }
81
+ if (!this.deliveryAuthorizer) {
82
+ for (const { client } of selected) client.sendRaw(cmd.frame);
83
+ return;
84
+ }
85
+ return Promise.all(selected.map(async ({ ws, client }) => {
86
+ let allowed = false;
87
+ try {
88
+ allowed = await this.deliveryAuthorizer(client) === true;
89
+ } catch {
90
+ allowed = false;
91
+ }
92
+ if (allowed) client.sendRaw(cmd.frame);
93
+ else this.reject(ws, "delivery authorization revoked");
94
+ })).then(() => void 0);
95
+ }
96
+ collectRooms(rooms) {
97
+ const set = /* @__PURE__ */ new Set();
98
+ for (const room of rooms) for (const ws of this.socketsForRoom(room)) set.add(ws);
99
+ return [...set];
100
+ }
101
+ socketsForRoom(room) {
102
+ return this.ctx.getWebSockets().filter((ws) => this.attachmentOf(ws).rooms.includes(room));
103
+ }
104
+ attachmentOf(ws) {
105
+ return socketAttachment(ws) ?? rejectedAttachment();
106
+ }
107
+ /** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */
108
+ clientFor(ws) {
109
+ this.reconcileFrameLimit(ws, this.attachmentOf(ws));
110
+ let gate = this.#sendGates.get(ws);
111
+ if (!gate) {
112
+ gate = new WebSocketSendGate(this.#sendPolicyForPath?.(this.attachmentOf(ws).path));
113
+ this.#sendGates.set(ws, gate);
114
+ }
115
+ return new CfWsClient(this.ctx, ws, gate);
116
+ }
117
+ reconcileFrameLimit(ws, attachment) {
118
+ const expected = this.frameLimitForPath?.(attachment.path);
119
+ if (expected !== void 0 && attachment.maxFrameBytes !== expected) {
120
+ attachment.maxFrameBytes = expected;
121
+ ws.serializeAttachment(attachment);
122
+ }
123
+ return attachment;
124
+ }
125
+ reject(ws, reason) {
126
+ try {
127
+ const attachment = this.attachmentOf(ws);
128
+ attachment.state = "rejected";
129
+ ws.serializeAttachment(attachment);
130
+ ws.close(1008, reason);
131
+ } catch {}
132
+ }
133
+ };
134
+ //#endregion
135
+ export { CfRoomRegistry };
136
+
137
+ //# sourceMappingURL=cf-room-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cf-room-registry.js","names":[],"sources":["../../src/websocket/cf-room-registry.ts"],"sourcesContent":["import { WebSocketSendGate, type WebSocketSendPolicy } from '@velajs/vela/websocket';\nimport { socketAttachment, rejectedAttachment } from './ws-attachment';\nimport type { BroadcastCommand, RoomRegistry, WsClient } from '@velajs/vela/websocket';\nimport type { DoStateLike, WsAttachment, WsLike } from './do-state';\nimport { CfWsClient } from './cf-ws-client';\n\n/**\n * Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room\n * membership lives in tags (hub room, set at accept) + the attachment (dynamic\n * `join()`), never in DO instance fields — so it survives hibernation with zero\n * rehydration.\n */\nexport class CfRoomRegistry implements RoomRegistry {\n readonly #sendGates = new WeakMap<WsLike, WebSocketSendGate>();\n // Sockets whose OnGatewayConnection hook is running in this instance. They\n // are still `pending`, so broadcasts skip them; any other pending socket\n // never finished admission and is closed when a broadcast reaches it.\n readonly #admitting = new WeakSet<WsLike>();\n #sendPolicyForPath?: (path: string) => WebSocketSendPolicy | undefined;\n\n setSendPolicyResolver(resolve: (path: string) => WebSocketSendPolicy | undefined): void {\n this.#sendPolicyForPath = resolve;\n }\n\n private deliveryAuthorizer?: (client: WsClient) => boolean | Promise<boolean>;\n private frameLimitForPath?: (path: string) => number | undefined;\n\n constructor(private readonly ctx: DoStateLike) {}\n\n setDeliveryAuthorizer(authorizer: (client: WsClient) => boolean | Promise<boolean>): void {\n this.deliveryAuthorizer = authorizer;\n }\n\n /** Reconcile pre-migration/woken attachments with authoritative gateway metadata. */\n setFrameLimitResolver(resolver: (path: string) => number | undefined): void {\n this.frameLimitForPath = resolver;\n }\n\n /**\n * Run `admit` (the connection hook) with `ws` marked as being admitted, so a\n * broadcast issued meanwhile (for example `server.emit()` announcing the new\n * connection) skips the still-pending socket instead of rejecting it.\n */\n async admitting<T>(ws: WsLike, admit: () => Promise<T>): Promise<T> {\n this.#admitting.add(ws);\n try {\n return await admit();\n } finally {\n this.#admitting.delete(ws);\n }\n }\n\n // CF sockets are registered with the DO by `acceptWebSocket`; nothing to track.\n register(_client: WsClient): void {}\n leaveAll(_client: WsClient): void {}\n\n join(client: WsClient, room: string): void | Promise<void> {\n return client.join(room);\n }\n\n leave(client: WsClient, room: string): void | Promise<void> {\n return client.leave(room);\n }\n\n localIdsInRoom(room: string): string[] {\n return this.socketsForRoom(room).map((ws) => this.attachmentOf(ws).connId);\n }\n\n deliverLocal(cmd: BroadcastCommand): void | Promise<void> {\n const excludeIds = new Set(cmd.exceptIds ?? []);\n const excludeRooms = cmd.exceptRooms ?? [];\n\n // Empty `rooms` => GLOBAL (every socket in this DO). Otherwise the union of\n // targeted rooms, deduped per connection.\n const targets =\n cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);\n\n const seen = new Set<string>();\n const selected: Array<{ ws: WsLike; client: WsClient }> = [];\n for (const ws of targets) {\n const att = this.reconcileFrameLimit(ws, this.attachmentOf(ws));\n // A socket still in its connection hook is not admitted yet: it receives no\n // room frames, and its own accept() settles it. Rejecting it here would\n // close every client whose handleConnection broadcasts to the room. Other\n // pending sockets fall through and are rejected below.\n if (att.state === 'pending' && this.#admitting.has(ws)) continue;\n if (\n att.state !== 'active' ||\n (att.expiresAtMs !== undefined &&\n (!Number.isSafeInteger(att.expiresAtMs) || att.expiresAtMs <= Date.now()))\n ) {\n try {\n att.state = 'rejected';\n ws.serializeAttachment(att);\n ws.close(1008, 'identity expired or connection rejected');\n } catch {\n // already closed or malformed attachment\n }\n continue;\n }\n if (seen.has(att.connId)) continue;\n seen.add(att.connId);\n if (excludeIds.has(att.connId)) continue;\n if (excludeRooms.some((r) => att.rooms.includes(r))) continue;\n selected.push({ ws, client: this.clientFor(ws) });\n }\n\n if (!this.deliveryAuthorizer) {\n for (const { client } of selected) client.sendRaw(cmd.frame);\n return;\n }\n return Promise.all(\n selected.map(async ({ ws, client }) => {\n let allowed = false;\n try {\n allowed = (await this.deliveryAuthorizer!(client)) === true;\n } catch {\n allowed = false;\n }\n if (allowed) client.sendRaw(cmd.frame);\n else this.reject(ws, 'delivery authorization revoked');\n }),\n ).then(() => undefined);\n }\n\n private collectRooms(rooms: string[]): WsLike[] {\n const set = new Set<WsLike>();\n for (const room of rooms) for (const ws of this.socketsForRoom(room)) set.add(ws);\n return [...set];\n }\n\n private socketsForRoom(room: string): WsLike[] {\n // Membership is the attachment's `rooms`, NOT the hibernation tag: tags are\n // immutable after acceptWebSocket, so a socket that left its hub room still\n // carries the tag. Since one DO ≈ one room, scanning all sockets in the DO\n // and filtering by attachment is both correct and cheap.\n return this.ctx.getWebSockets().filter((ws) => this.attachmentOf(ws).rooms.includes(room));\n }\n\n private attachmentOf(ws: WsLike): WsAttachment {\n return socketAttachment(ws) ?? rejectedAttachment();\n }\n\n /** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */\n clientFor(ws: WsLike): CfWsClient {\n this.reconcileFrameLimit(ws, this.attachmentOf(ws));\n let gate = this.#sendGates.get(ws);\n if (!gate) {\n gate = new WebSocketSendGate(this.#sendPolicyForPath?.(this.attachmentOf(ws).path));\n this.#sendGates.set(ws, gate);\n }\n return new CfWsClient(this.ctx, ws, gate);\n }\n\n private reconcileFrameLimit(ws: WsLike, attachment: WsAttachment): WsAttachment {\n const expected = this.frameLimitForPath?.(attachment.path);\n if (expected !== undefined && attachment.maxFrameBytes !== expected) {\n attachment.maxFrameBytes = expected;\n ws.serializeAttachment(attachment);\n }\n return attachment;\n }\n\n private reject(ws: WsLike, reason: string): void {\n try {\n const attachment = this.attachmentOf(ws);\n attachment.state = 'rejected';\n ws.serializeAttachment(attachment);\n ws.close(1008, reason);\n } catch {\n // already closed or malformed attachment\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { rejectedAttachment, socketAttachment } from "./ws-attachment.js";
2
+ import { MAX_WS_ATTACHMENT_BYTES } from "./do-state.js";
3
+ import { DEFAULT_WS_MAX_FRAME_BYTES, DEFAULT_WS_MAX_JOINED_ROOMS, WebSocketSendGate, assertWebSocketRoomId } from "@velajs/vela/websocket";
4
+ //#region src/websocket/cf-ws-client.ts
5
+ const encoder = new TextEncoder();
6
+ /**
7
+ * Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.
8
+ * Per-connection state lives in the hibernation attachment (survives eviction),
9
+ * so a fresh `CfWsClient` is reconstructed per message with no in-memory state.
10
+ */
11
+ var CfWsClient = class {
12
+ ctx;
13
+ ws;
14
+ sendGate;
15
+ #attachment;
16
+ constructor(ctx, ws, sendGate = new WebSocketSendGate()) {
17
+ this.ctx = ctx;
18
+ this.ws = ws;
19
+ this.sendGate = sendGate;
20
+ this.#attachment = socketAttachment(ws) ?? rejectedAttachment();
21
+ }
22
+ get id() {
23
+ return this.#attachment.connId;
24
+ }
25
+ /** The gateway route path this socket connected on (used to route messages). */
26
+ get path() {
27
+ return this.#attachment.path;
28
+ }
29
+ get rooms() {
30
+ return new Set(this.#attachment.rooms);
31
+ }
32
+ get data() {
33
+ return this.#attachment.data;
34
+ }
35
+ set data(value) {
36
+ this.#attachment.data = value;
37
+ }
38
+ get raw() {
39
+ return this.ws;
40
+ }
41
+ get maxFrameBytes() {
42
+ const value = this.#attachment.maxFrameBytes;
43
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_WS_MAX_FRAME_BYTES;
44
+ }
45
+ send(event, data, id) {
46
+ this.sendRaw(JSON.stringify(id !== void 0 ? {
47
+ id,
48
+ event,
49
+ data
50
+ } : {
51
+ event,
52
+ data
53
+ }));
54
+ }
55
+ sendRaw(payload) {
56
+ this.trySendRaw(payload);
57
+ }
58
+ trySendRaw(payload) {
59
+ if (this.#attachment.state === "rejected") return "closed";
60
+ return this.sendGate.trySend(this.ws, payload, this.maxFrameBytes);
61
+ }
62
+ join(room) {
63
+ assertWebSocketRoomId(room);
64
+ if (!this.#attachment.rooms.includes(room)) {
65
+ if (this.#attachment.rooms.length >= DEFAULT_WS_MAX_JOINED_ROOMS) throw new Error(`A WebSocket may join at most ${DEFAULT_WS_MAX_JOINED_ROOMS} rooms`);
66
+ this.#attachment.rooms.push(room);
67
+ this.persist();
68
+ }
69
+ }
70
+ leave(room) {
71
+ const next = this.#attachment.rooms.filter((r) => r !== room);
72
+ if (next.length !== this.#attachment.rooms.length) {
73
+ this.#attachment.rooms = next;
74
+ this.persist();
75
+ }
76
+ }
77
+ /** Persist `data`/room mutations to the hibernation attachment. */
78
+ commit() {
79
+ this.persist();
80
+ }
81
+ close(code, reason) {
82
+ this.ws.close(code, reason);
83
+ }
84
+ persist() {
85
+ if (this.#attachment.state === "rejected") throw new Error("Invalid WebSocket attachment");
86
+ const serialized = JSON.stringify(this.#attachment);
87
+ if (encoder.encode(serialized).length > MAX_WS_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.");
88
+ this.ws.serializeAttachment(this.#attachment);
89
+ }
90
+ };
91
+ //#endregion
92
+ export { CfWsClient };
93
+
94
+ //# sourceMappingURL=cf-ws-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cf-ws-client.js","names":[],"sources":["../../src/websocket/cf-ws-client.ts"],"sourcesContent":["import { socketAttachment, rejectedAttachment } from './ws-attachment';\nimport type { WebSocketSendResult, WsClient } from '@velajs/vela/websocket';\nimport {\n assertWebSocketRoomId,\n DEFAULT_WS_MAX_FRAME_BYTES,\n DEFAULT_WS_MAX_JOINED_ROOMS,\n WebSocketSendGate,\n} from '@velajs/vela/websocket';\nimport {\n MAX_WS_ATTACHMENT_BYTES,\n type DoStateLike,\n type WsAttachment,\n type WsLike,\n} from './do-state';\nconst encoder = new TextEncoder();\n\n/**\n * Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.\n * Per-connection state lives in the hibernation attachment (survives eviction),\n * so a fresh `CfWsClient` is reconstructed per message with no in-memory state.\n */\nexport class CfWsClient<\n TData extends Record<string, unknown> = Record<string, unknown>,\n> implements WsClient<TData> {\n readonly #attachment: WsAttachment;\n\n constructor(\n private readonly ctx: DoStateLike,\n private readonly ws: WsLike,\n private readonly sendGate = new WebSocketSendGate(),\n ) {\n this.#attachment = socketAttachment(ws) ?? rejectedAttachment();\n }\n\n get id(): string {\n return this.#attachment.connId;\n }\n\n /** The gateway route path this socket connected on (used to route messages). */\n get path(): string {\n return this.#attachment.path;\n }\n\n get rooms(): ReadonlySet<string> {\n return new Set(this.#attachment.rooms);\n }\n\n get data(): TData {\n return this.#attachment.data as TData;\n }\n\n set data(value: TData) {\n this.#attachment.data = value;\n }\n\n get raw(): unknown {\n return this.ws;\n }\n\n get maxFrameBytes(): number {\n const value = this.#attachment.maxFrameBytes;\n return typeof value === 'number' && Number.isSafeInteger(value) && value > 0\n ? value\n : DEFAULT_WS_MAX_FRAME_BYTES;\n }\n\n send(event: string, data?: unknown, id?: string): void {\n this.sendRaw(JSON.stringify(id !== undefined ? { id, event, data } : { event, data }));\n }\n\n sendRaw(payload: string): void {\n this.trySendRaw(payload);\n }\n\n trySendRaw(payload: string): WebSocketSendResult {\n if (this.#attachment.state === 'rejected') return 'closed';\n return this.sendGate.trySend(this.ws, payload, this.maxFrameBytes);\n }\n\n join(room: string): void {\n assertWebSocketRoomId(room);\n if (!this.#attachment.rooms.includes(room)) {\n if (this.#attachment.rooms.length >= DEFAULT_WS_MAX_JOINED_ROOMS) {\n throw new Error(`A WebSocket may join at most ${DEFAULT_WS_MAX_JOINED_ROOMS} rooms`);\n }\n this.#attachment.rooms.push(room);\n this.persist();\n }\n }\n\n leave(room: string): void {\n const next = this.#attachment.rooms.filter((r) => r !== room);\n if (next.length !== this.#attachment.rooms.length) {\n this.#attachment.rooms = next;\n this.persist();\n }\n }\n\n /** Persist `data`/room mutations to the hibernation attachment. */\n commit(): void {\n this.persist();\n }\n\n close(code?: number, reason?: string): void {\n this.ws.close(code, reason);\n }\n\n private persist(): void {\n if (this.#attachment.state === 'rejected') throw new Error('Invalid WebSocket attachment');\n const serialized = JSON.stringify(this.#attachment);\n if (encoder.encode(serialized).length > MAX_WS_ATTACHMENT_BYTES) {\n throw new Error(\n `WebSocket attachment exceeds the 16 KiB Cloudflare limit. Store large ` +\n `per-connection state in Durable Object storage keyed by connId instead.`,\n );\n }\n this.ws.serializeAttachment(this.#attachment);\n }\n}\n"],"mappings":";;;;AAcA,MAAM,UAAU,IAAI,YAAY;;;;;;AAOhC,IAAa,aAAb,MAE6B;CAIR;CACA;CACA;CALnB;CAEA,YACE,KACA,IACA,WAA4B,IAAI,kBAAkB,GAClD;EAHiB,KAAA,MAAA;EACA,KAAA,KAAA;EACA,KAAA,WAAA;EAEjB,KAAK,cAAc,iBAAiB,EAAE,KAAK,mBAAmB;CAChE;CAEA,IAAI,KAAa;EACf,OAAO,KAAK,YAAY;CAC1B;;CAGA,IAAI,OAAe;EACjB,OAAO,KAAK,YAAY;CAC1B;CAEA,IAAI,QAA6B;EAC/B,OAAO,IAAI,IAAI,KAAK,YAAY,KAAK;CACvC;CAEA,IAAI,OAAc;EAChB,OAAO,KAAK,YAAY;CAC1B;CAEA,IAAI,KAAK,OAAc;EACrB,KAAK,YAAY,OAAO;CAC1B;CAEA,IAAI,MAAe;EACjB,OAAO,KAAK;CACd;CAEA,IAAI,gBAAwB;EAC1B,MAAM,QAAQ,KAAK,YAAY;EAC/B,OAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,QAAQ,IACvE,QACA;CACN;CAEA,KAAK,OAAe,MAAgB,IAAmB;EACrD,KAAK,QAAQ,KAAK,UAAU,OAAO,KAAA,IAAY;GAAE;GAAI;GAAO;EAAK,IAAI;GAAE;GAAO;EAAK,CAAC,CAAC;CACvF;CAEA,QAAQ,SAAuB;EAC7B,KAAK,WAAW,OAAO;CACzB;CAEA,WAAW,SAAsC;EAC/C,IAAI,KAAK,YAAY,UAAU,YAAY,OAAO;EAClD,OAAO,KAAK,SAAS,QAAQ,KAAK,IAAI,SAAS,KAAK,aAAa;CACnE;CAEA,KAAK,MAAoB;EACvB,sBAAsB,IAAI;EAC1B,IAAI,CAAC,KAAK,YAAY,MAAM,SAAS,IAAI,GAAG;GAC1C,IAAI,KAAK,YAAY,MAAM,UAAU,6BACnC,MAAM,IAAI,MAAM,gCAAgC,4BAA4B,OAAO;GAErF,KAAK,YAAY,MAAM,KAAK,IAAI;GAChC,KAAK,QAAQ;EACf;CACF;CAEA,MAAM,MAAoB;EACxB,MAAM,OAAO,KAAK,YAAY,MAAM,QAAQ,MAAM,MAAM,IAAI;EAC5D,IAAI,KAAK,WAAW,KAAK,YAAY,MAAM,QAAQ;GACjD,KAAK,YAAY,QAAQ;GACzB,KAAK,QAAQ;EACf;CACF;;CAGA,SAAe;EACb,KAAK,QAAQ;CACf;CAEA,MAAM,MAAe,QAAuB;EAC1C,KAAK,GAAG,MAAM,MAAM,MAAM;CAC5B;CAEA,UAAwB;EACtB,IAAI,KAAK,YAAY,UAAU,YAAY,MAAM,IAAI,MAAM,8BAA8B;EACzF,MAAM,aAAa,KAAK,UAAU,KAAK,WAAW;EAClD,IAAI,QAAQ,OAAO,UAAU,CAAC,CAAC,SAAS,yBACtC,MAAM,IAAI,MACR,+IAEF;EAEF,KAAK,GAAG,oBAAoB,KAAK,WAAW;CAC9C;AACF"}
@@ -0,0 +1,25 @@
1
+ import { WsServerHolder } from "./ws-server-holder.js";
2
+ import { defineProvider } from "@velajs/vela";
3
+ import { WS_SERVER, WsDispatcher } from "@velajs/vela/websocket";
4
+ //#region src/websocket/cloudflare-websocket.module.ts
5
+ /**
6
+ * Cloudflare counterpart to the core `WebSocketModule.forRoot()`. Import this in
7
+ * your `AppModule` instead: it provides the gateway dispatcher plus a late-bound
8
+ * `WS_SERVER` (`WsServerHolder`) that the WebSocket Durable Object wires to a
9
+ * ctx-backed server per instance. `useClass` ensures a fresh holder per DI
10
+ * container so colocated DO instances never share a server.
11
+ */
12
+ var CloudflareWebSocketModule = class CloudflareWebSocketModule {
13
+ static forRoot() {
14
+ const providers = [defineProvider(WS_SERVER, { useClass: WsServerHolder }), WsDispatcher];
15
+ return {
16
+ module: CloudflareWebSocketModule,
17
+ providers,
18
+ exports: [WS_SERVER, WsDispatcher]
19
+ };
20
+ }
21
+ };
22
+ //#endregion
23
+ export { CloudflareWebSocketModule };
24
+
25
+ //# sourceMappingURL=cloudflare-websocket.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-websocket.module.js","names":[],"sources":["../../src/websocket/cloudflare-websocket.module.ts"],"sourcesContent":["import { defineProvider, type DynamicModule } from '@velajs/vela';\nimport { WsDispatcher, WS_SERVER } from '@velajs/vela/websocket';\nimport { WsServerHolder } from './ws-server-holder';\n\n/**\n * Cloudflare counterpart to the core `WebSocketModule.forRoot()`. Import this in\n * your `AppModule` instead: it provides the gateway dispatcher plus a late-bound\n * `WS_SERVER` (`WsServerHolder`) that the WebSocket Durable Object wires to a\n * ctx-backed server per instance. `useClass` ensures a fresh holder per DI\n * container so colocated DO instances never share a server.\n */\nexport class CloudflareWebSocketModule {\n static forRoot(): DynamicModule {\n const providers = [defineProvider(WS_SERVER, { useClass: WsServerHolder }), WsDispatcher];\n\n return {\n module: CloudflareWebSocketModule,\n providers,\n exports: [WS_SERVER, WsDispatcher],\n };\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,IAAa,4BAAb,MAAa,0BAA0B;CACrC,OAAO,UAAyB;EAC9B,MAAM,YAAY,CAAC,eAAe,WAAW,EAAE,UAAU,eAAe,CAAC,GAAG,YAAY;EAExF,OAAO;GACL,QAAQ;GACR;GACA,SAAS,CAAC,WAAW,YAAY;EACnC;CACF;AACF"}