@velajs/cloudflare 1.29.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js +8 -0
- package/dist/cloudflare-application.js +309 -0
- package/dist/cloudflare-application.js.map +1 -0
- package/dist/cloudflare-factory.js +109 -0
- package/dist/cloudflare-factory.js.map +1 -0
- package/dist/decorators/queue-consumer.js +39 -0
- package/dist/decorators/queue-consumer.js.map +1 -0
- package/dist/durable-objects.d.ts +3 -4
- package/dist/durable-objects.js +2 -524
- package/dist/environment.js +19 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +21 -1276
- package/dist/nonce/durable-object-nonce.store.js +33 -0
- package/dist/nonce/durable-object-nonce.store.js.map +1 -0
- package/dist/nonce/nonce-validation.js +18 -0
- package/dist/nonce/nonce-validation.js.map +1 -0
- package/dist/nonce/nonce.durable-object.js +54 -0
- package/dist/nonce/nonce.durable-object.js.map +1 -0
- package/dist/{nonce.durable-object-Df4CZi-0.d.ts → nonce.durable-object-Ci9AGssY.d.ts} +2 -3
- package/dist/queue/cloudflare-queues.js +215 -0
- package/dist/queue/cloudflare-queues.js.map +1 -0
- package/dist/queues.d.ts +1 -2
- package/dist/queues.js +1 -214
- package/dist/rate-limit/cloudflare-rate-limit.store.js +40 -0
- package/dist/rate-limit/cloudflare-rate-limit.store.js.map +1 -0
- package/dist/schedule-diagnostics.js +51 -0
- package/dist/schedule-diagnostics.js.map +1 -0
- package/dist/scheduled-event.js +68 -0
- package/dist/scheduled-event.js.map +1 -0
- package/dist/services/flagship-flag.driver.js +52 -0
- package/dist/services/flagship-flag.driver.js.map +1 -0
- package/dist/services/kv-cache.store.js +82 -0
- package/dist/services/kv-cache.store.js.map +1 -0
- package/dist/services/kv-flag.driver.js +62 -0
- package/dist/services/kv-flag.driver.js.map +1 -0
- package/dist/storage/r2-storage.driver.js +67 -0
- package/dist/storage/r2-storage.driver.js.map +1 -0
- package/dist/storage/storage-key-claim.js +74 -0
- package/dist/storage/storage-key-claim.js.map +1 -0
- package/dist/storage/storage-manager.service.js +38 -0
- package/dist/storage/storage-manager.service.js.map +1 -0
- package/dist/storage/storage.controller.js +64 -0
- package/dist/storage/storage.controller.js.map +1 -0
- package/dist/storage/storage.module.js +25 -0
- package/dist/storage/storage.module.js.map +1 -0
- package/dist/storage/storage.service.js +64 -0
- package/dist/storage/storage.service.js.map +1 -0
- package/dist/storage/storage.tokens.js +7 -0
- package/dist/storage/storage.tokens.js.map +1 -0
- package/dist/vela-env-DFvyoNT3.d.ts +1 -2
- package/dist/websocket/broadcast.js +32 -0
- package/dist/websocket/broadcast.js.map +1 -0
- package/dist/websocket/cf-room-registry.js +137 -0
- package/dist/websocket/cf-room-registry.js.map +1 -0
- package/dist/websocket/cf-ws-client.js +94 -0
- package/dist/websocket/cf-ws-client.js.map +1 -0
- package/dist/websocket/cloudflare-websocket.module.js +25 -0
- package/dist/websocket/cloudflare-websocket.module.js.map +1 -0
- package/dist/websocket/do-bootstrap.js +56 -0
- package/dist/websocket/do-bootstrap.js.map +1 -0
- package/dist/websocket/do-live.js +184 -0
- package/dist/websocket/do-live.js.map +1 -0
- package/dist/websocket/do-pitr.js +81 -0
- package/dist/websocket/do-pitr.js.map +1 -0
- package/dist/websocket/do-state.js +7 -0
- package/dist/websocket/do-state.js.map +1 -0
- package/dist/websocket/do-websocket-host.js +169 -0
- package/dist/websocket/do-websocket-host.js.map +1 -0
- package/dist/websocket/room-id.js +27 -0
- package/dist/websocket/room-id.js.map +1 -0
- package/dist/websocket/websocket-routing.js +133 -0
- package/dist/websocket/websocket-routing.js.map +1 -0
- package/dist/websocket/websocket.durable-object.js +138 -0
- package/dist/websocket/websocket.durable-object.js.map +1 -0
- package/dist/websocket/ws-attachment.js +79 -0
- package/dist/websocket/ws-attachment.js.map +1 -0
- package/dist/websocket/ws-server-holder.js +38 -0
- package/dist/websocket/ws-server-holder.js.map +1 -0
- package/package.json +9 -7
- package/dist/durable-objects.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/nonce-validation-Dy8z05A9.js +0 -522
- package/dist/nonce-validation-Dy8z05A9.js.map +0 -1
- package/dist/queues.js.map +0 -1
package/dist/durable-objects.js
CHANGED
|
@@ -1,525 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { WS_SERVER, WebSocketSendGate, WsDispatcher, WsMessageQueue, WsServerImpl, assertBroadcastCommandFits, local, readWsEntrypointMeta } from "@velajs/vela/websocket";
|
|
4
|
-
import { DurableObject } from "cloudflare:workers";
|
|
5
|
-
//#region src/websocket/cf-room-registry.ts
|
|
6
|
-
/**
|
|
7
|
-
* Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room
|
|
8
|
-
* membership lives in tags (hub room, set at accept) + the attachment (dynamic
|
|
9
|
-
* `join()`), never in DO instance fields — so it survives hibernation with zero
|
|
10
|
-
* rehydration.
|
|
11
|
-
*/
|
|
12
|
-
var CfRoomRegistry = class {
|
|
13
|
-
ctx;
|
|
14
|
-
#sendGates = /* @__PURE__ */ new WeakMap();
|
|
15
|
-
#admitting = /* @__PURE__ */ new WeakSet();
|
|
16
|
-
#sendPolicyForPath;
|
|
17
|
-
setSendPolicyResolver(resolve) {
|
|
18
|
-
this.#sendPolicyForPath = resolve;
|
|
19
|
-
}
|
|
20
|
-
deliveryAuthorizer;
|
|
21
|
-
frameLimitForPath;
|
|
22
|
-
constructor(ctx) {
|
|
23
|
-
this.ctx = ctx;
|
|
24
|
-
}
|
|
25
|
-
setDeliveryAuthorizer(authorizer) {
|
|
26
|
-
this.deliveryAuthorizer = authorizer;
|
|
27
|
-
}
|
|
28
|
-
/** Reconcile pre-migration/woken attachments with authoritative gateway metadata. */
|
|
29
|
-
setFrameLimitResolver(resolver) {
|
|
30
|
-
this.frameLimitForPath = resolver;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Run `admit` (the connection hook) with `ws` marked as being admitted, so a
|
|
34
|
-
* broadcast issued meanwhile (for example `server.emit()` announcing the new
|
|
35
|
-
* connection) skips the still-pending socket instead of rejecting it.
|
|
36
|
-
*/
|
|
37
|
-
async admitting(ws, admit) {
|
|
38
|
-
this.#admitting.add(ws);
|
|
39
|
-
try {
|
|
40
|
-
return await admit();
|
|
41
|
-
} finally {
|
|
42
|
-
this.#admitting.delete(ws);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
register(_client) {}
|
|
46
|
-
leaveAll(_client) {}
|
|
47
|
-
join(client, room) {
|
|
48
|
-
return client.join(room);
|
|
49
|
-
}
|
|
50
|
-
leave(client, room) {
|
|
51
|
-
return client.leave(room);
|
|
52
|
-
}
|
|
53
|
-
localIdsInRoom(room) {
|
|
54
|
-
return this.socketsForRoom(room).map((ws) => this.attachmentOf(ws).connId);
|
|
55
|
-
}
|
|
56
|
-
deliverLocal(cmd) {
|
|
57
|
-
const excludeIds = new Set(cmd.exceptIds ?? []);
|
|
58
|
-
const excludeRooms = cmd.exceptRooms ?? [];
|
|
59
|
-
const targets = cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);
|
|
60
|
-
const seen = /* @__PURE__ */ new Set();
|
|
61
|
-
const selected = [];
|
|
62
|
-
for (const ws of targets) {
|
|
63
|
-
const att = this.reconcileFrameLimit(ws, this.attachmentOf(ws));
|
|
64
|
-
if (att.state === "pending" && this.#admitting.has(ws)) continue;
|
|
65
|
-
if (att.state !== "active" || att.expiresAtMs !== void 0 && (!Number.isSafeInteger(att.expiresAtMs) || att.expiresAtMs <= Date.now())) {
|
|
66
|
-
try {
|
|
67
|
-
att.state = "rejected";
|
|
68
|
-
ws.serializeAttachment(att);
|
|
69
|
-
ws.close(1008, "identity expired or connection rejected");
|
|
70
|
-
} catch {}
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (seen.has(att.connId)) continue;
|
|
74
|
-
seen.add(att.connId);
|
|
75
|
-
if (excludeIds.has(att.connId)) continue;
|
|
76
|
-
if (excludeRooms.some((r) => att.rooms.includes(r))) continue;
|
|
77
|
-
selected.push({
|
|
78
|
-
ws,
|
|
79
|
-
client: this.clientFor(ws)
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (!this.deliveryAuthorizer) {
|
|
83
|
-
for (const { client } of selected) client.sendRaw(cmd.frame);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
return Promise.all(selected.map(async ({ ws, client }) => {
|
|
87
|
-
let allowed = false;
|
|
88
|
-
try {
|
|
89
|
-
allowed = await this.deliveryAuthorizer(client) === true;
|
|
90
|
-
} catch {
|
|
91
|
-
allowed = false;
|
|
92
|
-
}
|
|
93
|
-
if (allowed) client.sendRaw(cmd.frame);
|
|
94
|
-
else this.reject(ws, "delivery authorization revoked");
|
|
95
|
-
})).then(() => void 0);
|
|
96
|
-
}
|
|
97
|
-
collectRooms(rooms) {
|
|
98
|
-
const set = /* @__PURE__ */ new Set();
|
|
99
|
-
for (const room of rooms) for (const ws of this.socketsForRoom(room)) set.add(ws);
|
|
100
|
-
return [...set];
|
|
101
|
-
}
|
|
102
|
-
socketsForRoom(room) {
|
|
103
|
-
return this.ctx.getWebSockets().filter((ws) => this.attachmentOf(ws).rooms.includes(room));
|
|
104
|
-
}
|
|
105
|
-
attachmentOf(ws) {
|
|
106
|
-
return socketAttachment(ws) ?? rejectedAttachment();
|
|
107
|
-
}
|
|
108
|
-
/** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */
|
|
109
|
-
clientFor(ws) {
|
|
110
|
-
this.reconcileFrameLimit(ws, this.attachmentOf(ws));
|
|
111
|
-
let gate = this.#sendGates.get(ws);
|
|
112
|
-
if (!gate) {
|
|
113
|
-
gate = new WebSocketSendGate(this.#sendPolicyForPath?.(this.attachmentOf(ws).path));
|
|
114
|
-
this.#sendGates.set(ws, gate);
|
|
115
|
-
}
|
|
116
|
-
return new CfWsClient(this.ctx, ws, gate);
|
|
117
|
-
}
|
|
118
|
-
reconcileFrameLimit(ws, attachment) {
|
|
119
|
-
const expected = this.frameLimitForPath?.(attachment.path);
|
|
120
|
-
if (expected !== void 0 && attachment.maxFrameBytes !== expected) {
|
|
121
|
-
attachment.maxFrameBytes = expected;
|
|
122
|
-
ws.serializeAttachment(attachment);
|
|
123
|
-
}
|
|
124
|
-
return attachment;
|
|
125
|
-
}
|
|
126
|
-
reject(ws, reason) {
|
|
127
|
-
try {
|
|
128
|
-
const attachment = this.attachmentOf(ws);
|
|
129
|
-
attachment.state = "rejected";
|
|
130
|
-
ws.serializeAttachment(attachment);
|
|
131
|
-
ws.close(1008, reason);
|
|
132
|
-
} catch {}
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/websocket/do-websocket-host.ts
|
|
137
|
-
/**
|
|
138
|
-
* The socket lifecycle inside a WebSocket Durable Object, decoupled from the
|
|
139
|
-
* `cloudflare:workers` base class so it is unit-testable with fakes. The thin
|
|
140
|
-
* `VelaWebSocketDurableObject` shell forwards its hibernation callbacks here.
|
|
141
|
-
*/
|
|
142
|
-
var DoWebSocketHost = class {
|
|
143
|
-
ctx;
|
|
144
|
-
dispatcher;
|
|
145
|
-
registry;
|
|
146
|
-
gatewayPaths;
|
|
147
|
-
#messages = /* @__PURE__ */ new WeakMap();
|
|
148
|
-
constructor(ctx, dispatcher, registry, gatewayPaths = []) {
|
|
149
|
-
this.ctx = ctx;
|
|
150
|
-
this.dispatcher = dispatcher;
|
|
151
|
-
this.registry = registry;
|
|
152
|
-
this.gatewayPaths = gatewayPaths;
|
|
153
|
-
}
|
|
154
|
-
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */
|
|
155
|
-
defaultPath() {
|
|
156
|
-
return this.gatewayPaths[0];
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Accept a hibernatable socket: tag it with its hub room + connection id,
|
|
160
|
-
* persist the attachment, then await `OnGatewayConnection` before the caller
|
|
161
|
-
* returns 101. A rejected lifecycle hook closes and fails the upgrade.
|
|
162
|
-
*/
|
|
163
|
-
async accept(ws, path, roomId, userId, expiresAtMs, principal) {
|
|
164
|
-
const maxFrameBytes = this.dispatcher.getGatewayMaxFrameBytes(path);
|
|
165
|
-
if (maxFrameBytes === void 0) return false;
|
|
166
|
-
if (expiresAtMs !== void 0 && (!Number.isSafeInteger(expiresAtMs) || expiresAtMs <= Date.now())) return false;
|
|
167
|
-
if (principal !== void 0 && (expiresAtMs === void 0 || userId !== principal.subject || !principal.issuer || !principal.tenantId) || expiresAtMs !== void 0 && principal === void 0) return false;
|
|
168
|
-
const connId = crypto.randomUUID();
|
|
169
|
-
const attachment = {
|
|
170
|
-
version: 1,
|
|
171
|
-
connId,
|
|
172
|
-
state: "pending",
|
|
173
|
-
userId,
|
|
174
|
-
principal: principal === void 0 ? void 0 : {
|
|
175
|
-
issuer: principal.issuer,
|
|
176
|
-
subject: principal.subject,
|
|
177
|
-
principalType: principal.principalType
|
|
178
|
-
},
|
|
179
|
-
tenantId: principal?.tenantId,
|
|
180
|
-
expiresAtMs,
|
|
181
|
-
path,
|
|
182
|
-
maxFrameBytes,
|
|
183
|
-
rooms: [roomId],
|
|
184
|
-
data: {
|
|
185
|
-
...userId ? { userId } : {},
|
|
186
|
-
...principal ? {
|
|
187
|
-
principal: {
|
|
188
|
-
issuer: principal.issuer,
|
|
189
|
-
subject: principal.subject,
|
|
190
|
-
principalType: principal.principalType
|
|
191
|
-
},
|
|
192
|
-
tenantId: principal.tenantId
|
|
193
|
-
} : {},
|
|
194
|
-
...expiresAtMs !== void 0 ? { expiresAtMs } : {}
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
if (new TextEncoder().encode(JSON.stringify(attachment)).byteLength > 16384) return false;
|
|
198
|
-
try {
|
|
199
|
-
this.ctx.acceptWebSocket(ws, [roomTag(roomId), connTag(connId)]);
|
|
200
|
-
ws.serializeAttachment(attachment);
|
|
201
|
-
} catch {
|
|
202
|
-
try {
|
|
203
|
-
ws.close(1008, "Connection rejected");
|
|
204
|
-
} catch {}
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
const client = this.registry.clientFor(ws);
|
|
208
|
-
try {
|
|
209
|
-
await this.registry.admitting(ws, () => this.dispatcher.handleOpen(path, client));
|
|
210
|
-
if (!this.transition(ws, "active", "pending")) throw new Error(socketAttachment(ws)?.state === "rejected" ? "WebSocket was rejected while OnGatewayConnection was running: a frame arrived before the connection hook completed, or the connection closed or failed" : "Unable to persist authorized WebSocket state");
|
|
211
|
-
return true;
|
|
212
|
-
} catch (err) {
|
|
213
|
-
await this.dispatcher.handleError(path, client, err).catch(() => {});
|
|
214
|
-
this.transition(ws, "rejected");
|
|
215
|
-
try {
|
|
216
|
-
ws.close(1008, "Connection rejected");
|
|
217
|
-
} catch {}
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
async onMessage(ws, message) {
|
|
222
|
-
if (!this.isActive(ws)) {
|
|
223
|
-
this.reject(ws, "Connection is not authorized");
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
let queue = this.#messages.get(ws);
|
|
227
|
-
if (!queue) {
|
|
228
|
-
const path = socketAttachment(ws)?.path;
|
|
229
|
-
const options = this.dispatcher.collectEntrypoints().find((entry) => entry.meta.path === path)?.meta.options;
|
|
230
|
-
queue = new WsMessageQueue(() => this.reject(ws, "WebSocket message budget exceeded", 1013), options?.maxPendingMessages, options?.maxPendingBytes);
|
|
231
|
-
this.#messages.set(ws, queue);
|
|
232
|
-
}
|
|
233
|
-
await queue.run(message, async () => {
|
|
234
|
-
if (!this.isActive(ws)) return;
|
|
235
|
-
const client = this.registry.clientFor(ws);
|
|
236
|
-
await this.dispatcher.dispatchMessage(client.path, client, message);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
async onClose(ws, code, reason) {
|
|
240
|
-
this.#messages.get(ws)?.stop();
|
|
241
|
-
if (!this.isActive(ws, false)) {
|
|
242
|
-
this.transition(ws, "rejected");
|
|
243
|
-
try {
|
|
244
|
-
ws.close();
|
|
245
|
-
} catch {}
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const client = this.registry.clientFor(ws);
|
|
249
|
-
await this.dispatcher.handleClose(client.path, client, code, reason);
|
|
250
|
-
try {
|
|
251
|
-
if (code === 1e3 || code >= 3e3 && code <= 4999) ws.close(code, reason);
|
|
252
|
-
else ws.close();
|
|
253
|
-
} catch {}
|
|
254
|
-
}
|
|
255
|
-
async onError(ws, err) {
|
|
256
|
-
if (!this.isActive(ws, false)) {
|
|
257
|
-
this.reject(ws, "Connection failed before authorization");
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const client = this.registry.clientFor(ws);
|
|
261
|
-
await this.dispatcher.handleError(client.path, client, err);
|
|
262
|
-
}
|
|
263
|
-
/** Deliver a broadcast command to this DO's local sockets (RPC entry point). */
|
|
264
|
-
broadcast(cmd) {
|
|
265
|
-
assertBroadcastCommandFits(cmd, this.dispatcher.getMaximumGatewayFrameBytes());
|
|
266
|
-
return this.registry.deliverLocal(cmd);
|
|
267
|
-
}
|
|
268
|
-
transition(ws, state, expectedState) {
|
|
269
|
-
try {
|
|
270
|
-
const attachment = socketAttachment(ws);
|
|
271
|
-
if (!attachment) return false;
|
|
272
|
-
if (expectedState !== void 0 && attachment.state !== expectedState) return false;
|
|
273
|
-
attachment.state = state;
|
|
274
|
-
ws.serializeAttachment(attachment);
|
|
275
|
-
return true;
|
|
276
|
-
} catch {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
isActive(ws, checkExpiry = true) {
|
|
281
|
-
try {
|
|
282
|
-
const attachment = socketAttachment(ws);
|
|
283
|
-
if (!attachment || attachment.state !== "active") return false;
|
|
284
|
-
return !checkExpiry || attachment.expiresAtMs === void 0 || Number.isSafeInteger(attachment.expiresAtMs) && attachment.expiresAtMs > Date.now();
|
|
285
|
-
} catch {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
reject(ws, reason, code = 1008) {
|
|
290
|
-
this.#messages.get(ws)?.stop();
|
|
291
|
-
this.transition(ws, "rejected");
|
|
292
|
-
try {
|
|
293
|
-
ws.close(code, reason);
|
|
294
|
-
} catch {}
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
//#endregion
|
|
298
|
-
//#region src/websocket/do-bootstrap.ts
|
|
299
|
-
/**
|
|
300
|
-
* Slim DI bootstrap for the Durable Object isolate: wires the container and runs
|
|
301
|
-
* `OnModuleInit`/`OnApplicationBootstrap` (so `WsDispatcher` discovers gateways)
|
|
302
|
-
* WITHOUT building the Hono app/routes the DO never serves. The DO's `env` is
|
|
303
|
-
* seeded as the global ENV before providers construct, and the ctx-backed server
|
|
304
|
-
* is bound before bootstrap lifecycle so gateway `afterInit`/handlers see it.
|
|
305
|
-
*/
|
|
306
|
-
async function buildDoRuntime(rootModule, ctx, options) {
|
|
307
|
-
const { container, routeManager, loader } = await bootstrap(rootModule, { configureContainer: (container) => {
|
|
308
|
-
registerCloudflareEnvironment(container, options.env);
|
|
309
|
-
} });
|
|
310
|
-
const registry = new CfRoomRegistry(ctx);
|
|
311
|
-
const driver = local();
|
|
312
|
-
driver.bind(registry);
|
|
313
|
-
const server = new WsServerImpl(driver);
|
|
314
|
-
if (container.has(WS_SERVER)) {
|
|
315
|
-
const holder = await container.resolveAsync(WS_SERVER);
|
|
316
|
-
if (!(holder instanceof WsServerHolder)) throw new Error("[vela] The WebSocket Durable Object found a WS_SERVER from the core WebSocketModule, which cannot reach Durable Object sockets. On Cloudflare, import CloudflareWebSocketModule.forRoot() instead of WebSocketModule.forRoot().");
|
|
317
|
-
holder.setTarget(server);
|
|
318
|
-
}
|
|
319
|
-
const app = new VelaApplication(container, routeManager);
|
|
320
|
-
app.setInstances(await loader.resolveAllInstances());
|
|
321
|
-
initializeDoLiveResources(container, ctx);
|
|
322
|
-
await app.callOnModuleInit();
|
|
323
|
-
await app.callOnApplicationBootstrap();
|
|
324
|
-
const wsEntrypoints = app.entrypoints.ofKind("websocket", readWsEntrypointMeta);
|
|
325
|
-
const dispatcher = wsEntrypoints[0]?.meta.dispatcher ?? app.get(WsDispatcher);
|
|
326
|
-
registry.setSendPolicyResolver((path) => wsEntrypoints.find((entry) => entry.meta.path === path)?.meta.options.sendPolicy);
|
|
327
|
-
registry.setFrameLimitResolver((path) => dispatcher.getGatewayMaxFrameBytes(path));
|
|
328
|
-
registry.setDeliveryAuthorizer((client) => {
|
|
329
|
-
const path = client instanceof CfWsClient ? client.path : "";
|
|
330
|
-
return dispatcher.authorizeDelivery(path, client);
|
|
331
|
-
});
|
|
332
|
-
const live = initDoLive(app, ctx, registry);
|
|
333
|
-
return {
|
|
334
|
-
dispatcher,
|
|
335
|
-
registry,
|
|
336
|
-
server,
|
|
337
|
-
gatewayPaths: wsEntrypoints.map((ep) => ep.meta.path),
|
|
338
|
-
live,
|
|
339
|
-
close: (signal) => app.close(signal)
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
//#endregion
|
|
343
|
-
//#region src/websocket/websocket.durable-object.ts
|
|
344
|
-
const PING = "{\"event\":\"$ping\"}";
|
|
345
|
-
const PONG = "{\"event\":\"$pong\"}";
|
|
346
|
-
const MAX_IDENTITY_FIELD_BYTES = 2048;
|
|
347
|
-
const encoder = new TextEncoder();
|
|
348
|
-
function isIdentityField(value) {
|
|
349
|
-
return value !== null && value.length > 0 && encoder.encode(value).byteLength <= MAX_IDENTITY_FIELD_BYTES;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Base class for the WebSocket Durable Object. The user exports a named subclass
|
|
353
|
-
* (matching their `wrangler.toml` `class_name`) built from their `AppModule`, or
|
|
354
|
-
* from a `DynamicModule` declared at module scope:
|
|
355
|
-
*
|
|
356
|
-
* ```ts
|
|
357
|
-
* export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
|
|
358
|
-
* ```
|
|
359
|
-
*
|
|
360
|
-
* It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
|
|
361
|
-
* bridge DO hibernation) and forwards every event into the runtime-agnostic
|
|
362
|
-
* `WsDispatcher` via {@link DoWebSocketHost}. The DO's `env` is the
|
|
363
|
-
* application's ENV, as in the Worker.
|
|
364
|
-
*/
|
|
365
|
-
function VelaWebSocketDurableObject(rootModule) {
|
|
366
|
-
return class VelaWsDurableObject extends DurableObject {
|
|
367
|
-
host;
|
|
368
|
-
liveEngine;
|
|
369
|
-
ready;
|
|
370
|
-
constructor(ctx, env) {
|
|
371
|
-
super(ctx, env);
|
|
372
|
-
try {
|
|
373
|
-
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));
|
|
374
|
-
} catch {}
|
|
375
|
-
this.ready = ctx.blockConcurrencyWhile(async () => {
|
|
376
|
-
const runtime = await buildDoRuntime(rootModule, ctx, { env });
|
|
377
|
-
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
|
|
378
|
-
this.liveEngine = runtime.live;
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
/** Intra-worker RPC only; HTTP fetch never exposes this admin snapshot. */
|
|
382
|
-
async inspectLive() {
|
|
383
|
-
await this.ready;
|
|
384
|
-
return this.liveEngine?.inspect() ?? {
|
|
385
|
-
subscriptions: [],
|
|
386
|
-
rooms: []
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
async fetch(request) {
|
|
390
|
-
await this.ready;
|
|
391
|
-
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
392
|
-
const url = new URL(request.url);
|
|
393
|
-
const roomId = request.headers.get("x-vela-room") ?? this.ctx.id.name ?? url.pathname;
|
|
394
|
-
const path = request.headers.get("x-vela-path") ?? this.host.defaultPath() ?? url.pathname;
|
|
395
|
-
const userId = request.headers.get("x-vela-user") || void 0;
|
|
396
|
-
const rawExpiresAtMs = request.headers.get("x-vela-expires-at-ms");
|
|
397
|
-
const parsedExpiresAtMs = rawExpiresAtMs === null ? void 0 : Number(rawExpiresAtMs);
|
|
398
|
-
if (parsedExpiresAtMs !== void 0 && (!Number.isSafeInteger(parsedExpiresAtMs) || parsedExpiresAtMs <= 0)) return new Response("Invalid WebSocket identity expiry", { status: 403 });
|
|
399
|
-
const issuer = request.headers.get("x-vela-issuer");
|
|
400
|
-
const subject = request.headers.get("x-vela-subject");
|
|
401
|
-
const principalType = request.headers.get("x-vela-principal-type");
|
|
402
|
-
const tenantId = request.headers.get("x-vela-tenant");
|
|
403
|
-
const hasPrincipalHeader = issuer !== null || subject !== null || principalType !== null || tenantId !== null;
|
|
404
|
-
let principal;
|
|
405
|
-
if (hasPrincipalHeader) {
|
|
406
|
-
if (!isIdentityField(issuer) || !isIdentityField(subject) || principalType !== "user" && principalType !== "service" || !isIdentityField(tenantId) || parsedExpiresAtMs === void 0 || userId !== void 0 && userId !== subject) return new Response("Invalid WebSocket principal", { status: 403 });
|
|
407
|
-
principal = {
|
|
408
|
-
issuer,
|
|
409
|
-
subject,
|
|
410
|
-
principalType,
|
|
411
|
-
tenantId
|
|
412
|
-
};
|
|
413
|
-
} else if (parsedExpiresAtMs !== void 0) return new Response("WebSocket identity tuple is missing", { status: 403 });
|
|
414
|
-
if (parsedExpiresAtMs !== void 0 && parsedExpiresAtMs <= Date.now()) return new Response("WebSocket identity expired", { status: 403 });
|
|
415
|
-
const { 0: client, 1: server } = new WebSocketPair();
|
|
416
|
-
if (!await this.host.accept(server, path, roomId, userId, parsedExpiresAtMs, principal)) return new Response("WebSocket connection rejected", { status: 403 });
|
|
417
|
-
return new Response(null, {
|
|
418
|
-
status: 101,
|
|
419
|
-
webSocket: client
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
async webSocketMessage(ws, message) {
|
|
423
|
-
await this.ready;
|
|
424
|
-
await this.host.onMessage(ws, message);
|
|
425
|
-
}
|
|
426
|
-
async webSocketClose(ws, code, reason) {
|
|
427
|
-
await this.ready;
|
|
428
|
-
await this.host.onClose(ws, code, reason);
|
|
429
|
-
}
|
|
430
|
-
async webSocketError(ws, error) {
|
|
431
|
-
await this.ready;
|
|
432
|
-
await this.host.onError(ws, error);
|
|
433
|
-
}
|
|
434
|
-
/** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */
|
|
435
|
-
async broadcast(cmd) {
|
|
436
|
-
await this.ready;
|
|
437
|
-
await this.host.broadcast(cmd);
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /
|
|
441
|
-
* liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns
|
|
442
|
-
* the commit stamp (what `Vela-Commit-Cursor` carries); the subscription
|
|
443
|
-
* refreshes fan out asynchronously.
|
|
444
|
-
*/
|
|
445
|
-
async invalidate(cmd) {
|
|
446
|
-
await this.ready;
|
|
447
|
-
return this.liveEngine?.applyInvalidation(cmd);
|
|
448
|
-
}
|
|
449
|
-
/** DO RPC — the current PITR bookmark (typed-unavailable on a non-SQLite DO). */
|
|
450
|
-
async pitrCurrentBookmark() {
|
|
451
|
-
await this.ready;
|
|
452
|
-
return readDoPitrBookmark(this.ctx.storage);
|
|
453
|
-
}
|
|
454
|
-
/** DO RPC — the PITR bookmark closest to `time` (+ the current bookmark). */
|
|
455
|
-
async pitrBookmarkForTime(time) {
|
|
456
|
-
await this.ready;
|
|
457
|
-
return readDoPitrBookmark(this.ctx.storage, time);
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* DO RPC — arm a PITR restore and return the undo bookmark. When `restart` is
|
|
461
|
-
* requested, `ctx.abort()` is called AFTER the undo bookmark is computed so
|
|
462
|
-
* the recovery applies on the immediately-following session; otherwise the
|
|
463
|
-
* restore applies on the next natural restart (`restarted: false`).
|
|
464
|
-
*/
|
|
465
|
-
async pitrArmRestore(opts) {
|
|
466
|
-
await this.ready;
|
|
467
|
-
const result = await armDoPitr(this.ctx.storage, opts);
|
|
468
|
-
if (opts.restart === true) this.ctx.abort("vela PITR restore");
|
|
469
|
-
return result;
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
//#endregion
|
|
474
|
-
//#region src/nonce/nonce.durable-object.ts
|
|
475
|
-
const EXPIRED_DELETE_BATCH = 1024;
|
|
476
|
-
const CREATE_TABLE = "CREATE TABLE IF NOT EXISTS __vela_nonce_claims (nonce TEXT PRIMARY KEY NOT NULL, expires_at INTEGER NOT NULL CHECK (expires_at > 0)) WITHOUT ROWID";
|
|
477
|
-
const CREATE_EXPIRY_INDEX = "CREATE INDEX IF NOT EXISTS __vela_nonce_claims_expiry ON __vela_nonce_claims (expires_at)";
|
|
478
|
-
const DELETE_EXPIRED = "DELETE FROM __vela_nonce_claims WHERE nonce IN (SELECT nonce FROM __vela_nonce_claims WHERE expires_at < ? ORDER BY expires_at LIMIT ?)";
|
|
479
|
-
const INSERT_CLAIM = "INSERT INTO __vela_nonce_claims (nonce, expires_at) VALUES (?, ?) ON CONFLICT(nonce) DO NOTHING RETURNING nonce, expires_at";
|
|
480
|
-
/**
|
|
481
|
-
* SQLite Durable Object that atomically consumes nonces.
|
|
482
|
-
*
|
|
483
|
-
* Export this class from the Worker entry and register it through a Wrangler
|
|
484
|
-
* `new_sqlite_classes` migration. `INSERT ... ON CONFLICT DO NOTHING RETURNING`
|
|
485
|
-
* is the single-use decision; all SQL runs synchronously before the RPC method
|
|
486
|
-
* yields, and the nonce primary key is the final concurrency boundary.
|
|
487
|
-
*/
|
|
488
|
-
var VelaNonceDurableObject = class extends DurableObject {
|
|
489
|
-
sql;
|
|
490
|
-
constructor(ctx, env) {
|
|
491
|
-
super(ctx, env);
|
|
492
|
-
try {
|
|
493
|
-
const sql = ctx.storage?.sql;
|
|
494
|
-
if (!sql || typeof sql.exec !== "function") return;
|
|
495
|
-
sql.exec(CREATE_TABLE);
|
|
496
|
-
sql.exec(CREATE_EXPIRY_INDEX);
|
|
497
|
-
this.sql = sql;
|
|
498
|
-
} catch {
|
|
499
|
-
this.sql = void 0;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
async claim(nonce, expEpochSeconds) {
|
|
503
|
-
const sql = this.sql;
|
|
504
|
-
if (!sql) return false;
|
|
505
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
506
|
-
if (!isCanonicalBoundedText(nonce, 512) || !isValidExpiry(expEpochSeconds, now)) return false;
|
|
507
|
-
try {
|
|
508
|
-
sql.exec(DELETE_EXPIRED, now, EXPIRED_DELETE_BATCH);
|
|
509
|
-
const cursor = sql.exec(INSERT_CLAIM, nonce, expEpochSeconds);
|
|
510
|
-
if (!cursor || typeof cursor.toArray !== "function") return false;
|
|
511
|
-
const rows = cursor.toArray();
|
|
512
|
-
if (!Array.isArray(rows) || rows.length !== 1) return false;
|
|
513
|
-
const row = rows[0];
|
|
514
|
-
if (typeof row !== "object" || row === null) return false;
|
|
515
|
-
if (!Object.hasOwn(row, "nonce") || !Object.hasOwn(row, "expires_at") || !("nonce" in row) || !("expires_at" in row)) return false;
|
|
516
|
-
return row.nonce === nonce && row.expires_at === expEpochSeconds;
|
|
517
|
-
} catch {
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
//#endregion
|
|
1
|
+
import { VelaWebSocketDurableObject } from "./websocket/websocket.durable-object.js";
|
|
2
|
+
import { VelaNonceDurableObject } from "./nonce/nonce.durable-object.js";
|
|
523
3
|
export { VelaNonceDurableObject, VelaWebSocketDurableObject };
|
|
524
|
-
|
|
525
|
-
//# sourceMappingURL=durable-objects.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ENV, defineProvider } from "@velajs/vela";
|
|
2
|
+
//#region src/environment.ts
|
|
3
|
+
/**
|
|
4
|
+
* Seed the application's native environment as the global ENV before provider
|
|
5
|
+
* factories or lifecycle hooks run. No platform I/O is performed here; callers
|
|
6
|
+
* create applications inside an event or a Durable Object constructor.
|
|
7
|
+
*/
|
|
8
|
+
function registerCloudflareEnvironment(container, env) {
|
|
9
|
+
container.register(defineProvider(ENV, { useValue: env }));
|
|
10
|
+
container.markGlobalToken(ENV);
|
|
11
|
+
}
|
|
12
|
+
/** Reject accidental reuse of an application with another event's environment. */
|
|
13
|
+
function assertCloudflareEnvironment(expected, actual) {
|
|
14
|
+
if (actual !== expected) throw new Error("This Cloudflare application belongs to a different environment. Create an application with the current event environment.");
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { assertCloudflareEnvironment, registerCloudflareEnvironment };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.js","names":[],"sources":["../src/environment.ts"],"sourcesContent":["import { defineProvider, ENV } from '@velajs/vela';\nimport type { VelaEnv } from '@velajs/vela';\nimport type { Container } from '@velajs/vela/module-kit';\n\n/**\n * Seed the application's native environment as the global ENV before provider\n * factories or lifecycle hooks run. No platform I/O is performed here; callers\n * create applications inside an event or a Durable Object constructor.\n */\nexport function registerCloudflareEnvironment(container: Container, env: VelaEnv): void {\n container.register(defineProvider(ENV, { useValue: env }));\n container.markGlobalToken(ENV);\n}\n\n/** Reject accidental reuse of an application with another event's environment. */\nexport function assertCloudflareEnvironment<T extends object>(expected: T, actual: T): void {\n if (actual !== expected) {\n throw new Error(\n 'This Cloudflare application belongs to a different environment. ' +\n 'Create an application with the current event environment.',\n );\n }\n}\n"],"mappings":";;;;;;;AASA,SAAgB,8BAA8B,WAAsB,KAAoB;CACtF,UAAU,SAAS,eAAe,KAAK,EAAE,UAAU,IAAI,CAAC,CAAC;CACzD,UAAU,gBAAgB,GAAG;AAC/B;;AAGA,SAAgB,4BAA8C,UAAa,QAAiB;CAC1F,IAAI,WAAW,UACb,MAAM,IAAI,MACR,2HAEF;AAEJ"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import "./vela-env-DFvyoNT3.js";
|
|
2
|
-
import { a as DoPitrId, c as DoPitrUnavailableError, d as isDoPitrUnavailable, f as readDoPitrBookmark, i as DoPitrBookmarkRead, l as VelaDoPitrRpc, n as DoPitrArmOptions, o as DoPitrNamespace, p as CloudflareRoot, r as DoPitrArmResult, s as DoPitrStorage, t as VelaNonceDurableObject, u as armDoPitr } from "./nonce.durable-object-
|
|
3
|
-
import {
|
|
4
|
-
import { BroadcastCommand,
|
|
5
|
-
import { CommitStamp, CursorLog, InvalidationCommand, LiveDriver,
|
|
6
|
-
import { DownloadResult, PresignMethod, PresignedUrlResult, StorageBody, StorageDriver, UploadOptions, UploadResult } from "@velajs/vela/storage";
|
|
2
|
+
import { a as DoPitrId, c as DoPitrUnavailableError, d as isDoPitrUnavailable, f as readDoPitrBookmark, i as DoPitrBookmarkRead, l as VelaDoPitrRpc, n as DoPitrArmOptions, o as DoPitrNamespace, p as CloudflareRoot, r as DoPitrArmResult, s as DoPitrStorage, t as VelaNonceDurableObject, u as armDoPitr } from "./nonce.durable-object-Ci9AGssY.js";
|
|
3
|
+
import { DynamicModule, InjectionToken, Type, VelaApplication, VelaEnv, VelaMiddlewareHandler, VelaSecurityOptions } from "@velajs/vela";
|
|
4
|
+
import { BroadcastCommand, WebSocketGatewayOptions } from "@velajs/vela/websocket";
|
|
5
|
+
import { CommitStamp, CursorLog, InvalidationCommand, LiveDriver, LiveInvalidationSink, ResumeVerdict } from "@velajs/vela/live";
|
|
7
6
|
import { Context, ExecutionContext } from "hono";
|
|
8
|
-
import "@velajs/vela/
|
|
7
|
+
import { RuntimeAdapter } from "@velajs/vela/module-kit";
|
|
8
|
+
import { DownloadResult, PresignMethod, PresignedUrlResult, StorageBody, StorageDriver, UploadOptions, UploadResult } from "@velajs/vela/storage";
|
|
9
|
+
import { AsyncCacheStore, CacheEntry, CacheEntryReader, CacheEntryWriter, CacheInvalidationStore } from "@velajs/vela/cache";
|
|
9
10
|
import { FeatureFlagDriver, FlagContext } from "@velajs/feature-flags";
|
|
11
|
+
import { ThrottlerStore } from "@velajs/vela/throttler";
|
|
12
|
+
import { NonceStore } from "@velajs/vela/security";
|
|
10
13
|
//#region src/websocket/room-id.d.ts
|
|
11
14
|
/** Stable, collision-free Durable Object name for one gateway's room. */
|
|
12
15
|
export declare function durableObjectRoomName(gatewayPath: string, roomId: string): string;
|
|
@@ -141,7 +144,7 @@ export declare class CloudflareApplication {
|
|
|
141
144
|
*
|
|
142
145
|
* @example
|
|
143
146
|
* ```ts
|
|
144
|
-
* import { createOpenApiDocument } from '@velajs/vela';
|
|
147
|
+
* import { createOpenApiDocument } from '@velajs/vela/openapi';
|
|
145
148
|
*
|
|
146
149
|
* const app = await createCloudflareApp(AppModule, { env });
|
|
147
150
|
* const document = createOpenApiDocument(AppModule, {
|
|
@@ -678,5 +681,4 @@ interface DurableObjectNonceStoreOptions {
|
|
|
678
681
|
*/
|
|
679
682
|
export declare function durableObjectNonceStore(options: DurableObjectNonceStoreOptions): NonceStore;
|
|
680
683
|
//#endregion
|
|
681
|
-
export { type BroadcastNamespace, type CfLiveDriver, type CloudflareRateLimitBinding, type CloudflareRateLimitStoreOptions, type CloudflareRoot, type CloudflareScheduledEvent, type CloudflareWorkerOptions,
|
|
682
|
-
//# sourceMappingURL=index.d.ts.map
|
|
684
|
+
export { type BroadcastNamespace, type CfLiveDriver, type CloudflareRateLimitBinding, type CloudflareRateLimitStoreOptions, type CloudflareRoot, type CloudflareScheduledEvent, type CloudflareWorkerOptions, type CreateCloudflareAppOptions, type DiskConfig, type DoPitrArmOptions, type DoPitrArmResult, type DoPitrBookmarkRead, type DoPitrId, type DoPitrNamespace, type DoPitrStorage, DoPitrUnavailableError, type DurableObjectLiveOptions, type DurableObjectNonceNamespace, type DurableObjectNonceStoreOptions, type FlagshipBinding, type FlagshipFlagDriverOptions, type KvFlagDriverOptions, type LiveNamespace, type MountOpenApiOptions, type PresignedUrlConfig, type QueueConsumerMetadata, type ScheduledEvent, type StorageModuleOptions, type VelaDoPitrRpc, type WsGatewayRoute, armDoPitr, isDoPitrUnavailable, readDoPitrBookmark };
|