@lunora/runtime 0.0.0 → 1.0.0-alpha.1

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.
@@ -0,0 +1,2489 @@
1
+ import { LunoraError, toErrorResponse, isStructuralLunoraError, isStructuralConflictError } from './LunoraError-CL0aOtpo.mjs';
2
+ import { emitRpcEvent } from './emitRpcEvent-pEdtqAK8.mjs';
3
+ import { resolveShard } from './resolveShard-DDkzWtrU.mjs';
4
+ import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-DbISh_Wi.mjs';
5
+
6
+ const AUTH_BASE = "/_lunora/admin/auth";
7
+ const AUTH_ADMIN_ERROR_STATUS = {
8
+ PASSWORD_TOO_LONG: 400,
9
+ PASSWORD_TOO_SHORT: 400,
10
+ USER_ALREADY_EXISTS: 409,
11
+ USER_NOT_FOUND: 404
12
+ };
13
+ const requireBodyString = (body, field) => {
14
+ const value = body[field];
15
+ if (typeof value !== "string" || value === "") {
16
+ throw new LunoraError(`\`${field}\` is required`, { code: "BAD_REQUEST", status: 400 });
17
+ }
18
+ return value;
19
+ };
20
+ const requireQuery$1 = (query, name) => {
21
+ const value = query(name);
22
+ if (value === void 0) {
23
+ throw new LunoraError(`\`${name}\` query parameter is required`, { code: "BAD_REQUEST", status: 400 });
24
+ }
25
+ return value;
26
+ };
27
+ const parseRoleInput = (value) => {
28
+ if (typeof value === "string") {
29
+ return value;
30
+ }
31
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
32
+ return value;
33
+ }
34
+ return void 0;
35
+ };
36
+ const optionalBodyString = (body, field) => typeof body[field] === "string" ? body[field] : void 0;
37
+ const AUTH_ROUTES = {
38
+ [`${AUTH_BASE}/capabilities`]: {
39
+ build: () => {
40
+ return {};
41
+ },
42
+ http: "GET",
43
+ method: "capabilities"
44
+ },
45
+ [`${AUTH_BASE}/users`]: {
46
+ build: ({ paging, query }) => {
47
+ const direction = query("sortDirection");
48
+ return {
49
+ ...paging,
50
+ filterField: query("filterField"),
51
+ filterValue: query("filterValue"),
52
+ search: query("search"),
53
+ searchField: query("searchField"),
54
+ sortBy: query("sortBy"),
55
+ sortDirection: direction === "asc" || direction === "desc" ? direction : void 0
56
+ };
57
+ },
58
+ http: "GET",
59
+ method: "listUsers"
60
+ },
61
+ [`${AUTH_BASE}/sessions`]: {
62
+ build: ({ paging, query }) => {
63
+ return { ...paging, userId: query("userId") };
64
+ },
65
+ http: "GET",
66
+ method: "listSessions"
67
+ },
68
+ [`${AUTH_BASE}/accounts`]: {
69
+ build: ({ query }) => {
70
+ return { userId: requireQuery$1(query, "userId") };
71
+ },
72
+ http: "GET",
73
+ method: "listAccounts"
74
+ },
75
+ [`${AUTH_BASE}/passkeys`]: {
76
+ build: ({ query }) => {
77
+ return { userId: requireQuery$1(query, "userId") };
78
+ },
79
+ http: "GET",
80
+ method: "listPasskeys"
81
+ },
82
+ [`${AUTH_BASE}/organizations`]: {
83
+ build: ({ paging }) => {
84
+ return { ...paging };
85
+ },
86
+ http: "GET",
87
+ method: "listOrganizations"
88
+ },
89
+ [`${AUTH_BASE}/organizations/members`]: {
90
+ build: ({ paging, query }) => {
91
+ return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
92
+ },
93
+ http: "GET",
94
+ method: "listMembers"
95
+ },
96
+ [`${AUTH_BASE}/organizations/invitations`]: {
97
+ build: ({ paging, query }) => {
98
+ return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
99
+ },
100
+ http: "GET",
101
+ method: "listInvitations"
102
+ },
103
+ // --- mutations (POST) -------------------------------------------------------
104
+ [`${AUTH_BASE}/users/create`]: {
105
+ build: ({ body }) => {
106
+ return {
107
+ data: typeof body["data"] === "object" && body["data"] !== null && !Array.isArray(body["data"]) ? body["data"] : void 0,
108
+ email: requireBodyString(body, "email"),
109
+ name: requireBodyString(body, "name"),
110
+ password: optionalBodyString(body, "password"),
111
+ role: parseRoleInput(body["role"])
112
+ };
113
+ },
114
+ http: "POST",
115
+ method: "createUser"
116
+ },
117
+ [`${AUTH_BASE}/users/update`]: {
118
+ build: ({ body }) => {
119
+ const { data } = body;
120
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
121
+ throw new LunoraError("`data` object is required", { code: "BAD_REQUEST", status: 400 });
122
+ }
123
+ return { data, userId: requireBodyString(body, "userId") };
124
+ },
125
+ http: "POST",
126
+ method: "updateUser"
127
+ },
128
+ [`${AUTH_BASE}/users/role`]: {
129
+ build: ({ body }) => {
130
+ const role = parseRoleInput(body["role"]);
131
+ if (role === void 0 || typeof role === "string" && role.trim() === "") {
132
+ throw new LunoraError("`role` is required", { code: "BAD_REQUEST", status: 400 });
133
+ }
134
+ return { role, userId: requireBodyString(body, "userId") };
135
+ },
136
+ http: "POST",
137
+ method: "setRole"
138
+ },
139
+ [`${AUTH_BASE}/users/ban`]: {
140
+ build: ({ body }) => {
141
+ return {
142
+ expiresInSeconds: typeof body["expiresInSeconds"] === "number" ? body["expiresInSeconds"] : void 0,
143
+ reason: optionalBodyString(body, "reason"),
144
+ userId: requireBodyString(body, "userId")
145
+ };
146
+ },
147
+ http: "POST",
148
+ method: "banUser"
149
+ },
150
+ [`${AUTH_BASE}/users/unban`]: {
151
+ build: ({ body }) => {
152
+ return { userId: requireBodyString(body, "userId") };
153
+ },
154
+ http: "POST",
155
+ method: "unbanUser"
156
+ },
157
+ [`${AUTH_BASE}/users/password`]: {
158
+ build: ({ body }) => {
159
+ return { newPassword: requireBodyString(body, "newPassword"), userId: requireBodyString(body, "userId") };
160
+ },
161
+ http: "POST",
162
+ method: "setUserPassword",
163
+ returns: "void"
164
+ },
165
+ [`${AUTH_BASE}/users/remove`]: {
166
+ build: ({ body }) => {
167
+ return { userId: requireBodyString(body, "userId") };
168
+ },
169
+ http: "POST",
170
+ method: "removeUser",
171
+ returns: "void"
172
+ },
173
+ [`${AUTH_BASE}/users/impersonate`]: {
174
+ build: ({ body }) => {
175
+ return { userId: requireBodyString(body, "userId") };
176
+ },
177
+ http: "POST",
178
+ method: "impersonateUser"
179
+ },
180
+ [`${AUTH_BASE}/sessions/revoke`]: {
181
+ build: ({ body }) => {
182
+ return { sessionId: requireBodyString(body, "sessionId") };
183
+ },
184
+ http: "POST",
185
+ method: "revokeUserSession",
186
+ returns: "void"
187
+ },
188
+ [`${AUTH_BASE}/sessions/revoke-all`]: {
189
+ build: ({ body }) => {
190
+ return { userId: requireBodyString(body, "userId") };
191
+ },
192
+ http: "POST",
193
+ method: "revokeUserSessions",
194
+ returns: "void"
195
+ },
196
+ [`${AUTH_BASE}/accounts/unlink`]: {
197
+ build: ({ body }) => {
198
+ return { accountId: requireBodyString(body, "accountId"), userId: requireBodyString(body, "userId") };
199
+ },
200
+ http: "POST",
201
+ method: "unlinkAccount",
202
+ returns: "void"
203
+ },
204
+ [`${AUTH_BASE}/two-factor/disable`]: {
205
+ build: ({ body }) => {
206
+ return { userId: requireBodyString(body, "userId") };
207
+ },
208
+ http: "POST",
209
+ method: "disableTwoFactor",
210
+ returns: "void"
211
+ },
212
+ [`${AUTH_BASE}/passkeys/delete`]: {
213
+ build: ({ body }) => {
214
+ return { passkeyId: requireBodyString(body, "passkeyId") };
215
+ },
216
+ http: "POST",
217
+ method: "deletePasskey",
218
+ returns: "void"
219
+ },
220
+ [`${AUTH_BASE}/organizations/members/remove`]: {
221
+ build: ({ body }) => {
222
+ return { memberId: requireBodyString(body, "memberId") };
223
+ },
224
+ http: "POST",
225
+ method: "removeMember",
226
+ returns: "void"
227
+ },
228
+ [`${AUTH_BASE}/organizations/invitations/cancel`]: {
229
+ build: ({ body }) => {
230
+ return { invitationId: requireBodyString(body, "invitationId") };
231
+ },
232
+ http: "POST",
233
+ method: "cancelInvitation",
234
+ returns: "void"
235
+ }
236
+ };
237
+ const buildAuthAdminRoutes = (deps) => {
238
+ const runAuthOp = async (op) => {
239
+ try {
240
+ return await op();
241
+ } catch (error) {
242
+ if (error instanceof LunoraError) {
243
+ throw error;
244
+ }
245
+ const candidate = error;
246
+ const code = typeof candidate.code === "string" ? candidate.code : "AUTH_ADMIN_ERROR";
247
+ const message = typeof candidate.message === "string" ? candidate.message : "auth admin operation failed";
248
+ throw new LunoraError(message, { code, status: AUTH_ADMIN_ERROR_STATUS[code] ?? 400 });
249
+ }
250
+ };
251
+ const handle = async (request, descriptor) => {
252
+ if (request.method !== descriptor.http) {
253
+ throw new LunoraError(`Auth admin endpoint requires ${descriptor.http}`, { code: "METHOD_NOT_ALLOWED", status: 405 });
254
+ }
255
+ deps.assertAdmin(request);
256
+ const admin = deps.getAuthAdmin();
257
+ if (admin === void 0) {
258
+ throw new LunoraError("auth endpoints require an `authAdmin` on the worker", { code: "AUTH_NOT_CONFIGURED", status: 400 });
259
+ }
260
+ const method = admin[descriptor.method];
261
+ if (method === void 0) {
262
+ throw new LunoraError(`auth admin does not support \`${descriptor.method}\``, { code: "AUTH_OP_NOT_SUPPORTED", status: 400 });
263
+ }
264
+ const url = new URL(request.url);
265
+ const context = {
266
+ body: descriptor.http === "POST" ? await deps.readJsonBody(request) : {},
267
+ paging: deps.parsePaging(request),
268
+ query: (name) => deps.queryParameter(url, name)
269
+ };
270
+ const input = descriptor.build(context);
271
+ const result = await runAuthOp(() => method(input));
272
+ return Response.json(descriptor.returns === "void" ? { ok: true } : result, { headers: { "content-type": "application/json" }, status: 200 });
273
+ };
274
+ const routes = {};
275
+ for (const [path, descriptor] of Object.entries(AUTH_ROUTES)) {
276
+ routes[path] = (request) => handle(request, descriptor);
277
+ }
278
+ return routes;
279
+ };
280
+
281
+ const MAX_BODY_BYTES = 1048576;
282
+ const readBodyTextWithLimit = async (request, limit = MAX_BODY_BYTES) => {
283
+ if (!request.body) {
284
+ return "";
285
+ }
286
+ const reader = request.body.getReader();
287
+ const decoder = new TextDecoder();
288
+ let total = 0;
289
+ let text = "";
290
+ while (true) {
291
+ const { done, value } = await reader.read();
292
+ if (done) {
293
+ break;
294
+ }
295
+ if (value) {
296
+ total += value.byteLength;
297
+ if (total > limit) {
298
+ await reader.cancel().catch(() => {
299
+ });
300
+ throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
301
+ }
302
+ text += decoder.decode(value, { stream: true });
303
+ }
304
+ }
305
+ text += decoder.decode();
306
+ return text;
307
+ };
308
+ const readBodyBytesWithLimit = async (request, limit = MAX_BODY_BYTES) => {
309
+ if (!request.body) {
310
+ return new ArrayBuffer(0);
311
+ }
312
+ const reader = request.body.getReader();
313
+ const chunks = [];
314
+ let total = 0;
315
+ while (true) {
316
+ const { done, value } = await reader.read();
317
+ if (done) {
318
+ break;
319
+ }
320
+ if (value) {
321
+ total += value.byteLength;
322
+ if (total > limit) {
323
+ await reader.cancel().catch(() => {
324
+ });
325
+ throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
326
+ }
327
+ chunks.push(value);
328
+ }
329
+ }
330
+ const out = new Uint8Array(total);
331
+ let offset = 0;
332
+ for (const chunk of chunks) {
333
+ out.set(chunk, offset);
334
+ offset += chunk.byteLength;
335
+ }
336
+ return out.buffer;
337
+ };
338
+ const readJsonBodyWithLimit = async (request) => {
339
+ try {
340
+ const text = await readBodyTextWithLimit(request);
341
+ return text === "" ? {} : JSON.parse(text);
342
+ } catch (error) {
343
+ if (error instanceof LunoraError) {
344
+ throw error;
345
+ }
346
+ throw new LunoraError("Request body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
347
+ }
348
+ };
349
+
350
+ const CURSOR_ENCODER = new TextEncoder();
351
+ const encodeConnectorCursor = (state) => {
352
+ const json = JSON.stringify(state);
353
+ const bytes = CURSOR_ENCODER.encode(json);
354
+ let binary = "";
355
+ for (const byte of bytes) {
356
+ binary += String.fromCodePoint(byte);
357
+ }
358
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
359
+ };
360
+ const decodeConnectorCursor = (token) => {
361
+ const empty = { g: 0, s: {}, v: 1 };
362
+ if (typeof token !== "string" || token.length === 0) {
363
+ return empty;
364
+ }
365
+ try {
366
+ const binary = atob(token.replaceAll("-", "+").replaceAll("_", "/"));
367
+ const bytes = new Uint8Array(binary.length);
368
+ for (let index = 0; index < binary.length; index += 1) {
369
+ bytes[index] = binary.codePointAt(index) ?? 0;
370
+ }
371
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
372
+ const shards = parsed.s && typeof parsed.s === "object" ? parsed.s : {};
373
+ const sanitized = {};
374
+ for (const [key, value] of Object.entries(shards)) {
375
+ if (typeof value === "number" && Number.isFinite(value)) {
376
+ sanitized[key] = value;
377
+ }
378
+ }
379
+ return { g: typeof parsed.g === "number" && Number.isFinite(parsed.g) ? parsed.g : 0, s: sanitized, v: 1 };
380
+ } catch {
381
+ return empty;
382
+ }
383
+ };
384
+ const flattenCdcChange = (change) => {
385
+ const table = typeof change["table"] === "string" ? change["table"] : "";
386
+ const rawOp = typeof change["op"] === "string" ? change["op"] : "";
387
+ const op = rawOp === "delete" || rawOp === "insert" || rawOp === "update" ? rawOp : "upsert";
388
+ const id = typeof change["id"] === "string" ? change["id"] : void 0;
389
+ const postImage = change["doc"] && typeof change["doc"] === "object" ? change["doc"] : void 0;
390
+ const documentRow = postImage ?? (id === void 0 ? {} : { _id: id });
391
+ return { doc: documentRow, op, table };
392
+ };
393
+ const foldCdcPage = (changes, pageChanges, limit) => {
394
+ for (const change of pageChanges) {
395
+ changes.push(flattenCdcChange(change));
396
+ }
397
+ return limit !== void 0 && pageChanges.length >= limit;
398
+ };
399
+
400
+ const EXPORT_PATH = "/_lunora/admin/export";
401
+ const IMPORT_PATH = "/_lunora/admin/import";
402
+ const SYNC_PATH = "/_lunora/admin/sync";
403
+ const CONNECTOR_SYNC_PATH = "/_lunora/admin/connector/sync";
404
+ const APPLY_PATH = "/_lunora/admin/apply";
405
+ const NDJSON_ENCODER$1 = new TextEncoder();
406
+ const parseExportBody = async (request) => {
407
+ let body;
408
+ try {
409
+ const text = await readBodyTextWithLimit(request);
410
+ body = text === "" ? {} : JSON.parse(text);
411
+ } catch (error) {
412
+ if (error instanceof LunoraError) {
413
+ throw error;
414
+ }
415
+ throw new LunoraError("Export body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
416
+ }
417
+ const candidate = body ?? {};
418
+ if (candidate.tables === void 0) {
419
+ return { tables: void 0 };
420
+ }
421
+ if (!Array.isArray(candidate.tables)) {
422
+ throw new LunoraError("Export `tables` must be a string array", { code: "BAD_REQUEST", status: 400 });
423
+ }
424
+ const tables = [];
425
+ for (const entry of candidate.tables) {
426
+ if (typeof entry !== "string" || entry.length === 0) {
427
+ throw new LunoraError("Export `tables` entries must be non-empty strings", { code: "BAD_REQUEST", status: 400 });
428
+ }
429
+ tables.push(entry);
430
+ }
431
+ return { tables };
432
+ };
433
+ const buildDataMovementAdminRoutes = (deps) => {
434
+ const { applyGlobals, isAdmin, knownTables, queryCoordinator, resolveForwardContext, shardDO, streamExportRows, streamingImport, syncGlobals } = deps;
435
+ const handleExport = async (request, env) => {
436
+ if (request.method !== "POST") {
437
+ throw new LunoraError("Export endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
438
+ }
439
+ if (!isAdmin(request)) {
440
+ throw new LunoraError("admin export endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
441
+ }
442
+ const coordinator = queryCoordinator;
443
+ if (!coordinator) {
444
+ throw new LunoraError("Export endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
445
+ }
446
+ const body = await parseExportBody(request);
447
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
448
+ const stream = new ReadableStream({
449
+ async pull(controller) {
450
+ const writeRow = (row) => {
451
+ controller.enqueue(NDJSON_ENCODER$1.encode(`${JSON.stringify(row)}
452
+ `));
453
+ };
454
+ try {
455
+ await streamExportRows(coordinator, forwardedHeaders, body.tables, writeRow);
456
+ controller.close();
457
+ } catch (error) {
458
+ controller.error(error);
459
+ }
460
+ }
461
+ });
462
+ return new Response(stream, { headers: { "content-type": "application/x-ndjson" }, status: 200 });
463
+ };
464
+ const handleCdcSync = async (request, env) => {
465
+ if (request.method !== "POST") {
466
+ throw new LunoraError("Sync endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
467
+ }
468
+ if (!isAdmin(request)) {
469
+ throw new LunoraError("admin sync endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
470
+ }
471
+ const coordinator = queryCoordinator;
472
+ if (!coordinator) {
473
+ throw new LunoraError("Sync endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
474
+ }
475
+ const raw = await readJsonBodyWithLimit(request);
476
+ const cursors = typeof raw["cursors"] === "object" && raw["cursors"] !== null ? raw["cursors"] : {};
477
+ const limit = typeof raw["limit"] === "number" ? raw["limit"] : void 0;
478
+ const globalCursor = typeof raw["globalCursor"] === "number" ? raw["globalCursor"] : 0;
479
+ const requestedTables = Array.isArray(raw["tables"]) ? raw["tables"].filter((table) => typeof table === "string") : void 0;
480
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
481
+ const probeTables = requestedTables ?? knownTables();
482
+ const shardResult = await coordinator.orchestrateCdcSync(shardDO, {
483
+ cursors,
484
+ headers: forwardedHeaders,
485
+ limit,
486
+ tables: probeTables
487
+ });
488
+ const global = syncGlobals ? await syncGlobals({ limit, sinceSeq: globalCursor }) : void 0;
489
+ return Response.json({ global, shards: shardResult.shards }, { status: 200 });
490
+ };
491
+ const handleConnectorSync = async (request, env) => {
492
+ if (request.method !== "POST") {
493
+ throw new LunoraError("Connector sync endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
494
+ }
495
+ if (!isAdmin(request)) {
496
+ throw new LunoraError("admin connector sync endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
497
+ }
498
+ const coordinator = queryCoordinator;
499
+ if (!coordinator) {
500
+ throw new LunoraError("Connector sync endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
501
+ }
502
+ const raw = await readJsonBodyWithLimit(request);
503
+ const state = decodeConnectorCursor(raw["cursor"]);
504
+ const limit = typeof raw["limit"] === "number" && raw["limit"] > 0 ? raw["limit"] : void 0;
505
+ const requestedTables = Array.isArray(raw["tables"]) ? raw["tables"].filter((table) => typeof table === "string") : void 0;
506
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
507
+ const probeTables = requestedTables ?? knownTables();
508
+ const shardResult = await coordinator.orchestrateCdcSync(shardDO, {
509
+ cursors: state.s,
510
+ headers: forwardedHeaders,
511
+ limit,
512
+ tables: probeTables
513
+ });
514
+ const changes = [];
515
+ const nextShardCursors = { ...state.s };
516
+ let hasMore = false;
517
+ for (const shard of shardResult.shards) {
518
+ hasMore = foldCdcPage(changes, shard.changes ?? [], limit) || hasMore;
519
+ nextShardCursors[shard.shardKey] = shard.cursor;
520
+ }
521
+ let nextGlobalCursor = state.g;
522
+ if (syncGlobals) {
523
+ const global = await syncGlobals({ limit, sinceSeq: state.g });
524
+ hasMore = foldCdcPage(changes, global.changes, limit) || hasMore;
525
+ nextGlobalCursor = global.cursor;
526
+ }
527
+ const nextCursor = encodeConnectorCursor({ g: nextGlobalCursor, s: nextShardCursors, v: 1 });
528
+ const page = { changes, hasMore, nextCursor };
529
+ return Response.json(page, { status: 200 });
530
+ };
531
+ const handleApplyCdc = async (request, env) => {
532
+ if (request.method !== "POST") {
533
+ throw new LunoraError("Apply endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
534
+ }
535
+ if (!isAdmin(request)) {
536
+ throw new LunoraError("admin apply endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
537
+ }
538
+ const coordinator = queryCoordinator;
539
+ if (!coordinator) {
540
+ throw new LunoraError("Apply endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
541
+ }
542
+ const raw = await readJsonBodyWithLimit(request);
543
+ const rawBatches = Array.isArray(raw["batches"]) ? raw["batches"] : [];
544
+ const batches = rawBatches.map((batch) => batch).filter(
545
+ (batch) => (
546
+ // Guard object-ness before property access — a `null`/non-object
547
+ // entry (e.g. `{"batches":[null]}`) would otherwise throw a
548
+ // TypeError that surfaces as a confusing 500; here it's skipped.
549
+ batch !== null && typeof batch === "object" && typeof batch.shardKey === "string" && Array.isArray(batch.changes)
550
+ )
551
+ );
552
+ const globalChanges = Array.isArray(raw["globalChanges"]) ? raw["globalChanges"] : [];
553
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
554
+ const shardResult = await coordinator.orchestrateApplyCdc(shardDO, { batches, headers: forwardedHeaders });
555
+ const globalApplied = globalChanges.length > 0 && applyGlobals ? await applyGlobals({ changes: globalChanges }) : 0;
556
+ return Response.json({ applied: shardResult.applied + globalApplied, failed: shardResult.failed, ok: shardResult.ok }, { status: 200 });
557
+ };
558
+ const handleImport = async (request, env) => {
559
+ if (request.method !== "POST") {
560
+ throw new LunoraError("Import endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
561
+ }
562
+ if (!isAdmin(request)) {
563
+ throw new LunoraError("admin import endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
564
+ }
565
+ if (!queryCoordinator) {
566
+ throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
567
+ }
568
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
569
+ const result = await streamingImport(request, forwardedHeaders);
570
+ return Response.json(result, {
571
+ headers: { "content-type": "application/json" },
572
+ status: 200
573
+ });
574
+ };
575
+ return {
576
+ [APPLY_PATH]: handleApplyCdc,
577
+ [CONNECTOR_SYNC_PATH]: handleConnectorSync,
578
+ [EXPORT_PATH]: handleExport,
579
+ [IMPORT_PATH]: handleImport,
580
+ [SYNC_PATH]: handleCdcSync
581
+ };
582
+ };
583
+
584
+ const collectKnownTables = (_resolver) => [];
585
+ const partitionExportTables = (options, tables) => {
586
+ const shardLocalTables = [];
587
+ const globalTables = [];
588
+ if (tables && tables.length > 0) {
589
+ for (const table of tables) {
590
+ const info = options.resolveTableSharding?.(table);
591
+ if (info?.mode.kind === "global") {
592
+ globalTables.push(table);
593
+ } else {
594
+ shardLocalTables.push(table);
595
+ }
596
+ }
597
+ }
598
+ return { globalTables, shardLocalTables };
599
+ };
600
+ const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow) => {
601
+ if (tables !== void 0 && shardLocalTables.length === 0) {
602
+ return;
603
+ }
604
+ const exportTables = tables === void 0 ? [] : shardLocalTables;
605
+ const probeFallback = tables === void 0 ? collectKnownTables() : [];
606
+ const probeTables = exportTables.length > 0 ? exportTables : probeFallback;
607
+ const result = await coordinator.orchestrateExport(options.shardDO, {
608
+ args: { tables: exportTables },
609
+ headers: forwardedHeaders,
610
+ tables: probeTables
611
+ });
612
+ for (const shard of result.shards) {
613
+ if (shard.error) {
614
+ continue;
615
+ }
616
+ for (const row of shard.rows ?? []) {
617
+ writeRow(row);
618
+ }
619
+ }
620
+ };
621
+ const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow) => {
622
+ const { globalTables, shardLocalTables } = partitionExportTables(options, tables);
623
+ await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow);
624
+ const exportGlobalsFunction = options.exportGlobals;
625
+ const wantGlobals = tables === void 0 || globalTables.length > 0;
626
+ if (wantGlobals && exportGlobalsFunction) {
627
+ const tablesArgument = tables === void 0 ? [] : globalTables;
628
+ for await (const row of exportGlobalsFunction({ tables: tablesArgument })) {
629
+ writeRow(row);
630
+ }
631
+ }
632
+ };
633
+
634
+ const parseImportRow = (trimmed, lineNumber) => {
635
+ let parsed;
636
+ try {
637
+ parsed = JSON.parse(trimmed);
638
+ } catch {
639
+ return { error: { code: "BAD_ROW", line: lineNumber, message: "line is not valid JSON", table: "" }, ok: false };
640
+ }
641
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
642
+ return { error: { code: "BAD_ROW", line: lineNumber, message: "row must be a JSON object", table: "" }, ok: false };
643
+ }
644
+ const candidate = parsed;
645
+ if (typeof candidate.table !== "string" || candidate.table.length === 0) {
646
+ return { error: { code: "BAD_ROW", line: lineNumber, message: "row is missing `table`", table: "" }, ok: false };
647
+ }
648
+ if (!candidate.doc || typeof candidate.doc !== "object" || Array.isArray(candidate.doc)) {
649
+ return { error: { code: "BAD_ROW", line: lineNumber, message: "row is missing or malformed `doc`", table: candidate.table }, ok: false };
650
+ }
651
+ return { doc: candidate.doc, ok: true, table: candidate.table };
652
+ };
653
+ const resolveImportShardKey = (documentRow, table, info, defaultShard, lineNumber) => {
654
+ if (info?.mode.kind === "shardBy" && typeof info.mode.field === "string") {
655
+ const raw = documentRow[info.mode.field];
656
+ if (raw === void 0 || raw === null) {
657
+ return {
658
+ error: { code: "BAD_ROW", line: lineNumber, message: `row missing shard field "${info.mode.field}" for table "${table}"`, table },
659
+ ok: false
660
+ };
661
+ }
662
+ return { ok: true, shardKey: typeof raw === "string" ? raw : JSON.stringify(raw) };
663
+ }
664
+ return { ok: true, shardKey: defaultShard };
665
+ };
666
+ const bucketImportStream = async (request, options, defaultShard) => {
667
+ if (!request.body) {
668
+ throw new LunoraError("Import endpoint requires a request body", { code: "BAD_REQUEST", status: 400 });
669
+ }
670
+ const errors = [];
671
+ const globalRows = [];
672
+ const perShard = /* @__PURE__ */ new Map();
673
+ let physicalLine = 0;
674
+ const reader = request.body.getReader();
675
+ const decoder = new TextDecoder();
676
+ let buffer = "";
677
+ let totalBytes = 0;
678
+ const handleLine = (line) => {
679
+ physicalLine += 1;
680
+ const trimmed = line.trim();
681
+ if (trimmed.length === 0) {
682
+ return;
683
+ }
684
+ const row = parseImportRow(trimmed, physicalLine);
685
+ if (!row.ok) {
686
+ errors.push(row.error);
687
+ return;
688
+ }
689
+ const { doc: documentRow, table } = row;
690
+ const info = options.resolveTableSharding?.(table);
691
+ if (info?.mode.kind === "global") {
692
+ globalRows.push({ doc: documentRow, line: physicalLine, table });
693
+ return;
694
+ }
695
+ const resolved = resolveImportShardKey(documentRow, table, info, defaultShard, physicalLine);
696
+ if (!resolved.ok) {
697
+ errors.push(resolved.error);
698
+ return;
699
+ }
700
+ const existing = perShard.get(resolved.shardKey);
701
+ if (existing) {
702
+ existing.rows.push({ doc: documentRow, table });
703
+ } else {
704
+ perShard.set(resolved.shardKey, { rows: [{ doc: documentRow, table }], shardKey: resolved.shardKey, startLine: physicalLine });
705
+ }
706
+ };
707
+ while (true) {
708
+ const { done, value } = await reader.read();
709
+ if (done) {
710
+ break;
711
+ }
712
+ if (value) {
713
+ totalBytes += value.byteLength;
714
+ if (totalBytes > MAX_BODY_BYTES) {
715
+ await reader.cancel().catch(() => {
716
+ });
717
+ throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
718
+ }
719
+ }
720
+ buffer += decoder.decode(value, { stream: true });
721
+ let newlineIndex = buffer.indexOf("\n");
722
+ while (newlineIndex !== -1) {
723
+ const line = buffer.slice(0, newlineIndex);
724
+ buffer = buffer.slice(newlineIndex + 1);
725
+ handleLine(line);
726
+ newlineIndex = buffer.indexOf("\n");
727
+ }
728
+ }
729
+ if (buffer.length > 0) {
730
+ handleLine(buffer);
731
+ }
732
+ return { errors, globalRows, perShard };
733
+ };
734
+ const mergeImportResult = (totals, result) => {
735
+ for (const [table, count] of Object.entries(result.inserted)) {
736
+ totals.inserted[table] = (totals.inserted[table] ?? 0) + count;
737
+ }
738
+ for (const rowError of result.errors) {
739
+ totals.errors.push({ ...rowError });
740
+ }
741
+ totals.conflicts += result.conflicts;
742
+ };
743
+ const streamingImport = async (request, options, forwardedHeaders) => {
744
+ const defaultShard = options.defaultShardKey ?? "__root__";
745
+ const { errors, globalRows, perShard } = await bucketImportStream(request, options, defaultShard);
746
+ const totals = { conflicts: 0, errors, inserted: {} };
747
+ if (perShard.size > 0) {
748
+ const coordinator = options.queryCoordinator;
749
+ if (!coordinator) {
750
+ throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
751
+ }
752
+ const result = await coordinator.orchestrateImport(options.shardDO, {
753
+ batches: [...perShard.values()],
754
+ headers: forwardedHeaders
755
+ });
756
+ mergeImportResult(totals, result);
757
+ }
758
+ if (globalRows.length > 0) {
759
+ if (options.importGlobals) {
760
+ const startLine = globalRows[0]?.line ?? 1;
761
+ const result = await options.importGlobals({ rows: globalRows, startLine });
762
+ mergeImportResult(totals, result);
763
+ } else {
764
+ for (const globalRow of globalRows) {
765
+ totals.errors.push({
766
+ code: "GLOBAL_NOT_CONFIGURED",
767
+ line: globalRow.line,
768
+ message: `row targets global table "${globalRow.table}" but no \`importGlobals\` is configured`,
769
+ table: globalRow.table
770
+ });
771
+ }
772
+ }
773
+ }
774
+ return { conflicts: totals.conflicts, errors: totals.errors, inserted: totals.inserted };
775
+ };
776
+
777
+ const asValidator = (value) => typeof value === "object" && value !== null ? value : {};
778
+ const kindOf = (validator) => typeof validator.kind === "string" ? validator.kind : "unknown";
779
+ const describeArgument = (name, validator) => {
780
+ let current = asValidator(validator);
781
+ let optional = false;
782
+ if (kindOf(current) === "optional") {
783
+ optional = true;
784
+ current = asValidator(current._meta?.["inner"]);
785
+ }
786
+ const kind = kindOf(current);
787
+ const meta = current._meta ?? {};
788
+ const descriptor = { kind, name, optional };
789
+ if (kind === "id" && typeof meta["tableName"] === "string") {
790
+ descriptor.table = meta["tableName"];
791
+ }
792
+ if (kind === "array") {
793
+ const elementKind = kindOf(asValidator(meta["inner"]));
794
+ if (elementKind !== "unknown") {
795
+ descriptor.element = elementKind;
796
+ }
797
+ }
798
+ return descriptor;
799
+ };
800
+ const describeArguments = (args) => {
801
+ if (typeof args !== "object" || args === null) {
802
+ return [];
803
+ }
804
+ return Object.entries(args).map(([name, validator]) => describeArgument(name, validator)).toSorted((a, b) => a.name.localeCompare(b.name));
805
+ };
806
+
807
+ const FUNCTIONS_PATH = "/_lunora/admin/functions";
808
+ const CRON_JOBS_PATH = "/_lunora/admin/cron-jobs";
809
+ const OPENAPI_PATH = "/_lunora/admin/openapi";
810
+ const OPENRPC_PATH = "/_lunora/admin/openrpc";
811
+ const GLOBAL_TABLES_PATH = "/_lunora/admin/global/tables";
812
+ const GLOBAL_TABLE_PATH = "/_lunora/admin/global/table";
813
+ const GLOBAL_FACET_PATH = "/_lunora/admin/global/facet";
814
+ const parseGlobalFilters = (raw) => {
815
+ if (raw === void 0 || raw === "") {
816
+ return void 0;
817
+ }
818
+ let parsed;
819
+ try {
820
+ parsed = JSON.parse(raw);
821
+ } catch {
822
+ return void 0;
823
+ }
824
+ if (!Array.isArray(parsed)) {
825
+ return void 0;
826
+ }
827
+ const clauses = parsed.flatMap((entry) => {
828
+ if (typeof entry !== "object" || entry === null || typeof entry.column !== "string") {
829
+ return [];
830
+ }
831
+ const { column, value } = entry;
832
+ return [{ column, value }];
833
+ });
834
+ return clauses.length === 0 ? void 0 : clauses;
835
+ };
836
+ const EMPTY_OPENAPI_DOCUMENT = Object.freeze({
837
+ info: {
838
+ 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"`.',
839
+ title: "Lunora API",
840
+ version: "0.0.0"
841
+ },
842
+ openapi: "3.1.0",
843
+ paths: {}
844
+ });
845
+ const EMPTY_OPENRPC_DOCUMENT = Object.freeze({
846
+ info: {
847
+ 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"`.',
848
+ title: "Lunora RPC",
849
+ version: "0.0.0"
850
+ },
851
+ methods: [],
852
+ openrpc: "1.3.2"
853
+ });
854
+ const buildIntrospectionAdminRoutes = (deps) => {
855
+ const { assertAdmin, options, parsePaging, queryParameter, requireAdminOption } = deps;
856
+ const handleFunctionsList = (request) => {
857
+ if (request.method !== "GET") {
858
+ throw new LunoraError("Functions endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
859
+ }
860
+ const registry = requireAdminOption(request, options.functions, {
861
+ code: "FUNCTIONS_NOT_CONFIGURED",
862
+ message: "functions endpoint requires a `functions` registry on the worker"
863
+ });
864
+ const functions = Object.entries(registry).flatMap(([path, entry]) => {
865
+ if (entry.visibility === "internal" || entry.kind === "stream") {
866
+ return [];
867
+ }
868
+ return [{ args: describeArguments(entry.args), kind: entry.kind, path }];
869
+ }).toSorted((a, b) => a.path.localeCompare(b.path));
870
+ return Response.json({ functions }, { headers: { "content-type": "application/json" }, status: 200 });
871
+ };
872
+ const handleCronJobs = (request) => {
873
+ if (request.method !== "GET") {
874
+ throw new LunoraError("Cron-jobs endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
875
+ }
876
+ const registry = requireAdminOption(request, options.cronJobs, {
877
+ code: "CRON_JOBS_NOT_CONFIGURED",
878
+ message: "cron-jobs endpoint requires a `cronJobs` map on the worker"
879
+ });
880
+ const jobs = Object.entries(registry).flatMap(
881
+ ([cron, dispatches]) => dispatches.map((dispatch) => {
882
+ return {
883
+ args: dispatch.args,
884
+ cron,
885
+ functionPath: dispatch.functionPath,
886
+ name: dispatch.name,
887
+ shardKey: dispatch.shardKey,
888
+ workflow: dispatch.workflow
889
+ };
890
+ })
891
+ ).toSorted((a, b) => a.name.localeCompare(b.name));
892
+ return Response.json({ jobs }, { headers: { "content-type": "application/json" }, status: 200 });
893
+ };
894
+ const handleOpenApi = (request) => {
895
+ if (request.method !== "GET") {
896
+ throw new LunoraError("OpenAPI endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
897
+ }
898
+ assertAdmin(request);
899
+ return Response.json(options.openApiSpec ?? EMPTY_OPENAPI_DOCUMENT, { headers: { "content-type": "application/json" }, status: 200 });
900
+ };
901
+ const handleOpenRpc = (request) => {
902
+ if (request.method !== "GET") {
903
+ throw new LunoraError("OpenRPC endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
904
+ }
905
+ assertAdmin(request);
906
+ return Response.json(options.openRpcSpec ?? EMPTY_OPENRPC_DOCUMENT, { headers: { "content-type": "application/json" }, status: 200 });
907
+ };
908
+ const handleGlobalTables = async (request) => {
909
+ if (request.method !== "GET") {
910
+ throw new LunoraError("Global-tables endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
911
+ }
912
+ const introspector = requireAdminOption(request, options.globalIntrospector, {
913
+ code: "GLOBALS_NOT_CONFIGURED",
914
+ message: "global endpoints require a `globalIntrospector` on the worker"
915
+ });
916
+ return Response.json(await introspector.listTables(), { headers: { "content-type": "application/json" }, status: 200 });
917
+ };
918
+ const handleGlobalTablePage = async (request) => {
919
+ if (request.method !== "GET") {
920
+ throw new LunoraError("Global-table endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
921
+ }
922
+ const introspector = requireAdminOption(request, options.globalIntrospector, {
923
+ code: "GLOBALS_NOT_CONFIGURED",
924
+ message: "global endpoints require a `globalIntrospector` on the worker"
925
+ });
926
+ const url = new URL(request.url);
927
+ const table = queryParameter(url, "table");
928
+ if (table === void 0) {
929
+ throw new LunoraError("Global-table endpoint requires a `table` query param", { code: "BAD_REQUEST", status: 400 });
930
+ }
931
+ const page = await introspector.readTablePage({ ...parsePaging(request), filters: parseGlobalFilters(queryParameter(url, "filters")), table });
932
+ return Response.json(page, { headers: { "content-type": "application/json" }, status: 200 });
933
+ };
934
+ const handleGlobalFacet = async (request) => {
935
+ if (request.method !== "GET") {
936
+ throw new LunoraError("Global-facet endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
937
+ }
938
+ const introspector = requireAdminOption(request, options.globalIntrospector, {
939
+ code: "GLOBALS_NOT_CONFIGURED",
940
+ message: "global endpoints require a `globalIntrospector` on the worker"
941
+ });
942
+ const url = new URL(request.url);
943
+ const table = queryParameter(url, "table");
944
+ const column = queryParameter(url, "column");
945
+ if (table === void 0 || column === void 0) {
946
+ throw new LunoraError("Global-facet endpoint requires `table` and `column` query params", { code: "BAD_REQUEST", status: 400 });
947
+ }
948
+ const limitParameter = queryParameter(url, "limit");
949
+ const limit = limitParameter === void 0 ? void 0 : Number(limitParameter);
950
+ const result = await introspector.facetColumn({
951
+ column,
952
+ filters: parseGlobalFilters(queryParameter(url, "filters")),
953
+ limit: limit !== void 0 && Number.isFinite(limit) ? limit : void 0,
954
+ table
955
+ });
956
+ return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
957
+ };
958
+ return {
959
+ [CRON_JOBS_PATH]: handleCronJobs,
960
+ [FUNCTIONS_PATH]: handleFunctionsList,
961
+ [GLOBAL_FACET_PATH]: handleGlobalFacet,
962
+ [GLOBAL_TABLE_PATH]: handleGlobalTablePage,
963
+ [GLOBAL_TABLES_PATH]: handleGlobalTables,
964
+ [OPENAPI_PATH]: handleOpenApi,
965
+ [OPENRPC_PATH]: handleOpenRpc
966
+ };
967
+ };
968
+
969
+ const MIGRATE_PATH = "/_lunora/migrate";
970
+ const PITR_PATH = "/_lunora/admin/pitr";
971
+ const RANK_PATH = "/_lunora/admin/rank";
972
+ const RANKPAGE_PATH = "/_lunora/admin/rankpage";
973
+ const SHARD_TRAFFIC_PATH = "/_lunora/admin/shard-traffic";
974
+ const MIGRATION_ADMIN_OPS = /* @__PURE__ */ new Set(["__lunora_admin__:migrationStatus", "__lunora_admin__:runMigration"]);
975
+ const PITR_ADMIN_OPS = /* @__PURE__ */ new Set(["__lunora_admin__:getPitrBookmark", "__lunora_admin__:pitrRestore"]);
976
+ const parseMigrateRequest = async (request) => {
977
+ let body;
978
+ try {
979
+ const text = await readBodyTextWithLimit(request);
980
+ body = text === "" ? {} : JSON.parse(text);
981
+ } catch (error) {
982
+ if (error instanceof LunoraError) {
983
+ throw error;
984
+ }
985
+ throw new LunoraError("Migration body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
986
+ }
987
+ const candidate = body ?? {};
988
+ if (typeof candidate.table !== "string" || candidate.table.length === 0) {
989
+ throw new LunoraError("Migration request is missing `table`", { code: "BAD_REQUEST", status: 400 });
990
+ }
991
+ if (typeof candidate.functionPath !== "string" || !MIGRATION_ADMIN_OPS.has(candidate.functionPath)) {
992
+ throw new LunoraError("Migration request `functionPath` must be a migration admin op", { code: "BAD_REQUEST", status: 400 });
993
+ }
994
+ return {
995
+ args: candidate.args ?? {},
996
+ functionPath: candidate.functionPath,
997
+ table: candidate.table
998
+ };
999
+ };
1000
+ const parseRankRequest = async (request) => {
1001
+ let body;
1002
+ try {
1003
+ const text = await readBodyTextWithLimit(request);
1004
+ body = text === "" ? {} : JSON.parse(text);
1005
+ } catch (error) {
1006
+ if (error instanceof LunoraError) {
1007
+ throw error;
1008
+ }
1009
+ throw new LunoraError("Rank body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1010
+ }
1011
+ const candidate = body ?? {};
1012
+ if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1013
+ throw new LunoraError("Rank request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1014
+ }
1015
+ if (typeof candidate.index !== "string" || candidate.index.length === 0) {
1016
+ throw new LunoraError("Rank request is missing `index`", { code: "BAD_REQUEST", status: 400 });
1017
+ }
1018
+ if (typeof candidate.partitionKey !== "string") {
1019
+ throw new LunoraError("Rank request `partitionKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1020
+ }
1021
+ if (typeof candidate.rowId !== "string" || candidate.rowId.length === 0) {
1022
+ throw new LunoraError("Rank request is missing `rowId`", { code: "BAD_REQUEST", status: 400 });
1023
+ }
1024
+ if (!Array.isArray(candidate.sortValues)) {
1025
+ throw new LunoraError("Rank request `sortValues` must be an array", { code: "BAD_REQUEST", status: 400 });
1026
+ }
1027
+ return {
1028
+ index: candidate.index,
1029
+ partitionKey: candidate.partitionKey,
1030
+ rowId: candidate.rowId,
1031
+ sortValues: candidate.sortValues,
1032
+ table: candidate.table
1033
+ };
1034
+ };
1035
+ const parseRankPageDirections = (raw) => {
1036
+ if (raw === void 0) {
1037
+ return void 0;
1038
+ }
1039
+ if (!Array.isArray(raw) || raw.some((d) => d !== "asc" && d !== "desc")) {
1040
+ throw new LunoraError('Rank page request `directions` must be an array of "asc"|"desc"', { code: "BAD_REQUEST", status: 400 });
1041
+ }
1042
+ return raw;
1043
+ };
1044
+ const validateRankPageScalars = (candidate) => {
1045
+ if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1046
+ throw new LunoraError("Rank page request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1047
+ }
1048
+ if (typeof candidate.index !== "string" || candidate.index.length === 0) {
1049
+ throw new LunoraError("Rank page request is missing `index`", { code: "BAD_REQUEST", status: 400 });
1050
+ }
1051
+ if (candidate.partitionKey !== void 0 && typeof candidate.partitionKey !== "string") {
1052
+ throw new LunoraError("Rank page request `partitionKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1053
+ }
1054
+ if (candidate.take !== void 0 && (typeof candidate.take !== "number" || !Number.isFinite(candidate.take))) {
1055
+ throw new LunoraError("Rank page request `take` must be a number", { code: "BAD_REQUEST", status: 400 });
1056
+ }
1057
+ if (candidate.cursor !== void 0 && candidate.cursor !== null && typeof candidate.cursor !== "string") {
1058
+ throw new LunoraError("Rank page request `cursor` must be a string or null", { code: "BAD_REQUEST", status: 400 });
1059
+ }
1060
+ };
1061
+ const parseRankPageRequest = async (request) => {
1062
+ let body;
1063
+ try {
1064
+ const text = await readBodyTextWithLimit(request);
1065
+ body = text === "" ? {} : JSON.parse(text);
1066
+ } catch (error) {
1067
+ if (error instanceof LunoraError) {
1068
+ throw error;
1069
+ }
1070
+ throw new LunoraError("Rank page body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1071
+ }
1072
+ const candidate = body ?? {};
1073
+ validateRankPageScalars(candidate);
1074
+ const directions = parseRankPageDirections(candidate.directions);
1075
+ return {
1076
+ // 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
1077
+ cursor: typeof candidate.cursor === "string" ? candidate.cursor : null,
1078
+ directions,
1079
+ index: candidate.index,
1080
+ partitionKey: typeof candidate.partitionKey === "string" ? candidate.partitionKey : void 0,
1081
+ table: candidate.table,
1082
+ take: typeof candidate.take === "number" ? candidate.take : void 0
1083
+ };
1084
+ };
1085
+ const parseShardTrafficRequest = async (request) => {
1086
+ let body;
1087
+ try {
1088
+ const text = await readBodyTextWithLimit(request);
1089
+ body = text === "" ? {} : JSON.parse(text);
1090
+ } catch (error) {
1091
+ if (error instanceof LunoraError) {
1092
+ throw error;
1093
+ }
1094
+ throw new LunoraError("Shard-traffic body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1095
+ }
1096
+ const candidate = body ?? {};
1097
+ if (typeof candidate.table !== "string" || candidate.table.length === 0) {
1098
+ throw new LunoraError("Shard-traffic request is missing `table`", { code: "BAD_REQUEST", status: 400 });
1099
+ }
1100
+ return { table: candidate.table };
1101
+ };
1102
+ const parsePitrRequest = async (request) => {
1103
+ const body = await readJsonBodyWithLimit(request);
1104
+ const candidate = body;
1105
+ if (typeof candidate.functionPath !== "string" || !PITR_ADMIN_OPS.has(candidate.functionPath)) {
1106
+ throw new LunoraError("PITR request `functionPath` must be a PITR admin op", { code: "BAD_REQUEST", status: 400 });
1107
+ }
1108
+ if (candidate.shardKey !== void 0 && typeof candidate.shardKey !== "string") {
1109
+ throw new LunoraError("PITR `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1110
+ }
1111
+ return {
1112
+ args: candidate.args ?? {},
1113
+ functionPath: candidate.functionPath,
1114
+ shardKey: candidate.shardKey
1115
+ };
1116
+ };
1117
+ const buildOrchestrationAdminRoutes = (deps) => {
1118
+ const { defaultShard, forwardToShard, isAdmin, queryCoordinator, resolveForwardContext, shardDO } = deps;
1119
+ const handleMigrate = async (request, env) => {
1120
+ if (request.method !== "POST") {
1121
+ throw new LunoraError("Migration endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1122
+ }
1123
+ if (!isAdmin(request)) {
1124
+ throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1125
+ }
1126
+ if (!queryCoordinator) {
1127
+ throw new LunoraError("Migration endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1128
+ }
1129
+ const migrate = await parseMigrateRequest(request);
1130
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1131
+ const result = await queryCoordinator.orchestrateMigration(shardDO, {
1132
+ args: migrate.args,
1133
+ functionPath: migrate.functionPath,
1134
+ headers: forwardedHeaders,
1135
+ table: migrate.table
1136
+ });
1137
+ return Response.json(result, {
1138
+ headers: { "content-type": "application/json" },
1139
+ status: 200
1140
+ });
1141
+ };
1142
+ const handleRank = async (request, env) => {
1143
+ if (request.method !== "POST") {
1144
+ throw new LunoraError("Rank endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1145
+ }
1146
+ if (!isAdmin(request)) {
1147
+ throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1148
+ }
1149
+ if (!queryCoordinator) {
1150
+ throw new LunoraError("Rank endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1151
+ }
1152
+ const rank = await parseRankRequest(request);
1153
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1154
+ const result = await queryCoordinator.orchestrateRank(shardDO, {
1155
+ headers: forwardedHeaders,
1156
+ index: rank.index,
1157
+ partitionKey: rank.partitionKey,
1158
+ rowId: rank.rowId,
1159
+ sortValues: rank.sortValues,
1160
+ table: rank.table
1161
+ });
1162
+ return Response.json(result, {
1163
+ headers: { "content-type": "application/json" },
1164
+ status: 200
1165
+ });
1166
+ };
1167
+ const handleRankPage = async (request, env) => {
1168
+ if (request.method !== "POST") {
1169
+ throw new LunoraError("Rank page endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1170
+ }
1171
+ if (!isAdmin(request)) {
1172
+ throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1173
+ }
1174
+ if (!queryCoordinator) {
1175
+ throw new LunoraError("Rank page endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1176
+ }
1177
+ const rankPage = await parseRankPageRequest(request);
1178
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1179
+ const result = await queryCoordinator.orchestrateRankPage(shardDO, {
1180
+ ...rankPage,
1181
+ headers: forwardedHeaders
1182
+ });
1183
+ return Response.json(result, {
1184
+ headers: { "content-type": "application/json" },
1185
+ status: 200
1186
+ });
1187
+ };
1188
+ const handleShardTraffic = async (request, env) => {
1189
+ if (request.method !== "POST") {
1190
+ throw new LunoraError("Shard-traffic endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1191
+ }
1192
+ if (!isAdmin(request)) {
1193
+ throw new LunoraError("Admin auth required", { code: "FORBIDDEN", status: 403 });
1194
+ }
1195
+ if (!queryCoordinator) {
1196
+ throw new LunoraError("Shard-traffic endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
1197
+ }
1198
+ const trafficRequest = await parseShardTrafficRequest(request);
1199
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1200
+ const result = await queryCoordinator.orchestrateShardTraffic(shardDO, {
1201
+ headers: forwardedHeaders,
1202
+ table: trafficRequest.table
1203
+ });
1204
+ return Response.json(result, {
1205
+ headers: { "content-type": "application/json" },
1206
+ status: 200
1207
+ });
1208
+ };
1209
+ const handlePitr = async (request, env) => {
1210
+ if (request.method !== "POST") {
1211
+ throw new LunoraError("PITR endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1212
+ }
1213
+ if (!isAdmin(request)) {
1214
+ throw new LunoraError("admin PITR endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1215
+ }
1216
+ const pitr = await parsePitrRequest(request);
1217
+ const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
1218
+ const forwarded = new Request("https://shard.internal/rpc", {
1219
+ body: JSON.stringify({ args: pitr.args, functionPath: pitr.functionPath }),
1220
+ headers: forwardedHeaders,
1221
+ method: "POST"
1222
+ });
1223
+ return forwardToShard(shardDO, pitr.shardKey ?? defaultShard, forwarded);
1224
+ };
1225
+ return {
1226
+ [MIGRATE_PATH]: handleMigrate,
1227
+ [PITR_PATH]: handlePitr,
1228
+ [RANK_PATH]: handleRank,
1229
+ [RANKPAGE_PATH]: handleRankPage,
1230
+ [SHARD_TRAFFIC_PATH]: handleShardTraffic
1231
+ };
1232
+ };
1233
+
1234
+ const SCHEDULED_PATH = "/_lunora/admin/scheduled";
1235
+ const SCHEDULED_STATUS_PATH = "/_lunora/admin/scheduled/status";
1236
+ const SCHEDULED_WS_PATH = "/_lunora/admin/scheduled/ws";
1237
+ const SCHEDULED_CANCEL_PATH = "/_lunora/admin/scheduled/cancel";
1238
+ const SCHEDULED_DEAD_PATH = "/_lunora/admin/scheduled/dead";
1239
+ const SCHEDULED_DEAD_RETRY_PATH = "/_lunora/admin/scheduled/dead/retry";
1240
+ const SCHEDULED_DEAD_CANCEL_PATH = "/_lunora/admin/scheduled/dead/cancel";
1241
+ const buildScheduledAdminRoutes = (deps) => {
1242
+ const { checkWsAdmin, requireSchedulerNamespace, resolveSchedulerStub, schedulerInstanceName } = deps;
1243
+ const handleScheduledList = async (request) => {
1244
+ if (request.method !== "GET") {
1245
+ throw new LunoraError("Scheduled-list endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1246
+ }
1247
+ const stub = resolveSchedulerStub(request);
1248
+ return stub.fetch(new Request("https://scheduler.internal/list", { method: "GET" }));
1249
+ };
1250
+ const handleSchedulerStatus = async (request) => {
1251
+ if (request.method !== "GET") {
1252
+ throw new LunoraError("Scheduler-status endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1253
+ }
1254
+ const stub = resolveSchedulerStub(request);
1255
+ return stub.fetch(new Request("https://scheduler.internal/status", { method: "GET" }));
1256
+ };
1257
+ const handleScheduledWebSocket = (request) => {
1258
+ if (request.headers.get("Upgrade") !== "websocket") {
1259
+ throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
1260
+ }
1261
+ if (!checkWsAdmin(request)) {
1262
+ throw new LunoraError("admin authorization required", { code: "ADMIN_FORBIDDEN", status: 403 });
1263
+ }
1264
+ const namespace = requireSchedulerNamespace();
1265
+ const stub = resolveShard(namespace, schedulerInstanceName);
1266
+ return stub.fetch(new Request("https://scheduler.internal/ws", { headers: { Upgrade: "websocket" } }));
1267
+ };
1268
+ const handleScheduledCancel = async (request) => {
1269
+ if (request.method !== "POST") {
1270
+ throw new LunoraError("Scheduled-cancel endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1271
+ }
1272
+ const stub = resolveSchedulerStub(request);
1273
+ const body = await request.json().catch(() => void 0);
1274
+ if (typeof body?.id !== "string" || body.id === "") {
1275
+ throw new LunoraError("Scheduled-cancel requires a string `id`", { code: "BAD_REQUEST", status: 400 });
1276
+ }
1277
+ return stub.fetch(
1278
+ new Request("https://scheduler.internal/cancel", {
1279
+ body: JSON.stringify({ id: body.id }),
1280
+ headers: { "content-type": "application/json" },
1281
+ method: "POST"
1282
+ })
1283
+ );
1284
+ };
1285
+ const handleDeadList = async (request) => {
1286
+ if (request.method !== "GET") {
1287
+ throw new LunoraError("Scheduled dead-letter endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1288
+ }
1289
+ const stub = resolveSchedulerStub(request);
1290
+ return stub.fetch(new Request("https://scheduler.internal/dead", { method: "GET" }));
1291
+ };
1292
+ const proxyDeadAction = (doPath) => async (request) => {
1293
+ if (request.method !== "POST") {
1294
+ throw new LunoraError("Scheduled dead-letter action requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1295
+ }
1296
+ const stub = resolveSchedulerStub(request);
1297
+ const body = await request.json().catch(() => void 0);
1298
+ if (typeof body?.id !== "string" || body.id === "") {
1299
+ throw new LunoraError("Scheduled dead-letter action requires a string `id`", { code: "BAD_REQUEST", status: 400 });
1300
+ }
1301
+ return stub.fetch(
1302
+ new Request(`https://scheduler.internal${doPath}`, {
1303
+ body: JSON.stringify({ id: body.id }),
1304
+ headers: { "content-type": "application/json" },
1305
+ method: "POST"
1306
+ })
1307
+ );
1308
+ };
1309
+ return {
1310
+ [SCHEDULED_CANCEL_PATH]: handleScheduledCancel,
1311
+ [SCHEDULED_DEAD_CANCEL_PATH]: proxyDeadAction("/dead/cancel"),
1312
+ [SCHEDULED_DEAD_PATH]: handleDeadList,
1313
+ [SCHEDULED_DEAD_RETRY_PATH]: proxyDeadAction("/dead/retry"),
1314
+ [SCHEDULED_PATH]: handleScheduledList,
1315
+ [SCHEDULED_STATUS_PATH]: handleSchedulerStatus,
1316
+ [SCHEDULED_WS_PATH]: handleScheduledWebSocket
1317
+ };
1318
+ };
1319
+
1320
+ const STORAGE_PATH = "/_lunora/admin/storage";
1321
+ const STORAGE_URL_PATH = "/_lunora/admin/storage/url";
1322
+ const STORAGE_BUCKETS_PATH = "/_lunora/admin/storage/buckets";
1323
+ const MAX_STORAGE_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
1324
+ const buildStorageAdminRoutes = (deps) => {
1325
+ const { assertAdmin, parsePaging, queryParameter, readBodyBytes, requireAdminOption, storage } = deps;
1326
+ const requireStorageKey = (url) => {
1327
+ const key = queryParameter(url, "key");
1328
+ if (key === void 0) {
1329
+ throw new LunoraError("Storage endpoint requires a `key` query parameter", { code: "BAD_REQUEST", status: 400 });
1330
+ }
1331
+ return key;
1332
+ };
1333
+ const handleStorageList = async (request) => {
1334
+ const storageList = requireAdminOption(request, storage.storageList, {
1335
+ code: "STORAGE_NOT_CONFIGURED",
1336
+ message: "storage endpoint requires a `storageList` function on the worker"
1337
+ });
1338
+ const url = new URL(request.url);
1339
+ const result = await storageList(queryParameter(url, "prefix"), {
1340
+ bucket: queryParameter(url, "bucket"),
1341
+ cursor: queryParameter(url, "cursor"),
1342
+ ...parsePaging(request)
1343
+ });
1344
+ return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
1345
+ };
1346
+ const handleStorageBuckets = (request) => {
1347
+ if (request.method !== "GET") {
1348
+ throw new LunoraError("Storage-buckets endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1349
+ }
1350
+ assertAdmin(request);
1351
+ return Response.json({ buckets: storage.storageBuckets ?? [] }, { headers: { "content-type": "application/json" }, status: 200 });
1352
+ };
1353
+ const handleStorageDelete = async (request) => {
1354
+ const storageDelete = requireAdminOption(request, storage.storageDelete, {
1355
+ code: "STORAGE_DELETE_NOT_CONFIGURED",
1356
+ message: "storage delete requires a `storageDelete` function on the worker"
1357
+ });
1358
+ const url = new URL(request.url);
1359
+ const key = requireStorageKey(url);
1360
+ await storageDelete(key, { bucket: queryParameter(url, "bucket") });
1361
+ return Response.json({ deleted: true, key }, { headers: { "content-type": "application/json" }, status: 200 });
1362
+ };
1363
+ const handleStorageUpload = async (request) => {
1364
+ const storageUpload = requireAdminOption(request, storage.storageUpload, {
1365
+ code: "STORAGE_UPLOAD_NOT_CONFIGURED",
1366
+ message: "storage upload requires a `storageUpload` function on the worker"
1367
+ });
1368
+ const url = new URL(request.url);
1369
+ const key = requireStorageKey(url);
1370
+ const body = await readBodyBytes(request);
1371
+ const headerContentType = request.headers.get("content-type");
1372
+ const contentType = headerContentType === null || headerContentType === "" ? void 0 : headerContentType;
1373
+ const result = await storageUpload(key, body, { bucket: queryParameter(url, "bucket"), contentType });
1374
+ return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
1375
+ };
1376
+ const handleStorage = async (request) => {
1377
+ switch (request.method) {
1378
+ case "DELETE": {
1379
+ return handleStorageDelete(request);
1380
+ }
1381
+ case "GET": {
1382
+ return handleStorageList(request);
1383
+ }
1384
+ case "POST":
1385
+ case "PUT": {
1386
+ return handleStorageUpload(request);
1387
+ }
1388
+ default: {
1389
+ throw new LunoraError("Storage endpoint requires GET, PUT, POST, or DELETE", { code: "METHOD_NOT_ALLOWED", status: 405 });
1390
+ }
1391
+ }
1392
+ };
1393
+ const handleStorageSignedUrl = async (request) => {
1394
+ if (request.method !== "GET") {
1395
+ throw new LunoraError("Storage URL endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1396
+ }
1397
+ const storageSignedUrl = requireAdminOption(request, storage.storageSignedUrl, {
1398
+ code: "STORAGE_URL_NOT_CONFIGURED",
1399
+ message: "storage URL endpoint requires a `storageSignedUrl` function on the worker"
1400
+ });
1401
+ const url = new URL(request.url);
1402
+ const key = requireStorageKey(url);
1403
+ const expiresInRaw = Number(queryParameter(url, "expiresIn") ?? "");
1404
+ const expiresInSeconds = Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? Math.min(expiresInRaw, MAX_STORAGE_EXPIRES_IN_SECONDS) : void 0;
1405
+ const signedUrl = await storageSignedUrl(key, { bucket: queryParameter(url, "bucket"), expiresInSeconds });
1406
+ return Response.json({ key, url: signedUrl }, { headers: { "content-type": "application/json" }, status: 200 });
1407
+ };
1408
+ return {
1409
+ [STORAGE_BUCKETS_PATH]: handleStorageBuckets,
1410
+ [STORAGE_PATH]: handleStorage,
1411
+ [STORAGE_URL_PATH]: handleStorageSignedUrl
1412
+ };
1413
+ };
1414
+
1415
+ const VECTOR_INDEXES_PATH = "/_lunora/admin/vector/indexes";
1416
+ const VECTOR_QUERY_PATH = "/_lunora/admin/vector/query";
1417
+ const buildVectorAdminRoutes = (deps) => {
1418
+ const { readJsonBody, requireAdminOption } = deps;
1419
+ const handleVectorIndexes = async (request) => {
1420
+ if (request.method !== "GET") {
1421
+ throw new LunoraError("Vector-indexes endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1422
+ }
1423
+ const introspector = requireAdminOption(request, deps.vectorIntrospector, {
1424
+ code: "VECTORS_NOT_CONFIGURED",
1425
+ message: "vector endpoints require a `vectorIntrospector` on the worker"
1426
+ });
1427
+ return Response.json({ indexes: await introspector.listIndexes() }, { headers: { "content-type": "application/json" }, status: 200 });
1428
+ };
1429
+ const handleVectorQuery = async (request) => {
1430
+ if (request.method !== "POST") {
1431
+ throw new LunoraError("Vector-query endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1432
+ }
1433
+ const introspector = requireAdminOption(request, deps.vectorIntrospector, {
1434
+ code: "VECTORS_NOT_CONFIGURED",
1435
+ message: "vector endpoints require a `vectorIntrospector` on the worker"
1436
+ });
1437
+ if (introspector.queryIndex === void 0) {
1438
+ throw new LunoraError("vector index querying is not enabled on this worker", { code: "VECTOR_QUERY_UNSUPPORTED", status: 400 });
1439
+ }
1440
+ const body = await readJsonBody(request);
1441
+ const candidate = body;
1442
+ if (typeof candidate.name !== "string" || candidate.name === "") {
1443
+ throw new LunoraError("Vector-query request requires a `name` string", { code: "BAD_REQUEST", status: 400 });
1444
+ }
1445
+ if (typeof candidate.text !== "string" || candidate.text === "") {
1446
+ throw new LunoraError("Vector-query request requires a `text` string", { code: "BAD_REQUEST", status: 400 });
1447
+ }
1448
+ if (candidate.topK !== void 0 && (typeof candidate.topK !== "number" || !Number.isInteger(candidate.topK) || candidate.topK < 1)) {
1449
+ throw new LunoraError("Vector-query `topK` must be a positive integer", { code: "BAD_REQUEST", status: 400 });
1450
+ }
1451
+ const result = await introspector.queryIndex({ name: candidate.name, text: candidate.text, topK: candidate.topK });
1452
+ return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
1453
+ };
1454
+ return {
1455
+ [VECTOR_INDEXES_PATH]: handleVectorIndexes,
1456
+ [VECTOR_QUERY_PATH]: handleVectorQuery
1457
+ };
1458
+ };
1459
+
1460
+ const WORKFLOWS_INSTANCES_PATH = "/_lunora/admin/workflows/instances";
1461
+ const WORKFLOWS_INSTANCE_PATH = "/_lunora/admin/workflows/instance";
1462
+ const WORKFLOWS_STATUS_PATH = "/_lunora/admin/workflows/status";
1463
+ const INSTANCE_STATUSES = {
1464
+ complete: true,
1465
+ errored: true,
1466
+ paused: true,
1467
+ queued: true,
1468
+ running: true,
1469
+ terminated: true,
1470
+ unknown: true,
1471
+ waiting: true,
1472
+ waitingForPause: true
1473
+ };
1474
+ const toInstanceStatus = (value) => value !== null && Object.hasOwn(INSTANCE_STATUSES, value) ? value : void 0;
1475
+ const positiveIntParameter = (url, key) => {
1476
+ const raw = url.searchParams.get(key);
1477
+ if (raw === null) {
1478
+ return void 0;
1479
+ }
1480
+ const value = Number(raw);
1481
+ return Number.isInteger(value) && value > 0 ? value : void 0;
1482
+ };
1483
+ const requireQuery = (url, key) => {
1484
+ const value = url.searchParams.get(key);
1485
+ if (value === null || value === "") {
1486
+ throw new LunoraError(`Workflows admin endpoint requires a \`${key}\` query parameter`, { code: "BAD_REQUEST", status: 400 });
1487
+ }
1488
+ return value;
1489
+ };
1490
+ const throwNotConfigured = () => {
1491
+ throw new LunoraError("Workflow inspection is unconfigured. Set CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN in your .dev.vars to enable it.", {
1492
+ code: "WORKFLOWS_NOT_CONFIGURED",
1493
+ status: 501
1494
+ });
1495
+ };
1496
+ const buildWorkflowsAdminRoutes = (deps) => {
1497
+ const { assertAdmin, resolveWorkflowsClient } = deps;
1498
+ const handleInstances = async (request, env, url) => {
1499
+ if (request.method !== "GET") {
1500
+ throw new LunoraError("Workflows instances endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1501
+ }
1502
+ assertAdmin(request);
1503
+ const client = resolveWorkflowsClient(env);
1504
+ if (!client) {
1505
+ return throwNotConfigured();
1506
+ }
1507
+ const workflowName = requireQuery(url, "name");
1508
+ const status = toInstanceStatus(url.searchParams.get("status"));
1509
+ return Response.json(
1510
+ await client.listInstances({
1511
+ page: positiveIntParameter(url, "page"),
1512
+ perPage: positiveIntParameter(url, "perPage"),
1513
+ status,
1514
+ workflowName
1515
+ })
1516
+ );
1517
+ };
1518
+ const handleInstance = async (request, env, url) => {
1519
+ if (request.method !== "GET") {
1520
+ throw new LunoraError("Workflows instance endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1521
+ }
1522
+ assertAdmin(request);
1523
+ const client = resolveWorkflowsClient(env);
1524
+ if (!client) {
1525
+ return throwNotConfigured();
1526
+ }
1527
+ return Response.json(await client.getInstance({ instanceId: requireQuery(url, "id"), workflowName: requireQuery(url, "name") }));
1528
+ };
1529
+ const handleStatus = async (request, env) => {
1530
+ if (request.method !== "POST") {
1531
+ throw new LunoraError("Workflows status endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1532
+ }
1533
+ assertAdmin(request);
1534
+ const client = resolveWorkflowsClient(env);
1535
+ if (!client) {
1536
+ return throwNotConfigured();
1537
+ }
1538
+ const body = await request.json().catch(() => void 0);
1539
+ if (typeof body?.name !== "string" || body.name === "" || typeof body.id !== "string" || body.id === "") {
1540
+ throw new LunoraError("Workflows status action requires string `name` and `id`", { code: "BAD_REQUEST", status: 400 });
1541
+ }
1542
+ const { action } = body;
1543
+ if (action !== "pause" && action !== "resume" && action !== "terminate") {
1544
+ throw new LunoraError("Workflows status action must be one of: pause, resume, terminate", { code: "BAD_REQUEST", status: 400 });
1545
+ }
1546
+ return Response.json(await client.setInstanceStatus({ action, instanceId: body.id, workflowName: body.name }));
1547
+ };
1548
+ return {
1549
+ [WORKFLOWS_INSTANCE_PATH]: handleInstance,
1550
+ [WORKFLOWS_INSTANCES_PATH]: handleInstances,
1551
+ [WORKFLOWS_STATUS_PATH]: handleStatus
1552
+ };
1553
+ };
1554
+
1555
+ const NDJSON_ENCODER = new TextEncoder();
1556
+ const RPC_PATH = "/_lunora/rpc";
1557
+ const WS_PATH = "/_lunora/ws";
1558
+ const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
1559
+ const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
1560
+ const DEFAULT_AUTH_BASE_PATH = "/api/auth";
1561
+ const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
1562
+ const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
1563
+ const isAuthAttemptPath = (pathname, basePath) => {
1564
+ const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
1565
+ if (!pathname.startsWith(`${base}/`)) {
1566
+ return false;
1567
+ }
1568
+ const suffix = pathname.slice(base.length);
1569
+ return AUTH_ATTEMPT_SEGMENTS.some((segment) => suffix === segment || suffix.startsWith(`${segment}/`));
1570
+ };
1571
+ const buildErrorEvent = (functionPath, durationMs, error, extra) => {
1572
+ const mappable = error instanceof LunoraError || isStructuralLunoraError(error) || isStructuralConflictError(error);
1573
+ const code = mappable ? error.code : "INTERNAL_SERVER_ERROR";
1574
+ const status = mappable ? error.status : 500;
1575
+ const message = error instanceof Error ? error.message : String(error);
1576
+ return {
1577
+ durationMs,
1578
+ error: { code, message, status },
1579
+ functionPath,
1580
+ ok: false,
1581
+ ...extra.fanOut ? { fanOut: { failed: 0, shards: 0, table: extra.fanOut.table } } : {},
1582
+ ...extra.shardKey ? { shardKey: extra.shardKey } : {}
1583
+ };
1584
+ };
1585
+ const identityExpiryMs = (identity) => {
1586
+ const { exp, expiresAtMs } = identity;
1587
+ if (typeof expiresAtMs === "number" && Number.isFinite(expiresAtMs)) {
1588
+ return expiresAtMs;
1589
+ }
1590
+ if (typeof exp === "number" && Number.isFinite(exp)) {
1591
+ return exp * 1e3;
1592
+ }
1593
+ return void 0;
1594
+ };
1595
+ const resolveForwardContext = async (request, env, resolveIdentity) => {
1596
+ const headers = { "content-type": "application/json" };
1597
+ const authorization = request.headers.get("authorization");
1598
+ const cookie = request.headers.get("cookie");
1599
+ const bookmark = request.headers.get("x-d1-bookmark");
1600
+ const mutationId = request.headers.get("x-lunora-mutation-id");
1601
+ if (authorization) {
1602
+ headers["authorization"] = authorization;
1603
+ }
1604
+ if (cookie) {
1605
+ headers["cookie"] = cookie;
1606
+ }
1607
+ if (bookmark) {
1608
+ headers["x-d1-bookmark"] = bookmark;
1609
+ }
1610
+ if (mutationId) {
1611
+ headers["x-lunora-mutation-id"] = mutationId;
1612
+ }
1613
+ const clientIp = request.headers.get("cf-connecting-ip");
1614
+ if (clientIp) {
1615
+ headers["x-lunora-client-ip"] = clientIp;
1616
+ }
1617
+ if (!resolveIdentity) {
1618
+ return { claims: null, headers, identity: null, userId: null };
1619
+ }
1620
+ const identity = await resolveIdentity(request, env);
1621
+ if (!identity || typeof identity.userId !== "string" || identity.userId.length === 0) {
1622
+ return { claims: null, headers, identity: null, userId: null };
1623
+ }
1624
+ headers["x-lunora-userid"] = identity.userId;
1625
+ const expiresAtMs = identityExpiryMs(identity);
1626
+ if (expiresAtMs !== void 0) {
1627
+ headers["x-lunora-identity-exp"] = String(expiresAtMs);
1628
+ }
1629
+ const { userId, ...extra } = identity;
1630
+ const claims = Object.keys(extra).length > 0 ? extra : null;
1631
+ if (claims) {
1632
+ headers["x-lunora-identity"] = JSON.stringify(claims);
1633
+ }
1634
+ return { claims, headers, identity, userId };
1635
+ };
1636
+ const KNOWN_MERGE_KINDS = /* @__PURE__ */ new Set(["concat", "first", "groupBy", "max", "min", "rank", "sum", "topK"]);
1637
+ const validateFanOut = (fanOut) => {
1638
+ if (fanOut === void 0) {
1639
+ return void 0;
1640
+ }
1641
+ if (!fanOut || typeof fanOut !== "object") {
1642
+ throw new LunoraError("RPC `fanOut` must be an object", { code: "BAD_REQUEST", status: 400 });
1643
+ }
1644
+ const spec = fanOut;
1645
+ if (typeof spec.table !== "string" || spec.table.length === 0) {
1646
+ throw new LunoraError("RPC `fanOut.table` must be a non-empty string", { code: "BAD_REQUEST", status: 400 });
1647
+ }
1648
+ if (!spec.merge || typeof spec.merge !== "object") {
1649
+ throw new LunoraError("RPC `fanOut.merge` must be an object", { code: "BAD_REQUEST", status: 400 });
1650
+ }
1651
+ const merge = spec.merge;
1652
+ if (typeof merge.kind !== "string" || !KNOWN_MERGE_KINDS.has(merge.kind)) {
1653
+ throw new LunoraError("RPC `fanOut.merge.kind` is not a recognized merge strategy", { code: "BAD_REQUEST", status: 400 });
1654
+ }
1655
+ if (merge.kind === "topK") {
1656
+ if (typeof merge.k !== "number" || !Number.isInteger(merge.k) || merge.k < 0) {
1657
+ throw new LunoraError("RPC `fanOut.merge.k` must be a non-negative integer", { code: "BAD_REQUEST", status: 400 });
1658
+ }
1659
+ if (typeof merge.by !== "string" || merge.by.length === 0) {
1660
+ throw new LunoraError("RPC `fanOut.merge.by` must be a non-empty string", { code: "BAD_REQUEST", status: 400 });
1661
+ }
1662
+ }
1663
+ return spec;
1664
+ };
1665
+ const parseEnvelope = async (request) => {
1666
+ const text = await readBodyTextWithLimit(request);
1667
+ let body;
1668
+ try {
1669
+ body = JSON.parse(text);
1670
+ } catch {
1671
+ throw new LunoraError("RPC body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1672
+ }
1673
+ if (!body || typeof body !== "object" || typeof body.functionPath !== "string") {
1674
+ throw new LunoraError("RPC envelope is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
1675
+ }
1676
+ const raw = body;
1677
+ if (raw.args !== void 0 && (typeof raw.args !== "object" || raw.args === null || Array.isArray(raw.args))) {
1678
+ throw new LunoraError("RPC `args` must be an object", { code: "BAD_REQUEST", status: 400 });
1679
+ }
1680
+ if (raw.shardKey !== void 0 && typeof raw.shardKey !== "string") {
1681
+ throw new LunoraError("RPC `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1682
+ }
1683
+ const envelope = body;
1684
+ return {
1685
+ args: envelope.args ?? {},
1686
+ fanOut: validateFanOut(envelope.fanOut),
1687
+ functionPath: envelope.functionPath,
1688
+ shardKey: envelope.shardKey
1689
+ };
1690
+ };
1691
+ const forwardToShard = async (namespace, shardKey, request) => {
1692
+ const stub = resolveShard(namespace, shardKey);
1693
+ return stub.fetch(request);
1694
+ };
1695
+ const constantTimeEqual = (expected, supplied) => {
1696
+ const max = Math.max(expected.length, supplied.length);
1697
+ let diff = expected.length ^ supplied.length;
1698
+ for (let index = 0; index < max; index += 1) {
1699
+ const expectedCode = index < expected.length ? expected.codePointAt(index) ?? 0 : 0;
1700
+ const suppliedCode = index < supplied.length ? supplied.codePointAt(index) ?? 0 : 0;
1701
+ diff |= expectedCode ^ suppliedCode;
1702
+ }
1703
+ return diff === 0;
1704
+ };
1705
+ const verifyHmacSignature = async (secret, body, suppliedSignature) => {
1706
+ if (secret.length === 0 || suppliedSignature.length === 0) {
1707
+ return false;
1708
+ }
1709
+ const encoder = new TextEncoder();
1710
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
1711
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1712
+ const bytes = new Uint8Array(signature);
1713
+ let binary = "";
1714
+ for (const byte of bytes) {
1715
+ binary += String.fromCodePoint(byte);
1716
+ }
1717
+ const expected = btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
1718
+ return constantTimeEqual(expected, suppliedSignature);
1719
+ };
1720
+ const checkAdminAuth = (request, expected) => {
1721
+ if (!expected || expected.length === 0) {
1722
+ return false;
1723
+ }
1724
+ const authorization = request.headers.get("authorization");
1725
+ if (!authorization) {
1726
+ return false;
1727
+ }
1728
+ const [scheme, ...rest] = authorization.split(" ");
1729
+ if (scheme?.toLowerCase() !== "bearer") {
1730
+ return false;
1731
+ }
1732
+ return constantTimeEqual(expected, rest.join(" ").trim());
1733
+ };
1734
+ const checkAdminWsToken = (request, expected) => {
1735
+ if (!expected || expected.length === 0) {
1736
+ return false;
1737
+ }
1738
+ const supplied = new URL(request.url).searchParams.get("token");
1739
+ return supplied !== null && constantTimeEqual(expected, supplied);
1740
+ };
1741
+ const createWorker = (options) => {
1742
+ const defaultShard = options.defaultShardKey ?? "__root__";
1743
+ const hasAnyShardAuth = Boolean(options.authorizeShard) || Boolean(options.authorizeFanOut);
1744
+ let warnedUnauthenticatedShardAccess = false;
1745
+ const warnUnauthenticatedShardAccessOnce = (kind) => {
1746
+ if (hasAnyShardAuth || options.allowUnauthenticatedShardAccess || warnedUnauthenticatedShardAccess) {
1747
+ return;
1748
+ }
1749
+ warnedUnauthenticatedShardAccess = true;
1750
+ console.warn(
1751
+ [
1752
+ `[lunora] SECURITY: received ${kind} access but neither \`authorizeShard\` nor \`authorizeFanOut\` is configured — `,
1753
+ `any caller (including unauthenticated ones) can target any shard / fan out across the table. `,
1754
+ `Configure \`authorizeShard\`/\`authorizeFanOut\`, or set \`allowUnauthenticatedShardAccess: true\` to acknowledge this posture and silence this warning.`
1755
+ ].join("")
1756
+ );
1757
+ };
1758
+ const orchestrationAdminRoutes = buildOrchestrationAdminRoutes({
1759
+ defaultShard,
1760
+ forwardToShard,
1761
+ isAdmin: (request) => checkAdminAuth(request, options.adminToken),
1762
+ queryCoordinator: options.queryCoordinator,
1763
+ resolveForwardContext: (request, env) => resolveForwardContext(request, env, options.resolveIdentity),
1764
+ shardDO: options.shardDO
1765
+ });
1766
+ const dispatchToShard = async (functionPath, args, shardKey) => {
1767
+ if (options.authorizeShard) {
1768
+ const allowed = await options.authorizeShard(null, shardKey);
1769
+ if (!allowed) {
1770
+ throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
1771
+ }
1772
+ }
1773
+ const forwarded = new Request("https://shard.internal/rpc", {
1774
+ // `x-lunora-system` marks this as a trusted server-initiated dispatch
1775
+ // so the shard may run `internal` functions (scheduled/cron jobs are
1776
+ // typically internal). Authorization was already enforced above; this
1777
+ // header is set only here, never on the client RPC path.
1778
+ body: JSON.stringify({ args, functionPath }),
1779
+ headers: { "content-type": "application/json", "x-lunora-system": "1" },
1780
+ method: "POST"
1781
+ });
1782
+ return forwardToShard(options.shardDO, shardKey, forwarded);
1783
+ };
1784
+ const startCronWorkflow = async (binding, job, env) => {
1785
+ const candidate = env?.[binding];
1786
+ if (!candidate || typeof candidate.create !== "function") {
1787
+ throw new LunoraError(`cron job "${job.name}" targets workflow binding "${binding}", which is not bound on env`, {
1788
+ code: "CRON_JOB_FAILED",
1789
+ status: 500
1790
+ });
1791
+ }
1792
+ await candidate.create({ params: job.args ?? {} });
1793
+ };
1794
+ const runOneCronJob = async (job, env) => {
1795
+ if (job.workflow) {
1796
+ await startCronWorkflow(job.workflow, job, env);
1797
+ return;
1798
+ }
1799
+ if (job.functionPath === void 0) {
1800
+ throw new LunoraError(`cron job "${job.name}" has neither a function target nor a workflow target`, {
1801
+ code: "CRON_JOB_FAILED",
1802
+ status: 500
1803
+ });
1804
+ }
1805
+ const response = await dispatchToShard(job.functionPath, job.args ?? {}, job.shardKey ?? defaultShard);
1806
+ if (!response.ok) {
1807
+ throw new LunoraError(`cron job "${job.name}" (${job.functionPath}) failed with shard status ${String(response.status)}`, {
1808
+ code: "CRON_JOB_FAILED",
1809
+ status: 500
1810
+ });
1811
+ }
1812
+ };
1813
+ const runCronJobs = async (cron, env, errors, toError) => {
1814
+ const cronJobs = options.cronJobs?.[cron];
1815
+ if (!cronJobs) {
1816
+ return;
1817
+ }
1818
+ for (const job of cronJobs) {
1819
+ try {
1820
+ await runOneCronJob(job, env);
1821
+ } catch (error) {
1822
+ errors.push(toError(error));
1823
+ }
1824
+ }
1825
+ };
1826
+ const handleRunCronJob = async (request, env) => {
1827
+ if (!checkAdminAuth(request, options.adminToken)) {
1828
+ throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1829
+ }
1830
+ if (request.method !== "POST") {
1831
+ throw new LunoraError("cron-jobs run endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1832
+ }
1833
+ if (!options.cronJobs) {
1834
+ throw new LunoraError("cron-jobs run endpoint requires a `cronJobs` map on the worker", { code: "CRON_JOBS_NOT_CONFIGURED", status: 400 });
1835
+ }
1836
+ const body = await readJsonBodyWithLimit(request);
1837
+ const name = typeof body.name === "string" ? body.name : "";
1838
+ if (name === "") {
1839
+ throw new LunoraError("cron-jobs run endpoint requires a job `name`", { code: "BAD_REQUEST", status: 400 });
1840
+ }
1841
+ const job = Object.values(options.cronJobs).flat().find((candidate) => candidate.name === name);
1842
+ if (!job) {
1843
+ throw new LunoraError(`no cron job named "${name}" is registered`, { code: "CRON_JOB_NOT_FOUND", status: 404 });
1844
+ }
1845
+ await runOneCronJob(job, env);
1846
+ return Response.json({ name, ran: true }, { status: 200 });
1847
+ };
1848
+ const releasePoolSlot = async (candidate) => {
1849
+ const pool = typeof candidate.pool === "string" && candidate.pool.length > 0 ? candidate.pool : void 0;
1850
+ if (!pool || !options.schedulerDO || typeof candidate.id !== "string") {
1851
+ return;
1852
+ }
1853
+ const instanceName = typeof candidate.instanceName === "string" && candidate.instanceName.length > 0 ? candidate.instanceName : "default";
1854
+ try {
1855
+ await options.schedulerDO.get(options.schedulerDO.idFromName(instanceName)).fetch(
1856
+ new Request("https://scheduler.internal/complete", {
1857
+ body: JSON.stringify({ id: candidate.id, pool }),
1858
+ headers: { "content-type": "application/json" },
1859
+ method: "POST"
1860
+ })
1861
+ );
1862
+ } catch {
1863
+ }
1864
+ };
1865
+ const handleSchedulerDispatch = async (request, env) => {
1866
+ if (request.method !== "POST") {
1867
+ throw new LunoraError("Scheduler dispatch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
1868
+ }
1869
+ const rawBody = await readBodyTextWithLimit(request);
1870
+ const envRecord = env ?? {};
1871
+ const schedulerSecret = typeof envRecord["LUNORA_SCHEDULER_SECRET"] === "string" ? envRecord["LUNORA_SCHEDULER_SECRET"] : void 0;
1872
+ const adminBearer = options.adminToken ?? (typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0);
1873
+ const signatureHeader = request.headers.get("x-lunora-scheduler-signature");
1874
+ let authenticated = false;
1875
+ if (signatureHeader && schedulerSecret) {
1876
+ authenticated = await verifyHmacSignature(schedulerSecret, rawBody, signatureHeader);
1877
+ } else if (adminBearer) {
1878
+ authenticated = checkAdminAuth(request, adminBearer);
1879
+ }
1880
+ if (!authenticated) {
1881
+ throw new LunoraError("Scheduler dispatch requires a valid signature or admin bearer", { code: "FORBIDDEN", status: 403 });
1882
+ }
1883
+ let body;
1884
+ try {
1885
+ body = JSON.parse(rawBody);
1886
+ } catch {
1887
+ throw new LunoraError("Scheduler dispatch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1888
+ }
1889
+ const candidate = body ?? {};
1890
+ if (typeof candidate.functionPath !== "string" || candidate.functionPath.length === 0) {
1891
+ throw new LunoraError("Scheduler dispatch is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
1892
+ }
1893
+ const args = candidate.args ?? {};
1894
+ const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
1895
+ const response = await dispatchToShard(candidate.functionPath, args, shardKey);
1896
+ await releasePoolSlot(candidate);
1897
+ return response;
1898
+ };
1899
+ const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
1900
+ applyGlobals: options.applyGlobals,
1901
+ isAdmin: (request) => checkAdminAuth(request, options.adminToken),
1902
+ knownTables: () => collectKnownTables(),
1903
+ queryCoordinator: options.queryCoordinator,
1904
+ resolveForwardContext: (request, env) => resolveForwardContext(request, env, options.resolveIdentity),
1905
+ shardDO: options.shardDO,
1906
+ streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow),
1907
+ streamingImport: (request, headers) => streamingImport(request, options, headers),
1908
+ syncGlobals: options.syncGlobals
1909
+ });
1910
+ const assertAdminAuthorized = (request) => {
1911
+ if (!checkAdminAuth(request, options.adminToken)) {
1912
+ throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1913
+ }
1914
+ };
1915
+ const requireAdminOption = (request, value, notConfigured) => {
1916
+ assertAdminAuthorized(request);
1917
+ if (value === void 0) {
1918
+ throw new LunoraError(notConfigured.message, { code: notConfigured.code, status: 400 });
1919
+ }
1920
+ return value;
1921
+ };
1922
+ const queryParameter = (url, name) => {
1923
+ const value = url.searchParams.get(name);
1924
+ return value === null || value === "" ? void 0 : value;
1925
+ };
1926
+ const parsePaging = (request) => {
1927
+ const url = new URL(request.url);
1928
+ const limitParameter = url.searchParams.get("limit");
1929
+ const offsetParameter = url.searchParams.get("offset");
1930
+ const limit = limitParameter === null ? void 0 : Number.parseInt(limitParameter, 10);
1931
+ const offset = offsetParameter === null ? void 0 : Number.parseInt(offsetParameter, 10);
1932
+ return {
1933
+ limit: limit !== void 0 && Number.isFinite(limit) && limit >= 0 ? limit : void 0,
1934
+ offset: offset !== void 0 && Number.isFinite(offset) && offset >= 0 ? offset : void 0
1935
+ };
1936
+ };
1937
+ const requireSchedulerNamespace = () => {
1938
+ if (options.schedulerDO === void 0) {
1939
+ throw new LunoraError("scheduled endpoints require a `schedulerDO` namespace on the worker", { code: "SCHEDULER_NOT_CONFIGURED", status: 400 });
1940
+ }
1941
+ return options.schedulerDO;
1942
+ };
1943
+ const resolveSchedulerStub = (request) => {
1944
+ assertAdminAuthorized(request);
1945
+ return resolveShard(requireSchedulerNamespace(), options.schedulerInstanceName ?? "default");
1946
+ };
1947
+ const scheduledAdminRoutes = buildScheduledAdminRoutes({
1948
+ checkWsAdmin: (request) => checkAdminAuth(request, options.adminToken) || checkAdminWsToken(request, options.adminToken),
1949
+ requireSchedulerNamespace,
1950
+ resolveSchedulerStub,
1951
+ schedulerInstanceName: options.schedulerInstanceName ?? "default"
1952
+ });
1953
+ const workflowsAdminRoutes = buildWorkflowsAdminRoutes({
1954
+ assertAdmin: assertAdminAuthorized,
1955
+ resolveWorkflowsClient: options.workflowsClient ?? (() => void 0)
1956
+ });
1957
+ const storageAdminRoutes = buildStorageAdminRoutes({
1958
+ assertAdmin: assertAdminAuthorized,
1959
+ parsePaging,
1960
+ queryParameter,
1961
+ readBodyBytes: readBodyBytesWithLimit,
1962
+ requireAdminOption,
1963
+ storage: {
1964
+ storageBuckets: options.storageBuckets,
1965
+ storageDelete: options.storageDelete,
1966
+ storageList: options.storageList,
1967
+ storageSignedUrl: options.storageSignedUrl,
1968
+ storageUpload: options.storageUpload
1969
+ }
1970
+ });
1971
+ const vectorAdminRoutes = buildVectorAdminRoutes({
1972
+ readJsonBody: readJsonBodyWithLimit,
1973
+ requireAdminOption,
1974
+ vectorIntrospector: options.vectorIntrospector
1975
+ });
1976
+ const introspectionAdminRoutes = buildIntrospectionAdminRoutes({
1977
+ assertAdmin: assertAdminAuthorized,
1978
+ options: {
1979
+ cronJobs: options.cronJobs,
1980
+ functions: options.functions,
1981
+ globalIntrospector: options.globalIntrospector,
1982
+ openApiSpec: options.openApiSpec,
1983
+ openRpcSpec: options.openRpcSpec
1984
+ },
1985
+ parsePaging,
1986
+ queryParameter,
1987
+ requireAdminOption
1988
+ });
1989
+ const buildHttpActionContext = async (request, env) => {
1990
+ const { claims, headers, userId } = await resolveForwardContext(request, env, options.resolveIdentity);
1991
+ const run = async (reference, args = {}) => {
1992
+ const functionPath = reference.__lunoraRef;
1993
+ if (typeof functionPath !== "string") {
1994
+ throw new LunoraError("ctx.run*: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
1995
+ }
1996
+ const forwarded = new Request("https://shard.internal/rpc", {
1997
+ body: JSON.stringify({ args, functionPath }),
1998
+ headers,
1999
+ method: "POST"
2000
+ });
2001
+ const response = await forwardToShard(options.shardDO, defaultShard, forwarded);
2002
+ const payload = await response.json();
2003
+ if (payload.error) {
2004
+ throw new LunoraError(payload.error.message ?? "shard RPC failed", {
2005
+ code: payload.error.code ?? "INTERNAL",
2006
+ status: response.status
2007
+ });
2008
+ }
2009
+ return payload.result;
2010
+ };
2011
+ return {
2012
+ auth: {
2013
+ getIdentity: () => Promise.resolve(claims),
2014
+ userId
2015
+ },
2016
+ fetch: globalThis.fetch.bind(globalThis),
2017
+ runAction: run,
2018
+ runMutation: run,
2019
+ runQuery: run
2020
+ };
2021
+ };
2022
+ const dispatchHttpRoute = async (request, env, context) => {
2023
+ if (!options.httpRouter) {
2024
+ return void 0;
2025
+ }
2026
+ const httpContext = await buildHttpActionContext(request, env);
2027
+ try {
2028
+ return await options.httpRouter.fetch(request, { ...env, __lunoraCtx: httpContext }, context);
2029
+ } catch (error) {
2030
+ console.error("[lunora] httpRouter (SSR) handler threw:", error);
2031
+ return new Response("Internal Server Error", { status: 500 });
2032
+ }
2033
+ };
2034
+ const handleWebSocketUpgrade = async (request, env, url) => {
2035
+ if (request.headers.get("Upgrade") !== "websocket") {
2036
+ throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
2037
+ }
2038
+ const shardKey = url.searchParams.get("shard") ?? defaultShard;
2039
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2040
+ if (options.authorizeShard) {
2041
+ const allowed = await options.authorizeShard(identity, shardKey);
2042
+ if (!allowed) {
2043
+ throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
2044
+ }
2045
+ } else if (shardKey !== defaultShard) {
2046
+ warnUnauthenticatedShardAccessOnce("shard");
2047
+ }
2048
+ const upgradeHeaders = new Headers(request.headers);
2049
+ upgradeHeaders.delete("x-lunora-userid");
2050
+ upgradeHeaders.delete("x-lunora-identity");
2051
+ upgradeHeaders.delete("x-lunora-identity-exp");
2052
+ const forwardedUserId = forwardedHeaders["x-lunora-userid"];
2053
+ const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
2054
+ const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
2055
+ if (forwardedUserId !== void 0) {
2056
+ upgradeHeaders.set("x-lunora-userid", forwardedUserId);
2057
+ }
2058
+ if (forwardedIdentity !== void 0) {
2059
+ upgradeHeaders.set("x-lunora-identity", forwardedIdentity);
2060
+ }
2061
+ if (forwardedExp !== void 0) {
2062
+ upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
2063
+ }
2064
+ return forwardToShard(options.shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
2065
+ };
2066
+ const authorizeRpcEnvelope = async (envelope, identity) => {
2067
+ if (envelope.fanOut) {
2068
+ if (options.authorizeFanOut) {
2069
+ const allowed = await options.authorizeFanOut(identity, envelope.fanOut.table, envelope.functionPath);
2070
+ if (!allowed) {
2071
+ throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
2072
+ }
2073
+ } else if (envelope.functionPath.startsWith("__lunora_relation__:")) {
2074
+ throw new LunoraError(
2075
+ "reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker",
2076
+ {
2077
+ code: "FORBIDDEN_FANOUT",
2078
+ status: 403
2079
+ }
2080
+ );
2081
+ } else if (options.authorizeShard) {
2082
+ throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
2083
+ code: "FORBIDDEN_FANOUT",
2084
+ status: 403
2085
+ });
2086
+ } else {
2087
+ warnUnauthenticatedShardAccessOnce("fan-out");
2088
+ }
2089
+ return;
2090
+ }
2091
+ if (options.authorizeShard) {
2092
+ const shardKeyForAuth = envelope.shardKey ?? defaultShard;
2093
+ const allowed = await options.authorizeShard(identity, shardKeyForAuth);
2094
+ if (!allowed) {
2095
+ throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
2096
+ }
2097
+ } else if (envelope.shardKey !== void 0 && envelope.shardKey !== defaultShard) {
2098
+ warnUnauthenticatedShardAccessOnce("shard");
2099
+ }
2100
+ };
2101
+ const dispatchSingleShard = async (functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
2102
+ const rpcStartedAt = Date.now();
2103
+ const { observability } = options;
2104
+ const forwarded = new Request(`https://shard.internal/rpc`, {
2105
+ body: JSON.stringify({ args, functionPath }),
2106
+ headers: forwardedHeaders,
2107
+ method: "POST"
2108
+ });
2109
+ try {
2110
+ const response = await forwardToShard(options.shardDO, shardKey, forwarded);
2111
+ emitRpcEvent(
2112
+ observability,
2113
+ {
2114
+ durationMs: Date.now() - rpcStartedAt,
2115
+ functionPath,
2116
+ ok: response.ok,
2117
+ shardKey,
2118
+ ...response.ok ? {} : { error: { code: "SHARD_ERROR", message: `shard returned ${String(response.status)}`, status: response.status } }
2119
+ },
2120
+ sinkContext
2121
+ );
2122
+ const responseBookmark = response.headers.get("x-d1-bookmark");
2123
+ if (responseBookmark) {
2124
+ const headers = new Headers(response.headers);
2125
+ headers.set("x-d1-bookmark", responseBookmark);
2126
+ return new Response(response.body, { headers, status: response.status });
2127
+ }
2128
+ return response;
2129
+ } catch (error) {
2130
+ emitRpcEvent(observability, buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey }), sinkContext);
2131
+ throw error;
2132
+ }
2133
+ };
2134
+ const handleRpc = async (request, env, context) => {
2135
+ if (request.method !== "POST") {
2136
+ throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2137
+ }
2138
+ const envelope = await parseEnvelope(request);
2139
+ if (envelope.fanOut && envelope.shardKey) {
2140
+ throw new LunoraError("RPC envelope cannot set both `shardKey` and `fanOut`", { code: "BAD_REQUEST", status: 400 });
2141
+ }
2142
+ if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
2143
+ throw new LunoraError("`__lunora_relation__:*` is a fan-out-only reserved RPC and cannot be dispatched to a single shard", {
2144
+ code: "FORBIDDEN",
2145
+ status: 403
2146
+ });
2147
+ }
2148
+ if (envelope.fanOut && !options.queryCoordinator) {
2149
+ throw new LunoraError("RPC envelope set `fanOut` but no `queryCoordinator` is configured on the worker", {
2150
+ code: "BAD_REQUEST",
2151
+ status: 400
2152
+ });
2153
+ }
2154
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2155
+ await authorizeRpcEnvelope(envelope, identity);
2156
+ {
2157
+ const rpcStartedAt = Date.now();
2158
+ const { observability } = options;
2159
+ const sinkContext = context ? {
2160
+ waitUntil: (promise) => {
2161
+ context.waitUntil(promise);
2162
+ }
2163
+ } : void 0;
2164
+ if (envelope.fanOut) {
2165
+ const coordinator = options.queryCoordinator;
2166
+ if (!coordinator) {
2167
+ throw new LunoraError("RPC envelope set `fanOut` but no `queryCoordinator` is configured on the worker", {
2168
+ code: "BAD_REQUEST",
2169
+ status: 400
2170
+ });
2171
+ }
2172
+ try {
2173
+ const result = await coordinator.fanOut(options.shardDO, {
2174
+ args: envelope.args ?? {},
2175
+ fanOut: envelope.fanOut,
2176
+ functionPath: envelope.functionPath,
2177
+ headers: forwardedHeaders
2178
+ });
2179
+ emitRpcEvent(
2180
+ observability,
2181
+ {
2182
+ durationMs: Date.now() - rpcStartedAt,
2183
+ fanOut: {
2184
+ failed: result.failed,
2185
+ shards: result.ok + result.failed,
2186
+ table: envelope.fanOut.table
2187
+ },
2188
+ functionPath: envelope.functionPath,
2189
+ ok: true
2190
+ },
2191
+ sinkContext
2192
+ );
2193
+ return Response.json(result, {
2194
+ headers: { "content-type": "application/json" },
2195
+ status: 200
2196
+ });
2197
+ } catch (error) {
2198
+ emitRpcEvent(
2199
+ observability,
2200
+ buildErrorEvent(envelope.functionPath, Date.now() - rpcStartedAt, error, { fanOut: { table: envelope.fanOut.table } }),
2201
+ sinkContext
2202
+ );
2203
+ throw error;
2204
+ }
2205
+ }
2206
+ const shardKey = envelope.shardKey ?? defaultShard;
2207
+ return dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
2208
+ }
2209
+ };
2210
+ const serverQuery = async (request, env, reference, args = {}, callOptions = {}) => {
2211
+ try {
2212
+ const functionPath = reference.__lunoraRef;
2213
+ if (typeof functionPath !== "string") {
2214
+ throw new LunoraError("serverQuery: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
2215
+ }
2216
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2217
+ await authorizeRpcEnvelope({ functionPath, shardKey: callOptions.shardKey }, identity);
2218
+ const shardKey = callOptions.shardKey ?? defaultShard;
2219
+ return await dispatchSingleShard(functionPath, args, shardKey, forwardedHeaders);
2220
+ } catch (error) {
2221
+ return toErrorResponse(error);
2222
+ }
2223
+ };
2224
+ const MAX_PRUNE_PAGES = 1e3;
2225
+ const pruneBackups = async (store, prefix) => {
2226
+ const retain = options.backupRetain;
2227
+ if (retain === void 0 || retain <= 0) {
2228
+ return;
2229
+ }
2230
+ const manifestKeys = [];
2231
+ let cursor;
2232
+ for (let page = 0; page < MAX_PRUNE_PAGES; page += 1) {
2233
+ const listing = await store.list({ cursor, prefix });
2234
+ for (const object of listing.objects) {
2235
+ if (object.key.endsWith(".manifest.json")) {
2236
+ manifestKeys.push(object.key);
2237
+ }
2238
+ }
2239
+ if (!listing.truncated || listing.cursor === void 0) {
2240
+ break;
2241
+ }
2242
+ cursor = listing.cursor;
2243
+ }
2244
+ const stale = manifestKeys.toSorted((a, b) => b.localeCompare(a)).slice(retain);
2245
+ await Promise.all(
2246
+ stale.flatMap((manifestKey) => {
2247
+ const ndjsonKey = manifestKey.slice(0, -".manifest.json".length);
2248
+ return [store.delete(manifestKey), store.delete(ndjsonKey)];
2249
+ })
2250
+ );
2251
+ };
2252
+ const runScheduledBackup = async (controller) => {
2253
+ const store = options.backupStore;
2254
+ const coordinator = options.queryCoordinator;
2255
+ if (!store) {
2256
+ throw new LunoraError("scheduled backup requires a `backupStore` on the worker", { code: "BACKUP_NOT_CONFIGURED", status: 500 });
2257
+ }
2258
+ if (!coordinator) {
2259
+ throw new LunoraError("scheduled backup requires a `queryCoordinator` on the worker", { code: "BACKUP_NOT_CONFIGURED", status: 500 });
2260
+ }
2261
+ if (!options.adminToken || options.adminToken.length === 0) {
2262
+ throw new LunoraError("scheduled backup requires an `adminToken` to authenticate the per-shard export gate", {
2263
+ code: "BACKUP_NOT_CONFIGURED",
2264
+ status: 500
2265
+ });
2266
+ }
2267
+ const forwardedHeaders = { authorization: `Bearer ${options.adminToken}`, "content-type": "application/json" };
2268
+ const tables = options.backupTables;
2269
+ let rows = 0;
2270
+ let bytes = 0;
2271
+ let streamError;
2272
+ const stream = new ReadableStream({
2273
+ async pull(streamController) {
2274
+ const writeRow = (row) => {
2275
+ const encoded = NDJSON_ENCODER.encode(`${JSON.stringify(row)}
2276
+ `);
2277
+ rows += 1;
2278
+ bytes += encoded.byteLength;
2279
+ streamController.enqueue(encoded);
2280
+ };
2281
+ try {
2282
+ await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow);
2283
+ streamController.close();
2284
+ } catch (error) {
2285
+ streamError = error instanceof Error ? error : new Error(String(error));
2286
+ streamController.error(error);
2287
+ }
2288
+ }
2289
+ });
2290
+ const prefix = options.backupPrefix ?? "backups/";
2291
+ const timestamp = new Date(controller.scheduledTime).toISOString();
2292
+ const fileKey = `${prefix}lunora-backup-${timestamp.replaceAll(/[.:]/gu, "-")}.ndjson`;
2293
+ const manifestKey = `${fileKey}.manifest.json`;
2294
+ await store.put(fileKey, stream, { httpMetadata: { contentType: "application/x-ndjson" } });
2295
+ if (streamError !== void 0) {
2296
+ throw streamError;
2297
+ }
2298
+ const manifest = {
2299
+ bytes,
2300
+ createdAt: timestamp,
2301
+ cron: controller.cron,
2302
+ file: fileKey,
2303
+ id: timestamp,
2304
+ rows,
2305
+ scheduledTime: controller.scheduledTime,
2306
+ ...tables ? { tables: tables.join(",") } : {}
2307
+ };
2308
+ await store.put(manifestKey, `${JSON.stringify(manifest, void 0, 2)}
2309
+ `, { httpMetadata: { contentType: "application/json" } });
2310
+ await pruneBackups(store, prefix);
2311
+ };
2312
+ const handleScheduled = async (controller, env, context) => {
2313
+ const errors = [];
2314
+ const toError = (error) => error instanceof Error ? error : new Error(String(error));
2315
+ const userHandler = options.crons?.[controller.cron];
2316
+ if (userHandler) {
2317
+ try {
2318
+ await userHandler(controller, env, context);
2319
+ } catch (error) {
2320
+ errors.push(toError(error));
2321
+ }
2322
+ }
2323
+ await runCronJobs(controller.cron, env, errors, toError);
2324
+ if (options.backupStore && options.backupCron !== void 0 && options.backupCron === controller.cron) {
2325
+ try {
2326
+ await runScheduledBackup(controller);
2327
+ } catch (error) {
2328
+ errors.push(toError(error));
2329
+ }
2330
+ }
2331
+ const [first] = errors;
2332
+ if (errors.length === 1 && first) {
2333
+ throw first;
2334
+ }
2335
+ if (errors.length > 1) {
2336
+ throw new AggregateError(errors, `scheduled("${controller.cron}") had ${String(errors.length)} failure(s)`);
2337
+ }
2338
+ };
2339
+ const recordAuthAttempt = async (env, outcome) => {
2340
+ try {
2341
+ const envRecord = env ?? {};
2342
+ const adminBearer = options.adminToken ?? (typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0);
2343
+ if (!adminBearer || adminBearer.length === 0) {
2344
+ return;
2345
+ }
2346
+ const recordRequest = new Request("https://shard.internal/rpc", {
2347
+ body: JSON.stringify({ args: { outcome }, functionPath: RECORD_AUTH_EVENT_OP }),
2348
+ headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
2349
+ method: "POST"
2350
+ });
2351
+ await forwardToShard(options.shardDO, defaultShard, recordRequest);
2352
+ } catch {
2353
+ }
2354
+ };
2355
+ const dispatchAuth = async (request, env, url, context) => {
2356
+ if (!options.authHandler) {
2357
+ return void 0;
2358
+ }
2359
+ const authResponse = await options.authHandler(request);
2360
+ if (!authResponse) {
2361
+ return void 0;
2362
+ }
2363
+ const basePath = options.authBasePath ?? DEFAULT_AUTH_BASE_PATH;
2364
+ if (isAuthAttemptPath(url.pathname, basePath)) {
2365
+ context.waitUntil(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
2366
+ }
2367
+ return authResponse;
2368
+ };
2369
+ const internalRoutes = {
2370
+ [WS_PATH]: (request, env, url) => handleWebSocketUpgrade(request, env, url),
2371
+ [RPC_PATH]: (request, env, _url, context) => handleRpc(request, env, context),
2372
+ [SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
2373
+ [CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
2374
+ // Extracted handler clusters built above, merged in (mirroring the auth
2375
+ // plane below): orchestration (migrate / rank / rankpage / shard-traffic /
2376
+ // pitr), data-movement (export / import / sync / connector-sync / apply),
2377
+ // scheduled, storage, vector, and the static-introspection reads
2378
+ // (functions / cron-jobs / openapi / openrpc / global tables).
2379
+ ...orchestrationAdminRoutes,
2380
+ ...dataMovementAdminRoutes,
2381
+ ...scheduledAdminRoutes,
2382
+ ...workflowsAdminRoutes,
2383
+ ...storageAdminRoutes,
2384
+ ...vectorAdminRoutes,
2385
+ ...introspectionAdminRoutes,
2386
+ // `/_lunora/admin/auth/*` — the whole user-management plane, one route per
2387
+ // `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
2388
+ ...buildAuthAdminRoutes({
2389
+ assertAdmin: assertAdminAuthorized,
2390
+ // eslint-disable-next-line sonarjs/deprecation -- `authIntrospector` is the intentional read-only fallback
2391
+ getAuthAdmin: () => options.authAdmin ?? options.authIntrospector,
2392
+ parsePaging,
2393
+ queryParameter,
2394
+ readJsonBody: readJsonBodyWithLimit
2395
+ })
2396
+ };
2397
+ let resolvedSecurity = resolveSecurity(options.security);
2398
+ let securityEnvResolved = false;
2399
+ const ensureSecurityResolved = (env) => {
2400
+ if (!securityEnvResolved) {
2401
+ securityEnvResolved = true;
2402
+ resolvedSecurity = resolveSecurity(options.security, env ?? {});
2403
+ }
2404
+ };
2405
+ const handle = async (request, env, context) => {
2406
+ const url = new URL(request.url);
2407
+ if (request.method === "POST" || request.method === "PUT") {
2408
+ const contentLength = Number(request.headers.get("content-length") ?? "");
2409
+ if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
2410
+ throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
2411
+ }
2412
+ }
2413
+ const authResponse = await dispatchAuth(request, env, url, context);
2414
+ if (authResponse) {
2415
+ return authResponse;
2416
+ }
2417
+ const methodAndPath = `${request.method} ${url.pathname}`;
2418
+ const route = options.routes?.[methodAndPath] ?? options.routes?.[url.pathname];
2419
+ if (route) {
2420
+ return route(request, env, context);
2421
+ }
2422
+ const internalRoute = internalRoutes[url.pathname];
2423
+ if (internalRoute) {
2424
+ return internalRoute(request, env, url, context);
2425
+ }
2426
+ const httpRouteResponse = await dispatchHttpRoute(request, env, context);
2427
+ if (httpRouteResponse) {
2428
+ return httpRouteResponse;
2429
+ }
2430
+ return new Response("Not found", { status: 404 });
2431
+ };
2432
+ return {
2433
+ async fetch(request, env, context) {
2434
+ if (options.passThroughOnException) {
2435
+ context.passThroughOnException();
2436
+ }
2437
+ ensureSecurityResolved(env);
2438
+ const preflight = handleCorsPreflight(request, resolvedSecurity);
2439
+ if (preflight) {
2440
+ return preflight;
2441
+ }
2442
+ const blocked = enforceOrigin(request, resolvedSecurity);
2443
+ if (blocked) {
2444
+ return decorateResponse(blocked, request, resolvedSecurity);
2445
+ }
2446
+ try {
2447
+ const response = await handle(request, env, context);
2448
+ return decorateResponse(response, request, resolvedSecurity);
2449
+ } catch (error) {
2450
+ return decorateResponse(toErrorResponse(error), request, resolvedSecurity);
2451
+ }
2452
+ },
2453
+ async scheduled(controller, env, context) {
2454
+ await handleScheduled(controller, env, context);
2455
+ },
2456
+ serverQuery
2457
+ };
2458
+ };
2459
+ const composeWorker = (options) => createWorker(options);
2460
+ const toHttpRouter = (handler) => typeof handler === "function" ? { fetch: handler } : handler;
2461
+ const hasLunoraCrons = (options) => Boolean(options.crons ?? options.cronJobs ?? options.backupCron);
2462
+ const withFrameworkWorker = (host, optionsInput) => {
2463
+ const httpRouter = toHttpRouter(host);
2464
+ const hostScheduled = typeof host === "object" && typeof host.scheduled === "function" ? host.scheduled : void 0;
2465
+ const build = (options) => {
2466
+ const lunora = composeWorker({ ...options, httpRouter });
2467
+ if (hostScheduled !== void 0 && !hasLunoraCrons(options)) {
2468
+ return {
2469
+ ...lunora,
2470
+ scheduled: async (controller, env, context) => {
2471
+ await hostScheduled(controller, env, context);
2472
+ }
2473
+ };
2474
+ }
2475
+ return lunora;
2476
+ };
2477
+ if (typeof optionsInput !== "function") {
2478
+ return build(optionsInput);
2479
+ }
2480
+ const optionsFactory = optionsInput;
2481
+ return {
2482
+ fetch: (request, env, context) => build(optionsFactory(env)).fetch(request, env, context),
2483
+ scheduled: (controller, env, context) => build(optionsFactory(env)).scheduled(controller, env, context),
2484
+ serverQuery: (request, env, reference, args, options) => build(optionsFactory(env)).serverQuery(request, env, reference, args, options)
2485
+ };
2486
+ };
2487
+ const defineRpcEnvelope = (envelope) => envelope;
2488
+
2489
+ export { composeWorker, createWorker, defineRpcEnvelope, withFrameworkWorker };