@lunora/runtime 1.0.0-alpha.37 → 1.0.0-alpha.39

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 (51) hide show
  1. package/dist/index.d.mts +359 -8
  2. package/dist/index.d.ts +359 -8
  3. package/dist/index.mjs +1 -22
  4. package/dist/packem_shared/DEFAULT_LOG_COLUMNS-CpZ9kd-q.mjs +1 -0
  5. package/dist/packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-BBf3mNrD.mjs +1 -0
  6. package/dist/packem_shared/GET_AUTH_AUDIT_LOG_OP-DmxHQbZT.mjs +1 -0
  7. package/dist/packem_shared/HEALTH_PATH-DiZqjEGp.mjs +1 -0
  8. package/dist/packem_shared/LOG_ARCHIVE_NOT_CONFIGURED-CDpi4yFD.mjs +1 -0
  9. package/dist/packem_shared/LOG_ARCHIVE_PATH-e8U3ftfM.mjs +1 -0
  10. package/dist/packem_shared/LunoraError-C08OP5Uq.mjs +1 -0
  11. package/dist/packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs +1 -0
  12. package/dist/packem_shared/analyticsEngineSink-DOoyBx4o.mjs +1 -0
  13. package/dist/packem_shared/applyJurisdiction-uRQLx282.mjs +1 -0
  14. package/dist/packem_shared/argsFromQuery-0KrWTkNx.mjs +1 -0
  15. package/dist/packem_shared/composeIdentityResolvers-DlBbYmBJ.mjs +1 -0
  16. package/dist/packem_shared/composeWorker-D7Zi6kZR.mjs +6 -0
  17. package/dist/packem_shared/createCrossShardRelationCapabilities-B2EKbSEs.mjs +1 -0
  18. package/dist/packem_shared/createKvCursorStore-C24tEuYk.mjs +3 -0
  19. package/dist/packem_shared/createQueryCoordinator-DLsrcOkB.mjs +1 -0
  20. package/dist/packem_shared/createShardClient-62qcYKGl.mjs +1 -0
  21. package/dist/packem_shared/decorateResponse-C6TZSzID.mjs +1 -0
  22. package/dist/packem_shared/emitLogEvent-CEbCh6FQ.mjs +1 -0
  23. package/dist/packem_shared/memoizeIdentity-DRYIK1yT.mjs +1 -0
  24. package/dist/packem_shared/method-guard-BbuR0VfS.mjs +1 -0
  25. package/dist/packem_shared/observability-B0MYwNDB.mjs +1 -0
  26. package/dist/packem_shared/otlp-resource-cAjGEywx.mjs +1 -0
  27. package/dist/packem_shared/pipeline-log-reader-tDuvc_Ty.mjs +1 -0
  28. package/dist/packem_shared/toAirbyteMessages-DBTuFjb5.mjs +1 -0
  29. package/package.json +1 -1
  30. package/dist/packem_shared/DEFAULT_LOG_COLUMNS-B7H3YdJ3.mjs +0 -2
  31. package/dist/packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-B3pA7aXp.mjs +0 -79
  32. package/dist/packem_shared/HEALTH_PATH-e5J_NHBx.mjs +0 -150
  33. package/dist/packem_shared/LOG_ARCHIVE_NOT_CONFIGURED-acNcguqc.mjs +0 -3
  34. package/dist/packem_shared/LOG_ARCHIVE_PATH-CNs0bznX.mjs +0 -125
  35. package/dist/packem_shared/LunoraError-Bpb9EFJ3.mjs +0 -22
  36. package/dist/packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs +0 -8
  37. package/dist/packem_shared/analyticsEngineSink-yLFNjHDt.mjs +0 -429
  38. package/dist/packem_shared/applyJurisdiction-BkZtTkct.mjs +0 -20
  39. package/dist/packem_shared/argsFromQuery-c-U1WRy-.mjs +0 -121
  40. package/dist/packem_shared/composeIdentityResolvers-XGjO7V1J.mjs +0 -55
  41. package/dist/packem_shared/composeWorker-DiWwOXXt.mjs +0 -3738
  42. package/dist/packem_shared/createCrossShardRelationCapabilities-CbcWjkAn.mjs +0 -63
  43. package/dist/packem_shared/createKvCursorStore-g8aA6B4L.mjs +0 -165
  44. package/dist/packem_shared/createQueryCoordinator-DNCJzOZE.mjs +0 -847
  45. package/dist/packem_shared/decorateResponse-DRWQFNhF.mjs +0 -273
  46. package/dist/packem_shared/emitLogEvent-BlGMnKZK.mjs +0 -1
  47. package/dist/packem_shared/method-guard-Qzw99aCj.mjs +0 -3
  48. package/dist/packem_shared/observability--NOFYBFc.mjs +0 -47
  49. package/dist/packem_shared/otlp-resource-Dow6-F_u.mjs +0 -163
  50. package/dist/packem_shared/pipeline-log-reader-BXULGNC3.mjs +0 -135
  51. package/dist/packem_shared/toAirbyteMessages-DrHdplb4.mjs +0 -39
@@ -1,3738 +0,0 @@
1
- import { isLunoraError, toErrorBody } from '@lunora/errors';
2
- import { NOOP_EXECUTION_CONTEXT } from './NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
3
- import { LunoraError, toErrorResponse } from './LunoraError-Bpb9EFJ3.mjs';
4
- import { runExportTap } from './createKvCursorStore-g8aA6B4L.mjs';
5
- import { m as methodGuard } from './method-guard-Qzw99aCj.mjs';
6
- import { buildHealthRoutes, durableObjectProbe, d1Probe, presenceProbe } from './HEALTH_PATH-e5J_NHBx.mjs';
7
- import { wrapResolverWithContract } from './composeIdentityResolvers-XGjO7V1J.mjs';
8
- export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-XGjO7V1J.mjs';
9
- import { buildLogArchiveAdminRoutes } from './LOG_ARCHIVE_PATH-CNs0bznX.mjs';
10
- import { r as resolveTraceSampling, a as emitRpcEvent } from './observability--NOFYBFc.mjs';
11
- import { o as otlpRandomHex, b as buildTraceparent, p as parseTraceparent, m as mergeResourceAttributes, d as detectCloudflareResource, a as detectServiceResource, r as readerFromRecord } from './otlp-resource-Dow6-F_u.mjs';
12
- import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
13
- import { buildRestRoutes } from './argsFromQuery-c-U1WRy-.mjs';
14
- import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-DRWQFNhF.mjs';
15
-
16
- const evictOldestEntry = (map, capacity) => {
17
- if (map.size < capacity) {
18
- return;
19
- }
20
- const oldest = map.keys().next().value;
21
- if (oldest !== void 0) {
22
- map.delete(oldest);
23
- }
24
- };
25
-
26
- const RELAY_NAME_INFIX = "::relay::";
27
- const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
28
-
29
- const textEncoder = new TextEncoder();
30
- const toBase64Url = (bytes) => {
31
- const binary = String.fromCodePoint(...bytes);
32
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
33
- };
34
- const fromBase64Url = (input) => {
35
- const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
36
- const binary = atob(padded);
37
- const bytes = new Uint8Array(binary.length);
38
- for (let index = 0; index < binary.length; index += 1) {
39
- bytes[index] = binary.codePointAt(index) ?? 0;
40
- }
41
- return bytes;
42
- };
43
- const KEY_CACHE_MAX = 64;
44
- const keyCache = /* @__PURE__ */ new Map();
45
- const importHmacKey = async (secret) => {
46
- const cached = keyCache.get(secret);
47
- if (cached) {
48
- return cached;
49
- }
50
- evictOldestEntry(keyCache, KEY_CACHE_MAX);
51
- const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
52
- keyCache.set(secret, keyPromise);
53
- return keyPromise;
54
- };
55
- const signCanonical = async (secret, canonical) => {
56
- const cryptoKey = await importHmacKey(secret);
57
- const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonical));
58
- return toBase64Url(new Uint8Array(signature));
59
- };
60
- const verifyCanonical = async (secret, canonical, sigBytes) => {
61
- const cryptoKey = await importHmacKey(secret);
62
- return crypto.subtle.verify("HMAC", cryptoKey, sigBytes, textEncoder.encode(canonical));
63
- };
64
-
65
- const WS_ADMIN_TOKEN_VERSION = "v1";
66
- const WS_ADMIN_TOKEN_TTL_MS = 6e4;
67
- const mintWsAdminToken = async (secret, options = {}) => {
68
- const expiresAtMs = (options.now ?? Date.now()) + (options.ttlMs ?? WS_ADMIN_TOKEN_TTL_MS);
69
- const canonical = `${WS_ADMIN_TOKEN_VERSION}.${String(expiresAtMs)}`;
70
- const signature = await signCanonical(secret, canonical);
71
- return { expiresAtMs, token: `${canonical}.${signature}` };
72
- };
73
- const verifyWsAdminToken = async (secret, token, now = Date.now()) => {
74
- if (secret.length === 0 || token.length === 0) {
75
- return false;
76
- }
77
- const parts = token.split(".");
78
- if (parts.length !== 3) {
79
- return false;
80
- }
81
- const [version, expString, signature] = parts;
82
- if (version !== WS_ADMIN_TOKEN_VERSION || signature.length === 0) {
83
- return false;
84
- }
85
- const expiresAtMs = Number(expString);
86
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
87
- return false;
88
- }
89
- let signatureBytes;
90
- try {
91
- signatureBytes = fromBase64Url(signature);
92
- } catch {
93
- return false;
94
- }
95
- return verifyCanonical(secret, `${version}.${expString}`, signatureBytes);
96
- };
97
-
98
- const AUTH_BASE = "/_lunora/admin/auth";
99
- const AUTH_ADMIN_ERROR_STATUS = {
100
- INVITER_REQUIRED: 400,
101
- ORG_SLUG_INVALID: 400,
102
- ORG_SLUG_TAKEN: 409,
103
- PASSWORD_TOO_LONG: 400,
104
- PASSWORD_TOO_SHORT: 400,
105
- USER_ALREADY_EXISTS: 409,
106
- USER_NOT_FOUND: 404
107
- };
108
- const requireBodyString = (body, field) => {
109
- const value = body[field];
110
- if (typeof value !== "string" || value === "") {
111
- throw new LunoraError(`\`${field}\` is required`, { code: "BAD_REQUEST", status: 400 });
112
- }
113
- return value;
114
- };
115
- const requireQuery$1 = (query, name) => {
116
- const value = query(name);
117
- if (value === void 0) {
118
- throw new LunoraError(`\`${name}\` query parameter is required`, { code: "BAD_REQUEST", status: 400 });
119
- }
120
- return value;
121
- };
122
- const parseRoleInput = (value) => {
123
- if (typeof value === "string") {
124
- return value;
125
- }
126
- if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
127
- return value;
128
- }
129
- return void 0;
130
- };
131
- const optionalBodyString = (body, field) => typeof body[field] === "string" ? body[field] : void 0;
132
- const optionalBodyObject = (body, field) => {
133
- const value = body[field];
134
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
135
- };
136
- const requirePermission = (body) => {
137
- const value = body["permission"];
138
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
139
- throw new LunoraError("`permission` object is required", { code: "BAD_REQUEST", status: 400 });
140
- }
141
- const out = {};
142
- for (const [resource, actions] of Object.entries(value)) {
143
- if (Array.isArray(actions) && actions.every((action) => typeof action === "string")) {
144
- out[resource] = actions;
145
- }
146
- }
147
- return out;
148
- };
149
- const AUTH_ROUTES = {
150
- [`${AUTH_BASE}/capabilities`]: {
151
- build: () => {
152
- return {};
153
- },
154
- http: "GET",
155
- method: "capabilities"
156
- },
157
- [`${AUTH_BASE}/users`]: {
158
- build: ({ paging, query }) => {
159
- const direction = query("sortDirection");
160
- return {
161
- ...paging,
162
- filterField: query("filterField"),
163
- filterValue: query("filterValue"),
164
- search: query("search"),
165
- searchField: query("searchField"),
166
- sortBy: query("sortBy"),
167
- sortDirection: direction === "asc" || direction === "desc" ? direction : void 0
168
- };
169
- },
170
- http: "GET",
171
- method: "listUsers"
172
- },
173
- [`${AUTH_BASE}/sessions`]: {
174
- build: ({ paging, query }) => {
175
- return { ...paging, userId: query("userId") };
176
- },
177
- http: "GET",
178
- method: "listSessions"
179
- },
180
- [`${AUTH_BASE}/accounts`]: {
181
- build: ({ query }) => {
182
- return { userId: requireQuery$1(query, "userId") };
183
- },
184
- http: "GET",
185
- method: "listAccounts"
186
- },
187
- [`${AUTH_BASE}/passkeys`]: {
188
- build: ({ query }) => {
189
- return { userId: requireQuery$1(query, "userId") };
190
- },
191
- http: "GET",
192
- method: "listPasskeys"
193
- },
194
- [`${AUTH_BASE}/organizations`]: {
195
- build: ({ paging }) => {
196
- return { ...paging };
197
- },
198
- http: "GET",
199
- method: "listOrganizations"
200
- },
201
- [`${AUTH_BASE}/organizations/members`]: {
202
- build: ({ paging, query }) => {
203
- return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
204
- },
205
- http: "GET",
206
- method: "listMembers"
207
- },
208
- [`${AUTH_BASE}/organizations/invitations`]: {
209
- build: ({ paging, query }) => {
210
- return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
211
- },
212
- http: "GET",
213
- method: "listInvitations"
214
- },
215
- [`${AUTH_BASE}/config`]: {
216
- build: () => {
217
- return {};
218
- },
219
- http: "GET",
220
- method: "config"
221
- },
222
- [`${AUTH_BASE}/organizations/teams`]: {
223
- build: ({ paging, query }) => {
224
- return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
225
- },
226
- http: "GET",
227
- method: "listTeams"
228
- },
229
- [`${AUTH_BASE}/organizations/teams/members`]: {
230
- build: ({ paging, query }) => {
231
- return { ...paging, teamId: requireQuery$1(query, "teamId") };
232
- },
233
- http: "GET",
234
- method: "listTeamMembers"
235
- },
236
- [`${AUTH_BASE}/organizations/roles`]: {
237
- build: ({ paging, query }) => {
238
- return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
239
- },
240
- http: "GET",
241
- method: "listOrgRoles"
242
- },
243
- // --- mutations (POST) -------------------------------------------------------
244
- [`${AUTH_BASE}/users/create`]: {
245
- build: ({ body }) => {
246
- return {
247
- data: typeof body["data"] === "object" && body["data"] !== null && !Array.isArray(body["data"]) ? body["data"] : void 0,
248
- email: requireBodyString(body, "email"),
249
- name: requireBodyString(body, "name"),
250
- password: optionalBodyString(body, "password"),
251
- role: parseRoleInput(body["role"])
252
- };
253
- },
254
- http: "POST",
255
- method: "createUser"
256
- },
257
- [`${AUTH_BASE}/users/update`]: {
258
- build: ({ body }) => {
259
- const { data } = body;
260
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
261
- throw new LunoraError("`data` object is required", { code: "BAD_REQUEST", status: 400 });
262
- }
263
- return { data, userId: requireBodyString(body, "userId") };
264
- },
265
- http: "POST",
266
- method: "updateUser"
267
- },
268
- [`${AUTH_BASE}/users/role`]: {
269
- build: ({ body }) => {
270
- const role = parseRoleInput(body["role"]);
271
- if (role === void 0 || typeof role === "string" && role.trim() === "") {
272
- throw new LunoraError("`role` is required", { code: "BAD_REQUEST", status: 400 });
273
- }
274
- return { role, userId: requireBodyString(body, "userId") };
275
- },
276
- http: "POST",
277
- method: "setRole"
278
- },
279
- [`${AUTH_BASE}/users/ban`]: {
280
- build: ({ body }) => {
281
- return {
282
- expiresInSeconds: typeof body["expiresInSeconds"] === "number" ? body["expiresInSeconds"] : void 0,
283
- reason: optionalBodyString(body, "reason"),
284
- userId: requireBodyString(body, "userId")
285
- };
286
- },
287
- http: "POST",
288
- method: "banUser"
289
- },
290
- [`${AUTH_BASE}/users/unban`]: {
291
- build: ({ body }) => {
292
- return { userId: requireBodyString(body, "userId") };
293
- },
294
- http: "POST",
295
- method: "unbanUser"
296
- },
297
- [`${AUTH_BASE}/users/password`]: {
298
- build: ({ body }) => {
299
- return { newPassword: requireBodyString(body, "newPassword"), userId: requireBodyString(body, "userId") };
300
- },
301
- http: "POST",
302
- method: "setUserPassword",
303
- returns: "void"
304
- },
305
- [`${AUTH_BASE}/users/remove`]: {
306
- build: ({ body }) => {
307
- return { userId: requireBodyString(body, "userId") };
308
- },
309
- http: "POST",
310
- method: "removeUser",
311
- returns: "void"
312
- },
313
- [`${AUTH_BASE}/users/impersonate`]: {
314
- build: ({ body }) => {
315
- return { userId: requireBodyString(body, "userId") };
316
- },
317
- http: "POST",
318
- method: "impersonateUser"
319
- },
320
- [`${AUTH_BASE}/sessions/revoke`]: {
321
- build: ({ body }) => {
322
- return { sessionId: requireBodyString(body, "sessionId") };
323
- },
324
- http: "POST",
325
- method: "revokeUserSession",
326
- returns: "void"
327
- },
328
- [`${AUTH_BASE}/sessions/revoke-all`]: {
329
- build: ({ body }) => {
330
- return { userId: requireBodyString(body, "userId") };
331
- },
332
- http: "POST",
333
- method: "revokeUserSessions",
334
- returns: "void"
335
- },
336
- [`${AUTH_BASE}/accounts/unlink`]: {
337
- build: ({ body }) => {
338
- return { accountId: requireBodyString(body, "accountId"), userId: requireBodyString(body, "userId") };
339
- },
340
- http: "POST",
341
- method: "unlinkAccount",
342
- returns: "void"
343
- },
344
- [`${AUTH_BASE}/two-factor/disable`]: {
345
- build: ({ body }) => {
346
- return { userId: requireBodyString(body, "userId") };
347
- },
348
- http: "POST",
349
- method: "disableTwoFactor",
350
- returns: "void"
351
- },
352
- [`${AUTH_BASE}/passkeys/delete`]: {
353
- build: ({ body }) => {
354
- return { passkeyId: requireBodyString(body, "passkeyId") };
355
- },
356
- http: "POST",
357
- method: "deletePasskey",
358
- returns: "void"
359
- },
360
- [`${AUTH_BASE}/organizations/members/remove`]: {
361
- build: ({ body }) => {
362
- return { memberId: requireBodyString(body, "memberId") };
363
- },
364
- http: "POST",
365
- method: "removeMember",
366
- returns: "void"
367
- },
368
- [`${AUTH_BASE}/organizations/invitations/cancel`]: {
369
- build: ({ body }) => {
370
- return { invitationId: requireBodyString(body, "invitationId") };
371
- },
372
- http: "POST",
373
- method: "cancelInvitation",
374
- returns: "void"
375
- },
376
- [`${AUTH_BASE}/organizations/create`]: {
377
- build: ({ body }) => {
378
- return {
379
- logo: optionalBodyString(body, "logo"),
380
- metadata: optionalBodyObject(body, "metadata"),
381
- name: requireBodyString(body, "name"),
382
- ownerId: optionalBodyString(body, "ownerId"),
383
- slug: optionalBodyString(body, "slug")
384
- };
385
- },
386
- http: "POST",
387
- method: "createOrganization"
388
- },
389
- [`${AUTH_BASE}/organizations/update`]: {
390
- build: ({ body }) => {
391
- return {
392
- logo: optionalBodyString(body, "logo"),
393
- metadata: optionalBodyObject(body, "metadata"),
394
- name: optionalBodyString(body, "name"),
395
- organizationId: requireBodyString(body, "organizationId"),
396
- slug: optionalBodyString(body, "slug")
397
- };
398
- },
399
- http: "POST",
400
- method: "updateOrganization"
401
- },
402
- [`${AUTH_BASE}/organizations/remove`]: {
403
- build: ({ body }) => {
404
- return { organizationId: requireBodyString(body, "organizationId") };
405
- },
406
- http: "POST",
407
- method: "deleteOrganization",
408
- returns: "void"
409
- },
410
- [`${AUTH_BASE}/organizations/members/add`]: {
411
- build: ({ body }) => {
412
- return {
413
- organizationId: requireBodyString(body, "organizationId"),
414
- role: optionalBodyString(body, "role"),
415
- userId: requireBodyString(body, "userId")
416
- };
417
- },
418
- http: "POST",
419
- method: "addMember"
420
- },
421
- [`${AUTH_BASE}/organizations/members/invite`]: {
422
- build: ({ body }) => {
423
- return {
424
- email: requireBodyString(body, "email"),
425
- inviterId: optionalBodyString(body, "inviterId"),
426
- organizationId: requireBodyString(body, "organizationId"),
427
- role: optionalBodyString(body, "role")
428
- };
429
- },
430
- http: "POST",
431
- method: "inviteMember"
432
- },
433
- [`${AUTH_BASE}/organizations/members/role`]: {
434
- build: ({ body }) => {
435
- const role = parseRoleInput(body["role"]);
436
- if (role === void 0 || typeof role === "string" && role.trim() === "") {
437
- throw new LunoraError("`role` is required", { code: "BAD_REQUEST", status: 400 });
438
- }
439
- return { memberId: requireBodyString(body, "memberId"), role };
440
- },
441
- http: "POST",
442
- method: "updateMemberRole"
443
- },
444
- [`${AUTH_BASE}/organizations/teams/create`]: {
445
- build: ({ body }) => {
446
- return { name: requireBodyString(body, "name"), organizationId: requireBodyString(body, "organizationId") };
447
- },
448
- http: "POST",
449
- method: "createTeam"
450
- },
451
- [`${AUTH_BASE}/organizations/teams/update`]: {
452
- build: ({ body }) => {
453
- return { name: requireBodyString(body, "name"), teamId: requireBodyString(body, "teamId") };
454
- },
455
- http: "POST",
456
- method: "updateTeam"
457
- },
458
- [`${AUTH_BASE}/organizations/teams/remove`]: {
459
- build: ({ body }) => {
460
- return { teamId: requireBodyString(body, "teamId") };
461
- },
462
- http: "POST",
463
- method: "removeTeam",
464
- returns: "void"
465
- },
466
- [`${AUTH_BASE}/organizations/teams/members/add`]: {
467
- build: ({ body }) => {
468
- return { teamId: requireBodyString(body, "teamId"), userId: requireBodyString(body, "userId") };
469
- },
470
- http: "POST",
471
- method: "addTeamMember"
472
- },
473
- [`${AUTH_BASE}/organizations/teams/members/remove`]: {
474
- build: ({ body }) => {
475
- return { teamMemberId: requireBodyString(body, "teamMemberId") };
476
- },
477
- http: "POST",
478
- method: "removeTeamMember",
479
- returns: "void"
480
- },
481
- [`${AUTH_BASE}/organizations/roles/create`]: {
482
- build: ({ body }) => {
483
- return {
484
- organizationId: requireBodyString(body, "organizationId"),
485
- permission: requirePermission(body),
486
- role: requireBodyString(body, "role")
487
- };
488
- },
489
- http: "POST",
490
- method: "createOrgRole"
491
- },
492
- [`${AUTH_BASE}/organizations/roles/update`]: {
493
- build: ({ body }) => {
494
- return { permission: requirePermission(body), roleId: requireBodyString(body, "roleId") };
495
- },
496
- http: "POST",
497
- method: "updateOrgRole"
498
- },
499
- [`${AUTH_BASE}/organizations/roles/remove`]: {
500
- build: ({ body }) => {
501
- return { roleId: requireBodyString(body, "roleId") };
502
- },
503
- http: "POST",
504
- method: "deleteOrgRole",
505
- returns: "void"
506
- }
507
- };
508
- const buildAuthAdminRoutes = (deps) => {
509
- const runAuthOp = async (op) => {
510
- try {
511
- return await op();
512
- } catch (error) {
513
- if (error instanceof LunoraError) {
514
- throw error;
515
- }
516
- const candidate = error;
517
- const code = typeof candidate.code === "string" ? candidate.code : "AUTH_ADMIN_ERROR";
518
- console.error("[lunora] auth admin operation failed:", error);
519
- throw new LunoraError("auth admin operation failed", { code, status: AUTH_ADMIN_ERROR_STATUS[code] ?? 500 });
520
- }
521
- };
522
- const handle = async (request, descriptor) => {
523
- deps.assertAdmin(request);
524
- if (request.method !== descriptor.http) {
525
- throw new LunoraError(`Auth admin endpoint requires ${descriptor.http}`, { code: "METHOD_NOT_ALLOWED", status: 405 });
526
- }
527
- const admin = deps.getAuthAdmin();
528
- if (admin === void 0) {
529
- throw new LunoraError("auth endpoints require an `authAdmin` on the worker", { code: "AUTH_NOT_CONFIGURED", status: 400 });
530
- }
531
- const method = admin[descriptor.method];
532
- if (method === void 0) {
533
- throw new LunoraError(`auth admin does not support \`${descriptor.method}\``, { code: "AUTH_OP_NOT_SUPPORTED", status: 400 });
534
- }
535
- const url = new URL(request.url);
536
- const context = {
537
- body: descriptor.http === "POST" ? await deps.readJsonBody(request) : {},
538
- paging: deps.parsePaging(request),
539
- query: (name) => deps.queryParameter(url, name)
540
- };
541
- const input = descriptor.build(context);
542
- const result = await runAuthOp(() => method(input));
543
- return Response.json(descriptor.returns === "void" ? { ok: true } : result, { headers: { "content-type": "application/json" }, status: 200 });
544
- };
545
- const routes = {};
546
- for (const [path, descriptor] of Object.entries(AUTH_ROUTES)) {
547
- routes[path] = (request) => handle(request, descriptor);
548
- }
549
- return routes;
550
- };
551
-
552
- const GET_AUTH_AUDIT_LOG_OP = "__lunora_admin__:getAuthAuditLog";
553
- const stringArgument = (value) => typeof value === "string" && value !== "" ? value : void 0;
554
- const numericArgument = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
555
- const buildGetAuthAuditLog = (deps) => {
556
- const handle = async (request, args) => {
557
- deps.assertAdmin(request);
558
- const reader = deps.getReader();
559
- if (reader === void 0) {
560
- throw new LunoraError("the auth/security audit endpoint requires an `authAuditReader` on the worker", {
561
- code: "AUTH_AUDIT_NOT_CONFIGURED",
562
- status: 400
563
- });
564
- }
565
- const query = {};
566
- const actorId = stringArgument(args["actorId"]);
567
- const event = stringArgument(args["event"]);
568
- const sinceSeq = numericArgument(args["sinceSeq"]);
569
- const limit = numericArgument(args["limit"]);
570
- if (actorId !== void 0) {
571
- query.actorId = actorId;
572
- }
573
- if (event !== void 0) {
574
- query.event = event;
575
- }
576
- if (sinceSeq !== void 0) {
577
- query.sinceSeq = sinceSeq;
578
- }
579
- if (limit !== void 0) {
580
- query.limit = limit;
581
- }
582
- let entries;
583
- try {
584
- entries = await reader.read(query);
585
- } catch (error) {
586
- if (error instanceof LunoraError) {
587
- throw error;
588
- }
589
- console.error("[lunora] auth audit read failed:", error);
590
- throw new LunoraError("auth audit read failed", { code: "AUTH_AUDIT_READ_FAILED", status: 500 });
591
- }
592
- const result = { entries };
593
- return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
594
- };
595
- return handle;
596
- };
597
-
598
- const MAX_BATCH_ENTRIES = 500;
599
-
600
- const normalizeBatchCall = (raw, index, defaultShard) => {
601
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
602
- throw new LunoraError("each batch call must be an object", { code: "BAD_REQUEST", status: 400 });
603
- }
604
- const call = raw;
605
- if (typeof call.functionPath !== "string") {
606
- throw new LunoraError("each batch call needs a string `functionPath`", { code: "BAD_REQUEST", status: 400 });
607
- }
608
- if (call.functionPath.startsWith("__lunora_relation__:") || call.functionPath.startsWith("__lunora_admin__")) {
609
- throw new LunoraError("reserved function path cannot be batched", { code: "FORBIDDEN", status: 403 });
610
- }
611
- if (call.args !== void 0 && (typeof call.args !== "object" || call.args === null || Array.isArray(call.args))) {
612
- throw new LunoraError("each batch call `args` must be an object", { code: "BAD_REQUEST", status: 400 });
613
- }
614
- return {
615
- entry: {
616
- args: call.args === void 0 ? {} : call.args,
617
- clientId: typeof call.clientId === "string" ? call.clientId : void 0,
618
- clientSeq: typeof call.clientSeq === "number" ? call.clientSeq : void 0,
619
- functionPath: call.functionPath,
620
- id: typeof call.id === "number" ? call.id : index,
621
- mutationId: typeof call.mutationId === "string" ? call.mutationId : void 0
622
- },
623
- shardKey: typeof call.shardKey === "string" ? call.shardKey : defaultShard
624
- };
625
- };
626
- const groupBatchCallsByShard = (calls, defaultShard) => {
627
- if (calls.length > MAX_BATCH_ENTRIES) {
628
- throw new LunoraError(`RPC batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit`, { code: "BAD_REQUEST", status: 400 });
629
- }
630
- const groups = /* @__PURE__ */ new Map();
631
- for (const [index, raw] of calls.entries()) {
632
- const { entry, shardKey } = normalizeBatchCall(raw, index, defaultShard);
633
- const group = groups.get(shardKey) ?? [];
634
- group.push(entry);
635
- groups.set(shardKey, group);
636
- }
637
- return groups;
638
- };
639
-
640
- const MAX_BODY_BYTES = 1048576;
641
- const readBodyTextWithLimit = async (request, limit = MAX_BODY_BYTES) => {
642
- if (!request.body) {
643
- return "";
644
- }
645
- const reader = request.body.getReader();
646
- const decoder = new TextDecoder();
647
- let total = 0;
648
- let text = "";
649
- while (true) {
650
- const { done, value } = await reader.read();
651
- if (done) {
652
- break;
653
- }
654
- if (value) {
655
- total += value.byteLength;
656
- if (total > limit) {
657
- await reader.cancel().catch(() => {
658
- });
659
- throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
660
- }
661
- text += decoder.decode(value, { stream: true });
662
- }
663
- }
664
- text += decoder.decode();
665
- return text;
666
- };
667
- const readBodyBytesWithLimit = async (request, limit = MAX_BODY_BYTES) => {
668
- if (!request.body) {
669
- return new ArrayBuffer(0);
670
- }
671
- const reader = request.body.getReader();
672
- const chunks = [];
673
- let total = 0;
674
- while (true) {
675
- const { done, value } = await reader.read();
676
- if (done) {
677
- break;
678
- }
679
- if (value) {
680
- total += value.byteLength;
681
- if (total > limit) {
682
- await reader.cancel().catch(() => {
683
- });
684
- throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
685
- }
686
- chunks.push(value);
687
- }
688
- }
689
- const out = new Uint8Array(total);
690
- let offset = 0;
691
- for (const chunk of chunks) {
692
- out.set(chunk, offset);
693
- offset += chunk.byteLength;
694
- }
695
- return out.buffer;
696
- };
697
- const readJsonBodyWithLimit = async (request, limit = MAX_BODY_BYTES) => {
698
- try {
699
- const text = await readBodyTextWithLimit(request, limit);
700
- return text === "" ? {} : JSON.parse(text);
701
- } catch (error) {
702
- if (error instanceof LunoraError) {
703
- throw error;
704
- }
705
- throw new LunoraError("Request body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
706
- }
707
- };
708
-
709
- const CURSOR_ENCODER = new TextEncoder();
710
- const encodeConnectorCursor = (state) => {
711
- const json = JSON.stringify(state);
712
- const bytes = CURSOR_ENCODER.encode(json);
713
- let binary = "";
714
- for (const byte of bytes) {
715
- binary += String.fromCodePoint(byte);
716
- }
717
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
718
- };
719
- const decodeConnectorCursor = (token) => {
720
- const empty = { g: 0, s: {}, v: 1 };
721
- if (typeof token !== "string" || token.length === 0) {
722
- return empty;
723
- }
724
- try {
725
- const binary = atob(token.replaceAll("-", "+").replaceAll("_", "/"));
726
- const bytes = new Uint8Array(binary.length);
727
- for (let index = 0; index < binary.length; index += 1) {
728
- bytes[index] = binary.codePointAt(index) ?? 0;
729
- }
730
- const parsed = JSON.parse(new TextDecoder().decode(bytes));
731
- const shards = parsed.s && typeof parsed.s === "object" ? parsed.s : {};
732
- const sanitized = {};
733
- for (const [key, value] of Object.entries(shards)) {
734
- if (typeof value === "number" && Number.isFinite(value)) {
735
- sanitized[key] = value;
736
- }
737
- }
738
- return { g: typeof parsed.g === "number" && Number.isFinite(parsed.g) ? parsed.g : 0, s: sanitized, v: 1 };
739
- } catch {
740
- return empty;
741
- }
742
- };
743
- const flattenCdcChange = (change) => {
744
- const table = typeof change["table"] === "string" ? change["table"] : "";
745
- const rawOp = typeof change["op"] === "string" ? change["op"] : "";
746
- const op = rawOp === "delete" || rawOp === "insert" || rawOp === "update" ? rawOp : "upsert";
747
- const id = typeof change["id"] === "string" ? change["id"] : void 0;
748
- const postImage = change["doc"] && typeof change["doc"] === "object" ? change["doc"] : void 0;
749
- const documentRow = postImage ?? (id === void 0 ? {} : { _id: id });
750
- return { doc: documentRow, op, table };
751
- };
752
- const foldCdcPage = (changes, pageChanges, limit) => {
753
- for (const change of pageChanges) {
754
- changes.push(flattenCdcChange(change));
755
- }
756
- return limit !== void 0 && pageChanges.length >= limit;
757
- };
758
-
759
- const EXPORT_PATH = "/_lunora/admin/export";
760
- const IMPORT_PATH = "/_lunora/admin/import";
761
- const SYNC_PATH = "/_lunora/admin/sync";
762
- const CONNECTOR_SYNC_PATH = "/_lunora/admin/connector/sync";
763
- const APPLY_PATH = "/_lunora/admin/apply";
764
- const EXPORT_TAP_RUN_PATH = "/_lunora/admin/export-tap/run";
765
- const NDJSON_ENCODER$1 = new TextEncoder();
766
- const parseExportBody = async (request) => {
767
- let body;
768
- try {
769
- const text = await readBodyTextWithLimit(request);
770
- body = text === "" ? {} : JSON.parse(text);
771
- } catch (error) {
772
- if (error instanceof LunoraError) {
773
- throw error;
774
- }
775
- throw new LunoraError("Export body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
776
- }
777
- const candidate = body ?? {};
778
- if (candidate.tables === void 0) {
779
- return { tables: void 0 };
780
- }
781
- if (!Array.isArray(candidate.tables)) {
782
- throw new LunoraError("Export `tables` must be a string array", { code: "BAD_REQUEST", status: 400 });
783
- }
784
- const tables = [];
785
- for (const entry of candidate.tables) {
786
- if (typeof entry !== "string" || entry.length === 0) {
787
- throw new LunoraError("Export `tables` entries must be non-empty strings", { code: "BAD_REQUEST", status: 400 });
788
- }
789
- tables.push(entry);
790
- }
791
- return { tables };
792
- };
793
- const buildDataMovementAdminRoutes = (deps) => {
794
- const {
795
- applyGlobals,
796
- exportCursorStore,
797
- exportSinks,
798
- knownTables,
799
- queryCoordinator,
800
- requireAdminOption,
801
- resolveForwardContext,
802
- shardDO,
803
- streamExportRows,
804
- streamingImport,
805
- syncGlobals
806
- } = deps;
807
- const handleExport = async (request, env) => {
808
- const wrongMethod = methodGuard(request, ["POST"]);
809
- if (wrongMethod) {
810
- return wrongMethod;
811
- }
812
- const coordinator = requireAdminOption(request, queryCoordinator, {
813
- code: "BAD_REQUEST",
814
- message: "Export endpoint requires a `queryCoordinator` on the worker"
815
- });
816
- const body = await parseExportBody(request);
817
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
818
- const stream = new ReadableStream({
819
- async pull(controller) {
820
- const writeRow = (row) => {
821
- controller.enqueue(NDJSON_ENCODER$1.encode(`${JSON.stringify(row)}
822
- `));
823
- };
824
- try {
825
- await streamExportRows(coordinator, forwardedHeaders, body.tables, writeRow);
826
- controller.close();
827
- } catch (error) {
828
- controller.error(error);
829
- }
830
- }
831
- });
832
- return new Response(stream, { headers: { "content-type": "application/x-ndjson" }, status: 200 });
833
- };
834
- const handleCdcSync = async (request, env) => {
835
- const wrongMethod = methodGuard(request, ["POST"]);
836
- if (wrongMethod) {
837
- return wrongMethod;
838
- }
839
- const coordinator = requireAdminOption(request, queryCoordinator, {
840
- code: "BAD_REQUEST",
841
- message: "Sync endpoint requires a `queryCoordinator` on the worker"
842
- });
843
- const raw = await readJsonBodyWithLimit(request);
844
- const cursors = typeof raw["cursors"] === "object" && raw["cursors"] !== null ? raw["cursors"] : {};
845
- const limit = typeof raw["limit"] === "number" ? raw["limit"] : void 0;
846
- const globalCursor = typeof raw["globalCursor"] === "number" ? raw["globalCursor"] : 0;
847
- const requestedTables = Array.isArray(raw["tables"]) ? raw["tables"].filter((table) => typeof table === "string") : void 0;
848
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
849
- const probeTables = requestedTables ?? knownTables();
850
- const shardResult = await coordinator.orchestrateCdcSync(shardDO, {
851
- cursors,
852
- headers: forwardedHeaders,
853
- limit,
854
- tables: probeTables
855
- });
856
- const global = syncGlobals ? await syncGlobals({ limit, sinceSeq: globalCursor }) : void 0;
857
- return Response.json({ global, shards: shardResult.shards }, { status: 200 });
858
- };
859
- const handleConnectorSync = async (request, env) => {
860
- const wrongMethod = methodGuard(request, ["POST"]);
861
- if (wrongMethod) {
862
- return wrongMethod;
863
- }
864
- const coordinator = requireAdminOption(request, queryCoordinator, {
865
- code: "BAD_REQUEST",
866
- message: "Connector sync endpoint requires a `queryCoordinator` on the worker"
867
- });
868
- const raw = await readJsonBodyWithLimit(request);
869
- const state = decodeConnectorCursor(raw["cursor"]);
870
- const limit = typeof raw["limit"] === "number" && raw["limit"] > 0 ? raw["limit"] : void 0;
871
- const requestedTables = Array.isArray(raw["tables"]) ? raw["tables"].filter((table) => typeof table === "string") : void 0;
872
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
873
- const probeTables = requestedTables ?? knownTables();
874
- const shardResult = await coordinator.orchestrateCdcSync(shardDO, {
875
- cursors: state.s,
876
- headers: forwardedHeaders,
877
- limit,
878
- tables: probeTables
879
- });
880
- const changes = [];
881
- const nextShardCursors = { ...state.s };
882
- let hasMore = false;
883
- for (const shard of shardResult.shards) {
884
- hasMore = foldCdcPage(changes, shard.changes ?? [], limit) || hasMore;
885
- nextShardCursors[shard.shardKey] = shard.cursor;
886
- }
887
- let nextGlobalCursor = state.g;
888
- if (syncGlobals) {
889
- const global = await syncGlobals({ limit, sinceSeq: state.g });
890
- hasMore = foldCdcPage(changes, global.changes, limit) || hasMore;
891
- nextGlobalCursor = global.cursor;
892
- }
893
- const nextCursor = encodeConnectorCursor({ g: nextGlobalCursor, s: nextShardCursors, v: 1 });
894
- const page = { changes, hasMore, nextCursor };
895
- return Response.json(page, { status: 200 });
896
- };
897
- const handleApplyCdc = async (request, env) => {
898
- const wrongMethod = methodGuard(request, ["POST"]);
899
- if (wrongMethod) {
900
- return wrongMethod;
901
- }
902
- const coordinator = requireAdminOption(request, queryCoordinator, {
903
- code: "BAD_REQUEST",
904
- message: "Apply endpoint requires a `queryCoordinator` on the worker"
905
- });
906
- const raw = await readJsonBodyWithLimit(request);
907
- const rawBatches = Array.isArray(raw["batches"]) ? raw["batches"] : [];
908
- const batches = rawBatches.map((batch) => batch).filter(
909
- (batch) => (
910
- // Guard object-ness before property access — a `null`/non-object
911
- // entry (e.g. `{"batches":[null]}`) would otherwise throw a
912
- // TypeError that surfaces as a confusing 500; here it's skipped.
913
- batch !== null && typeof batch === "object" && typeof batch.shardKey === "string" && Array.isArray(batch.changes)
914
- )
915
- );
916
- const globalChanges = Array.isArray(raw["globalChanges"]) ? raw["globalChanges"] : [];
917
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
918
- const shardResult = await coordinator.orchestrateApplyCdc(shardDO, { batches, headers: forwardedHeaders });
919
- const globalApplied = globalChanges.length > 0 && applyGlobals ? await applyGlobals({ changes: globalChanges }) : 0;
920
- return Response.json({ applied: shardResult.applied + globalApplied, failed: shardResult.failed, ok: shardResult.ok }, { status: 200 });
921
- };
922
- const handleImport = async (request, env) => {
923
- const wrongMethod = methodGuard(request, ["POST"]);
924
- if (wrongMethod) {
925
- return wrongMethod;
926
- }
927
- requireAdminOption(request, queryCoordinator, { code: "BAD_REQUEST", message: "Import endpoint requires a `queryCoordinator` on the worker" });
928
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
929
- const result = await streamingImport(request, forwardedHeaders);
930
- return Response.json(result, {
931
- headers: { "content-type": "application/json" },
932
- status: 200
933
- });
934
- };
935
- const handleExportTapRun = async (request, env) => {
936
- const wrongMethod = methodGuard(request, ["POST"]);
937
- if (wrongMethod) {
938
- return wrongMethod;
939
- }
940
- const coordinator = requireAdminOption(request, queryCoordinator, {
941
- code: "BAD_REQUEST",
942
- message: "Export-tap endpoint requires a `queryCoordinator` on the worker"
943
- });
944
- if (exportSinks === void 0 || Object.keys(exportSinks).length === 0 || exportCursorStore === void 0) {
945
- throw new LunoraError("Export-tap endpoint requires `exportSinks` + `exportCursorStore` on the worker", {
946
- code: "EXPORT_TAP_NOT_CONFIGURED",
947
- status: 400
948
- });
949
- }
950
- const raw = await readJsonBodyWithLimit(request);
951
- const sinkName = typeof raw["sink"] === "string" ? raw["sink"] : void 0;
952
- const limit = typeof raw["limit"] === "number" && raw["limit"] > 0 ? raw["limit"] : void 0;
953
- const requestedTables = Array.isArray(raw["tables"]) ? raw["tables"].filter((table) => typeof table === "string") : void 0;
954
- if (sinkName === void 0) {
955
- throw new LunoraError("Export-tap `sink` must name a configured sink", { code: "BAD_REQUEST", status: 400 });
956
- }
957
- const sink = exportSinks[sinkName];
958
- if (sink === void 0) {
959
- throw new LunoraError(`Export-tap sink "${sinkName}" is not configured`, { code: "NOT_FOUND", status: 404 });
960
- }
961
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
962
- const probeTables = requestedTables ?? knownTables();
963
- const result = await runExportTap({
964
- coordinator,
965
- cursorStore: exportCursorStore,
966
- headers: forwardedHeaders,
967
- limit,
968
- shardDO,
969
- sink,
970
- tables: probeTables
971
- });
972
- return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
973
- };
974
- return {
975
- [APPLY_PATH]: handleApplyCdc,
976
- [CONNECTOR_SYNC_PATH]: handleConnectorSync,
977
- [EXPORT_PATH]: handleExport,
978
- [EXPORT_TAP_RUN_PATH]: handleExportTapRun,
979
- [IMPORT_PATH]: handleImport,
980
- [SYNC_PATH]: handleCdcSync
981
- };
982
- };
983
-
984
- const collectKnownTables = (_resolver) => [];
985
- const partitionExportTables = (options, tables) => {
986
- const shardLocalTables = [];
987
- const globalTables = [];
988
- if (tables && tables.length > 0) {
989
- for (const table of tables) {
990
- const info = options.resolveTableSharding?.(table);
991
- if (info?.mode.kind === "global") {
992
- globalTables.push(table);
993
- } else {
994
- shardLocalTables.push(table);
995
- }
996
- }
997
- }
998
- return { globalTables, shardLocalTables };
999
- };
1000
- const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace) => {
1001
- if (tables !== void 0 && shardLocalTables.length === 0) {
1002
- return;
1003
- }
1004
- const exportTables = tables === void 0 ? [] : shardLocalTables;
1005
- const probeFallback = tables === void 0 ? collectKnownTables() : [];
1006
- const probeTables = exportTables.length > 0 ? exportTables : probeFallback;
1007
- const result = await coordinator.orchestrateExport(namespace, {
1008
- args: { tables: exportTables },
1009
- headers: forwardedHeaders,
1010
- tables: probeTables
1011
- });
1012
- for (const shard of result.shards) {
1013
- if (shard.error) {
1014
- continue;
1015
- }
1016
- for (const row of shard.rows ?? []) {
1017
- writeRow(row);
1018
- }
1019
- }
1020
- };
1021
- const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow, namespace) => {
1022
- const { globalTables, shardLocalTables } = partitionExportTables(options, tables);
1023
- await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace);
1024
- const exportGlobalsFunction = options.exportGlobals;
1025
- const wantGlobals = tables === void 0 || globalTables.length > 0;
1026
- if (wantGlobals && exportGlobalsFunction) {
1027
- const tablesArgument = tables === void 0 ? [] : globalTables;
1028
- for await (const row of exportGlobalsFunction({ tables: tablesArgument })) {
1029
- writeRow(row);
1030
- }
1031
- }
1032
- };
1033
-
1034
- const parseImportRow = (trimmed, lineNumber) => {
1035
- let parsed;
1036
- try {
1037
- parsed = JSON.parse(trimmed);
1038
- } catch {
1039
- return { error: { code: "BAD_ROW", line: lineNumber, message: "line is not valid JSON", table: "" }, ok: false };
1040
- }
1041
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1042
- return { error: { code: "BAD_ROW", line: lineNumber, message: "row must be a JSON object", table: "" }, ok: false };
1043
- }
1044
- const candidate = parsed;
1045
- if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1046
- return { error: { code: "BAD_ROW", line: lineNumber, message: "row is missing `table`", table: "" }, ok: false };
1047
- }
1048
- if (!candidate.doc || typeof candidate.doc !== "object" || Array.isArray(candidate.doc)) {
1049
- return { error: { code: "BAD_ROW", line: lineNumber, message: "row is missing or malformed `doc`", table: candidate.table }, ok: false };
1050
- }
1051
- return { doc: candidate.doc, ok: true, table: candidate.table };
1052
- };
1053
- const resolveImportShardKey = (documentRow, table, info, defaultShard, lineNumber) => {
1054
- if (info?.mode.kind === "shardBy" && typeof info.mode.field === "string") {
1055
- const raw = documentRow[info.mode.field];
1056
- if (raw === void 0 || raw === null) {
1057
- return {
1058
- error: { code: "BAD_ROW", line: lineNumber, message: `row missing shard field "${info.mode.field}" for table "${table}"`, table },
1059
- ok: false
1060
- };
1061
- }
1062
- return { ok: true, shardKey: typeof raw === "string" ? raw : JSON.stringify(raw) };
1063
- }
1064
- return { ok: true, shardKey: defaultShard };
1065
- };
1066
- const bucketImportStream = async (request, options, defaultShard) => {
1067
- if (!request.body) {
1068
- throw new LunoraError("Import endpoint requires a request body", { code: "BAD_REQUEST", status: 400 });
1069
- }
1070
- const errors = [];
1071
- const globalRows = [];
1072
- const perShard = /* @__PURE__ */ new Map();
1073
- let physicalLine = 0;
1074
- const reader = request.body.getReader();
1075
- const decoder = new TextDecoder();
1076
- let buffer = "";
1077
- let totalBytes = 0;
1078
- const handleLine = (line) => {
1079
- physicalLine += 1;
1080
- const trimmed = line.trim();
1081
- if (trimmed.length === 0) {
1082
- return;
1083
- }
1084
- const row = parseImportRow(trimmed, physicalLine);
1085
- if (!row.ok) {
1086
- errors.push(row.error);
1087
- return;
1088
- }
1089
- const { doc: documentRow, table } = row;
1090
- const info = options.resolveTableSharding?.(table);
1091
- if (info?.mode.kind === "global") {
1092
- globalRows.push({ doc: documentRow, line: physicalLine, table });
1093
- return;
1094
- }
1095
- const resolved = resolveImportShardKey(documentRow, table, info, defaultShard, physicalLine);
1096
- if (!resolved.ok) {
1097
- errors.push(resolved.error);
1098
- return;
1099
- }
1100
- const existing = perShard.get(resolved.shardKey);
1101
- if (existing) {
1102
- existing.rows.push({ doc: documentRow, table });
1103
- } else {
1104
- perShard.set(resolved.shardKey, { rows: [{ doc: documentRow, table }], shardKey: resolved.shardKey, startLine: physicalLine });
1105
- }
1106
- };
1107
- while (true) {
1108
- const { done, value } = await reader.read();
1109
- if (done) {
1110
- break;
1111
- }
1112
- if (value) {
1113
- totalBytes += value.byteLength;
1114
- if (totalBytes > MAX_BODY_BYTES) {
1115
- await reader.cancel().catch(() => {
1116
- });
1117
- throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
1118
- }
1119
- }
1120
- buffer += decoder.decode(value, { stream: true });
1121
- let newlineIndex = buffer.indexOf("\n");
1122
- while (newlineIndex !== -1) {
1123
- const line = buffer.slice(0, newlineIndex);
1124
- buffer = buffer.slice(newlineIndex + 1);
1125
- handleLine(line);
1126
- newlineIndex = buffer.indexOf("\n");
1127
- }
1128
- }
1129
- if (buffer.length > 0) {
1130
- handleLine(buffer);
1131
- }
1132
- return { errors, globalRows, perShard };
1133
- };
1134
- const mergeImportResult = (totals, result) => {
1135
- for (const [table, count] of Object.entries(result.inserted)) {
1136
- totals.inserted[table] = (totals.inserted[table] ?? 0) + count;
1137
- }
1138
- for (const rowError of result.errors) {
1139
- totals.errors.push({ ...rowError });
1140
- }
1141
- totals.conflicts += result.conflicts;
1142
- };
1143
- const streamingImport = async (request, options, forwardedHeaders, namespace) => {
1144
- const defaultShard = options.defaultShardKey ?? "__root__";
1145
- const { errors, globalRows, perShard } = await bucketImportStream(request, options, defaultShard);
1146
- const totals = { conflicts: 0, errors, inserted: {} };
1147
- if (perShard.size > 0) {
1148
- const coordinator = options.queryCoordinator;
1149
- if (!coordinator) {
1150
- throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1151
- }
1152
- const result = await coordinator.orchestrateImport(namespace, {
1153
- batches: [...perShard.values()],
1154
- headers: forwardedHeaders
1155
- });
1156
- mergeImportResult(totals, result);
1157
- }
1158
- if (globalRows.length > 0) {
1159
- if (options.importGlobals) {
1160
- const startLine = globalRows[0]?.line ?? 1;
1161
- const result = await options.importGlobals({ rows: globalRows, startLine });
1162
- mergeImportResult(totals, result);
1163
- } else {
1164
- for (const globalRow of globalRows) {
1165
- totals.errors.push({
1166
- code: "GLOBAL_NOT_CONFIGURED",
1167
- line: globalRow.line,
1168
- message: `row targets global table "${globalRow.table}" but no \`importGlobals\` is configured`,
1169
- table: globalRow.table
1170
- });
1171
- }
1172
- }
1173
- }
1174
- return { conflicts: totals.conflicts, errors: totals.errors, inserted: totals.inserted };
1175
- };
1176
-
1177
- const asValidator = (value) => typeof value === "object" && value !== null ? value : {};
1178
- const kindOf = (validator) => typeof validator.kind === "string" ? validator.kind : "unknown";
1179
- const describeArgument = (name, validator) => {
1180
- let current = asValidator(validator);
1181
- let optional = false;
1182
- if (kindOf(current) === "optional") {
1183
- optional = true;
1184
- current = asValidator(current._meta?.["inner"]);
1185
- }
1186
- const kind = kindOf(current);
1187
- const meta = current._meta ?? {};
1188
- const descriptor = { kind, name, optional };
1189
- if (kind === "id" && typeof meta["tableName"] === "string") {
1190
- descriptor.table = meta["tableName"];
1191
- }
1192
- if (kind === "array") {
1193
- const elementKind = kindOf(asValidator(meta["inner"]));
1194
- if (elementKind !== "unknown") {
1195
- descriptor.element = elementKind;
1196
- }
1197
- }
1198
- return descriptor;
1199
- };
1200
- const describeArguments = (args) => {
1201
- if (typeof args !== "object" || args === null) {
1202
- return [];
1203
- }
1204
- return Object.entries(args).map(([name, validator]) => describeArgument(name, validator)).toSorted((a, b) => a.name.localeCompare(b.name));
1205
- };
1206
-
1207
- const FUNCTIONS_PATH = "/_lunora/admin/functions";
1208
- const CRON_JOBS_PATH = "/_lunora/admin/cron-jobs";
1209
- const OPENAPI_PATH = "/_lunora/admin/openapi";
1210
- const OPENRPC_PATH = "/_lunora/admin/openrpc";
1211
- const GLOBAL_TABLES_PATH = "/_lunora/admin/global/tables";
1212
- const GLOBAL_TABLE_PATH = "/_lunora/admin/global/table";
1213
- const GLOBAL_FACET_PATH = "/_lunora/admin/global/facet";
1214
- const parseGlobalFilters = (raw) => {
1215
- if (raw === void 0 || raw === "") {
1216
- return void 0;
1217
- }
1218
- let parsed;
1219
- try {
1220
- parsed = JSON.parse(raw);
1221
- } catch {
1222
- return void 0;
1223
- }
1224
- if (!Array.isArray(parsed)) {
1225
- return void 0;
1226
- }
1227
- const clauses = parsed.flatMap((entry) => {
1228
- if (typeof entry !== "object" || entry === null || typeof entry.column !== "string") {
1229
- return [];
1230
- }
1231
- const { column, value } = entry;
1232
- return [{ column, value }];
1233
- });
1234
- return clauses.length === 0 ? void 0 : clauses;
1235
- };
1236
- const EMPTY_OPENAPI_DOCUMENT = Object.freeze({
1237
- info: {
1238
- description: 'No OpenAPI spec is configured on this worker. Run `lunora codegen`, then wire the generated module to `createWorker`: `import { openApiSpec } from "./lunora/_generated/openapi"`.',
1239
- title: "Lunora API",
1240
- version: "0.0.0"
1241
- },
1242
- openapi: "3.1.0",
1243
- paths: {}
1244
- });
1245
- const EMPTY_OPENRPC_DOCUMENT = Object.freeze({
1246
- info: {
1247
- description: 'No OpenRPC spec is configured on this worker. Run `lunora codegen --api-spec openrpc` (or `both`), then wire the generated module to `createWorker`: `import { openRpcSpec } from "./lunora/_generated/openrpc"`.',
1248
- title: "Lunora RPC",
1249
- version: "0.0.0"
1250
- },
1251
- methods: [],
1252
- openrpc: "1.3.2"
1253
- });
1254
- const buildIntrospectionAdminRoutes = (deps) => {
1255
- const { assertAdmin, options, parsePaging, queryParameter, requireAdminOption } = deps;
1256
- const handleFunctionsList = (request) => {
1257
- if (request.method !== "GET") {
1258
- throw new LunoraError("Functions endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1259
- }
1260
- const registry = requireAdminOption(request, options.functions, {
1261
- code: "FUNCTIONS_NOT_CONFIGURED",
1262
- message: "functions endpoint requires a `functions` registry on the worker"
1263
- });
1264
- const functions = Object.entries(registry).flatMap(([path, entry]) => {
1265
- if (entry.visibility === "internal" || entry.kind === "stream") {
1266
- return [];
1267
- }
1268
- return [{ args: describeArguments(entry.args), kind: entry.kind, path }];
1269
- }).toSorted((a, b) => a.path.localeCompare(b.path));
1270
- return Response.json({ functions }, { headers: { "content-type": "application/json" }, status: 200 });
1271
- };
1272
- const handleCronJobs = (request) => {
1273
- if (request.method !== "GET") {
1274
- throw new LunoraError("Cron-jobs endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1275
- }
1276
- const registry = requireAdminOption(request, options.cronJobs, {
1277
- code: "CRON_JOBS_NOT_CONFIGURED",
1278
- message: "cron-jobs endpoint requires a `cronJobs` map on the worker"
1279
- });
1280
- const jobs = Object.entries(registry).flatMap(
1281
- ([cron, dispatches]) => dispatches.map((dispatch) => {
1282
- return {
1283
- args: dispatch.args,
1284
- cron,
1285
- functionPath: dispatch.functionPath,
1286
- name: dispatch.name,
1287
- shardKey: dispatch.shardKey,
1288
- workflow: dispatch.workflow
1289
- };
1290
- })
1291
- ).toSorted((a, b) => a.name.localeCompare(b.name));
1292
- return Response.json({ jobs }, { headers: { "content-type": "application/json" }, status: 200 });
1293
- };
1294
- const handleOpenApi = (request) => {
1295
- if (request.method !== "GET") {
1296
- throw new LunoraError("OpenAPI endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1297
- }
1298
- assertAdmin(request);
1299
- return Response.json(options.openApiSpec ?? EMPTY_OPENAPI_DOCUMENT, { headers: { "content-type": "application/json" }, status: 200 });
1300
- };
1301
- const handleOpenRpc = (request) => {
1302
- if (request.method !== "GET") {
1303
- throw new LunoraError("OpenRPC endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1304
- }
1305
- assertAdmin(request);
1306
- return Response.json(options.openRpcSpec ?? EMPTY_OPENRPC_DOCUMENT, { headers: { "content-type": "application/json" }, status: 200 });
1307
- };
1308
- const handleGlobalTables = async (request) => {
1309
- if (request.method !== "GET") {
1310
- throw new LunoraError("Global-tables endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1311
- }
1312
- const introspector = requireAdminOption(request, options.globalIntrospector, {
1313
- code: "GLOBALS_NOT_CONFIGURED",
1314
- message: "global endpoints require a `globalIntrospector` on the worker"
1315
- });
1316
- return Response.json(await introspector.listTables(), { headers: { "content-type": "application/json" }, status: 200 });
1317
- };
1318
- const handleGlobalTablePage = async (request) => {
1319
- if (request.method !== "GET") {
1320
- throw new LunoraError("Global-table endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1321
- }
1322
- const introspector = requireAdminOption(request, options.globalIntrospector, {
1323
- code: "GLOBALS_NOT_CONFIGURED",
1324
- message: "global endpoints require a `globalIntrospector` on the worker"
1325
- });
1326
- const url = new URL(request.url);
1327
- const table = queryParameter(url, "table");
1328
- if (table === void 0) {
1329
- throw new LunoraError("Global-table endpoint requires a `table` query param", { code: "BAD_REQUEST", status: 400 });
1330
- }
1331
- const page = await introspector.readTablePage({ ...parsePaging(request), filters: parseGlobalFilters(queryParameter(url, "filters")), table });
1332
- return Response.json(page, { headers: { "content-type": "application/json" }, status: 200 });
1333
- };
1334
- const handleGlobalFacet = async (request) => {
1335
- if (request.method !== "GET") {
1336
- throw new LunoraError("Global-facet endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1337
- }
1338
- const introspector = requireAdminOption(request, options.globalIntrospector, {
1339
- code: "GLOBALS_NOT_CONFIGURED",
1340
- message: "global endpoints require a `globalIntrospector` on the worker"
1341
- });
1342
- const url = new URL(request.url);
1343
- const table = queryParameter(url, "table");
1344
- const column = queryParameter(url, "column");
1345
- if (table === void 0 || column === void 0) {
1346
- throw new LunoraError("Global-facet endpoint requires `table` and `column` query params", { code: "BAD_REQUEST", status: 400 });
1347
- }
1348
- const limitParameter = queryParameter(url, "limit");
1349
- const limit = limitParameter === void 0 ? void 0 : Number(limitParameter);
1350
- const result = await introspector.facetColumn({
1351
- column,
1352
- filters: parseGlobalFilters(queryParameter(url, "filters")),
1353
- limit: limit !== void 0 && Number.isFinite(limit) ? limit : void 0,
1354
- table
1355
- });
1356
- return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
1357
- };
1358
- return {
1359
- [CRON_JOBS_PATH]: handleCronJobs,
1360
- [FUNCTIONS_PATH]: handleFunctionsList,
1361
- [GLOBAL_FACET_PATH]: handleGlobalFacet,
1362
- [GLOBAL_TABLE_PATH]: handleGlobalTablePage,
1363
- [GLOBAL_TABLES_PATH]: handleGlobalTables,
1364
- [OPENAPI_PATH]: handleOpenApi,
1365
- [OPENRPC_PATH]: handleOpenRpc
1366
- };
1367
- };
1368
-
1369
- const KV_NAMESPACES_PATH = "/_lunora/admin/kv/namespaces";
1370
- const KV_KEYS_PATH = "/_lunora/admin/kv/keys";
1371
- const KV_VALUE_PATH = "/_lunora/admin/kv/value";
1372
- const KV_VALUE_MAX_BODY_BYTES = 32 * 1048576;
1373
- const KV_MIN_EXPIRATION_SECONDS = 60;
1374
- const buildKvAdminRoutes = (deps) => {
1375
- const { readJsonBody, requireAdminOption } = deps;
1376
- const gate = (request) => requireAdminOption(request, deps.kvIntrospector, {
1377
- code: "KV_NOT_CONFIGURED",
1378
- message: "KV endpoints require a `kvIntrospector` on the worker"
1379
- });
1380
- const ok = (payload) => Response.json(payload, { headers: { "content-type": "application/json" }, status: 200 });
1381
- const requireNamespaceAndKey = (request, verb) => {
1382
- const url = new URL(request.url);
1383
- const namespace = url.searchParams.get("namespace") ?? "";
1384
- const key = url.searchParams.get("key") ?? "";
1385
- if (namespace === "") {
1386
- throw new LunoraError(`KV-value ${verb} request requires a \`namespace\` query parameter`, { code: "BAD_REQUEST", status: 400 });
1387
- }
1388
- if (key === "") {
1389
- throw new LunoraError(`KV-value ${verb} request requires a \`key\` query parameter`, { code: "BAD_REQUEST", status: 400 });
1390
- }
1391
- return { key, namespace };
1392
- };
1393
- const requireKnownNamespace = async (introspector, namespace) => {
1394
- const namespaces = await introspector.listNamespaces();
1395
- if (!namespaces.some((entry) => entry.binding === namespace)) {
1396
- throw new LunoraError(`Unknown KV namespace binding \`${namespace}\``, { code: "NOT_FOUND", status: 404 });
1397
- }
1398
- };
1399
- const handleKvNamespaces = async (request) => {
1400
- if (request.method !== "GET") {
1401
- throw new LunoraError("KV-namespaces endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1402
- }
1403
- return ok({ namespaces: await gate(request).listNamespaces() });
1404
- };
1405
- const handleKvKeys = async (request) => {
1406
- if (request.method !== "GET") {
1407
- throw new LunoraError("KV-keys endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1408
- }
1409
- const introspector = gate(request);
1410
- const url = new URL(request.url);
1411
- const namespace = url.searchParams.get("namespace") ?? "";
1412
- if (namespace === "") {
1413
- throw new LunoraError("KV-keys request requires a `namespace` query parameter", { code: "BAD_REQUEST", status: 400 });
1414
- }
1415
- const prefix = url.searchParams.get("prefix") ?? void 0;
1416
- const cursor = url.searchParams.get("cursor") ?? void 0;
1417
- const limitRaw = url.searchParams.get("limit");
1418
- const parsedLimit = limitRaw === null ? void 0 : Number.parseInt(limitRaw, 10);
1419
- if (parsedLimit !== void 0 && (!Number.isInteger(parsedLimit) || parsedLimit < 1)) {
1420
- throw new LunoraError("KV-keys `limit` must be a positive integer", { code: "BAD_REQUEST", status: 400 });
1421
- }
1422
- const limit = parsedLimit === void 0 ? void 0 : Math.min(parsedLimit, 1e3);
1423
- await requireKnownNamespace(introspector, namespace);
1424
- return ok(await introspector.listKeys({ cursor, limit, namespace, prefix }));
1425
- };
1426
- const handleKvValueGet = async (request) => {
1427
- const introspector = gate(request);
1428
- const params = requireNamespaceAndKey(request, "GET");
1429
- await requireKnownNamespace(introspector, params.namespace);
1430
- return ok(await introspector.getValue(params));
1431
- };
1432
- const handleKvValuePut = async (request) => {
1433
- const introspector = gate(request);
1434
- const candidate = await readJsonBody(request, KV_VALUE_MAX_BODY_BYTES);
1435
- if (typeof candidate.namespace !== "string" || candidate.namespace === "") {
1436
- throw new LunoraError("KV-value PUT request requires a `namespace` string", { code: "BAD_REQUEST", status: 400 });
1437
- }
1438
- if (typeof candidate.key !== "string" || candidate.key === "") {
1439
- throw new LunoraError("KV-value PUT request requires a `key` string", { code: "BAD_REQUEST", status: 400 });
1440
- }
1441
- if (typeof candidate.value !== "string") {
1442
- throw new LunoraError("KV-value PUT request requires a `value` string", { code: "BAD_REQUEST", status: 400 });
1443
- }
1444
- if (candidate.expirationTtl !== void 0 && (typeof candidate.expirationTtl !== "number" || !Number.isInteger(candidate.expirationTtl) || candidate.expirationTtl < KV_MIN_EXPIRATION_SECONDS)) {
1445
- throw new LunoraError("KV-value PUT `expirationTtl` must be an integer ≥ 60", { code: "BAD_REQUEST", status: 400 });
1446
- }
1447
- const minExpiration = Math.floor(Date.now() / 1e3) + KV_MIN_EXPIRATION_SECONDS;
1448
- if (candidate.expiration !== void 0 && (typeof candidate.expiration !== "number" || !Number.isInteger(candidate.expiration) || candidate.expiration < minExpiration)) {
1449
- throw new LunoraError("KV-value PUT `expiration` must be a Unix-seconds timestamp at least 60 seconds in the future", {
1450
- code: "BAD_REQUEST",
1451
- status: 400
1452
- });
1453
- }
1454
- await requireKnownNamespace(introspector, candidate.namespace);
1455
- await introspector.putValue({
1456
- expiration: candidate.expiration,
1457
- expirationTtl: candidate.expirationTtl,
1458
- key: candidate.key,
1459
- metadata: candidate.metadata,
1460
- namespace: candidate.namespace,
1461
- value: candidate.value
1462
- });
1463
- return ok({ ok: true });
1464
- };
1465
- const handleKvValueDelete = async (request) => {
1466
- const introspector = gate(request);
1467
- const params = requireNamespaceAndKey(request, "DELETE");
1468
- await requireKnownNamespace(introspector, params.namespace);
1469
- await introspector.deleteKey(params);
1470
- return ok({ deleted: true });
1471
- };
1472
- const kvValueHandlers = {
1473
- DELETE: handleKvValueDelete,
1474
- GET: handleKvValueGet,
1475
- PUT: handleKvValuePut
1476
- };
1477
- const handleKvValue = (request) => {
1478
- const handler = kvValueHandlers[request.method];
1479
- if (!handler) {
1480
- throw new LunoraError("KV-value endpoint requires GET, PUT, or DELETE", { code: "METHOD_NOT_ALLOWED", status: 405 });
1481
- }
1482
- return handler(request);
1483
- };
1484
- return {
1485
- [KV_NAMESPACES_PATH]: handleKvNamespaces,
1486
- [KV_KEYS_PATH]: handleKvKeys,
1487
- [KV_VALUE_PATH]: handleKvValue
1488
- };
1489
- };
1490
-
1491
- const MIGRATE_PATH$1 = "/_lunora/migrate";
1492
- const PITR_PATH = "/_lunora/admin/pitr";
1493
- const RANK_PATH = "/_lunora/admin/rank";
1494
- const RANKPAGE_PATH = "/_lunora/admin/rankpage";
1495
- const SHARD_TRAFFIC_PATH = "/_lunora/admin/shard-traffic";
1496
- const MIGRATION_ADMIN_OPS = /* @__PURE__ */ new Set(["__lunora_admin__:migrationStatus", "__lunora_admin__:runMigration"]);
1497
- const PITR_ADMIN_OPS = /* @__PURE__ */ new Set(["__lunora_admin__:getPitrBookmark", "__lunora_admin__:pitrRestore"]);
1498
- const parseMigrateRequest = async (request) => {
1499
- let body;
1500
- try {
1501
- const text = await readBodyTextWithLimit(request);
1502
- body = text === "" ? {} : JSON.parse(text);
1503
- } catch (error) {
1504
- if (error instanceof LunoraError) {
1505
- throw error;
1506
- }
1507
- throw new LunoraError("Migration body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1508
- }
1509
- const candidate = body ?? {};
1510
- if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1511
- throw new LunoraError("Migration request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1512
- }
1513
- if (typeof candidate.functionPath !== "string" || !MIGRATION_ADMIN_OPS.has(candidate.functionPath)) {
1514
- throw new LunoraError("Migration request `functionPath` must be a migration admin op", { code: "BAD_REQUEST", status: 400 });
1515
- }
1516
- return {
1517
- args: candidate.args ?? {},
1518
- functionPath: candidate.functionPath,
1519
- table: candidate.table
1520
- };
1521
- };
1522
- const parseRankRequest = async (request) => {
1523
- let body;
1524
- try {
1525
- const text = await readBodyTextWithLimit(request);
1526
- body = text === "" ? {} : JSON.parse(text);
1527
- } catch (error) {
1528
- if (error instanceof LunoraError) {
1529
- throw error;
1530
- }
1531
- throw new LunoraError("Rank body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1532
- }
1533
- const candidate = body ?? {};
1534
- if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1535
- throw new LunoraError("Rank request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1536
- }
1537
- if (typeof candidate.index !== "string" || candidate.index.length === 0) {
1538
- throw new LunoraError("Rank request is missing `index`", { code: "BAD_REQUEST", status: 400 });
1539
- }
1540
- if (typeof candidate.partitionKey !== "string") {
1541
- throw new LunoraError("Rank request `partitionKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1542
- }
1543
- if (typeof candidate.rowId !== "string" || candidate.rowId.length === 0) {
1544
- throw new LunoraError("Rank request is missing `rowId`", { code: "BAD_REQUEST", status: 400 });
1545
- }
1546
- if (!Array.isArray(candidate.sortValues)) {
1547
- throw new LunoraError("Rank request `sortValues` must be an array", { code: "BAD_REQUEST", status: 400 });
1548
- }
1549
- return {
1550
- index: candidate.index,
1551
- partitionKey: candidate.partitionKey,
1552
- rowId: candidate.rowId,
1553
- sortValues: candidate.sortValues,
1554
- table: candidate.table
1555
- };
1556
- };
1557
- const parseRankPageDirections = (raw) => {
1558
- if (raw === void 0) {
1559
- return void 0;
1560
- }
1561
- if (!Array.isArray(raw) || raw.some((d) => d !== "asc" && d !== "desc")) {
1562
- throw new LunoraError('Rank page request `directions` must be an array of "asc"|"desc"', { code: "BAD_REQUEST", status: 400 });
1563
- }
1564
- return raw;
1565
- };
1566
- const validateRankPageScalars = (candidate) => {
1567
- if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1568
- throw new LunoraError("Rank page request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1569
- }
1570
- if (typeof candidate.index !== "string" || candidate.index.length === 0) {
1571
- throw new LunoraError("Rank page request is missing `index`", { code: "BAD_REQUEST", status: 400 });
1572
- }
1573
- if (candidate.partitionKey !== void 0 && typeof candidate.partitionKey !== "string") {
1574
- throw new LunoraError("Rank page request `partitionKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1575
- }
1576
- if (candidate.take !== void 0 && (typeof candidate.take !== "number" || !Number.isFinite(candidate.take))) {
1577
- throw new LunoraError("Rank page request `take` must be a number", { code: "BAD_REQUEST", status: 400 });
1578
- }
1579
- if (candidate.cursor !== void 0 && candidate.cursor !== null && typeof candidate.cursor !== "string") {
1580
- throw new LunoraError("Rank page request `cursor` must be a string or null", { code: "BAD_REQUEST", status: 400 });
1581
- }
1582
- };
1583
- const parseRankPageRequest = async (request) => {
1584
- let body;
1585
- try {
1586
- const text = await readBodyTextWithLimit(request);
1587
- body = text === "" ? {} : JSON.parse(text);
1588
- } catch (error) {
1589
- if (error instanceof LunoraError) {
1590
- throw error;
1591
- }
1592
- throw new LunoraError("Rank page body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1593
- }
1594
- const candidate = body ?? {};
1595
- validateRankPageScalars(candidate);
1596
- const directions = parseRankPageDirections(candidate.directions);
1597
- return {
1598
- // eslint-disable-next-line unicorn/no-null -- the wire cursor is `null | string`; normalize an absent cursor to null so the coordinator starts at the first page
1599
- cursor: typeof candidate.cursor === "string" ? candidate.cursor : null,
1600
- directions,
1601
- index: candidate.index,
1602
- partitionKey: typeof candidate.partitionKey === "string" ? candidate.partitionKey : void 0,
1603
- table: candidate.table,
1604
- take: typeof candidate.take === "number" ? candidate.take : void 0
1605
- };
1606
- };
1607
- const parseShardTrafficRequest = async (request) => {
1608
- let body;
1609
- try {
1610
- const text = await readBodyTextWithLimit(request);
1611
- body = text === "" ? {} : JSON.parse(text);
1612
- } catch (error) {
1613
- if (error instanceof LunoraError) {
1614
- throw error;
1615
- }
1616
- throw new LunoraError("Shard-traffic body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1617
- }
1618
- const candidate = body ?? {};
1619
- if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1620
- throw new LunoraError("Shard-traffic request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1621
- }
1622
- return { table: candidate.table };
1623
- };
1624
- const parsePitrRequest = async (request) => {
1625
- const body = await readJsonBodyWithLimit(request);
1626
- const candidate = body;
1627
- if (typeof candidate.functionPath !== "string" || !PITR_ADMIN_OPS.has(candidate.functionPath)) {
1628
- throw new LunoraError("PITR request `functionPath` must be a PITR admin op", { code: "BAD_REQUEST", status: 400 });
1629
- }
1630
- if (candidate.shardKey !== void 0 && typeof candidate.shardKey !== "string") {
1631
- throw new LunoraError("PITR `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1632
- }
1633
- return {
1634
- args: candidate.args ?? {},
1635
- functionPath: candidate.functionPath,
1636
- shardKey: candidate.shardKey
1637
- };
1638
- };
1639
- const buildOrchestrationAdminRoutes = (deps) => {
1640
- const { defaultShard, forwardToShard, isAdmin, queryCoordinator, resolveForwardContext, shardDO } = deps;
1641
- const handleMigrate = async (request, env) => {
1642
- if (request.method !== "POST") {
1643
- throw new LunoraError("Migration endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1644
- }
1645
- if (!isAdmin(request)) {
1646
- throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1647
- }
1648
- if (!queryCoordinator) {
1649
- throw new LunoraError("Migration endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1650
- }
1651
- const migrate = await parseMigrateRequest(request);
1652
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1653
- const result = await queryCoordinator.orchestrateMigration(shardDO, {
1654
- args: migrate.args,
1655
- functionPath: migrate.functionPath,
1656
- headers: forwardedHeaders,
1657
- table: migrate.table
1658
- });
1659
- return Response.json(result, {
1660
- headers: { "content-type": "application/json" },
1661
- status: 200
1662
- });
1663
- };
1664
- const handleRank = async (request, env) => {
1665
- if (request.method !== "POST") {
1666
- throw new LunoraError("Rank endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1667
- }
1668
- if (!isAdmin(request)) {
1669
- throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1670
- }
1671
- if (!queryCoordinator) {
1672
- throw new LunoraError("Rank endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1673
- }
1674
- const rank = await parseRankRequest(request);
1675
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1676
- const result = await queryCoordinator.orchestrateRank(shardDO, {
1677
- headers: forwardedHeaders,
1678
- index: rank.index,
1679
- partitionKey: rank.partitionKey,
1680
- rowId: rank.rowId,
1681
- sortValues: rank.sortValues,
1682
- table: rank.table
1683
- });
1684
- return Response.json(result, {
1685
- headers: { "content-type": "application/json" },
1686
- status: 200
1687
- });
1688
- };
1689
- const handleRankPage = async (request, env) => {
1690
- if (request.method !== "POST") {
1691
- throw new LunoraError("Rank page endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1692
- }
1693
- if (!isAdmin(request)) {
1694
- throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1695
- }
1696
- if (!queryCoordinator) {
1697
- throw new LunoraError("Rank page endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1698
- }
1699
- const rankPage = await parseRankPageRequest(request);
1700
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1701
- const result = await queryCoordinator.orchestrateRankPage(shardDO, {
1702
- ...rankPage,
1703
- headers: forwardedHeaders
1704
- });
1705
- return Response.json(result, {
1706
- headers: { "content-type": "application/json" },
1707
- status: 200
1708
- });
1709
- };
1710
- const handleShardTraffic = async (request, env) => {
1711
- if (request.method !== "POST") {
1712
- throw new LunoraError("Shard-traffic endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1713
- }
1714
- if (!isAdmin(request)) {
1715
- throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1716
- }
1717
- if (!queryCoordinator) {
1718
- throw new LunoraError("Shard-traffic endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1719
- }
1720
- const trafficRequest = await parseShardTrafficRequest(request);
1721
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1722
- const result = await queryCoordinator.orchestrateShardTraffic(shardDO, {
1723
- headers: forwardedHeaders,
1724
- table: trafficRequest.table
1725
- });
1726
- return Response.json(result, {
1727
- headers: { "content-type": "application/json" },
1728
- status: 200
1729
- });
1730
- };
1731
- const handlePitr = async (request, env) => {
1732
- if (request.method !== "POST") {
1733
- throw new LunoraError("PITR endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1734
- }
1735
- if (!isAdmin(request)) {
1736
- throw new LunoraError("admin PITR endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1737
- }
1738
- const pitr = await parsePitrRequest(request);
1739
- const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1740
- const forwarded = new Request("https://shard.internal/rpc", {
1741
- body: JSON.stringify({ args: pitr.args, functionPath: pitr.functionPath }),
1742
- headers: forwardedHeaders,
1743
- method: "POST"
1744
- });
1745
- return forwardToShard(shardDO, pitr.shardKey ?? defaultShard, forwarded);
1746
- };
1747
- return {
1748
- [MIGRATE_PATH$1]: handleMigrate,
1749
- [PITR_PATH]: handlePitr,
1750
- [RANK_PATH]: handleRank,
1751
- [RANKPAGE_PATH]: handleRankPage,
1752
- [SHARD_TRAFFIC_PATH]: handleShardTraffic
1753
- };
1754
- };
1755
-
1756
- const SAMPLED_FLAG = 1;
1757
- const UNSAMPLED_FLAG = 0;
1758
- const MAX_TRACE_STATE_MEMBERS = 32;
1759
- const MAX_TRACE_STATE_LENGTH = 512;
1760
- const TRACE_STATE_MEMBER = /^[a-z][\d_a-z*/-]{0,255}(?:@[a-z][\d_a-z*/-]{0,13})?=[\u0020-\u002B\u002D-\u003C\u003E-\u007E]{1,256}$/;
1761
- const sanitizeTraceState = (header) => {
1762
- if (header === null || header === void 0) {
1763
- return void 0;
1764
- }
1765
- const trimmed = header.trim();
1766
- if (trimmed.length === 0 || trimmed.length > MAX_TRACE_STATE_LENGTH) {
1767
- return void 0;
1768
- }
1769
- const members = trimmed.split(",");
1770
- if (members.length > MAX_TRACE_STATE_MEMBERS) {
1771
- return void 0;
1772
- }
1773
- for (const member of members) {
1774
- if (!TRACE_STATE_MEMBER.test(member.trim())) {
1775
- return void 0;
1776
- }
1777
- }
1778
- return trimmed;
1779
- };
1780
- const extractTraceContext = (request) => {
1781
- const parsed = parseTraceparent(request.headers.get("traceparent"));
1782
- if (parsed === void 0) {
1783
- return void 0;
1784
- }
1785
- const traceState = sanitizeTraceState(request.headers.get("tracestate"));
1786
- return {
1787
- parentSpanId: parsed.parentSpanId,
1788
- sampled: parsed.sampled,
1789
- traceId: parsed.traceId,
1790
- ...traceState === void 0 ? {} : { traceState }
1791
- };
1792
- };
1793
- const beginDispatchTrace = (request, options = {}) => {
1794
- const upstream = extractTraceContext(request);
1795
- const trusted = options.trustInbound === true ? upstream : void 0;
1796
- const spanId = otlpRandomHex(8);
1797
- const traceId = trusted?.traceId ?? otlpRandomHex(16);
1798
- const decision = resolveTraceSampling(options.sampling, trusted === void 0 ? spanId : traceId);
1799
- const sampled = decision.isTraced && (trusted === void 0 || trusted.sampled);
1800
- return {
1801
- decision,
1802
- // A parseable upstream trace existed, and we chose not to join it.
1803
- ignoredUpstream: upstream !== void 0 && trusted === void 0,
1804
- trace: {
1805
- sampled,
1806
- spanId,
1807
- traceFlags: sampled ? SAMPLED_FLAG : UNSAMPLED_FLAG,
1808
- traceId,
1809
- ...trusted?.parentSpanId === void 0 ? {} : { parentSpanId: trusted.parentSpanId },
1810
- ...trusted?.traceState === void 0 ? {} : { traceState: trusted.traceState }
1811
- }
1812
- };
1813
- };
1814
- const injectTraceContext = (trace, headers) => {
1815
- headers.traceparent = buildTraceparent(trace.traceId, trace.spanId, trace.sampled);
1816
- if (trace.traceState !== void 0) {
1817
- headers.tracestate = trace.traceState;
1818
- }
1819
- };
1820
-
1821
- const createResourceAttributeResolver = (environment, request) => {
1822
- let resolved;
1823
- return () => {
1824
- if (resolved === void 0) {
1825
- const read = readerFromRecord(environment);
1826
- const cf = request === void 0 ? void 0 : request.cf;
1827
- resolved = mergeResourceAttributes(detectServiceResource(read), detectCloudflareResource(read, cf));
1828
- }
1829
- return resolved;
1830
- };
1831
- };
1832
-
1833
- const SCHEDULED_PATH = "/_lunora/admin/scheduled";
1834
- const SCHEDULED_STATUS_PATH = "/_lunora/admin/scheduled/status";
1835
- const SCHEDULED_WS_PATH = "/_lunora/admin/scheduled/ws";
1836
- const SCHEDULED_CANCEL_PATH = "/_lunora/admin/scheduled/cancel";
1837
- const SCHEDULED_DEAD_PATH = "/_lunora/admin/scheduled/dead";
1838
- const SCHEDULED_DEAD_RETRY_PATH = "/_lunora/admin/scheduled/dead/retry";
1839
- const SCHEDULED_DEAD_CANCEL_PATH = "/_lunora/admin/scheduled/dead/cancel";
1840
- const buildScheduledAdminRoutes = (deps) => {
1841
- const { checkWsAdmin, requireSchedulerNamespace, resolveSchedulerStub, schedulerInstanceName } = deps;
1842
- const handleScheduledList = async (request) => {
1843
- if (request.method !== "GET") {
1844
- throw new LunoraError("Scheduled-list endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1845
- }
1846
- const stub = resolveSchedulerStub(request);
1847
- return stub.fetch(new Request("https://scheduler.internal/list", { method: "GET" }));
1848
- };
1849
- const handleSchedulerStatus = async (request) => {
1850
- if (request.method !== "GET") {
1851
- throw new LunoraError("Scheduler-status endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1852
- }
1853
- const stub = resolveSchedulerStub(request);
1854
- return stub.fetch(new Request("https://scheduler.internal/status", { method: "GET" }));
1855
- };
1856
- const handleScheduledWebSocket = async (request) => {
1857
- if (request.headers.get("Upgrade") !== "websocket") {
1858
- throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
1859
- }
1860
- if (!await checkWsAdmin(request)) {
1861
- throw new LunoraError("admin authorization required", { code: "ADMIN_FORBIDDEN", status: 403 });
1862
- }
1863
- const namespace = requireSchedulerNamespace();
1864
- const stub = resolveShard(namespace, schedulerInstanceName);
1865
- return stub.fetch(new Request("https://scheduler.internal/ws", { headers: { Upgrade: "websocket" } }));
1866
- };
1867
- const handleScheduledCancel = async (request) => {
1868
- if (request.method !== "POST") {
1869
- throw new LunoraError("Scheduled-cancel endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1870
- }
1871
- const stub = resolveSchedulerStub(request);
1872
- const body = await request.json().catch(() => void 0);
1873
- if (typeof body?.id !== "string" || body.id === "") {
1874
- throw new LunoraError("Scheduled-cancel requires a string `id`", { code: "BAD_REQUEST", status: 400 });
1875
- }
1876
- return stub.fetch(
1877
- new Request("https://scheduler.internal/cancel", {
1878
- body: JSON.stringify({ id: body.id }),
1879
- headers: { "content-type": "application/json" },
1880
- method: "POST"
1881
- })
1882
- );
1883
- };
1884
- const handleDeadList = async (request) => {
1885
- if (request.method !== "GET") {
1886
- throw new LunoraError("Scheduled dead-letter endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1887
- }
1888
- const stub = resolveSchedulerStub(request);
1889
- return stub.fetch(new Request("https://scheduler.internal/dead", { method: "GET" }));
1890
- };
1891
- const proxyDeadAction = (doPath) => async (request) => {
1892
- if (request.method !== "POST") {
1893
- throw new LunoraError("Scheduled dead-letter action requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1894
- }
1895
- const stub = resolveSchedulerStub(request);
1896
- const body = await request.json().catch(() => void 0);
1897
- if (typeof body?.id !== "string" || body.id === "") {
1898
- throw new LunoraError("Scheduled dead-letter action requires a string `id`", { code: "BAD_REQUEST", status: 400 });
1899
- }
1900
- return stub.fetch(
1901
- new Request(`https://scheduler.internal${doPath}`, {
1902
- body: JSON.stringify({ id: body.id }),
1903
- headers: { "content-type": "application/json" },
1904
- method: "POST"
1905
- })
1906
- );
1907
- };
1908
- return {
1909
- [SCHEDULED_CANCEL_PATH]: handleScheduledCancel,
1910
- [SCHEDULED_DEAD_CANCEL_PATH]: proxyDeadAction("/dead/cancel"),
1911
- [SCHEDULED_DEAD_PATH]: handleDeadList,
1912
- [SCHEDULED_DEAD_RETRY_PATH]: proxyDeadAction("/dead/retry"),
1913
- [SCHEDULED_PATH]: handleScheduledList,
1914
- [SCHEDULED_STATUS_PATH]: handleSchedulerStatus,
1915
- [SCHEDULED_WS_PATH]: handleScheduledWebSocket
1916
- };
1917
- };
1918
-
1919
- const STORAGE_PATH = "/_lunora/admin/storage";
1920
- const STORAGE_URL_PATH = "/_lunora/admin/storage/url";
1921
- const STORAGE_BUCKETS_PATH = "/_lunora/admin/storage/buckets";
1922
- const MAX_STORAGE_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
1923
- const buildStorageAdminRoutes = (deps) => {
1924
- const { assertAdmin, parsePaging, queryParameter, readBodyBytes, requireAdminOption, storage } = deps;
1925
- const requireStorageKey = (url) => {
1926
- const key = queryParameter(url, "key");
1927
- if (key === void 0) {
1928
- throw new LunoraError("Storage endpoint requires a `key` query parameter", { code: "BAD_REQUEST", status: 400 });
1929
- }
1930
- return key;
1931
- };
1932
- const handleStorageList = async (request) => {
1933
- const storageList = requireAdminOption(request, storage.storageList, {
1934
- code: "STORAGE_NOT_CONFIGURED",
1935
- message: "storage endpoint requires a `storageList` function on the worker"
1936
- });
1937
- const url = new URL(request.url);
1938
- const result = await storageList(queryParameter(url, "prefix"), {
1939
- bucket: queryParameter(url, "bucket"),
1940
- cursor: queryParameter(url, "cursor"),
1941
- ...parsePaging(request)
1942
- });
1943
- return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
1944
- };
1945
- const handleStorageBuckets = (request) => {
1946
- if (request.method !== "GET") {
1947
- throw new LunoraError("Storage-buckets endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1948
- }
1949
- assertAdmin(request);
1950
- return Response.json({ buckets: storage.storageBuckets ?? [] }, { headers: { "content-type": "application/json" }, status: 200 });
1951
- };
1952
- const handleStorageDelete = async (request) => {
1953
- const storageDelete = requireAdminOption(request, storage.storageDelete, {
1954
- code: "STORAGE_DELETE_NOT_CONFIGURED",
1955
- message: "storage delete requires a `storageDelete` function on the worker"
1956
- });
1957
- const url = new URL(request.url);
1958
- const key = requireStorageKey(url);
1959
- await storageDelete(key, { bucket: queryParameter(url, "bucket") });
1960
- return Response.json({ deleted: true, key }, { headers: { "content-type": "application/json" }, status: 200 });
1961
- };
1962
- const handleStorageUpload = async (request) => {
1963
- const storageUpload = requireAdminOption(request, storage.storageUpload, {
1964
- code: "STORAGE_UPLOAD_NOT_CONFIGURED",
1965
- message: "storage upload requires a `storageUpload` function on the worker"
1966
- });
1967
- const url = new URL(request.url);
1968
- const key = requireStorageKey(url);
1969
- const body = await readBodyBytes(request);
1970
- const headerContentType = request.headers.get("content-type");
1971
- const contentType = headerContentType === null || headerContentType === "" ? void 0 : headerContentType;
1972
- const result = await storageUpload(key, body, { bucket: queryParameter(url, "bucket"), contentType });
1973
- return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
1974
- };
1975
- const handleStorage = async (request) => {
1976
- switch (request.method) {
1977
- case "DELETE": {
1978
- return handleStorageDelete(request);
1979
- }
1980
- case "GET": {
1981
- return handleStorageList(request);
1982
- }
1983
- case "POST":
1984
- case "PUT": {
1985
- return handleStorageUpload(request);
1986
- }
1987
- default: {
1988
- throw new LunoraError("Storage endpoint requires GET, PUT, POST, or DELETE", { code: "METHOD_NOT_ALLOWED", status: 405 });
1989
- }
1990
- }
1991
- };
1992
- const handleStorageSignedUrl = async (request) => {
1993
- if (request.method !== "GET") {
1994
- throw new LunoraError("Storage URL endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1995
- }
1996
- const storageSignedUrl = requireAdminOption(request, storage.storageSignedUrl, {
1997
- code: "STORAGE_URL_NOT_CONFIGURED",
1998
- message: "storage URL endpoint requires a `storageSignedUrl` function on the worker"
1999
- });
2000
- const url = new URL(request.url);
2001
- const key = requireStorageKey(url);
2002
- const expiresInRaw = Number(queryParameter(url, "expiresIn") ?? "");
2003
- const expiresInSeconds = Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? Math.min(expiresInRaw, MAX_STORAGE_EXPIRES_IN_SECONDS) : void 0;
2004
- const signedUrl = await storageSignedUrl(key, { bucket: queryParameter(url, "bucket"), expiresInSeconds });
2005
- return Response.json({ key, url: signedUrl }, { headers: { "content-type": "application/json" }, status: 200 });
2006
- };
2007
- return {
2008
- [STORAGE_BUCKETS_PATH]: handleStorageBuckets,
2009
- [STORAGE_PATH]: handleStorage,
2010
- [STORAGE_URL_PATH]: handleStorageSignedUrl
2011
- };
2012
- };
2013
-
2014
- const cfString = (request, ...path) => {
2015
- let current = request.cf;
2016
- for (const key of path) {
2017
- if (typeof current !== "object" || current === null) {
2018
- return void 0;
2019
- }
2020
- current = current[key];
2021
- }
2022
- return typeof current === "string" ? current : void 0;
2023
- };
2024
- const SIGNAL_CHECKS = {
2025
- // Edge-verified client certificate — the platform sets `certVerified`, so
2026
- // unlike a header this cannot be spoofed by the caller.
2027
- mtls: (request) => cfString(request, "tlsClientAuth", "certVerified") === "SUCCESS"
2028
- };
2029
- const resolveTraceTrust = (option) => {
2030
- if (option === void 0 || option === false) {
2031
- return () => false;
2032
- }
2033
- if (option === true) {
2034
- return () => true;
2035
- }
2036
- if (typeof option === "function") {
2037
- return option;
2038
- }
2039
- const check = Object.entries(SIGNAL_CHECKS).find(([signal]) => signal === option)?.[1];
2040
- return check ?? (() => false);
2041
- };
2042
- const createDroppedTraceNotice = (option) => {
2043
- if (option !== void 0) {
2044
- return () => {
2045
- };
2046
- }
2047
- let notified = false;
2048
- return () => {
2049
- if (notified) {
2050
- return;
2051
- }
2052
- notified = true;
2053
- console.warn(
2054
- '[lunora] Ignored an inbound `traceparent`, so this request starts a new trace instead of joining the caller\'s. That is the safe default: the header is caller-supplied, and trusting it lets any client choose which trace its spans and logs join. If this worker sits behind a gateway, service mesh, or Cloudflare Access that sets `traceparent` itself, set `trustInboundTraceContext: true` on createWorker() (or `"mtls"` to trust only edge-verified client certificates). Set it to `false` to keep this behaviour and silence this message.'
2055
- );
2056
- };
2057
- };
2058
-
2059
- const VECTOR_INDEXES_PATH = "/_lunora/admin/vector/indexes";
2060
- const VECTOR_QUERY_PATH = "/_lunora/admin/vector/query";
2061
- const buildVectorAdminRoutes = (deps) => {
2062
- const { readJsonBody, requireAdminOption } = deps;
2063
- const handleVectorIndexes = async (request) => {
2064
- if (request.method !== "GET") {
2065
- throw new LunoraError("Vector-indexes endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
2066
- }
2067
- const introspector = requireAdminOption(request, deps.vectorIntrospector, {
2068
- code: "VECTORS_NOT_CONFIGURED",
2069
- message: "vector endpoints require a `vectorIntrospector` on the worker"
2070
- });
2071
- return Response.json({ indexes: await introspector.listIndexes() }, { headers: { "content-type": "application/json" }, status: 200 });
2072
- };
2073
- const handleVectorQuery = async (request) => {
2074
- if (request.method !== "POST") {
2075
- throw new LunoraError("Vector-query endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2076
- }
2077
- const introspector = requireAdminOption(request, deps.vectorIntrospector, {
2078
- code: "VECTORS_NOT_CONFIGURED",
2079
- message: "vector endpoints require a `vectorIntrospector` on the worker"
2080
- });
2081
- if (introspector.queryIndex === void 0) {
2082
- throw new LunoraError("vector index querying is not enabled on this worker", { code: "VECTOR_QUERY_UNSUPPORTED", status: 400 });
2083
- }
2084
- const body = await readJsonBody(request);
2085
- const candidate = body;
2086
- if (typeof candidate.name !== "string" || candidate.name === "") {
2087
- throw new LunoraError("Vector-query request requires a `name` string", { code: "BAD_REQUEST", status: 400 });
2088
- }
2089
- if (typeof candidate.text !== "string" || candidate.text === "") {
2090
- throw new LunoraError("Vector-query request requires a `text` string", { code: "BAD_REQUEST", status: 400 });
2091
- }
2092
- if (candidate.topK !== void 0 && (typeof candidate.topK !== "number" || !Number.isInteger(candidate.topK) || candidate.topK < 1)) {
2093
- throw new LunoraError("Vector-query `topK` must be a positive integer", { code: "BAD_REQUEST", status: 400 });
2094
- }
2095
- const result = await introspector.queryIndex({ name: candidate.name, text: candidate.text, topK: candidate.topK });
2096
- return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
2097
- };
2098
- return {
2099
- [VECTOR_INDEXES_PATH]: handleVectorIndexes,
2100
- [VECTOR_QUERY_PATH]: handleVectorQuery
2101
- };
2102
- };
2103
-
2104
- const WORKFLOWS_INSTANCES_PATH = "/_lunora/admin/workflows/instances";
2105
- const WORKFLOWS_INSTANCE_PATH = "/_lunora/admin/workflows/instance";
2106
- const WORKFLOWS_STATUS_PATH = "/_lunora/admin/workflows/status";
2107
- const INSTANCE_STATUSES = {
2108
- complete: true,
2109
- errored: true,
2110
- paused: true,
2111
- queued: true,
2112
- running: true,
2113
- terminated: true,
2114
- unknown: true,
2115
- waiting: true,
2116
- waitingForPause: true
2117
- };
2118
- const toInstanceStatus = (value) => value !== null && Object.hasOwn(INSTANCE_STATUSES, value) ? value : void 0;
2119
- const positiveIntParameter = (url, key) => {
2120
- const raw = url.searchParams.get(key);
2121
- if (raw === null) {
2122
- return void 0;
2123
- }
2124
- const value = Number(raw);
2125
- return Number.isInteger(value) && value > 0 ? value : void 0;
2126
- };
2127
- const requireQuery = (url, key) => {
2128
- const value = url.searchParams.get(key);
2129
- if (value === null || value === "") {
2130
- throw new LunoraError(`Workflows admin endpoint requires a \`${key}\` query parameter`, { code: "BAD_REQUEST", status: 400 });
2131
- }
2132
- return value;
2133
- };
2134
- const throwNotConfigured = () => {
2135
- throw new LunoraError("Workflow inspection is unconfigured. Set CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN in your .dev.vars to enable it.", {
2136
- code: "WORKFLOWS_NOT_CONFIGURED",
2137
- status: 501
2138
- });
2139
- };
2140
- const buildWorkflowsAdminRoutes = (deps) => {
2141
- const { assertAdmin, resolveWorkflowsClient } = deps;
2142
- const handleInstances = async (request, env, url) => {
2143
- if (request.method !== "GET") {
2144
- throw new LunoraError("Workflows instances endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
2145
- }
2146
- assertAdmin(request);
2147
- const client = resolveWorkflowsClient(env);
2148
- if (!client) {
2149
- return Response.json({ configured: false, instances: [], page: 1, perPage: 0, totalCount: 0 });
2150
- }
2151
- const workflowName = requireQuery(url, "name");
2152
- const status = toInstanceStatus(url.searchParams.get("status"));
2153
- return Response.json(
2154
- await client.listInstances({
2155
- page: positiveIntParameter(url, "page"),
2156
- perPage: positiveIntParameter(url, "perPage"),
2157
- status,
2158
- workflowName
2159
- })
2160
- );
2161
- };
2162
- const handleInstance = async (request, env, url) => {
2163
- if (request.method !== "GET") {
2164
- throw new LunoraError("Workflows instance endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
2165
- }
2166
- assertAdmin(request);
2167
- const client = resolveWorkflowsClient(env);
2168
- if (!client) {
2169
- return throwNotConfigured();
2170
- }
2171
- return Response.json(await client.getInstance({ instanceId: requireQuery(url, "id"), workflowName: requireQuery(url, "name") }));
2172
- };
2173
- const handleStatus = async (request, env) => {
2174
- if (request.method !== "POST") {
2175
- throw new LunoraError("Workflows status endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2176
- }
2177
- assertAdmin(request);
2178
- const client = resolveWorkflowsClient(env);
2179
- if (!client) {
2180
- return throwNotConfigured();
2181
- }
2182
- const body = await request.json().catch(() => void 0);
2183
- if (typeof body?.name !== "string" || body.name === "" || typeof body.id !== "string" || body.id === "") {
2184
- throw new LunoraError("Workflows status action requires string `name` and `id`", { code: "BAD_REQUEST", status: 400 });
2185
- }
2186
- const { action } = body;
2187
- if (action !== "pause" && action !== "resume" && action !== "terminate") {
2188
- throw new LunoraError("Workflows status action must be one of: pause, resume, terminate", { code: "BAD_REQUEST", status: 400 });
2189
- }
2190
- return Response.json(await client.setInstanceStatus({ action, instanceId: body.id, workflowName: body.name }));
2191
- };
2192
- return {
2193
- [WORKFLOWS_INSTANCE_PATH]: handleInstance,
2194
- [WORKFLOWS_INSTANCES_PATH]: handleInstances,
2195
- [WORKFLOWS_STATUS_PATH]: handleStatus
2196
- };
2197
- };
2198
-
2199
- const NDJSON_ENCODER = new TextEncoder();
2200
- const RPC_PATH = "/_lunora/rpc";
2201
- const RPC_BATCH_PATH = "/_lunora/rpc-batch";
2202
- const WS_PATH = "/_lunora/ws";
2203
- const buildSinkContext = (environment, request, waitUntil) => {
2204
- return {
2205
- resourceAttributes: createResourceAttributeResolver(environment, request),
2206
- ...waitUntil === void 0 ? {} : { waitUntil }
2207
- };
2208
- };
2209
- const traceEventFields = (trace) => {
2210
- return {
2211
- spanId: trace.spanId,
2212
- traceFlags: trace.traceFlags,
2213
- traceId: trace.traceId,
2214
- ...trace.parentSpanId === void 0 ? {} : { parentSpanId: trace.parentSpanId }
2215
- };
2216
- };
2217
- const requestTelemetryMeta = (request) => {
2218
- const { method } = request;
2219
- const userAgent = request.headers.get("user-agent") ?? void 0;
2220
- let url;
2221
- try {
2222
- url = new URL(request.url);
2223
- } catch {
2224
- return { method, userAgent };
2225
- }
2226
- const port = url.port === "" ? void 0 : Number(url.port);
2227
- return {
2228
- host: url.hostname,
2229
- method,
2230
- path: url.pathname,
2231
- port: Number.isNaN(port) ? void 0 : port,
2232
- scheme: url.protocol.replace(":", ""),
2233
- userAgent
2234
- };
2235
- };
2236
- const VOICE_PATH_PREFIX = "/_lunora/voice/";
2237
- const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
2238
- const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
2239
- const ADMIN_WS_TOKEN_PATH = "/_lunora/admin/ws-token";
2240
- const ADMIN_PATH_PREFIX = "/_lunora/admin/";
2241
- const MIGRATE_PATH = "/_lunora/migrate";
2242
- const STATUS_PATH = "/_lunora/status";
2243
- const isAdminPath = (pathname) => pathname.startsWith(ADMIN_PATH_PREFIX) || pathname === MIGRATE_PATH;
2244
- const REQUIRE_EPHEMERAL_ENV_VALUES = /* @__PURE__ */ new Set(["1", "enabled", "on", "true", "yes"]);
2245
- const readForwardedIdentity = (request) => {
2246
- const forwardedUserId = request.headers.get("x-lunora-userid");
2247
- const forwardedIdentity = request.headers.get("x-lunora-identity");
2248
- if (forwardedUserId === null && forwardedIdentity === null) {
2249
- return void 0;
2250
- }
2251
- return {
2252
- ...forwardedIdentity === null ? {} : { identity: forwardedIdentity },
2253
- ...forwardedUserId === null ? {} : { userId: forwardedUserId }
2254
- };
2255
- };
2256
- const DEFAULT_AUTH_BASE_PATH = "/api/auth";
2257
- const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
2258
- const LIST_PUSH_SUBSCRIPTIONS_OP = "__lunora_admin__:listPushSubscriptions";
2259
- const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
2260
- const isAuthAttemptPath = (pathname, basePath) => {
2261
- const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
2262
- if (!pathname.startsWith(`${base}/`)) {
2263
- return false;
2264
- }
2265
- const suffix = pathname.slice(base.length);
2266
- return AUTH_ATTEMPT_SEGMENTS.some((segment) => suffix === segment || suffix.startsWith(`${segment}/`));
2267
- };
2268
- const buildErrorEvent = (functionPath, durationMs, error, extra) => {
2269
- const mappable = isLunoraError(error);
2270
- const code = mappable ? error.code : "INTERNAL_SERVER_ERROR";
2271
- const status = mappable ? error.status : 500;
2272
- const message = error instanceof Error ? error.message : String(error);
2273
- return {
2274
- durationMs,
2275
- error: { code, message, status },
2276
- functionPath,
2277
- ok: false,
2278
- ...extra.fanOut ? { fanOut: { failed: 0, shards: 0, table: extra.fanOut.table } } : {},
2279
- ...extra.shardKey ? { shardKey: extra.shardKey } : {}
2280
- };
2281
- };
2282
- const identityExpiryMs = (identity) => {
2283
- const { exp, expiresAtMs } = identity;
2284
- if (typeof expiresAtMs === "number" && Number.isFinite(expiresAtMs)) {
2285
- return expiresAtMs;
2286
- }
2287
- if (typeof exp === "number" && Number.isFinite(exp)) {
2288
- return exp * 1e3;
2289
- }
2290
- return void 0;
2291
- };
2292
- const resolveForwardContext = async (request, env, resolveIdentity) => {
2293
- const headers = { "content-type": "application/json" };
2294
- const authorization = request.headers.get("authorization");
2295
- const cookie = request.headers.get("cookie");
2296
- const bookmark = request.headers.get("x-d1-bookmark");
2297
- const mutationId = request.headers.get("x-lunora-mutation-id");
2298
- const clientId = request.headers.get("x-lunora-client-id");
2299
- const clientSeq = request.headers.get("x-lunora-client-seq");
2300
- if (authorization) {
2301
- headers["authorization"] = authorization;
2302
- }
2303
- if (cookie) {
2304
- headers["cookie"] = cookie;
2305
- }
2306
- if (bookmark) {
2307
- headers["x-d1-bookmark"] = bookmark;
2308
- }
2309
- if (mutationId) {
2310
- headers["x-lunora-mutation-id"] = mutationId;
2311
- }
2312
- if (clientId) {
2313
- headers["x-lunora-client-id"] = clientId;
2314
- }
2315
- if (clientSeq) {
2316
- headers["x-lunora-client-seq"] = clientSeq;
2317
- }
2318
- const clientIp = request.headers.get("cf-connecting-ip");
2319
- if (clientIp) {
2320
- headers["x-lunora-client-ip"] = clientIp;
2321
- }
2322
- if (!resolveIdentity) {
2323
- return { claims: null, headers, identity: null, userId: null };
2324
- }
2325
- const identity = await resolveIdentity(request, env);
2326
- if (!identity || typeof identity.userId !== "string" || identity.userId.length === 0) {
2327
- return { claims: null, headers, identity: null, userId: null };
2328
- }
2329
- headers["x-lunora-userid"] = identity.userId;
2330
- const expiresAtMs = identityExpiryMs(identity);
2331
- if (expiresAtMs !== void 0) {
2332
- headers["x-lunora-identity-exp"] = String(expiresAtMs);
2333
- }
2334
- const { userId, ...extra } = identity;
2335
- const claims = Object.keys(extra).length > 0 ? extra : null;
2336
- if (claims) {
2337
- headers["x-lunora-identity"] = JSON.stringify(claims);
2338
- }
2339
- return { claims, headers, identity, userId };
2340
- };
2341
- const KNOWN_MERGE_KINDS = /* @__PURE__ */ new Set(["concat", "first", "groupBy", "max", "min", "rank", "sum", "topK"]);
2342
- const validateFanOut = (fanOut) => {
2343
- if (fanOut === void 0) {
2344
- return void 0;
2345
- }
2346
- if (!fanOut || typeof fanOut !== "object") {
2347
- throw new LunoraError("RPC `fanOut` must be an object", { code: "BAD_REQUEST", status: 400 });
2348
- }
2349
- const spec = fanOut;
2350
- if (typeof spec.table !== "string" || spec.table.length === 0) {
2351
- throw new LunoraError("RPC `fanOut.table` must be a non-empty string", { code: "BAD_REQUEST", status: 400 });
2352
- }
2353
- if (!spec.merge || typeof spec.merge !== "object") {
2354
- throw new LunoraError("RPC `fanOut.merge` must be an object", { code: "BAD_REQUEST", status: 400 });
2355
- }
2356
- const merge = spec.merge;
2357
- if (typeof merge.kind !== "string" || !KNOWN_MERGE_KINDS.has(merge.kind)) {
2358
- throw new LunoraError("RPC `fanOut.merge.kind` is not a recognized merge strategy", { code: "BAD_REQUEST", status: 400 });
2359
- }
2360
- if (merge.kind === "topK") {
2361
- if (typeof merge.k !== "number" || !Number.isInteger(merge.k) || merge.k < 0) {
2362
- throw new LunoraError("RPC `fanOut.merge.k` must be a non-negative integer", { code: "BAD_REQUEST", status: 400 });
2363
- }
2364
- if (typeof merge.by !== "string" || merge.by.length === 0) {
2365
- throw new LunoraError("RPC `fanOut.merge.by` must be a non-empty string", { code: "BAD_REQUEST", status: 400 });
2366
- }
2367
- }
2368
- return spec;
2369
- };
2370
- const logRpcDebug = (env, envelope) => {
2371
- if (!env?.LUNORA_DEBUG_RPC) {
2372
- return;
2373
- }
2374
- console.warn(`[lunora:rpc] ${envelope.fanOut ? "fan-out" : `shard=${envelope.shardKey ?? "(root)"}`} ${envelope.functionPath}`);
2375
- };
2376
- const resolveX402Charge = (envelope, options) => {
2377
- const x402Tag = options.functions?.[envelope.functionPath]?.x402;
2378
- if (!x402Tag) {
2379
- return void 0;
2380
- }
2381
- if (envelope.fanOut) {
2382
- throw new LunoraError("a paid (`.x402`) function cannot be fanned out", { code: "BAD_REQUEST", status: 400 });
2383
- }
2384
- if (!options.x402Charge) {
2385
- throw new LunoraError(`function "${envelope.functionPath}" is marked paid (.x402) but no x402Charge gate is configured on the worker`, {
2386
- code: "MISCONFIGURED",
2387
- status: 500
2388
- });
2389
- }
2390
- return x402Tag;
2391
- };
2392
- const parseEnvelope = async (request) => {
2393
- const text = await readBodyTextWithLimit(request);
2394
- let body;
2395
- try {
2396
- body = JSON.parse(text);
2397
- } catch {
2398
- throw new LunoraError("RPC body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
2399
- }
2400
- if (!body || typeof body !== "object" || typeof body.functionPath !== "string") {
2401
- throw new LunoraError("RPC envelope is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
2402
- }
2403
- const raw = body;
2404
- if (raw.args !== void 0 && (typeof raw.args !== "object" || raw.args === null || Array.isArray(raw.args))) {
2405
- throw new LunoraError("RPC `args` must be an object", { code: "BAD_REQUEST", status: 400 });
2406
- }
2407
- if (raw.shardKey !== void 0 && typeof raw.shardKey !== "string") {
2408
- throw new LunoraError("RPC `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
2409
- }
2410
- const envelope = body;
2411
- const fanOut = validateFanOut(envelope.fanOut);
2412
- const args = envelope.args ?? {};
2413
- if (fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
2414
- const requestedTable = args.table;
2415
- if (typeof requestedTable === "string" && requestedTable !== fanOut.table) {
2416
- throw new LunoraError("RPC `args.table` must match the authorized `fanOut.table` for a relation fan-out", { code: "BAD_REQUEST", status: 400 });
2417
- }
2418
- args.table = fanOut.table;
2419
- }
2420
- return {
2421
- args,
2422
- fanOut,
2423
- functionPath: envelope.functionPath,
2424
- shardKey: envelope.shardKey
2425
- };
2426
- };
2427
- const forwardToShard = async (namespace, shardKey, request) => {
2428
- const stub = resolveShard(namespace, shardKey);
2429
- return stub.fetch(request);
2430
- };
2431
- const relayProbeCache = /* @__PURE__ */ new Map();
2432
- const RELAY_PROBE_TTL_MS = 5e3;
2433
- const RELAY_PROBE_MAX_ENTRIES = 4096;
2434
- const probeRelayCount = async (namespace, shardKey) => {
2435
- const now = Date.now();
2436
- const cached = relayProbeCache.get(shardKey);
2437
- if (cached !== void 0 && cached.expiresMs > now) {
2438
- return cached.relayCount;
2439
- }
2440
- if (cached !== void 0) {
2441
- relayProbeCache.delete(shardKey);
2442
- }
2443
- let relayCount = 0;
2444
- try {
2445
- const response = await resolveShard(namespace, shardKey).fetch(new Request("https://shard.internal/_lunora/route"));
2446
- if (response.ok) {
2447
- const body = await response.json();
2448
- const reported = body.relayCount;
2449
- if (typeof reported === "number" && reported > 0) {
2450
- relayCount = Math.floor(reported);
2451
- }
2452
- }
2453
- } catch {
2454
- relayCount = 0;
2455
- }
2456
- evictOldestEntry(relayProbeCache, RELAY_PROBE_MAX_ENTRIES);
2457
- relayProbeCache.set(shardKey, { expiresMs: now + RELAY_PROBE_TTL_MS, relayCount });
2458
- return relayCount;
2459
- };
2460
- const resolveShardBindingName = (env, namespace) => {
2461
- if (env === null || typeof env !== "object") {
2462
- return void 0;
2463
- }
2464
- for (const [key, value] of Object.entries(env)) {
2465
- if (value === namespace) {
2466
- return key;
2467
- }
2468
- }
2469
- return void 0;
2470
- };
2471
- const constantTimeEqual = (expected, supplied) => {
2472
- const max = Math.max(expected.length, supplied.length);
2473
- let diff = expected.length ^ supplied.length;
2474
- for (let index = 0; index < max; index += 1) {
2475
- const expectedCode = index < expected.length ? expected.codePointAt(index) ?? 0 : 0;
2476
- const suppliedCode = index < supplied.length ? supplied.codePointAt(index) ?? 0 : 0;
2477
- diff |= expectedCode ^ suppliedCode;
2478
- }
2479
- return diff === 0;
2480
- };
2481
- const verifyHmacSignature = async (secret, body, suppliedSignature) => {
2482
- if (secret.length === 0 || suppliedSignature.length === 0) {
2483
- return false;
2484
- }
2485
- const encoder = new TextEncoder();
2486
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
2487
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
2488
- const bytes = new Uint8Array(signature);
2489
- let binary = "";
2490
- for (const byte of bytes) {
2491
- binary += String.fromCodePoint(byte);
2492
- }
2493
- const expected = btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
2494
- return constantTimeEqual(expected, suppliedSignature);
2495
- };
2496
- const checkAdminAuth = (request, expected) => {
2497
- if (!expected || expected.length === 0) {
2498
- return false;
2499
- }
2500
- const authorization = request.headers.get("authorization");
2501
- if (!authorization) {
2502
- return false;
2503
- }
2504
- const [scheme, ...rest] = authorization.split(" ");
2505
- if (scheme?.toLowerCase() !== "bearer") {
2506
- return false;
2507
- }
2508
- return constantTimeEqual(expected, rest.join(" ").trim());
2509
- };
2510
- const checkAdminWsToken = async (request, expected, requireEphemeral) => {
2511
- if (!expected || expected.length === 0) {
2512
- return false;
2513
- }
2514
- const supplied = new URL(request.url).searchParams.get("token");
2515
- if (supplied === null) {
2516
- return false;
2517
- }
2518
- if (await verifyWsAdminToken(expected, supplied)) {
2519
- return true;
2520
- }
2521
- if (requireEphemeral) {
2522
- return false;
2523
- }
2524
- return constantTimeEqual(expected, supplied);
2525
- };
2526
- const detectBindingProbe = (key, value) => {
2527
- if (value === null || typeof value !== "object" && typeof value !== "function") {
2528
- return void 0;
2529
- }
2530
- const shape = value;
2531
- if (typeof shape["prepare"] === "function" && typeof shape["batch"] === "function" && typeof shape["dump"] === "function") {
2532
- return d1Probe(`d1:${key}`, value);
2533
- }
2534
- if (typeof shape["list"] === "function" && typeof shape["head"] === "function" && typeof shape["createMultipartUpload"] === "function") {
2535
- return presenceProbe(`r2:${key}`, true);
2536
- }
2537
- if (typeof shape["send"] === "function" && typeof shape["sendBatch"] === "function" && typeof shape["get"] !== "function") {
2538
- return presenceProbe(`queue:${key}`, true);
2539
- }
2540
- if (typeof shape["connectionString"] === "string") {
2541
- return presenceProbe(`hyperdrive:${key}`, true);
2542
- }
2543
- return void 0;
2544
- };
2545
- const createWorker = (options) => {
2546
- const isTrustedUpstream = resolveTraceTrust(options.trustInboundTraceContext);
2547
- const noticeDroppedTrace = createDroppedTraceNotice(options.trustInboundTraceContext);
2548
- const defaultShard = options.defaultShardKey ?? "__root__";
2549
- const publicResolveIdentity = wrapResolverWithContract(options.resolveIdentity, options.identity);
2550
- const shardDO = applyJurisdiction(options.shardDO, options.jurisdiction);
2551
- const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
2552
- let envAdminToken;
2553
- const effectiveAdminToken = () => options.adminToken ?? envAdminToken;
2554
- let envRequireEphemeralWsToken;
2555
- const effectiveRequireEphemeralWsToken = () => options.requireEphemeralWsToken ?? envRequireEphemeralWsToken ?? false;
2556
- const resolveAdminTokenFromEnv = (env) => {
2557
- const record = env ?? {};
2558
- if (envRequireEphemeralWsToken === void 0 && options.requireEphemeralWsToken === void 0) {
2559
- const raw = record["LUNORA_REQUIRE_EPHEMERAL_WS_TOKEN"];
2560
- if (typeof raw === "string" && raw.length > 0) {
2561
- envRequireEphemeralWsToken = REQUIRE_EPHEMERAL_ENV_VALUES.has(raw.trim().toLowerCase());
2562
- }
2563
- }
2564
- if (envAdminToken !== void 0 || options.adminToken !== void 0) {
2565
- return;
2566
- }
2567
- const value = record["LUNORA_ADMIN_TOKEN"];
2568
- if (typeof value === "string" && value.length > 0) {
2569
- envAdminToken = value;
2570
- }
2571
- };
2572
- const accessAdminGrants = /* @__PURE__ */ new WeakSet();
2573
- const requestIsAdmin = (request) => checkAdminAuth(request, effectiveAdminToken()) || accessAdminGrants.has(request);
2574
- const resolveAdminForwardContext = async (request, env) => {
2575
- const context = await resolveForwardContext(request, env, options.resolveIdentity);
2576
- if (accessAdminGrants.has(request) && context.headers["authorization"] === void 0) {
2577
- const token = effectiveAdminToken();
2578
- if (token !== void 0) {
2579
- context.headers["authorization"] = `Bearer ${token}`;
2580
- }
2581
- }
2582
- return context;
2583
- };
2584
- let warnedUnauthenticatedShardAccess = false;
2585
- const guardUnauthenticatedShardAccess = (kind) => {
2586
- if (!options.allowUnauthenticatedShardAccess) {
2587
- const callback = kind === "fan-out" ? "authorizeFanOut" : "authorizeShard";
2588
- throw new LunoraError(
2589
- `${kind} access is default-denied: configure \`${callback}\` on the worker, or set \`allowUnauthenticatedShardAccess: true\` to explicitly allow unauthenticated ${kind} access (relying solely on per-row RLS).`,
2590
- { code: kind === "fan-out" ? "FORBIDDEN_FANOUT" : "FORBIDDEN_SHARD", status: 403 }
2591
- );
2592
- }
2593
- if (warnedUnauthenticatedShardAccess) {
2594
- return;
2595
- }
2596
- warnedUnauthenticatedShardAccess = true;
2597
- console.warn(
2598
- [
2599
- `[lunora] SECURITY: serving ${kind} access with \`allowUnauthenticatedShardAccess: true\` and no \`authorizeShard\`/\`authorizeFanOut\` — `,
2600
- `any caller (including unauthenticated ones) can target any shard / fan out across the table. `,
2601
- `This is safe only if every table is protected by per-row RLS. Configure \`authorizeShard\`/\`authorizeFanOut\` to gate it.`
2602
- ].join("")
2603
- );
2604
- };
2605
- const orchestrationAdminRoutes = buildOrchestrationAdminRoutes({
2606
- defaultShard,
2607
- forwardToShard,
2608
- isAdmin: requestIsAdmin,
2609
- queryCoordinator: options.queryCoordinator,
2610
- resolveForwardContext: resolveAdminForwardContext,
2611
- shardDO
2612
- });
2613
- const dispatchToShard = async (functionPath, args, shardKey, mutationId, forwardedIdentity) => {
2614
- if (options.authorizeShard) {
2615
- const allowed = await options.authorizeShard(null, shardKey);
2616
- if (!allowed) {
2617
- throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
2618
- }
2619
- }
2620
- const headers = { "content-type": "application/json", "x-lunora-system": "1" };
2621
- if (forwardedIdentity?.userId !== void 0 && forwardedIdentity.userId.length > 0) {
2622
- headers["x-lunora-userid"] = forwardedIdentity.userId;
2623
- }
2624
- if (forwardedIdentity?.identity !== void 0 && forwardedIdentity.identity.length > 0) {
2625
- headers["x-lunora-identity"] = forwardedIdentity.identity;
2626
- }
2627
- if (mutationId !== void 0 && mutationId.length > 0) {
2628
- headers["x-lunora-mutation-id"] = mutationId;
2629
- }
2630
- const forwarded = new Request("https://shard.internal/rpc", {
2631
- body: JSON.stringify({ args, functionPath }),
2632
- headers,
2633
- method: "POST"
2634
- });
2635
- return forwardToShard(shardDO, shardKey, forwarded);
2636
- };
2637
- const startWorkflowInstance = async (binding, args, env, label) => {
2638
- const candidate = env?.[binding];
2639
- if (!candidate || typeof candidate.create !== "function") {
2640
- throw new LunoraError(`${label} targets workflow binding "${binding}", which is not bound on env`, {
2641
- code: "CRON_JOB_FAILED",
2642
- status: 500
2643
- });
2644
- }
2645
- await candidate.create({ params: args });
2646
- };
2647
- const startCronWorkflow = async (binding, job, env) => startWorkflowInstance(binding, job.args ?? {}, env, `cron job "${job.name}"`);
2648
- const runOneCronJob = async (job, env) => {
2649
- if (job.workflow) {
2650
- await startCronWorkflow(job.workflow, job, env);
2651
- return;
2652
- }
2653
- if (job.functionPath === void 0) {
2654
- throw new LunoraError(`cron job "${job.name}" has neither a function target nor a workflow target`, {
2655
- code: "CRON_JOB_FAILED",
2656
- status: 500
2657
- });
2658
- }
2659
- const response = await dispatchToShard(job.functionPath, job.args ?? {}, job.shardKey ?? defaultShard);
2660
- if (!response.ok) {
2661
- throw new LunoraError(`cron job "${job.name}" (${job.functionPath}) failed with shard status ${String(response.status)}`, {
2662
- code: "CRON_JOB_FAILED",
2663
- status: 500
2664
- });
2665
- }
2666
- };
2667
- const runCronJobs = async (cron, env, errors, toError) => {
2668
- const cronJobs = options.cronJobs?.[cron];
2669
- if (!cronJobs) {
2670
- return;
2671
- }
2672
- for (const job of cronJobs) {
2673
- try {
2674
- await runOneCronJob(job, env);
2675
- } catch (error) {
2676
- errors.push(toError(error));
2677
- }
2678
- }
2679
- };
2680
- const handleRunCronJob = async (request, env) => {
2681
- if (!requestIsAdmin(request)) {
2682
- throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
2683
- }
2684
- if (request.method !== "POST") {
2685
- throw new LunoraError("cron-jobs run endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2686
- }
2687
- if (!options.cronJobs) {
2688
- throw new LunoraError("cron-jobs run endpoint requires a `cronJobs` map on the worker", { code: "CRON_JOBS_NOT_CONFIGURED", status: 400 });
2689
- }
2690
- const body = await readJsonBodyWithLimit(request);
2691
- const name = typeof body.name === "string" ? body.name : "";
2692
- if (name === "") {
2693
- throw new LunoraError("cron-jobs run endpoint requires a job `name`", { code: "BAD_REQUEST", status: 400 });
2694
- }
2695
- const job = Object.values(options.cronJobs).flat().find((candidate) => candidate.name === name);
2696
- if (!job) {
2697
- throw new LunoraError(`no cron job named "${name}" is registered`, { code: "CRON_JOB_NOT_FOUND", status: 404 });
2698
- }
2699
- await runOneCronJob(job, env);
2700
- return Response.json({ name, ran: true }, { status: 200 });
2701
- };
2702
- const releasePoolSlot = async (candidate) => {
2703
- const pool = typeof candidate.pool === "string" && candidate.pool.length > 0 ? candidate.pool : void 0;
2704
- if (!pool || !schedulerDO || typeof candidate.id !== "string") {
2705
- return;
2706
- }
2707
- const instanceName = typeof candidate.instanceName === "string" && candidate.instanceName.length > 0 ? candidate.instanceName : "default";
2708
- try {
2709
- await schedulerDO.get(schedulerDO.idFromName(instanceName)).fetch(
2710
- new Request("https://scheduler.internal/complete", {
2711
- body: JSON.stringify({ id: candidate.id, pool }),
2712
- headers: { "content-type": "application/json" },
2713
- method: "POST"
2714
- })
2715
- );
2716
- } catch {
2717
- }
2718
- };
2719
- const handleSchedulerDispatch = async (request, env) => {
2720
- if (request.method !== "POST") {
2721
- throw new LunoraError("Scheduler dispatch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2722
- }
2723
- const rawBody = await readBodyTextWithLimit(request);
2724
- const envRecord = env ?? {};
2725
- const schedulerSecret = typeof envRecord["LUNORA_SCHEDULER_SECRET"] === "string" ? envRecord["LUNORA_SCHEDULER_SECRET"] : void 0;
2726
- const adminBearer = options.adminToken ?? (typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0);
2727
- const signatureHeader = request.headers.get("x-lunora-scheduler-signature");
2728
- let authenticated = false;
2729
- if (signatureHeader && schedulerSecret) {
2730
- authenticated = await verifyHmacSignature(schedulerSecret, rawBody, signatureHeader);
2731
- } else if (adminBearer) {
2732
- authenticated = checkAdminAuth(request, adminBearer);
2733
- }
2734
- if (!authenticated) {
2735
- throw new LunoraError("Scheduler dispatch requires a valid signature or admin bearer", { code: "FORBIDDEN", status: 403 });
2736
- }
2737
- let body;
2738
- try {
2739
- body = JSON.parse(rawBody);
2740
- } catch {
2741
- throw new LunoraError("Scheduler dispatch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
2742
- }
2743
- const candidate = body ?? {};
2744
- const args = candidate.args ?? {};
2745
- if (typeof candidate.workflow === "string" && candidate.workflow.length > 0) {
2746
- await startWorkflowInstance(candidate.workflow, args, env, "scheduled workflow");
2747
- return Response.json({ ok: true }, { status: 200 });
2748
- }
2749
- if (typeof candidate.functionPath !== "string" || candidate.functionPath.length === 0) {
2750
- throw new LunoraError("Scheduler dispatch is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
2751
- }
2752
- const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
2753
- const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
2754
- const identity = readForwardedIdentity(request);
2755
- const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId, identity);
2756
- await releasePoolSlot(candidate);
2757
- return response;
2758
- };
2759
- const assertAdminAuthorized = (request) => {
2760
- if (!requestIsAdmin(request)) {
2761
- throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
2762
- }
2763
- };
2764
- const requireAdminOption = (request, value, notConfigured) => {
2765
- assertAdminAuthorized(request);
2766
- if (value === void 0) {
2767
- throw new LunoraError(notConfigured.message, { code: notConfigured.code, status: 400 });
2768
- }
2769
- return value;
2770
- };
2771
- const getAuthAuditLog = buildGetAuthAuditLog({
2772
- assertAdmin: assertAdminAuthorized,
2773
- getReader: () => options.authAuditReader
2774
- });
2775
- const listPushSubscriptions = async (request, args) => {
2776
- assertAdminAuthorized(request);
2777
- const store = options.notifySubscriptionStore;
2778
- if (store === void 0) {
2779
- return Response.json({ subscriptions: [] }, { headers: { "content-type": "application/json" }, status: 200 });
2780
- }
2781
- const rawKind = args?.["kind"];
2782
- const rawUserId = args?.["userId"];
2783
- const kindFilter = typeof rawKind === "string" && rawKind !== "" ? rawKind : void 0;
2784
- const userIdFilter = typeof rawUserId === "string" && rawUserId !== "" ? rawUserId : void 0;
2785
- const stored = await store.list();
2786
- const subscriptions = stored.filter((device) => (kindFilter === void 0 || device.kind === kindFilter) && (userIdFilter === void 0 || device.userId === userIdFilter)).map(({ keys: _keys, token: _token, ...device }) => device);
2787
- return Response.json({ subscriptions }, { headers: { "content-type": "application/json" }, status: 200 });
2788
- };
2789
- const serveReservedWorkerRpc = async (request, envelope) => {
2790
- if (envelope.fanOut) {
2791
- return void 0;
2792
- }
2793
- if (envelope.functionPath === GET_AUTH_AUDIT_LOG_OP) {
2794
- return getAuthAuditLog(request, envelope.args ?? {});
2795
- }
2796
- if (envelope.functionPath === LIST_PUSH_SUBSCRIPTIONS_OP) {
2797
- return listPushSubscriptions(request, envelope.args);
2798
- }
2799
- return void 0;
2800
- };
2801
- const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
2802
- applyGlobals: options.applyGlobals,
2803
- exportCursorStore: options.exportCursorStore,
2804
- exportSinks: options.exportSinks,
2805
- knownTables: () => collectKnownTables(),
2806
- queryCoordinator: options.queryCoordinator,
2807
- requireAdminOption,
2808
- resolveForwardContext: resolveAdminForwardContext,
2809
- shardDO,
2810
- streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow, shardDO),
2811
- streamingImport: (request, headers) => streamingImport(request, options, headers, shardDO),
2812
- syncGlobals: options.syncGlobals
2813
- });
2814
- const queryParameter = (url, name) => {
2815
- const value = url.searchParams.get(name);
2816
- return value === null || value === "" ? void 0 : value;
2817
- };
2818
- const parsePaging = (request) => {
2819
- const url = new URL(request.url);
2820
- const limitParameter = url.searchParams.get("limit");
2821
- const offsetParameter = url.searchParams.get("offset");
2822
- const limit = limitParameter === null ? void 0 : Number.parseInt(limitParameter, 10);
2823
- const offset = offsetParameter === null ? void 0 : Number.parseInt(offsetParameter, 10);
2824
- return {
2825
- limit: limit !== void 0 && Number.isFinite(limit) && limit >= 0 ? limit : void 0,
2826
- offset: offset !== void 0 && Number.isFinite(offset) && offset >= 0 ? offset : void 0
2827
- };
2828
- };
2829
- const requireSchedulerNamespace = () => {
2830
- if (schedulerDO === void 0) {
2831
- throw new LunoraError("scheduled endpoints require a `schedulerDO` namespace on the worker", { code: "SCHEDULER_NOT_CONFIGURED", status: 400 });
2832
- }
2833
- return schedulerDO;
2834
- };
2835
- const resolveSchedulerStub = (request) => {
2836
- assertAdminAuthorized(request);
2837
- return resolveShard(requireSchedulerNamespace(), options.schedulerInstanceName ?? "default");
2838
- };
2839
- const scheduledAdminRoutes = buildScheduledAdminRoutes({
2840
- checkWsAdmin: async (request) => requestIsAdmin(request) || checkAdminWsToken(request, effectiveAdminToken(), effectiveRequireEphemeralWsToken()),
2841
- requireSchedulerNamespace,
2842
- resolveSchedulerStub,
2843
- schedulerInstanceName: options.schedulerInstanceName ?? "default"
2844
- });
2845
- const workflowsAdminRoutes = buildWorkflowsAdminRoutes({
2846
- assertAdmin: assertAdminAuthorized,
2847
- resolveWorkflowsClient: options.workflowsClient ?? (() => void 0)
2848
- });
2849
- const storageAdminRoutes = buildStorageAdminRoutes({
2850
- assertAdmin: assertAdminAuthorized,
2851
- parsePaging,
2852
- queryParameter,
2853
- readBodyBytes: readBodyBytesWithLimit,
2854
- requireAdminOption,
2855
- storage: {
2856
- storageBuckets: options.storageBuckets,
2857
- storageDelete: options.storageDelete,
2858
- storageList: options.storageList,
2859
- storageSignedUrl: options.storageSignedUrl,
2860
- storageUpload: options.storageUpload
2861
- }
2862
- });
2863
- const vectorAdminRoutes = buildVectorAdminRoutes({
2864
- readJsonBody: readJsonBodyWithLimit,
2865
- requireAdminOption,
2866
- vectorIntrospector: options.vectorIntrospector
2867
- });
2868
- const kvAdminRoutes = buildKvAdminRoutes({
2869
- kvIntrospector: options.kvIntrospector,
2870
- readJsonBody: readJsonBodyWithLimit,
2871
- requireAdminOption
2872
- });
2873
- const logArchiveAdminRoutes = buildLogArchiveAdminRoutes({
2874
- logArchive: options.logArchive,
2875
- readJsonBody: readJsonBodyWithLimit,
2876
- requireAdminOption
2877
- });
2878
- const introspectionAdminRoutes = buildIntrospectionAdminRoutes({
2879
- assertAdmin: assertAdminAuthorized,
2880
- options: {
2881
- cronJobs: options.cronJobs,
2882
- functions: options.functions,
2883
- globalIntrospector: options.globalIntrospector,
2884
- openApiSpec: options.openApiSpec,
2885
- openRpcSpec: options.openRpcSpec
2886
- },
2887
- parsePaging,
2888
- queryParameter,
2889
- requireAdminOption
2890
- });
2891
- const resolveHealthProbes = (env) => {
2892
- const probes = [];
2893
- const namespace = shardDO ?? env?.SHARD;
2894
- if (namespace !== void 0) {
2895
- probes.push(durableObjectProbe("durable-object", namespace, defaultShard));
2896
- }
2897
- if (options.health?.disableBindingProbes !== true) {
2898
- for (const [key, value] of Object.entries(env ?? {})) {
2899
- const probe = detectBindingProbe(key, value);
2900
- if (probe !== void 0) {
2901
- probes.push(probe);
2902
- }
2903
- }
2904
- }
2905
- for (const probe of options.health?.probes ?? []) {
2906
- probes.push(probe);
2907
- }
2908
- return probes;
2909
- };
2910
- const healthRoutes = buildHealthRoutes({
2911
- appName: options.health?.appName,
2912
- appVersion: options.health?.appVersion,
2913
- auth: options.health?.auth ?? "public",
2914
- isAdmin: requestIsAdmin,
2915
- resolveProbes: resolveHealthProbes
2916
- });
2917
- const buildHttpActionContext = async (request, env, context) => {
2918
- const { claims, headers, userId } = await resolveForwardContext(request, env, publicResolveIdentity);
2919
- const run = async (reference, args = {}) => {
2920
- const functionPath = reference.__lunoraRef;
2921
- if (typeof functionPath !== "string") {
2922
- throw new LunoraError("ctx.run*: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
2923
- }
2924
- const forwarded = new Request("https://shard.internal/rpc", {
2925
- body: JSON.stringify({ args, functionPath }),
2926
- headers,
2927
- method: "POST"
2928
- });
2929
- const response = await forwardToShard(shardDO, defaultShard, forwarded);
2930
- const payload = await response.json();
2931
- if (payload.error) {
2932
- throw new LunoraError(payload.error.message ?? "shard RPC failed", {
2933
- code: payload.error.code ?? "INTERNAL",
2934
- status: response.status
2935
- });
2936
- }
2937
- return payload.result;
2938
- };
2939
- return {
2940
- auth: {
2941
- getIdentity: () => Promise.resolve(claims),
2942
- userId
2943
- },
2944
- cache: context.cache,
2945
- fetch: globalThis.fetch.bind(globalThis),
2946
- runAction: run,
2947
- runMutation: run,
2948
- runQuery: run
2949
- };
2950
- };
2951
- const dispatchHttpRoute = async (request, env, context) => {
2952
- if (!options.httpRouter) {
2953
- return void 0;
2954
- }
2955
- const httpContext = await buildHttpActionContext(request, env, context);
2956
- try {
2957
- return await options.httpRouter.fetch(request, { ...env, __lunoraCtx: httpContext }, context);
2958
- } catch (error) {
2959
- console.error("[lunora] httpRouter (SSR) handler threw:", error);
2960
- return new Response("Internal Server Error", { status: 500 });
2961
- }
2962
- };
2963
- const handleWebSocketUpgrade = async (request, env, url) => {
2964
- if (request.headers.get("Upgrade") !== "websocket") {
2965
- throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
2966
- }
2967
- const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
2968
- if (blockedUpgrade) {
2969
- return blockedUpgrade;
2970
- }
2971
- const shardKey = url.searchParams.get("shard") ?? defaultShard;
2972
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2973
- if (options.authorizeShard) {
2974
- const allowed = await options.authorizeShard(identity, shardKey);
2975
- if (!allowed) {
2976
- throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
2977
- }
2978
- } else if (shardKey !== defaultShard) {
2979
- guardUnauthenticatedShardAccess("shard");
2980
- }
2981
- const upgradeHeaders = new Headers(request.headers);
2982
- const clientHeaderNames = [...upgradeHeaders.keys()];
2983
- for (const name of clientHeaderNames) {
2984
- if (name.startsWith("x-lunora-")) {
2985
- upgradeHeaders.delete(name);
2986
- }
2987
- }
2988
- const forwardedUserId = forwardedHeaders["x-lunora-userid"];
2989
- const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
2990
- const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
2991
- if (forwardedUserId !== void 0) {
2992
- upgradeHeaders.set("x-lunora-userid", forwardedUserId);
2993
- }
2994
- if (forwardedIdentity !== void 0) {
2995
- upgradeHeaders.set("x-lunora-identity", forwardedIdentity);
2996
- }
2997
- if (forwardedExp !== void 0) {
2998
- upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
2999
- }
3000
- const binding = resolveShardBindingName(env, options.shardDO);
3001
- if (binding !== void 0) {
3002
- upgradeHeaders.set("x-lunora-shard-binding", binding);
3003
- const relayCount = await probeRelayCount(shardDO, shardKey);
3004
- if (relayCount > 0) {
3005
- const target = relayName(shardKey, Math.floor(Math.random() * relayCount));
3006
- return forwardToShard(shardDO, target, new Request(request, { headers: upgradeHeaders }));
3007
- }
3008
- }
3009
- return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
3010
- };
3011
- const handleVoiceUpgrade = async (request, env, url) => {
3012
- const { voiceAgents } = options;
3013
- if (voiceAgents === void 0) {
3014
- return new Response("Not found", { status: 404 });
3015
- }
3016
- if (request.headers.get("Upgrade") !== "websocket") {
3017
- return new Response("Expected a WebSocket upgrade", { headers: { allow: "GET" }, status: 426 });
3018
- }
3019
- const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
3020
- if (blockedUpgrade) {
3021
- return blockedUpgrade;
3022
- }
3023
- let agentName;
3024
- try {
3025
- agentName = decodeURIComponent(url.pathname.slice(VOICE_PATH_PREFIX.length));
3026
- } catch {
3027
- return new Response("Unknown voice agent", { status: 404 });
3028
- }
3029
- const namespace = Object.hasOwn(voiceAgents, agentName) ? voiceAgents[agentName] : void 0;
3030
- if (namespace === void 0) {
3031
- return new Response("Unknown voice agent", { status: 404 });
3032
- }
3033
- const threadKey = url.searchParams.get("threadKey");
3034
- if (threadKey === null || threadKey.length === 0) {
3035
- return new Response("Missing threadKey", { status: 400 });
3036
- }
3037
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
3038
- if (options.authorizeShard) {
3039
- const allowed = await options.authorizeShard(identity, threadKey);
3040
- if (!allowed) {
3041
- return new Response("Forbidden", { status: 403 });
3042
- }
3043
- } else {
3044
- guardUnauthenticatedShardAccess("shard");
3045
- }
3046
- const upgradeHeaders = new Headers(request.headers);
3047
- upgradeHeaders.delete("x-lunora-userid");
3048
- upgradeHeaders.delete("x-lunora-identity");
3049
- upgradeHeaders.delete("x-lunora-identity-exp");
3050
- const forwardedUserId = forwardedHeaders["x-lunora-userid"];
3051
- const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
3052
- const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
3053
- if (forwardedUserId !== void 0) {
3054
- upgradeHeaders.set("x-lunora-userid", forwardedUserId);
3055
- }
3056
- if (forwardedIdentity !== void 0) {
3057
- upgradeHeaders.set("x-lunora-identity", forwardedIdentity);
3058
- }
3059
- if (forwardedExp !== void 0) {
3060
- upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
3061
- }
3062
- return forwardToShard(namespace, threadKey, new Request(request, { headers: upgradeHeaders }));
3063
- };
3064
- const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
3065
- if (options.authorizeFanOut) {
3066
- const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
3067
- if (!allowed) {
3068
- throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
3069
- }
3070
- return;
3071
- }
3072
- if (functionPath.startsWith("__lunora_relation__:")) {
3073
- throw new LunoraError("reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker", {
3074
- code: "FORBIDDEN_FANOUT",
3075
- status: 403
3076
- });
3077
- }
3078
- if (options.authorizeShard) {
3079
- throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
3080
- code: "FORBIDDEN_FANOUT",
3081
- status: 403
3082
- });
3083
- }
3084
- guardUnauthenticatedShardAccess("fan-out");
3085
- };
3086
- const authorizeRpcEnvelope = async (envelope, identity) => {
3087
- if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_admin__:")) {
3088
- return;
3089
- }
3090
- if (envelope.fanOut) {
3091
- await authorizeFanOutEnvelope(envelope.fanOut, envelope.functionPath, identity);
3092
- return;
3093
- }
3094
- if (options.authorizeShard) {
3095
- const shardKeyForAuth = envelope.shardKey ?? defaultShard;
3096
- const allowed = await options.authorizeShard(identity, shardKeyForAuth);
3097
- if (!allowed) {
3098
- throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
3099
- }
3100
- } else if (envelope.shardKey !== void 0 && envelope.shardKey !== defaultShard) {
3101
- guardUnauthenticatedShardAccess("shard");
3102
- }
3103
- };
3104
- const dispatchSingleShard = async (request, functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
3105
- const rpcStartedAt = Date.now();
3106
- const { observability, sampling } = options;
3107
- const requestMeta = requestTelemetryMeta(request);
3108
- const { decision, ignoredUpstream, trace } = beginDispatchTrace(request, {
3109
- ...sampling === void 0 ? {} : { sampling },
3110
- trustInbound: isTrustedUpstream(request)
3111
- });
3112
- if (ignoredUpstream) {
3113
- noticeDroppedTrace();
3114
- }
3115
- const outgoingHeaders = {
3116
- ...forwardedHeaders,
3117
- "x-lunora-sample-errors": decision.keepErrors ? "1" : "0"
3118
- };
3119
- injectTraceContext(trace, outgoingHeaders);
3120
- const forwarded = new Request(`https://shard.internal/rpc`, {
3121
- body: JSON.stringify({ args, functionPath }),
3122
- headers: outgoingHeaders,
3123
- method: "POST"
3124
- });
3125
- try {
3126
- const response = await forwardToShard(shardDO, shardKey, forwarded);
3127
- emitRpcEvent(
3128
- observability,
3129
- {
3130
- ...requestMeta,
3131
- ...traceEventFields(trace),
3132
- durationMs: Date.now() - rpcStartedAt,
3133
- functionPath,
3134
- ok: response.ok,
3135
- shardKey,
3136
- ...response.ok ? {} : { error: { code: "SHARD_ERROR", message: `shard returned ${String(response.status)}`, status: response.status } }
3137
- },
3138
- sinkContext,
3139
- sampling
3140
- );
3141
- return response;
3142
- } catch (error) {
3143
- emitRpcEvent(
3144
- observability,
3145
- {
3146
- ...requestMeta,
3147
- ...traceEventFields(trace),
3148
- ...buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey })
3149
- },
3150
- sinkContext,
3151
- sampling
3152
- );
3153
- throw error;
3154
- }
3155
- };
3156
- const assertDispatchableEnvelope = (envelope) => {
3157
- if (envelope.fanOut && envelope.shardKey) {
3158
- throw new LunoraError("RPC envelope cannot set both `shardKey` and `fanOut`", { code: "BAD_REQUEST", status: 400 });
3159
- }
3160
- if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
3161
- throw new LunoraError("`__lunora_relation__:*` is a fan-out-only reserved RPC and cannot be dispatched to a single shard", {
3162
- code: "FORBIDDEN",
3163
- status: 403
3164
- });
3165
- }
3166
- if (envelope.fanOut && !options.queryCoordinator) {
3167
- throw new LunoraError("RPC envelope set `fanOut` but no `queryCoordinator` is configured on the worker", {
3168
- code: "BAD_REQUEST",
3169
- status: 400
3170
- });
3171
- }
3172
- };
3173
- const handleRpc = async (request, env, context) => {
3174
- if (request.method !== "POST") {
3175
- throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
3176
- }
3177
- const envelope = await parseEnvelope(request);
3178
- logRpcDebug(env, envelope);
3179
- assertDispatchableEnvelope(envelope);
3180
- const reservedResponse = await serveReservedWorkerRpc(request, envelope);
3181
- if (reservedResponse !== void 0) {
3182
- return reservedResponse;
3183
- }
3184
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
3185
- await authorizeRpcEnvelope(envelope, identity);
3186
- const x402Tag = resolveX402Charge(envelope, options);
3187
- {
3188
- const rpcStartedAt = Date.now();
3189
- const { observability } = options;
3190
- const requestMeta = requestTelemetryMeta(request);
3191
- const sinkContext = buildSinkContext(env, request, context && ((promise) => context.waitUntil?.(promise)));
3192
- if (envelope.fanOut) {
3193
- const coordinator = options.queryCoordinator;
3194
- if (!coordinator) {
3195
- throw new LunoraError("RPC envelope set `fanOut` but no `queryCoordinator` is configured on the worker", {
3196
- code: "BAD_REQUEST",
3197
- status: 400
3198
- });
3199
- }
3200
- try {
3201
- const result = await coordinator.fanOut(shardDO, {
3202
- args: envelope.args ?? {},
3203
- fanOut: envelope.fanOut,
3204
- functionPath: envelope.functionPath,
3205
- headers: forwardedHeaders
3206
- });
3207
- emitRpcEvent(
3208
- observability,
3209
- {
3210
- durationMs: Date.now() - rpcStartedAt,
3211
- fanOut: {
3212
- failed: result.failed,
3213
- shards: result.ok + result.failed,
3214
- table: envelope.fanOut.table
3215
- },
3216
- functionPath: envelope.functionPath,
3217
- ...requestMeta,
3218
- ok: true
3219
- },
3220
- sinkContext
3221
- );
3222
- return Response.json(result, {
3223
- headers: { "content-type": "application/json" },
3224
- status: 200
3225
- });
3226
- } catch (error) {
3227
- emitRpcEvent(
3228
- observability,
3229
- {
3230
- ...buildErrorEvent(envelope.functionPath, Date.now() - rpcStartedAt, error, { fanOut: { table: envelope.fanOut.table } }),
3231
- ...requestMeta
3232
- },
3233
- sinkContext
3234
- );
3235
- throw error;
3236
- }
3237
- }
3238
- const shardKey = envelope.shardKey ?? defaultShard;
3239
- const dispatch = () => dispatchSingleShard(request, envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
3240
- if (x402Tag && options.x402Charge) {
3241
- return options.x402Charge(request, { functionPath: envelope.functionPath, price: x402Tag.price }, dispatch);
3242
- }
3243
- return dispatch();
3244
- }
3245
- };
3246
- const handleBatchRpc = async (request, env, context) => {
3247
- if (request.method !== "POST") {
3248
- throw new LunoraError("RPC batch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
3249
- }
3250
- const text = await readBodyTextWithLimit(request);
3251
- let body;
3252
- try {
3253
- body = JSON.parse(text);
3254
- } catch {
3255
- throw new LunoraError("RPC batch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
3256
- }
3257
- if (typeof body !== "object" || body === null || Array.isArray(body)) {
3258
- throw new LunoraError("RPC batch body must be an object", { code: "BAD_REQUEST", status: 400 });
3259
- }
3260
- const { calls } = body;
3261
- if (!Array.isArray(calls)) {
3262
- throw new LunoraError("RPC batch `calls` must be an array", { code: "BAD_REQUEST", status: 400 });
3263
- }
3264
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
3265
- const groups = groupBatchCallsByShard(calls, defaultShard);
3266
- for (const entries of groups.values()) {
3267
- for (const entry of entries) {
3268
- if (options.functions?.[entry.functionPath]?.x402) {
3269
- throw new LunoraError(
3270
- `paid (\`.x402\`) function "${entry.functionPath}" cannot be called in a batch; dispatch it individually over ${RPC_PATH}`,
3271
- {
3272
- code: "BAD_REQUEST",
3273
- status: 400
3274
- }
3275
- );
3276
- }
3277
- }
3278
- }
3279
- await Promise.all(
3280
- [...groups.entries()].flatMap(
3281
- ([shardKey, entries]) => entries.map((entry) => authorizeRpcEnvelope({ functionPath: entry.functionPath, shardKey }, identity))
3282
- )
3283
- );
3284
- const { observability } = options;
3285
- const sinkContext = buildSinkContext(env, request, context && ((promise) => context.waitUntil?.(promise)));
3286
- const requestMeta = requestTelemetryMeta(request);
3287
- const results = [];
3288
- const bookmarks = [];
3289
- const slotError = (entry, status, code, message) => {
3290
- return { body: { error: { code, message } }, id: entry.id, status };
3291
- };
3292
- const failSubBatch = (entries, status, code, message, eventFor) => {
3293
- for (const entry of entries) {
3294
- emitRpcEvent(observability, eventFor(entry), sinkContext);
3295
- results.push(slotError(entry, status, code, message));
3296
- }
3297
- };
3298
- const emitEntryEvents = (entries, shardKey, durationMs, statusById, fallbackStatus) => {
3299
- for (const entry of entries) {
3300
- const status = statusById.get(entry.id) ?? fallbackStatus;
3301
- const ok = status < 400;
3302
- emitRpcEvent(
3303
- observability,
3304
- {
3305
- durationMs,
3306
- functionPath: entry.functionPath,
3307
- ...requestMeta,
3308
- ok,
3309
- shardKey,
3310
- ...ok ? {} : { error: { code: "SHARD_ERROR", message: `batched call returned ${String(status)}`, status } }
3311
- },
3312
- sinkContext
3313
- );
3314
- }
3315
- };
3316
- await Promise.all(
3317
- [...groups.entries()].map(async ([shardKey, entries]) => {
3318
- const headers = new Headers(forwardedHeaders);
3319
- headers.set("content-type", "application/json");
3320
- const subRequest = new Request("https://shard.internal/rpc-batch", { body: JSON.stringify({ calls: entries }), headers, method: "POST" });
3321
- const subStartedAt = Date.now();
3322
- let response;
3323
- try {
3324
- response = await forwardToShard(shardDO, shardKey, subRequest);
3325
- } catch (error) {
3326
- const durationMs2 = Date.now() - subStartedAt;
3327
- const { body: errorBody } = toErrorBody(error, { fallbackCode: "SHARD_UNAVAILABLE", redactedMessage: "shard unavailable" });
3328
- failSubBatch(entries, 502, errorBody.code, errorBody.message, (entry) => {
3329
- return {
3330
- ...buildErrorEvent(entry.functionPath, durationMs2, error, { shardKey }),
3331
- ...requestMeta
3332
- };
3333
- });
3334
- return;
3335
- }
3336
- const durationMs = Date.now() - subStartedAt;
3337
- const bookmark = response.headers.get("x-d1-bookmark");
3338
- if (bookmark) {
3339
- bookmarks.push(bookmark);
3340
- }
3341
- let parsed;
3342
- try {
3343
- parsed = await response.json();
3344
- } catch {
3345
- const message = `shard batch returned a non-JSON response (${String(response.status)})`;
3346
- failSubBatch(entries, response.status, "SHARD_ERROR", message, (entry) => {
3347
- return {
3348
- durationMs,
3349
- error: { code: "SHARD_ERROR", message, status: response.status },
3350
- functionPath: entry.functionPath,
3351
- ...requestMeta,
3352
- ok: false,
3353
- shardKey
3354
- };
3355
- });
3356
- return;
3357
- }
3358
- const entryResults = Array.isArray(parsed.results) ? parsed.results : [];
3359
- const statusById = new Map(entryResults.map((entry) => [entry.id, entry.status ?? response.status]));
3360
- const seenIds = new Set(entryResults.map((entry) => entry.id));
3361
- emitEntryEvents(entries, shardKey, durationMs, statusById, response.status);
3362
- results.push(...entryResults);
3363
- for (const entry of entries) {
3364
- if (!seenIds.has(entry.id)) {
3365
- results.push(slotError(entry, response.status, "SHARD_ERROR", `shard batch omitted result for call ${String(entry.id)}`));
3366
- }
3367
- }
3368
- })
3369
- );
3370
- const responseHeaders = { "content-type": "application/json" };
3371
- const [onlyBookmark] = bookmarks;
3372
- if (bookmarks.length === 1 && onlyBookmark !== void 0) {
3373
- responseHeaders["x-d1-bookmark"] = onlyBookmark;
3374
- }
3375
- return Response.json({ results }, { headers: responseHeaders, status: 200 });
3376
- };
3377
- const serverQuery = async (request, env, reference, args = {}, callOptions = {}) => {
3378
- try {
3379
- const functionPath = reference.__lunoraRef;
3380
- if (typeof functionPath !== "string") {
3381
- throw new LunoraError("serverQuery: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
3382
- }
3383
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
3384
- await authorizeRpcEnvelope({ functionPath, shardKey: callOptions.shardKey }, identity);
3385
- const shardKey = callOptions.shardKey ?? defaultShard;
3386
- const serverSinkContext = buildSinkContext(env, request, callOptions.waitUntil);
3387
- return await dispatchSingleShard(request, functionPath, args, shardKey, forwardedHeaders, serverSinkContext);
3388
- } catch (error) {
3389
- return toErrorResponse(error);
3390
- }
3391
- };
3392
- const MAX_PRUNE_PAGES = 1e3;
3393
- const pruneBackups = async (store, prefix) => {
3394
- const retain = options.backupRetain;
3395
- if (retain === void 0 || retain <= 0) {
3396
- return;
3397
- }
3398
- const manifestKeys = [];
3399
- let cursor;
3400
- for (let page = 0; page < MAX_PRUNE_PAGES; page += 1) {
3401
- const listing = await store.list({ cursor, prefix });
3402
- for (const object of listing.objects) {
3403
- if (object.key.endsWith(".manifest.json")) {
3404
- manifestKeys.push(object.key);
3405
- }
3406
- }
3407
- if (!listing.truncated || listing.cursor === void 0) {
3408
- break;
3409
- }
3410
- cursor = listing.cursor;
3411
- }
3412
- const stale = manifestKeys.toSorted((a, b) => b.localeCompare(a)).slice(retain);
3413
- await Promise.all(
3414
- stale.flatMap((manifestKey) => {
3415
- const ndjsonKey = manifestKey.slice(0, -".manifest.json".length);
3416
- return [store.delete(manifestKey), store.delete(ndjsonKey)];
3417
- })
3418
- );
3419
- };
3420
- const runScheduledBackup = async (controller) => {
3421
- const store = options.backupStore;
3422
- const coordinator = options.queryCoordinator;
3423
- if (!store) {
3424
- throw new LunoraError("scheduled backup requires a `backupStore` on the worker", { code: "BACKUP_NOT_CONFIGURED", status: 500 });
3425
- }
3426
- if (!coordinator) {
3427
- throw new LunoraError("scheduled backup requires a `queryCoordinator` on the worker", { code: "BACKUP_NOT_CONFIGURED", status: 500 });
3428
- }
3429
- const adminToken = effectiveAdminToken();
3430
- if (!adminToken || adminToken.length === 0) {
3431
- throw new LunoraError("scheduled backup requires an `adminToken` (or `env.LUNORA_ADMIN_TOKEN`) to authenticate the per-shard export gate", {
3432
- code: "BACKUP_NOT_CONFIGURED",
3433
- status: 500
3434
- });
3435
- }
3436
- const forwardedHeaders = { authorization: `Bearer ${adminToken}`, "content-type": "application/json" };
3437
- const tables = options.backupTables;
3438
- let rows = 0;
3439
- let bytes = 0;
3440
- const parts = [];
3441
- const writeRow = (row) => {
3442
- const line = `${JSON.stringify(row)}
3443
- `;
3444
- rows += 1;
3445
- bytes += NDJSON_ENCODER.encode(line).byteLength;
3446
- parts.push(line);
3447
- };
3448
- await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow, shardDO);
3449
- const prefix = options.backupPrefix ?? "backups/";
3450
- const timestamp = new Date(controller.scheduledTime).toISOString();
3451
- const fileKey = `${prefix}lunora-backup-${timestamp.replaceAll(/[.:]/gu, "-")}.ndjson`;
3452
- const manifestKey = `${fileKey}.manifest.json`;
3453
- await store.put(fileKey, new Blob(parts, { type: "application/x-ndjson" }), {
3454
- httpMetadata: { contentType: "application/x-ndjson" }
3455
- });
3456
- const manifest = {
3457
- bytes,
3458
- createdAt: timestamp,
3459
- cron: controller.cron,
3460
- file: fileKey,
3461
- id: timestamp,
3462
- rows,
3463
- scheduledTime: controller.scheduledTime,
3464
- ...tables ? { tables: tables.join(",") } : {}
3465
- };
3466
- await store.put(manifestKey, `${JSON.stringify(manifest, void 0, 2)}
3467
- `, { httpMetadata: { contentType: "application/json" } });
3468
- await pruneBackups(store, prefix);
3469
- };
3470
- const handleScheduled = async (controller, env, context) => {
3471
- resolveAdminTokenFromEnv(env);
3472
- const errors = [];
3473
- const toError = (error) => error instanceof Error ? error : new Error(String(error));
3474
- const userHandler = options.crons?.[controller.cron];
3475
- if (userHandler) {
3476
- try {
3477
- await userHandler(controller, env, context);
3478
- } catch (error) {
3479
- errors.push(toError(error));
3480
- }
3481
- }
3482
- await runCronJobs(controller.cron, env, errors, toError);
3483
- if (options.backupStore && options.backupCron !== void 0 && options.backupCron === controller.cron) {
3484
- try {
3485
- await runScheduledBackup(controller);
3486
- } catch (error) {
3487
- errors.push(toError(error));
3488
- }
3489
- }
3490
- const [first] = errors;
3491
- if (errors.length === 1 && first) {
3492
- throw first;
3493
- }
3494
- if (errors.length > 1) {
3495
- throw new AggregateError(errors, `scheduled("${controller.cron}") had ${String(errors.length)} failure(s)`);
3496
- }
3497
- };
3498
- const recordAuthAttempt = async (env, outcome) => {
3499
- try {
3500
- const envRecord = env ?? {};
3501
- const adminBearer = options.adminToken ?? (typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0);
3502
- if (!adminBearer || adminBearer.length === 0) {
3503
- return;
3504
- }
3505
- const recordRequest = new Request("https://shard.internal/rpc", {
3506
- body: JSON.stringify({ args: { outcome }, functionPath: RECORD_AUTH_EVENT_OP }),
3507
- headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
3508
- method: "POST"
3509
- });
3510
- await forwardToShard(shardDO, defaultShard, recordRequest);
3511
- } catch {
3512
- }
3513
- };
3514
- const dispatchAuth = async (request, env, url, context) => {
3515
- if (!options.authHandler) {
3516
- return void 0;
3517
- }
3518
- const authResponse = await options.authHandler(request);
3519
- if (!authResponse) {
3520
- return void 0;
3521
- }
3522
- const basePath = options.authBasePath ?? DEFAULT_AUTH_BASE_PATH;
3523
- if (isAuthAttemptPath(url.pathname, basePath)) {
3524
- context.waitUntil?.(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
3525
- }
3526
- return authResponse;
3527
- };
3528
- const invokeExposed = async ({ args, env, functionPath, request, shardKey, waitUntil }) => {
3529
- const envelope = { functionPath, ...shardKey === void 0 ? {} : { shardKey } };
3530
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
3531
- await authorizeRpcEnvelope(envelope, identity);
3532
- const resolvedShardKey = shardKey ?? defaultShard;
3533
- const restSinkContext = buildSinkContext(env, request, waitUntil);
3534
- const dispatch = () => dispatchSingleShard(request, functionPath, args, resolvedShardKey, forwardedHeaders, restSinkContext);
3535
- const x402Tag = resolveX402Charge(envelope, options);
3536
- if (x402Tag && options.x402Charge) {
3537
- return options.x402Charge(request, { functionPath, price: x402Tag.price }, dispatch);
3538
- }
3539
- return dispatch();
3540
- };
3541
- const restRoutes = buildRestRoutes({
3542
- functions: options.functions ?? {},
3543
- invoke: invokeExposed,
3544
- readJsonBody: readJsonBodyWithLimit,
3545
- ...options.restRateLimit ? { rateLimit: options.restRateLimit } : {}
3546
- });
3547
- const customRoutes = options.routes !== void 0 && Object.keys(options.routes).length > 0 ? options.routes : void 0;
3548
- const internalRoutes = {
3549
- [STATUS_PATH]: (request) => {
3550
- if (request.method !== "GET" && request.method !== "HEAD") {
3551
- return new Response(void 0, { headers: { allow: "GET, HEAD" }, status: 405 });
3552
- }
3553
- return Response.json({ ok: true }, { headers: { "cache-control": "no-store" } });
3554
- },
3555
- [WS_PATH]: (request, env, url) => handleWebSocketUpgrade(request, env, url),
3556
- [RPC_PATH]: (request, env, _url, context) => handleRpc(request, env, context),
3557
- [RPC_BATCH_PATH]: (request, env, _url, context) => handleBatchRpc(request, env, context),
3558
- [SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
3559
- [CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
3560
- // Mint a short-lived HMAC-signed WS admin sub-token. Gated by the master
3561
- // admin bearer (header) / `adminGate`; the studio then sends the minted
3562
- // token — not the master credential — in the WS `?token=`
3563
- // query string. Signed with the master token itself, so both isolates
3564
- // verify statelessly and rotating `LUNORA_ADMIN_TOKEN` invalidates every
3565
- // outstanding sub-token. `no-store` keeps the token out of caches.
3566
- [ADMIN_WS_TOKEN_PATH]: async (request) => {
3567
- if (request.method !== "POST") {
3568
- throw new LunoraError("ws-token endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
3569
- }
3570
- assertAdminAuthorized(request);
3571
- const signingSecret = effectiveAdminToken();
3572
- if (signingSecret === void 0) {
3573
- throw new LunoraError("ws-token minting requires a configured admin token", { code: "ADMIN_TOKEN_NOT_CONFIGURED", status: 400 });
3574
- }
3575
- const minted = await mintWsAdminToken(signingSecret);
3576
- return Response.json(minted, { headers: { "cache-control": "no-store" } });
3577
- },
3578
- // Extracted handler clusters built above, merged in (mirroring the auth
3579
- // plane below): orchestration (migrate / rank / rankpage / shard-traffic /
3580
- // pitr), data-movement (export / import / sync / connector-sync / apply),
3581
- // scheduled, storage, vector, and the static-introspection reads
3582
- // (functions / cron-jobs / openapi / openrpc / global tables).
3583
- ...orchestrationAdminRoutes,
3584
- ...dataMovementAdminRoutes,
3585
- ...scheduledAdminRoutes,
3586
- ...workflowsAdminRoutes,
3587
- ...storageAdminRoutes,
3588
- ...vectorAdminRoutes,
3589
- ...kvAdminRoutes,
3590
- ...logArchiveAdminRoutes,
3591
- ...introspectionAdminRoutes,
3592
- // `/_lunora/health` + `/_lunora/health/ready` — public (or admin-gated)
3593
- // liveness/readiness probes; not under the admin prefix, so `applyAdminGate`
3594
- // never runs on them (the handler self-gates when `auth: "admin"`).
3595
- ...healthRoutes,
3596
- // `/_lunora/rest/<namespace>/<fn>` — the opt-in public REST surface, one
3597
- // route per `.expose({ rest: true })` procedure (default-closed). Routes
3598
- // THROUGH the procedure, so auth/RLS/validators are enforced at the shard.
3599
- ...restRoutes,
3600
- // `/_lunora/admin/auth/*` — the whole user-management plane, one route per
3601
- // `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
3602
- ...buildAuthAdminRoutes({
3603
- assertAdmin: assertAdminAuthorized,
3604
- getAuthAdmin: () => options.authAdmin,
3605
- parsePaging,
3606
- queryParameter,
3607
- readJsonBody: readJsonBodyWithLimit
3608
- })
3609
- };
3610
- let resolvedSecurity = resolveSecurity(options.security);
3611
- let securityEnvResolved = false;
3612
- const ensureSecurityResolved = (env) => {
3613
- if (!securityEnvResolved) {
3614
- securityEnvResolved = true;
3615
- resolvedSecurity = resolveSecurity(options.security, env ?? {});
3616
- }
3617
- };
3618
- const applyAdminGate = async (request, pathname) => {
3619
- if (options.adminGate === void 0 || !isAdminPath(pathname)) {
3620
- return;
3621
- }
3622
- try {
3623
- if (await options.adminGate(request)) {
3624
- accessAdminGrants.add(request);
3625
- }
3626
- } catch {
3627
- }
3628
- };
3629
- const handle = async (request, env, context) => {
3630
- const url = new URL(request.url);
3631
- if (request.method === "POST" || request.method === "PUT") {
3632
- const contentLength = Number(request.headers.get("content-length") ?? "");
3633
- const maxBodyBytes = url.pathname === KV_VALUE_PATH ? KV_VALUE_MAX_BODY_BYTES : MAX_BODY_BYTES;
3634
- if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
3635
- throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
3636
- }
3637
- }
3638
- const authResponse = await dispatchAuth(request, env, url, context);
3639
- if (authResponse) {
3640
- return authResponse;
3641
- }
3642
- if (customRoutes) {
3643
- const methodAndPath = `${request.method} ${url.pathname}`;
3644
- const route = customRoutes[methodAndPath] ?? customRoutes[url.pathname];
3645
- if (route) {
3646
- return route(request, env, context);
3647
- }
3648
- }
3649
- const internalRoute = internalRoutes[url.pathname];
3650
- if (internalRoute) {
3651
- await applyAdminGate(request, url.pathname);
3652
- return internalRoute(request, env, url, context);
3653
- }
3654
- if (options.voiceAgents !== void 0 && url.pathname.startsWith(VOICE_PATH_PREFIX)) {
3655
- return handleVoiceUpgrade(request, env, url);
3656
- }
3657
- const httpRouteResponse = await dispatchHttpRoute(request, env, context);
3658
- if (httpRouteResponse) {
3659
- return httpRouteResponse;
3660
- }
3661
- return new Response("Not found", { status: 404 });
3662
- };
3663
- return {
3664
- async fetch(request, env, context) {
3665
- if (options.passThroughOnException) {
3666
- context.passThroughOnException?.();
3667
- }
3668
- ensureSecurityResolved(env);
3669
- resolveAdminTokenFromEnv(env);
3670
- const preflight = handleCorsPreflight(request, resolvedSecurity);
3671
- if (preflight) {
3672
- return preflight;
3673
- }
3674
- const blocked = enforceOrigin(request, resolvedSecurity);
3675
- if (blocked) {
3676
- return decorateResponse(blocked, request, resolvedSecurity);
3677
- }
3678
- try {
3679
- const response = await handle(request, env, context);
3680
- return decorateResponse(response, request, resolvedSecurity);
3681
- } catch (error) {
3682
- return decorateResponse(toErrorResponse(error), request, resolvedSecurity);
3683
- }
3684
- },
3685
- async queue(batch, env, context) {
3686
- await options.queue?.(batch, env, context);
3687
- },
3688
- async scheduled(controller, env, context) {
3689
- await handleScheduled(controller, env, context);
3690
- },
3691
- serverQuery
3692
- };
3693
- };
3694
- const composeWorker = (options) => createWorker(options);
3695
- const toHttpRouter = (handler) => typeof handler === "function" ? { fetch: handler } : handler;
3696
- const hasLunoraCrons = (options) => Boolean(options.crons ?? options.cronJobs ?? options.backupCron);
3697
- const withFrameworkWorker = (host, optionsInput) => {
3698
- const httpRouter = toHttpRouter(host);
3699
- const hostScheduled = typeof host === "object" && typeof host.scheduled === "function" ? host.scheduled : void 0;
3700
- const build = (options) => {
3701
- const lunora = composeWorker({ ...options, httpRouter });
3702
- if (hostScheduled !== void 0 && !hasLunoraCrons(options)) {
3703
- return {
3704
- ...lunora,
3705
- scheduled: async (controller, env, context) => {
3706
- await hostScheduled(controller, env, context);
3707
- }
3708
- };
3709
- }
3710
- return lunora;
3711
- };
3712
- if (typeof optionsInput !== "function") {
3713
- return build(optionsInput);
3714
- }
3715
- const optionsFactory = optionsInput;
3716
- return {
3717
- fetch: (request, env, context) => build(optionsFactory(env)).fetch(request, env, context),
3718
- queue: (batch, env, context) => build(optionsFactory(env)).queue?.(batch, env, context) ?? Promise.resolve(),
3719
- scheduled: (controller, env, context) => build(optionsFactory(env)).scheduled(controller, env, context),
3720
- serverQuery: (request, env, reference, args, options) => build(optionsFactory(env)).serverQuery(request, env, reference, args, options)
3721
- };
3722
- };
3723
- const resolveLunoraOptions = (options, env) => {
3724
- if (typeof options === "function") {
3725
- return options(env);
3726
- }
3727
- const shardDO = options.shardDO ?? env?.SHARD;
3728
- if (!shardDO) {
3729
- throw new LunoraError(
3730
- "@lunora/runtime: no shard Durable Object namespace found. Bind `SHARD` in wrangler.jsonc, or pass `createLunoraHandler({ shardDO: env.MY_SHARD })`."
3731
- );
3732
- }
3733
- return { ...options, shardDO };
3734
- };
3735
- const createLunoraHandler = (options = {}) => (request, env, context) => createWorker(resolveLunoraOptions(options, env)).fetch(request, env, context ?? NOOP_EXECUTION_CONTEXT);
3736
- const defineRpcEnvelope = (envelope) => envelope;
3737
-
3738
- export { GET_AUTH_AUDIT_LOG_OP, NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, probeRelayCount, resolveLunoraOptions, withFrameworkWorker };