@lunora/runtime 1.0.0-alpha.3 → 1.0.0-alpha.31

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.
@@ -1,10 +1,100 @@
1
- import { LunoraError, toErrorResponse, isStructuralLunoraError, isStructuralConflictError } from './LunoraError-CL0aOtpo.mjs';
1
+ import { isLunoraError, toErrorBody } from '@lunora/errors';
2
+ import { NOOP_EXECUTION_CONTEXT } from './NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
3
+ import { o as otlpRandomHex, b as buildTraceparent } from './otlp-DOLuy1Aj.mjs';
4
+ import { LunoraError, toErrorResponse } from './LunoraError-Bpb9EFJ3.mjs';
5
+ import { wrapResolverWithContract } from './composeIdentityResolvers-XGjO7V1J.mjs';
6
+ export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-XGjO7V1J.mjs';
2
7
  import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
3
- import { resolveShard } from './resolveShard-DDkzWtrU.mjs';
4
- import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-DbISh_Wi.mjs';
8
+ import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
9
+ import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-DRWQFNhF.mjs';
10
+
11
+ const evictOldestEntry = (map, capacity) => {
12
+ if (map.size < capacity) {
13
+ return;
14
+ }
15
+ const oldest = map.keys().next().value;
16
+ if (oldest !== void 0) {
17
+ map.delete(oldest);
18
+ }
19
+ };
20
+
21
+ const RELAY_NAME_INFIX = "::relay::";
22
+ const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
23
+
24
+ const textEncoder = new TextEncoder();
25
+ const toBase64Url = (bytes) => {
26
+ const binary = String.fromCodePoint(...bytes);
27
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
28
+ };
29
+ const fromBase64Url = (input) => {
30
+ const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
31
+ const binary = atob(padded);
32
+ const bytes = new Uint8Array(binary.length);
33
+ for (let index = 0; index < binary.length; index += 1) {
34
+ bytes[index] = binary.codePointAt(index) ?? 0;
35
+ }
36
+ return bytes;
37
+ };
38
+ const KEY_CACHE_MAX = 64;
39
+ const keyCache = /* @__PURE__ */ new Map();
40
+ const importHmacKey = async (secret) => {
41
+ const cached = keyCache.get(secret);
42
+ if (cached) {
43
+ return cached;
44
+ }
45
+ evictOldestEntry(keyCache, KEY_CACHE_MAX);
46
+ const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
47
+ keyCache.set(secret, keyPromise);
48
+ return keyPromise;
49
+ };
50
+ const signCanonical = async (secret, canonical) => {
51
+ const cryptoKey = await importHmacKey(secret);
52
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonical));
53
+ return toBase64Url(new Uint8Array(signature));
54
+ };
55
+ const verifyCanonical = async (secret, canonical, sigBytes) => {
56
+ const cryptoKey = await importHmacKey(secret);
57
+ return crypto.subtle.verify("HMAC", cryptoKey, sigBytes, textEncoder.encode(canonical));
58
+ };
59
+
60
+ const WS_ADMIN_TOKEN_VERSION = "v1";
61
+ const WS_ADMIN_TOKEN_TTL_MS = 6e4;
62
+ const mintWsAdminToken = async (secret, options = {}) => {
63
+ const expiresAtMs = (options.now ?? Date.now()) + (options.ttlMs ?? WS_ADMIN_TOKEN_TTL_MS);
64
+ const canonical = `${WS_ADMIN_TOKEN_VERSION}.${String(expiresAtMs)}`;
65
+ const signature = await signCanonical(secret, canonical);
66
+ return { expiresAtMs, token: `${canonical}.${signature}` };
67
+ };
68
+ const verifyWsAdminToken = async (secret, token, now = Date.now()) => {
69
+ if (secret.length === 0 || token.length === 0) {
70
+ return false;
71
+ }
72
+ const parts = token.split(".");
73
+ if (parts.length !== 3) {
74
+ return false;
75
+ }
76
+ const [version, expString, signature] = parts;
77
+ if (version !== WS_ADMIN_TOKEN_VERSION || signature.length === 0) {
78
+ return false;
79
+ }
80
+ const expiresAtMs = Number(expString);
81
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
82
+ return false;
83
+ }
84
+ let signatureBytes;
85
+ try {
86
+ signatureBytes = fromBase64Url(signature);
87
+ } catch {
88
+ return false;
89
+ }
90
+ return verifyCanonical(secret, `${version}.${expString}`, signatureBytes);
91
+ };
5
92
 
6
93
  const AUTH_BASE = "/_lunora/admin/auth";
7
94
  const AUTH_ADMIN_ERROR_STATUS = {
95
+ INVITER_REQUIRED: 400,
96
+ ORG_SLUG_INVALID: 400,
97
+ ORG_SLUG_TAKEN: 409,
8
98
  PASSWORD_TOO_LONG: 400,
9
99
  PASSWORD_TOO_SHORT: 400,
10
100
  USER_ALREADY_EXISTS: 409,
@@ -34,6 +124,23 @@ const parseRoleInput = (value) => {
34
124
  return void 0;
35
125
  };
36
126
  const optionalBodyString = (body, field) => typeof body[field] === "string" ? body[field] : void 0;
127
+ const optionalBodyObject = (body, field) => {
128
+ const value = body[field];
129
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
130
+ };
131
+ const requirePermission = (body) => {
132
+ const value = body["permission"];
133
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
134
+ throw new LunoraError("`permission` object is required", { code: "BAD_REQUEST", status: 400 });
135
+ }
136
+ const out = {};
137
+ for (const [resource, actions] of Object.entries(value)) {
138
+ if (Array.isArray(actions) && actions.every((action) => typeof action === "string")) {
139
+ out[resource] = actions;
140
+ }
141
+ }
142
+ return out;
143
+ };
37
144
  const AUTH_ROUTES = {
38
145
  [`${AUTH_BASE}/capabilities`]: {
39
146
  build: () => {
@@ -100,6 +207,34 @@ const AUTH_ROUTES = {
100
207
  http: "GET",
101
208
  method: "listInvitations"
102
209
  },
210
+ [`${AUTH_BASE}/config`]: {
211
+ build: () => {
212
+ return {};
213
+ },
214
+ http: "GET",
215
+ method: "config"
216
+ },
217
+ [`${AUTH_BASE}/organizations/teams`]: {
218
+ build: ({ paging, query }) => {
219
+ return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
220
+ },
221
+ http: "GET",
222
+ method: "listTeams"
223
+ },
224
+ [`${AUTH_BASE}/organizations/teams/members`]: {
225
+ build: ({ paging, query }) => {
226
+ return { ...paging, teamId: requireQuery$1(query, "teamId") };
227
+ },
228
+ http: "GET",
229
+ method: "listTeamMembers"
230
+ },
231
+ [`${AUTH_BASE}/organizations/roles`]: {
232
+ build: ({ paging, query }) => {
233
+ return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
234
+ },
235
+ http: "GET",
236
+ method: "listOrgRoles"
237
+ },
103
238
  // --- mutations (POST) -------------------------------------------------------
104
239
  [`${AUTH_BASE}/users/create`]: {
105
240
  build: ({ body }) => {
@@ -232,6 +367,137 @@ const AUTH_ROUTES = {
232
367
  http: "POST",
233
368
  method: "cancelInvitation",
234
369
  returns: "void"
370
+ },
371
+ [`${AUTH_BASE}/organizations/create`]: {
372
+ build: ({ body }) => {
373
+ return {
374
+ logo: optionalBodyString(body, "logo"),
375
+ metadata: optionalBodyObject(body, "metadata"),
376
+ name: requireBodyString(body, "name"),
377
+ ownerId: optionalBodyString(body, "ownerId"),
378
+ slug: optionalBodyString(body, "slug")
379
+ };
380
+ },
381
+ http: "POST",
382
+ method: "createOrganization"
383
+ },
384
+ [`${AUTH_BASE}/organizations/update`]: {
385
+ build: ({ body }) => {
386
+ return {
387
+ logo: optionalBodyString(body, "logo"),
388
+ metadata: optionalBodyObject(body, "metadata"),
389
+ name: optionalBodyString(body, "name"),
390
+ organizationId: requireBodyString(body, "organizationId"),
391
+ slug: optionalBodyString(body, "slug")
392
+ };
393
+ },
394
+ http: "POST",
395
+ method: "updateOrganization"
396
+ },
397
+ [`${AUTH_BASE}/organizations/remove`]: {
398
+ build: ({ body }) => {
399
+ return { organizationId: requireBodyString(body, "organizationId") };
400
+ },
401
+ http: "POST",
402
+ method: "deleteOrganization",
403
+ returns: "void"
404
+ },
405
+ [`${AUTH_BASE}/organizations/members/add`]: {
406
+ build: ({ body }) => {
407
+ return {
408
+ organizationId: requireBodyString(body, "organizationId"),
409
+ role: optionalBodyString(body, "role"),
410
+ userId: requireBodyString(body, "userId")
411
+ };
412
+ },
413
+ http: "POST",
414
+ method: "addMember"
415
+ },
416
+ [`${AUTH_BASE}/organizations/members/invite`]: {
417
+ build: ({ body }) => {
418
+ return {
419
+ email: requireBodyString(body, "email"),
420
+ inviterId: optionalBodyString(body, "inviterId"),
421
+ organizationId: requireBodyString(body, "organizationId"),
422
+ role: optionalBodyString(body, "role")
423
+ };
424
+ },
425
+ http: "POST",
426
+ method: "inviteMember"
427
+ },
428
+ [`${AUTH_BASE}/organizations/members/role`]: {
429
+ build: ({ body }) => {
430
+ const role = parseRoleInput(body["role"]);
431
+ if (role === void 0 || typeof role === "string" && role.trim() === "") {
432
+ throw new LunoraError("`role` is required", { code: "BAD_REQUEST", status: 400 });
433
+ }
434
+ return { memberId: requireBodyString(body, "memberId"), role };
435
+ },
436
+ http: "POST",
437
+ method: "updateMemberRole"
438
+ },
439
+ [`${AUTH_BASE}/organizations/teams/create`]: {
440
+ build: ({ body }) => {
441
+ return { name: requireBodyString(body, "name"), organizationId: requireBodyString(body, "organizationId") };
442
+ },
443
+ http: "POST",
444
+ method: "createTeam"
445
+ },
446
+ [`${AUTH_BASE}/organizations/teams/update`]: {
447
+ build: ({ body }) => {
448
+ return { name: requireBodyString(body, "name"), teamId: requireBodyString(body, "teamId") };
449
+ },
450
+ http: "POST",
451
+ method: "updateTeam"
452
+ },
453
+ [`${AUTH_BASE}/organizations/teams/remove`]: {
454
+ build: ({ body }) => {
455
+ return { teamId: requireBodyString(body, "teamId") };
456
+ },
457
+ http: "POST",
458
+ method: "removeTeam",
459
+ returns: "void"
460
+ },
461
+ [`${AUTH_BASE}/organizations/teams/members/add`]: {
462
+ build: ({ body }) => {
463
+ return { teamId: requireBodyString(body, "teamId"), userId: requireBodyString(body, "userId") };
464
+ },
465
+ http: "POST",
466
+ method: "addTeamMember"
467
+ },
468
+ [`${AUTH_BASE}/organizations/teams/members/remove`]: {
469
+ build: ({ body }) => {
470
+ return { teamMemberId: requireBodyString(body, "teamMemberId") };
471
+ },
472
+ http: "POST",
473
+ method: "removeTeamMember",
474
+ returns: "void"
475
+ },
476
+ [`${AUTH_BASE}/organizations/roles/create`]: {
477
+ build: ({ body }) => {
478
+ return {
479
+ organizationId: requireBodyString(body, "organizationId"),
480
+ permission: requirePermission(body),
481
+ role: requireBodyString(body, "role")
482
+ };
483
+ },
484
+ http: "POST",
485
+ method: "createOrgRole"
486
+ },
487
+ [`${AUTH_BASE}/organizations/roles/update`]: {
488
+ build: ({ body }) => {
489
+ return { permission: requirePermission(body), roleId: requireBodyString(body, "roleId") };
490
+ },
491
+ http: "POST",
492
+ method: "updateOrgRole"
493
+ },
494
+ [`${AUTH_BASE}/organizations/roles/remove`]: {
495
+ build: ({ body }) => {
496
+ return { roleId: requireBodyString(body, "roleId") };
497
+ },
498
+ http: "POST",
499
+ method: "deleteOrgRole",
500
+ returns: "void"
235
501
  }
236
502
  };
237
503
  const buildAuthAdminRoutes = (deps) => {
@@ -244,15 +510,15 @@ const buildAuthAdminRoutes = (deps) => {
244
510
  }
245
511
  const candidate = error;
246
512
  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 });
513
+ console.error("[lunora] auth admin operation failed:", error);
514
+ throw new LunoraError("auth admin operation failed", { code, status: AUTH_ADMIN_ERROR_STATUS[code] ?? 500 });
249
515
  }
250
516
  };
251
517
  const handle = async (request, descriptor) => {
518
+ deps.assertAdmin(request);
252
519
  if (request.method !== descriptor.http) {
253
520
  throw new LunoraError(`Auth admin endpoint requires ${descriptor.http}`, { code: "METHOD_NOT_ALLOWED", status: 405 });
254
521
  }
255
- deps.assertAdmin(request);
256
522
  const admin = deps.getAuthAdmin();
257
523
  if (admin === void 0) {
258
524
  throw new LunoraError("auth endpoints require an `authAdmin` on the worker", { code: "AUTH_NOT_CONFIGURED", status: 400 });
@@ -278,6 +544,48 @@ const buildAuthAdminRoutes = (deps) => {
278
544
  return routes;
279
545
  };
280
546
 
547
+ const MAX_BATCH_ENTRIES = 500;
548
+
549
+ const normalizeBatchCall = (raw, index, defaultShard) => {
550
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
551
+ throw new LunoraError("each batch call must be an object", { code: "BAD_REQUEST", status: 400 });
552
+ }
553
+ const call = raw;
554
+ if (typeof call.functionPath !== "string") {
555
+ throw new LunoraError("each batch call needs a string `functionPath`", { code: "BAD_REQUEST", status: 400 });
556
+ }
557
+ if (call.functionPath.startsWith("__lunora_relation__:") || call.functionPath.startsWith("__lunora_admin__")) {
558
+ throw new LunoraError("reserved function path cannot be batched", { code: "FORBIDDEN", status: 403 });
559
+ }
560
+ if (call.args !== void 0 && (typeof call.args !== "object" || call.args === null || Array.isArray(call.args))) {
561
+ throw new LunoraError("each batch call `args` must be an object", { code: "BAD_REQUEST", status: 400 });
562
+ }
563
+ return {
564
+ entry: {
565
+ args: call.args === void 0 ? {} : call.args,
566
+ clientId: typeof call.clientId === "string" ? call.clientId : void 0,
567
+ clientSeq: typeof call.clientSeq === "number" ? call.clientSeq : void 0,
568
+ functionPath: call.functionPath,
569
+ id: typeof call.id === "number" ? call.id : index,
570
+ mutationId: typeof call.mutationId === "string" ? call.mutationId : void 0
571
+ },
572
+ shardKey: typeof call.shardKey === "string" ? call.shardKey : defaultShard
573
+ };
574
+ };
575
+ const groupBatchCallsByShard = (calls, defaultShard) => {
576
+ if (calls.length > MAX_BATCH_ENTRIES) {
577
+ throw new LunoraError(`RPC batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit`, { code: "BAD_REQUEST", status: 400 });
578
+ }
579
+ const groups = /* @__PURE__ */ new Map();
580
+ for (const [index, raw] of calls.entries()) {
581
+ const { entry, shardKey } = normalizeBatchCall(raw, index, defaultShard);
582
+ const group = groups.get(shardKey) ?? [];
583
+ group.push(entry);
584
+ groups.set(shardKey, group);
585
+ }
586
+ return groups;
587
+ };
588
+
281
589
  const MAX_BODY_BYTES = 1048576;
282
590
  const readBodyTextWithLimit = async (request, limit = MAX_BODY_BYTES) => {
283
591
  if (!request.body) {
@@ -335,9 +643,9 @@ const readBodyBytesWithLimit = async (request, limit = MAX_BODY_BYTES) => {
335
643
  }
336
644
  return out.buffer;
337
645
  };
338
- const readJsonBodyWithLimit = async (request) => {
646
+ const readJsonBodyWithLimit = async (request, limit = MAX_BODY_BYTES) => {
339
647
  try {
340
- const text = await readBodyTextWithLimit(request);
648
+ const text = await readBodyTextWithLimit(request, limit);
341
649
  return text === "" ? {} : JSON.parse(text);
342
650
  } catch (error) {
343
651
  if (error instanceof LunoraError) {
@@ -597,14 +905,14 @@ const partitionExportTables = (options, tables) => {
597
905
  }
598
906
  return { globalTables, shardLocalTables };
599
907
  };
600
- const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow) => {
908
+ const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace) => {
601
909
  if (tables !== void 0 && shardLocalTables.length === 0) {
602
910
  return;
603
911
  }
604
912
  const exportTables = tables === void 0 ? [] : shardLocalTables;
605
913
  const probeFallback = tables === void 0 ? collectKnownTables() : [];
606
914
  const probeTables = exportTables.length > 0 ? exportTables : probeFallback;
607
- const result = await coordinator.orchestrateExport(options.shardDO, {
915
+ const result = await coordinator.orchestrateExport(namespace, {
608
916
  args: { tables: exportTables },
609
917
  headers: forwardedHeaders,
610
918
  tables: probeTables
@@ -618,9 +926,9 @@ const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tabl
618
926
  }
619
927
  }
620
928
  };
621
- const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow) => {
929
+ const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow, namespace) => {
622
930
  const { globalTables, shardLocalTables } = partitionExportTables(options, tables);
623
- await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow);
931
+ await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace);
624
932
  const exportGlobalsFunction = options.exportGlobals;
625
933
  const wantGlobals = tables === void 0 || globalTables.length > 0;
626
934
  if (wantGlobals && exportGlobalsFunction) {
@@ -740,7 +1048,7 @@ const mergeImportResult = (totals, result) => {
740
1048
  }
741
1049
  totals.conflicts += result.conflicts;
742
1050
  };
743
- const streamingImport = async (request, options, forwardedHeaders) => {
1051
+ const streamingImport = async (request, options, forwardedHeaders, namespace) => {
744
1052
  const defaultShard = options.defaultShardKey ?? "__root__";
745
1053
  const { errors, globalRows, perShard } = await bucketImportStream(request, options, defaultShard);
746
1054
  const totals = { conflicts: 0, errors, inserted: {} };
@@ -749,7 +1057,7 @@ const streamingImport = async (request, options, forwardedHeaders) => {
749
1057
  if (!coordinator) {
750
1058
  throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
751
1059
  }
752
- const result = await coordinator.orchestrateImport(options.shardDO, {
1060
+ const result = await coordinator.orchestrateImport(namespace, {
753
1061
  batches: [...perShard.values()],
754
1062
  headers: forwardedHeaders
755
1063
  });
@@ -966,7 +1274,129 @@ const buildIntrospectionAdminRoutes = (deps) => {
966
1274
  };
967
1275
  };
968
1276
 
969
- const MIGRATE_PATH = "/_lunora/migrate";
1277
+ const KV_NAMESPACES_PATH = "/_lunora/admin/kv/namespaces";
1278
+ const KV_KEYS_PATH = "/_lunora/admin/kv/keys";
1279
+ const KV_VALUE_PATH = "/_lunora/admin/kv/value";
1280
+ const KV_VALUE_MAX_BODY_BYTES = 32 * 1048576;
1281
+ const KV_MIN_EXPIRATION_SECONDS = 60;
1282
+ const buildKvAdminRoutes = (deps) => {
1283
+ const { readJsonBody, requireAdminOption } = deps;
1284
+ const gate = (request) => requireAdminOption(request, deps.kvIntrospector, {
1285
+ code: "KV_NOT_CONFIGURED",
1286
+ message: "KV endpoints require a `kvIntrospector` on the worker"
1287
+ });
1288
+ const ok = (payload) => Response.json(payload, { headers: { "content-type": "application/json" }, status: 200 });
1289
+ const requireNamespaceAndKey = (request, verb) => {
1290
+ const url = new URL(request.url);
1291
+ const namespace = url.searchParams.get("namespace") ?? "";
1292
+ const key = url.searchParams.get("key") ?? "";
1293
+ if (namespace === "") {
1294
+ throw new LunoraError(`KV-value ${verb} request requires a \`namespace\` query parameter`, { code: "BAD_REQUEST", status: 400 });
1295
+ }
1296
+ if (key === "") {
1297
+ throw new LunoraError(`KV-value ${verb} request requires a \`key\` query parameter`, { code: "BAD_REQUEST", status: 400 });
1298
+ }
1299
+ return { key, namespace };
1300
+ };
1301
+ const requireKnownNamespace = async (introspector, namespace) => {
1302
+ const namespaces = await introspector.listNamespaces();
1303
+ if (!namespaces.some((entry) => entry.binding === namespace)) {
1304
+ throw new LunoraError(`Unknown KV namespace binding \`${namespace}\``, { code: "NOT_FOUND", status: 404 });
1305
+ }
1306
+ };
1307
+ const handleKvNamespaces = async (request) => {
1308
+ if (request.method !== "GET") {
1309
+ throw new LunoraError("KV-namespaces endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1310
+ }
1311
+ return ok({ namespaces: await gate(request).listNamespaces() });
1312
+ };
1313
+ const handleKvKeys = async (request) => {
1314
+ if (request.method !== "GET") {
1315
+ throw new LunoraError("KV-keys endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
1316
+ }
1317
+ const introspector = gate(request);
1318
+ const url = new URL(request.url);
1319
+ const namespace = url.searchParams.get("namespace") ?? "";
1320
+ if (namespace === "") {
1321
+ throw new LunoraError("KV-keys request requires a `namespace` query parameter", { code: "BAD_REQUEST", status: 400 });
1322
+ }
1323
+ const prefix = url.searchParams.get("prefix") ?? void 0;
1324
+ const cursor = url.searchParams.get("cursor") ?? void 0;
1325
+ const limitRaw = url.searchParams.get("limit");
1326
+ const parsedLimit = limitRaw === null ? void 0 : Number.parseInt(limitRaw, 10);
1327
+ if (parsedLimit !== void 0 && (!Number.isInteger(parsedLimit) || parsedLimit < 1)) {
1328
+ throw new LunoraError("KV-keys `limit` must be a positive integer", { code: "BAD_REQUEST", status: 400 });
1329
+ }
1330
+ const limit = parsedLimit === void 0 ? void 0 : Math.min(parsedLimit, 1e3);
1331
+ await requireKnownNamespace(introspector, namespace);
1332
+ return ok(await introspector.listKeys({ cursor, limit, namespace, prefix }));
1333
+ };
1334
+ const handleKvValueGet = async (request) => {
1335
+ const introspector = gate(request);
1336
+ const params = requireNamespaceAndKey(request, "GET");
1337
+ await requireKnownNamespace(introspector, params.namespace);
1338
+ return ok(await introspector.getValue(params));
1339
+ };
1340
+ const handleKvValuePut = async (request) => {
1341
+ const introspector = gate(request);
1342
+ const candidate = await readJsonBody(request, KV_VALUE_MAX_BODY_BYTES);
1343
+ if (typeof candidate.namespace !== "string" || candidate.namespace === "") {
1344
+ throw new LunoraError("KV-value PUT request requires a `namespace` string", { code: "BAD_REQUEST", status: 400 });
1345
+ }
1346
+ if (typeof candidate.key !== "string" || candidate.key === "") {
1347
+ throw new LunoraError("KV-value PUT request requires a `key` string", { code: "BAD_REQUEST", status: 400 });
1348
+ }
1349
+ if (typeof candidate.value !== "string") {
1350
+ throw new LunoraError("KV-value PUT request requires a `value` string", { code: "BAD_REQUEST", status: 400 });
1351
+ }
1352
+ if (candidate.expirationTtl !== void 0 && (typeof candidate.expirationTtl !== "number" || !Number.isInteger(candidate.expirationTtl) || candidate.expirationTtl < KV_MIN_EXPIRATION_SECONDS)) {
1353
+ throw new LunoraError("KV-value PUT `expirationTtl` must be an integer ≥ 60", { code: "BAD_REQUEST", status: 400 });
1354
+ }
1355
+ const minExpiration = Math.floor(Date.now() / 1e3) + KV_MIN_EXPIRATION_SECONDS;
1356
+ if (candidate.expiration !== void 0 && (typeof candidate.expiration !== "number" || !Number.isInteger(candidate.expiration) || candidate.expiration < minExpiration)) {
1357
+ throw new LunoraError("KV-value PUT `expiration` must be a Unix-seconds timestamp at least 60 seconds in the future", {
1358
+ code: "BAD_REQUEST",
1359
+ status: 400
1360
+ });
1361
+ }
1362
+ await requireKnownNamespace(introspector, candidate.namespace);
1363
+ await introspector.putValue({
1364
+ expiration: candidate.expiration,
1365
+ expirationTtl: candidate.expirationTtl,
1366
+ key: candidate.key,
1367
+ metadata: candidate.metadata,
1368
+ namespace: candidate.namespace,
1369
+ value: candidate.value
1370
+ });
1371
+ return ok({ ok: true });
1372
+ };
1373
+ const handleKvValueDelete = async (request) => {
1374
+ const introspector = gate(request);
1375
+ const params = requireNamespaceAndKey(request, "DELETE");
1376
+ await requireKnownNamespace(introspector, params.namespace);
1377
+ await introspector.deleteKey(params);
1378
+ return ok({ deleted: true });
1379
+ };
1380
+ const kvValueHandlers = {
1381
+ DELETE: handleKvValueDelete,
1382
+ GET: handleKvValueGet,
1383
+ PUT: handleKvValuePut
1384
+ };
1385
+ const handleKvValue = (request) => {
1386
+ const handler = kvValueHandlers[request.method];
1387
+ if (!handler) {
1388
+ throw new LunoraError("KV-value endpoint requires GET, PUT, or DELETE", { code: "METHOD_NOT_ALLOWED", status: 405 });
1389
+ }
1390
+ return handler(request);
1391
+ };
1392
+ return {
1393
+ [KV_NAMESPACES_PATH]: handleKvNamespaces,
1394
+ [KV_KEYS_PATH]: handleKvKeys,
1395
+ [KV_VALUE_PATH]: handleKvValue
1396
+ };
1397
+ };
1398
+
1399
+ const MIGRATE_PATH$1 = "/_lunora/migrate";
970
1400
  const PITR_PATH = "/_lunora/admin/pitr";
971
1401
  const RANK_PATH = "/_lunora/admin/rank";
972
1402
  const RANKPAGE_PATH = "/_lunora/admin/rankpage";
@@ -1223,7 +1653,7 @@ const buildOrchestrationAdminRoutes = (deps) => {
1223
1653
  return forwardToShard(shardDO, pitr.shardKey ?? defaultShard, forwarded);
1224
1654
  };
1225
1655
  return {
1226
- [MIGRATE_PATH]: handleMigrate,
1656
+ [MIGRATE_PATH$1]: handleMigrate,
1227
1657
  [PITR_PATH]: handlePitr,
1228
1658
  [RANK_PATH]: handleRank,
1229
1659
  [RANKPAGE_PATH]: handleRankPage,
@@ -1254,11 +1684,11 @@ const buildScheduledAdminRoutes = (deps) => {
1254
1684
  const stub = resolveSchedulerStub(request);
1255
1685
  return stub.fetch(new Request("https://scheduler.internal/status", { method: "GET" }));
1256
1686
  };
1257
- const handleScheduledWebSocket = (request) => {
1687
+ const handleScheduledWebSocket = async (request) => {
1258
1688
  if (request.headers.get("Upgrade") !== "websocket") {
1259
1689
  throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
1260
1690
  }
1261
- if (!checkWsAdmin(request)) {
1691
+ if (!await checkWsAdmin(request)) {
1262
1692
  throw new LunoraError("admin authorization required", { code: "ADMIN_FORBIDDEN", status: 403 });
1263
1693
  }
1264
1694
  const namespace = requireSchedulerNamespace();
@@ -1502,7 +1932,7 @@ const buildWorkflowsAdminRoutes = (deps) => {
1502
1932
  assertAdmin(request);
1503
1933
  const client = resolveWorkflowsClient(env);
1504
1934
  if (!client) {
1505
- return throwNotConfigured();
1935
+ return Response.json({ configured: false, instances: [], page: 1, perPage: 0, totalCount: 0 });
1506
1936
  }
1507
1937
  const workflowName = requireQuery(url, "name");
1508
1938
  const status = toInstanceStatus(url.searchParams.get("status"));
@@ -1554,9 +1984,28 @@ const buildWorkflowsAdminRoutes = (deps) => {
1554
1984
 
1555
1985
  const NDJSON_ENCODER = new TextEncoder();
1556
1986
  const RPC_PATH = "/_lunora/rpc";
1987
+ const RPC_BATCH_PATH = "/_lunora/rpc-batch";
1557
1988
  const WS_PATH = "/_lunora/ws";
1989
+ const VOICE_PATH_PREFIX = "/_lunora/voice/";
1558
1990
  const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
1559
1991
  const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
1992
+ const ADMIN_WS_TOKEN_PATH = "/_lunora/admin/ws-token";
1993
+ const ADMIN_PATH_PREFIX = "/_lunora/admin/";
1994
+ const MIGRATE_PATH = "/_lunora/migrate";
1995
+ const STATUS_PATH = "/_lunora/status";
1996
+ const isAdminPath = (pathname) => pathname.startsWith(ADMIN_PATH_PREFIX) || pathname === MIGRATE_PATH;
1997
+ const REQUIRE_EPHEMERAL_ENV_VALUES = /* @__PURE__ */ new Set(["1", "enabled", "on", "true", "yes"]);
1998
+ const readForwardedIdentity = (request) => {
1999
+ const forwardedUserId = request.headers.get("x-lunora-userid");
2000
+ const forwardedIdentity = request.headers.get("x-lunora-identity");
2001
+ if (forwardedUserId === null && forwardedIdentity === null) {
2002
+ return void 0;
2003
+ }
2004
+ return {
2005
+ ...forwardedIdentity === null ? {} : { identity: forwardedIdentity },
2006
+ ...forwardedUserId === null ? {} : { userId: forwardedUserId }
2007
+ };
2008
+ };
1560
2009
  const DEFAULT_AUTH_BASE_PATH = "/api/auth";
1561
2010
  const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
1562
2011
  const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
@@ -1569,7 +2018,7 @@ const isAuthAttemptPath = (pathname, basePath) => {
1569
2018
  return AUTH_ATTEMPT_SEGMENTS.some((segment) => suffix === segment || suffix.startsWith(`${segment}/`));
1570
2019
  };
1571
2020
  const buildErrorEvent = (functionPath, durationMs, error, extra) => {
1572
- const mappable = error instanceof LunoraError || isStructuralLunoraError(error) || isStructuralConflictError(error);
2021
+ const mappable = isLunoraError(error);
1573
2022
  const code = mappable ? error.code : "INTERNAL_SERVER_ERROR";
1574
2023
  const status = mappable ? error.status : 500;
1575
2024
  const message = error instanceof Error ? error.message : String(error);
@@ -1598,6 +2047,8 @@ const resolveForwardContext = async (request, env, resolveIdentity) => {
1598
2047
  const cookie = request.headers.get("cookie");
1599
2048
  const bookmark = request.headers.get("x-d1-bookmark");
1600
2049
  const mutationId = request.headers.get("x-lunora-mutation-id");
2050
+ const clientId = request.headers.get("x-lunora-client-id");
2051
+ const clientSeq = request.headers.get("x-lunora-client-seq");
1601
2052
  if (authorization) {
1602
2053
  headers["authorization"] = authorization;
1603
2054
  }
@@ -1610,6 +2061,12 @@ const resolveForwardContext = async (request, env, resolveIdentity) => {
1610
2061
  if (mutationId) {
1611
2062
  headers["x-lunora-mutation-id"] = mutationId;
1612
2063
  }
2064
+ if (clientId) {
2065
+ headers["x-lunora-client-id"] = clientId;
2066
+ }
2067
+ if (clientSeq) {
2068
+ headers["x-lunora-client-seq"] = clientSeq;
2069
+ }
1613
2070
  const clientIp = request.headers.get("cf-connecting-ip");
1614
2071
  if (clientIp) {
1615
2072
  headers["x-lunora-client-ip"] = clientIp;
@@ -1662,6 +2119,28 @@ const validateFanOut = (fanOut) => {
1662
2119
  }
1663
2120
  return spec;
1664
2121
  };
2122
+ const logRpcDebug = (env, envelope) => {
2123
+ if (!env?.LUNORA_DEBUG_RPC) {
2124
+ return;
2125
+ }
2126
+ console.warn(`[lunora:rpc] ${envelope.fanOut ? "fan-out" : `shard=${envelope.shardKey ?? "(root)"}`} ${envelope.functionPath}`);
2127
+ };
2128
+ const resolveX402Charge = (envelope, options) => {
2129
+ const x402Tag = options.functions?.[envelope.functionPath]?.x402;
2130
+ if (!x402Tag) {
2131
+ return void 0;
2132
+ }
2133
+ if (envelope.fanOut) {
2134
+ throw new LunoraError("a paid (`.x402`) function cannot be fanned out", { code: "BAD_REQUEST", status: 400 });
2135
+ }
2136
+ if (!options.x402Charge) {
2137
+ throw new LunoraError(`function "${envelope.functionPath}" is marked paid (.x402) but no x402Charge gate is configured on the worker`, {
2138
+ code: "MISCONFIGURED",
2139
+ status: 500
2140
+ });
2141
+ }
2142
+ return x402Tag;
2143
+ };
1665
2144
  const parseEnvelope = async (request) => {
1666
2145
  const text = await readBodyTextWithLimit(request);
1667
2146
  let body;
@@ -1681,9 +2160,18 @@ const parseEnvelope = async (request) => {
1681
2160
  throw new LunoraError("RPC `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
1682
2161
  }
1683
2162
  const envelope = body;
2163
+ const fanOut = validateFanOut(envelope.fanOut);
2164
+ const args = envelope.args ?? {};
2165
+ if (fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
2166
+ const requestedTable = args.table;
2167
+ if (typeof requestedTable === "string" && requestedTable !== fanOut.table) {
2168
+ throw new LunoraError("RPC `args.table` must match the authorized `fanOut.table` for a relation fan-out", { code: "BAD_REQUEST", status: 400 });
2169
+ }
2170
+ args.table = fanOut.table;
2171
+ }
1684
2172
  return {
1685
- args: envelope.args ?? {},
1686
- fanOut: validateFanOut(envelope.fanOut),
2173
+ args,
2174
+ fanOut,
1687
2175
  functionPath: envelope.functionPath,
1688
2176
  shardKey: envelope.shardKey
1689
2177
  };
@@ -1692,6 +2180,46 @@ const forwardToShard = async (namespace, shardKey, request) => {
1692
2180
  const stub = resolveShard(namespace, shardKey);
1693
2181
  return stub.fetch(request);
1694
2182
  };
2183
+ const relayProbeCache = /* @__PURE__ */ new Map();
2184
+ const RELAY_PROBE_TTL_MS = 5e3;
2185
+ const RELAY_PROBE_MAX_ENTRIES = 4096;
2186
+ const probeRelayCount = async (namespace, shardKey) => {
2187
+ const now = Date.now();
2188
+ const cached = relayProbeCache.get(shardKey);
2189
+ if (cached !== void 0 && cached.expiresMs > now) {
2190
+ return cached.relayCount;
2191
+ }
2192
+ if (cached !== void 0) {
2193
+ relayProbeCache.delete(shardKey);
2194
+ }
2195
+ let relayCount = 0;
2196
+ try {
2197
+ const response = await resolveShard(namespace, shardKey).fetch(new Request("https://shard.internal/_lunora/route"));
2198
+ if (response.ok) {
2199
+ const body = await response.json();
2200
+ const reported = body.relayCount;
2201
+ if (typeof reported === "number" && reported > 0) {
2202
+ relayCount = Math.floor(reported);
2203
+ }
2204
+ }
2205
+ } catch {
2206
+ relayCount = 0;
2207
+ }
2208
+ evictOldestEntry(relayProbeCache, RELAY_PROBE_MAX_ENTRIES);
2209
+ relayProbeCache.set(shardKey, { expiresMs: now + RELAY_PROBE_TTL_MS, relayCount });
2210
+ return relayCount;
2211
+ };
2212
+ const resolveShardBindingName = (env, namespace) => {
2213
+ if (env === null || typeof env !== "object") {
2214
+ return void 0;
2215
+ }
2216
+ for (const [key, value] of Object.entries(env)) {
2217
+ if (value === namespace) {
2218
+ return key;
2219
+ }
2220
+ }
2221
+ return void 0;
2222
+ };
1695
2223
  const constantTimeEqual = (expected, supplied) => {
1696
2224
  const max = Math.max(expected.length, supplied.length);
1697
2225
  let diff = expected.length ^ supplied.length;
@@ -1731,77 +2259,123 @@ const checkAdminAuth = (request, expected) => {
1731
2259
  }
1732
2260
  return constantTimeEqual(expected, rest.join(" ").trim());
1733
2261
  };
1734
- const checkAdminWsToken = (request, expected) => {
2262
+ const checkAdminWsToken = async (request, expected, requireEphemeral) => {
1735
2263
  if (!expected || expected.length === 0) {
1736
2264
  return false;
1737
2265
  }
1738
2266
  const supplied = new URL(request.url).searchParams.get("token");
1739
- return supplied !== null && constantTimeEqual(expected, supplied);
2267
+ if (supplied === null) {
2268
+ return false;
2269
+ }
2270
+ if (await verifyWsAdminToken(expected, supplied)) {
2271
+ return true;
2272
+ }
2273
+ if (requireEphemeral) {
2274
+ return false;
2275
+ }
2276
+ return constantTimeEqual(expected, supplied);
1740
2277
  };
1741
2278
  const createWorker = (options) => {
1742
2279
  const defaultShard = options.defaultShardKey ?? "__root__";
2280
+ const publicResolveIdentity = wrapResolverWithContract(options.resolveIdentity, options.identity);
2281
+ const shardDO = applyJurisdiction(options.shardDO, options.jurisdiction);
2282
+ const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
1743
2283
  let envAdminToken;
1744
2284
  const effectiveAdminToken = () => options.adminToken ?? envAdminToken;
2285
+ let envRequireEphemeralWsToken;
2286
+ const effectiveRequireEphemeralWsToken = () => options.requireEphemeralWsToken ?? envRequireEphemeralWsToken ?? false;
1745
2287
  const resolveAdminTokenFromEnv = (env) => {
2288
+ const record = env ?? {};
2289
+ if (envRequireEphemeralWsToken === void 0 && options.requireEphemeralWsToken === void 0) {
2290
+ const raw = record["LUNORA_REQUIRE_EPHEMERAL_WS_TOKEN"];
2291
+ if (typeof raw === "string" && raw.length > 0) {
2292
+ envRequireEphemeralWsToken = REQUIRE_EPHEMERAL_ENV_VALUES.has(raw.trim().toLowerCase());
2293
+ }
2294
+ }
1746
2295
  if (envAdminToken !== void 0 || options.adminToken !== void 0) {
1747
2296
  return;
1748
2297
  }
1749
- const value = (env ?? {})["LUNORA_ADMIN_TOKEN"];
2298
+ const value = record["LUNORA_ADMIN_TOKEN"];
1750
2299
  if (typeof value === "string" && value.length > 0) {
1751
2300
  envAdminToken = value;
1752
2301
  }
1753
2302
  };
1754
- const hasAnyShardAuth = Boolean(options.authorizeShard) || Boolean(options.authorizeFanOut);
2303
+ const accessAdminGrants = /* @__PURE__ */ new WeakSet();
2304
+ const requestIsAdmin = (request) => checkAdminAuth(request, effectiveAdminToken()) || accessAdminGrants.has(request);
2305
+ const resolveAdminForwardContext = async (request, env) => {
2306
+ const context = await resolveForwardContext(request, env, options.resolveIdentity);
2307
+ if (accessAdminGrants.has(request) && context.headers["authorization"] === void 0) {
2308
+ const token = effectiveAdminToken();
2309
+ if (token !== void 0) {
2310
+ context.headers["authorization"] = `Bearer ${token}`;
2311
+ }
2312
+ }
2313
+ return context;
2314
+ };
1755
2315
  let warnedUnauthenticatedShardAccess = false;
1756
- const warnUnauthenticatedShardAccessOnce = (kind) => {
1757
- if (hasAnyShardAuth || options.allowUnauthenticatedShardAccess || warnedUnauthenticatedShardAccess) {
2316
+ const guardUnauthenticatedShardAccess = (kind) => {
2317
+ if (!options.allowUnauthenticatedShardAccess) {
2318
+ const callback = kind === "fan-out" ? "authorizeFanOut" : "authorizeShard";
2319
+ throw new LunoraError(
2320
+ `${kind} access is default-denied: configure \`${callback}\` on the worker, or set \`allowUnauthenticatedShardAccess: true\` to explicitly allow unauthenticated ${kind} access (relying solely on per-row RLS).`,
2321
+ { code: kind === "fan-out" ? "FORBIDDEN_FANOUT" : "FORBIDDEN_SHARD", status: 403 }
2322
+ );
2323
+ }
2324
+ if (warnedUnauthenticatedShardAccess) {
1758
2325
  return;
1759
2326
  }
1760
2327
  warnedUnauthenticatedShardAccess = true;
1761
2328
  console.warn(
1762
2329
  [
1763
- `[lunora] SECURITY: received ${kind} access but neither \`authorizeShard\` nor \`authorizeFanOut\` is configured — `,
2330
+ `[lunora] SECURITY: serving ${kind} access with \`allowUnauthenticatedShardAccess: true\` and no \`authorizeShard\`/\`authorizeFanOut\` — `,
1764
2331
  `any caller (including unauthenticated ones) can target any shard / fan out across the table. `,
1765
- `Configure \`authorizeShard\`/\`authorizeFanOut\`, or set \`allowUnauthenticatedShardAccess: true\` to acknowledge this posture and silence this warning.`
2332
+ `This is safe only if every table is protected by per-row RLS. Configure \`authorizeShard\`/\`authorizeFanOut\` to gate it.`
1766
2333
  ].join("")
1767
2334
  );
1768
2335
  };
1769
2336
  const orchestrationAdminRoutes = buildOrchestrationAdminRoutes({
1770
2337
  defaultShard,
1771
2338
  forwardToShard,
1772
- isAdmin: (request) => checkAdminAuth(request, effectiveAdminToken()),
2339
+ isAdmin: requestIsAdmin,
1773
2340
  queryCoordinator: options.queryCoordinator,
1774
- resolveForwardContext: (request, env) => resolveForwardContext(request, env, options.resolveIdentity),
1775
- shardDO: options.shardDO
2341
+ resolveForwardContext: resolveAdminForwardContext,
2342
+ shardDO
1776
2343
  });
1777
- const dispatchToShard = async (functionPath, args, shardKey) => {
2344
+ const dispatchToShard = async (functionPath, args, shardKey, mutationId, forwardedIdentity) => {
1778
2345
  if (options.authorizeShard) {
1779
2346
  const allowed = await options.authorizeShard(null, shardKey);
1780
2347
  if (!allowed) {
1781
2348
  throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
1782
2349
  }
1783
2350
  }
2351
+ const headers = { "content-type": "application/json", "x-lunora-system": "1" };
2352
+ if (forwardedIdentity?.userId !== void 0 && forwardedIdentity.userId.length > 0) {
2353
+ headers["x-lunora-userid"] = forwardedIdentity.userId;
2354
+ }
2355
+ if (forwardedIdentity?.identity !== void 0 && forwardedIdentity.identity.length > 0) {
2356
+ headers["x-lunora-identity"] = forwardedIdentity.identity;
2357
+ }
2358
+ if (mutationId !== void 0 && mutationId.length > 0) {
2359
+ headers["x-lunora-mutation-id"] = mutationId;
2360
+ }
1784
2361
  const forwarded = new Request("https://shard.internal/rpc", {
1785
- // `x-lunora-system` marks this as a trusted server-initiated dispatch
1786
- // so the shard may run `internal` functions (scheduled/cron jobs are
1787
- // typically internal). Authorization was already enforced above; this
1788
- // header is set only here, never on the client RPC path.
1789
2362
  body: JSON.stringify({ args, functionPath }),
1790
- headers: { "content-type": "application/json", "x-lunora-system": "1" },
2363
+ headers,
1791
2364
  method: "POST"
1792
2365
  });
1793
- return forwardToShard(options.shardDO, shardKey, forwarded);
2366
+ return forwardToShard(shardDO, shardKey, forwarded);
1794
2367
  };
1795
- const startCronWorkflow = async (binding, job, env) => {
2368
+ const startWorkflowInstance = async (binding, args, env, label) => {
1796
2369
  const candidate = env?.[binding];
1797
2370
  if (!candidate || typeof candidate.create !== "function") {
1798
- throw new LunoraError(`cron job "${job.name}" targets workflow binding "${binding}", which is not bound on env`, {
2371
+ throw new LunoraError(`${label} targets workflow binding "${binding}", which is not bound on env`, {
1799
2372
  code: "CRON_JOB_FAILED",
1800
2373
  status: 500
1801
2374
  });
1802
2375
  }
1803
- await candidate.create({ params: job.args ?? {} });
2376
+ await candidate.create({ params: args });
1804
2377
  };
2378
+ const startCronWorkflow = async (binding, job, env) => startWorkflowInstance(binding, job.args ?? {}, env, `cron job "${job.name}"`);
1805
2379
  const runOneCronJob = async (job, env) => {
1806
2380
  if (job.workflow) {
1807
2381
  await startCronWorkflow(job.workflow, job, env);
@@ -1835,7 +2409,7 @@ const createWorker = (options) => {
1835
2409
  }
1836
2410
  };
1837
2411
  const handleRunCronJob = async (request, env) => {
1838
- if (!checkAdminAuth(request, effectiveAdminToken())) {
2412
+ if (!requestIsAdmin(request)) {
1839
2413
  throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1840
2414
  }
1841
2415
  if (request.method !== "POST") {
@@ -1858,12 +2432,12 @@ const createWorker = (options) => {
1858
2432
  };
1859
2433
  const releasePoolSlot = async (candidate) => {
1860
2434
  const pool = typeof candidate.pool === "string" && candidate.pool.length > 0 ? candidate.pool : void 0;
1861
- if (!pool || !options.schedulerDO || typeof candidate.id !== "string") {
2435
+ if (!pool || !schedulerDO || typeof candidate.id !== "string") {
1862
2436
  return;
1863
2437
  }
1864
2438
  const instanceName = typeof candidate.instanceName === "string" && candidate.instanceName.length > 0 ? candidate.instanceName : "default";
1865
2439
  try {
1866
- await options.schedulerDO.get(options.schedulerDO.idFromName(instanceName)).fetch(
2440
+ await schedulerDO.get(schedulerDO.idFromName(instanceName)).fetch(
1867
2441
  new Request("https://scheduler.internal/complete", {
1868
2442
  body: JSON.stringify({ id: candidate.id, pool }),
1869
2443
  headers: { "content-type": "application/json" },
@@ -1898,28 +2472,34 @@ const createWorker = (options) => {
1898
2472
  throw new LunoraError("Scheduler dispatch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
1899
2473
  }
1900
2474
  const candidate = body ?? {};
2475
+ const args = candidate.args ?? {};
2476
+ if (typeof candidate.workflow === "string" && candidate.workflow.length > 0) {
2477
+ await startWorkflowInstance(candidate.workflow, args, env, "scheduled workflow");
2478
+ return Response.json({ ok: true }, { status: 200 });
2479
+ }
1901
2480
  if (typeof candidate.functionPath !== "string" || candidate.functionPath.length === 0) {
1902
2481
  throw new LunoraError("Scheduler dispatch is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
1903
2482
  }
1904
- const args = candidate.args ?? {};
1905
2483
  const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
1906
- const response = await dispatchToShard(candidate.functionPath, args, shardKey);
2484
+ const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
2485
+ const identity = readForwardedIdentity(request);
2486
+ const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId, identity);
1907
2487
  await releasePoolSlot(candidate);
1908
2488
  return response;
1909
2489
  };
1910
2490
  const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
1911
2491
  applyGlobals: options.applyGlobals,
1912
- isAdmin: (request) => checkAdminAuth(request, effectiveAdminToken()),
2492
+ isAdmin: requestIsAdmin,
1913
2493
  knownTables: () => collectKnownTables(),
1914
2494
  queryCoordinator: options.queryCoordinator,
1915
- resolveForwardContext: (request, env) => resolveForwardContext(request, env, options.resolveIdentity),
1916
- shardDO: options.shardDO,
1917
- streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow),
1918
- streamingImport: (request, headers) => streamingImport(request, options, headers),
2495
+ resolveForwardContext: resolveAdminForwardContext,
2496
+ shardDO,
2497
+ streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow, shardDO),
2498
+ streamingImport: (request, headers) => streamingImport(request, options, headers, shardDO),
1919
2499
  syncGlobals: options.syncGlobals
1920
2500
  });
1921
2501
  const assertAdminAuthorized = (request) => {
1922
- if (!checkAdminAuth(request, effectiveAdminToken())) {
2502
+ if (!requestIsAdmin(request)) {
1923
2503
  throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1924
2504
  }
1925
2505
  };
@@ -1946,17 +2526,17 @@ const createWorker = (options) => {
1946
2526
  };
1947
2527
  };
1948
2528
  const requireSchedulerNamespace = () => {
1949
- if (options.schedulerDO === void 0) {
2529
+ if (schedulerDO === void 0) {
1950
2530
  throw new LunoraError("scheduled endpoints require a `schedulerDO` namespace on the worker", { code: "SCHEDULER_NOT_CONFIGURED", status: 400 });
1951
2531
  }
1952
- return options.schedulerDO;
2532
+ return schedulerDO;
1953
2533
  };
1954
2534
  const resolveSchedulerStub = (request) => {
1955
2535
  assertAdminAuthorized(request);
1956
2536
  return resolveShard(requireSchedulerNamespace(), options.schedulerInstanceName ?? "default");
1957
2537
  };
1958
2538
  const scheduledAdminRoutes = buildScheduledAdminRoutes({
1959
- checkWsAdmin: (request) => checkAdminAuth(request, effectiveAdminToken()) || checkAdminWsToken(request, effectiveAdminToken()),
2539
+ checkWsAdmin: async (request) => requestIsAdmin(request) || checkAdminWsToken(request, effectiveAdminToken(), effectiveRequireEphemeralWsToken()),
1960
2540
  requireSchedulerNamespace,
1961
2541
  resolveSchedulerStub,
1962
2542
  schedulerInstanceName: options.schedulerInstanceName ?? "default"
@@ -1984,6 +2564,11 @@ const createWorker = (options) => {
1984
2564
  requireAdminOption,
1985
2565
  vectorIntrospector: options.vectorIntrospector
1986
2566
  });
2567
+ const kvAdminRoutes = buildKvAdminRoutes({
2568
+ kvIntrospector: options.kvIntrospector,
2569
+ readJsonBody: readJsonBodyWithLimit,
2570
+ requireAdminOption
2571
+ });
1987
2572
  const introspectionAdminRoutes = buildIntrospectionAdminRoutes({
1988
2573
  assertAdmin: assertAdminAuthorized,
1989
2574
  options: {
@@ -1997,8 +2582,8 @@ const createWorker = (options) => {
1997
2582
  queryParameter,
1998
2583
  requireAdminOption
1999
2584
  });
2000
- const buildHttpActionContext = async (request, env) => {
2001
- const { claims, headers, userId } = await resolveForwardContext(request, env, options.resolveIdentity);
2585
+ const buildHttpActionContext = async (request, env, context) => {
2586
+ const { claims, headers, userId } = await resolveForwardContext(request, env, publicResolveIdentity);
2002
2587
  const run = async (reference, args = {}) => {
2003
2588
  const functionPath = reference.__lunoraRef;
2004
2589
  if (typeof functionPath !== "string") {
@@ -2009,7 +2594,7 @@ const createWorker = (options) => {
2009
2594
  headers,
2010
2595
  method: "POST"
2011
2596
  });
2012
- const response = await forwardToShard(options.shardDO, defaultShard, forwarded);
2597
+ const response = await forwardToShard(shardDO, defaultShard, forwarded);
2013
2598
  const payload = await response.json();
2014
2599
  if (payload.error) {
2015
2600
  throw new LunoraError(payload.error.message ?? "shard RPC failed", {
@@ -2024,6 +2609,7 @@ const createWorker = (options) => {
2024
2609
  getIdentity: () => Promise.resolve(claims),
2025
2610
  userId
2026
2611
  },
2612
+ cache: context.cache,
2027
2613
  fetch: globalThis.fetch.bind(globalThis),
2028
2614
  runAction: run,
2029
2615
  runMutation: run,
@@ -2034,7 +2620,7 @@ const createWorker = (options) => {
2034
2620
  if (!options.httpRouter) {
2035
2621
  return void 0;
2036
2622
  }
2037
- const httpContext = await buildHttpActionContext(request, env);
2623
+ const httpContext = await buildHttpActionContext(request, env, context);
2038
2624
  try {
2039
2625
  return await options.httpRouter.fetch(request, { ...env, __lunoraCtx: httpContext }, context);
2040
2626
  } catch (error) {
@@ -2046,15 +2632,84 @@ const createWorker = (options) => {
2046
2632
  if (request.headers.get("Upgrade") !== "websocket") {
2047
2633
  throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
2048
2634
  }
2635
+ const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
2636
+ if (blockedUpgrade) {
2637
+ return blockedUpgrade;
2638
+ }
2049
2639
  const shardKey = url.searchParams.get("shard") ?? defaultShard;
2050
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2640
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2051
2641
  if (options.authorizeShard) {
2052
2642
  const allowed = await options.authorizeShard(identity, shardKey);
2053
2643
  if (!allowed) {
2054
2644
  throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
2055
2645
  }
2056
2646
  } else if (shardKey !== defaultShard) {
2057
- warnUnauthenticatedShardAccessOnce("shard");
2647
+ guardUnauthenticatedShardAccess("shard");
2648
+ }
2649
+ const upgradeHeaders = new Headers(request.headers);
2650
+ const clientHeaderNames = [...upgradeHeaders.keys()];
2651
+ for (const name of clientHeaderNames) {
2652
+ if (name.startsWith("x-lunora-")) {
2653
+ upgradeHeaders.delete(name);
2654
+ }
2655
+ }
2656
+ const forwardedUserId = forwardedHeaders["x-lunora-userid"];
2657
+ const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
2658
+ const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
2659
+ if (forwardedUserId !== void 0) {
2660
+ upgradeHeaders.set("x-lunora-userid", forwardedUserId);
2661
+ }
2662
+ if (forwardedIdentity !== void 0) {
2663
+ upgradeHeaders.set("x-lunora-identity", forwardedIdentity);
2664
+ }
2665
+ if (forwardedExp !== void 0) {
2666
+ upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
2667
+ }
2668
+ const binding = resolveShardBindingName(env, options.shardDO);
2669
+ if (binding !== void 0) {
2670
+ upgradeHeaders.set("x-lunora-shard-binding", binding);
2671
+ const relayCount = await probeRelayCount(shardDO, shardKey);
2672
+ if (relayCount > 0) {
2673
+ const target = relayName(shardKey, Math.floor(Math.random() * relayCount));
2674
+ return forwardToShard(shardDO, target, new Request(request, { headers: upgradeHeaders }));
2675
+ }
2676
+ }
2677
+ return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
2678
+ };
2679
+ const handleVoiceUpgrade = async (request, env, url) => {
2680
+ const { voiceAgents } = options;
2681
+ if (voiceAgents === void 0) {
2682
+ return new Response("Not found", { status: 404 });
2683
+ }
2684
+ if (request.headers.get("Upgrade") !== "websocket") {
2685
+ return new Response("Expected a WebSocket upgrade", { headers: { allow: "GET" }, status: 426 });
2686
+ }
2687
+ const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
2688
+ if (blockedUpgrade) {
2689
+ return blockedUpgrade;
2690
+ }
2691
+ let agentName;
2692
+ try {
2693
+ agentName = decodeURIComponent(url.pathname.slice(VOICE_PATH_PREFIX.length));
2694
+ } catch {
2695
+ return new Response("Unknown voice agent", { status: 404 });
2696
+ }
2697
+ const namespace = Object.hasOwn(voiceAgents, agentName) ? voiceAgents[agentName] : void 0;
2698
+ if (namespace === void 0) {
2699
+ return new Response("Unknown voice agent", { status: 404 });
2700
+ }
2701
+ const threadKey = url.searchParams.get("threadKey");
2702
+ if (threadKey === null || threadKey.length === 0) {
2703
+ return new Response("Missing threadKey", { status: 400 });
2704
+ }
2705
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2706
+ if (options.authorizeShard) {
2707
+ const allowed = await options.authorizeShard(identity, threadKey);
2708
+ if (!allowed) {
2709
+ return new Response("Forbidden", { status: 403 });
2710
+ }
2711
+ } else {
2712
+ guardUnauthenticatedShardAccess("shard");
2058
2713
  }
2059
2714
  const upgradeHeaders = new Headers(request.headers);
2060
2715
  upgradeHeaders.delete("x-lunora-userid");
@@ -2072,31 +2727,36 @@ const createWorker = (options) => {
2072
2727
  if (forwardedExp !== void 0) {
2073
2728
  upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
2074
2729
  }
2075
- return forwardToShard(options.shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
2730
+ return forwardToShard(namespace, threadKey, new Request(request, { headers: upgradeHeaders }));
2731
+ };
2732
+ const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
2733
+ if (options.authorizeFanOut) {
2734
+ const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
2735
+ if (!allowed) {
2736
+ throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
2737
+ }
2738
+ return;
2739
+ }
2740
+ if (functionPath.startsWith("__lunora_relation__:")) {
2741
+ throw new LunoraError("reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker", {
2742
+ code: "FORBIDDEN_FANOUT",
2743
+ status: 403
2744
+ });
2745
+ }
2746
+ if (options.authorizeShard) {
2747
+ throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
2748
+ code: "FORBIDDEN_FANOUT",
2749
+ status: 403
2750
+ });
2751
+ }
2752
+ guardUnauthenticatedShardAccess("fan-out");
2076
2753
  };
2077
2754
  const authorizeRpcEnvelope = async (envelope, identity) => {
2755
+ if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_admin__:")) {
2756
+ return;
2757
+ }
2078
2758
  if (envelope.fanOut) {
2079
- if (options.authorizeFanOut) {
2080
- const allowed = await options.authorizeFanOut(identity, envelope.fanOut.table, envelope.functionPath);
2081
- if (!allowed) {
2082
- throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
2083
- }
2084
- } else if (envelope.functionPath.startsWith("__lunora_relation__:")) {
2085
- throw new LunoraError(
2086
- "reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker",
2087
- {
2088
- code: "FORBIDDEN_FANOUT",
2089
- status: 403
2090
- }
2091
- );
2092
- } else if (options.authorizeShard) {
2093
- throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
2094
- code: "FORBIDDEN_FANOUT",
2095
- status: 403
2096
- });
2097
- } else {
2098
- warnUnauthenticatedShardAccessOnce("fan-out");
2099
- }
2759
+ await authorizeFanOutEnvelope(envelope.fanOut, envelope.functionPath, identity);
2100
2760
  return;
2101
2761
  }
2102
2762
  if (options.authorizeShard) {
@@ -2106,19 +2766,21 @@ const createWorker = (options) => {
2106
2766
  throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
2107
2767
  }
2108
2768
  } else if (envelope.shardKey !== void 0 && envelope.shardKey !== defaultShard) {
2109
- warnUnauthenticatedShardAccessOnce("shard");
2769
+ guardUnauthenticatedShardAccess("shard");
2110
2770
  }
2111
2771
  };
2112
2772
  const dispatchSingleShard = async (functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
2113
2773
  const rpcStartedAt = Date.now();
2114
2774
  const { observability } = options;
2775
+ const traceId = otlpRandomHex(16);
2776
+ const spanId = otlpRandomHex(8);
2115
2777
  const forwarded = new Request(`https://shard.internal/rpc`, {
2116
2778
  body: JSON.stringify({ args, functionPath }),
2117
- headers: forwardedHeaders,
2779
+ headers: { ...forwardedHeaders, traceparent: buildTraceparent(traceId, spanId) },
2118
2780
  method: "POST"
2119
2781
  });
2120
2782
  try {
2121
- const response = await forwardToShard(options.shardDO, shardKey, forwarded);
2783
+ const response = await forwardToShard(shardDO, shardKey, forwarded);
2122
2784
  emitRpcEvent(
2123
2785
  observability,
2124
2786
  {
@@ -2126,19 +2788,15 @@ const createWorker = (options) => {
2126
2788
  functionPath,
2127
2789
  ok: response.ok,
2128
2790
  shardKey,
2791
+ spanId,
2792
+ traceId,
2129
2793
  ...response.ok ? {} : { error: { code: "SHARD_ERROR", message: `shard returned ${String(response.status)}`, status: response.status } }
2130
2794
  },
2131
2795
  sinkContext
2132
2796
  );
2133
- const responseBookmark = response.headers.get("x-d1-bookmark");
2134
- if (responseBookmark) {
2135
- const headers = new Headers(response.headers);
2136
- headers.set("x-d1-bookmark", responseBookmark);
2137
- return new Response(response.body, { headers, status: response.status });
2138
- }
2139
2797
  return response;
2140
2798
  } catch (error) {
2141
- emitRpcEvent(observability, buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey }), sinkContext);
2799
+ emitRpcEvent(observability, { ...buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey }), spanId, traceId }, sinkContext);
2142
2800
  throw error;
2143
2801
  }
2144
2802
  };
@@ -2147,6 +2805,7 @@ const createWorker = (options) => {
2147
2805
  throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2148
2806
  }
2149
2807
  const envelope = await parseEnvelope(request);
2808
+ logRpcDebug(env, envelope);
2150
2809
  if (envelope.fanOut && envelope.shardKey) {
2151
2810
  throw new LunoraError("RPC envelope cannot set both `shardKey` and `fanOut`", { code: "BAD_REQUEST", status: 400 });
2152
2811
  }
@@ -2162,14 +2821,15 @@ const createWorker = (options) => {
2162
2821
  status: 400
2163
2822
  });
2164
2823
  }
2165
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2824
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2166
2825
  await authorizeRpcEnvelope(envelope, identity);
2826
+ const x402Tag = resolveX402Charge(envelope, options);
2167
2827
  {
2168
2828
  const rpcStartedAt = Date.now();
2169
2829
  const { observability } = options;
2170
2830
  const sinkContext = context ? {
2171
2831
  waitUntil: (promise) => {
2172
- context.waitUntil(promise);
2832
+ context.waitUntil?.(promise);
2173
2833
  }
2174
2834
  } : void 0;
2175
2835
  if (envelope.fanOut) {
@@ -2181,7 +2841,7 @@ const createWorker = (options) => {
2181
2841
  });
2182
2842
  }
2183
2843
  try {
2184
- const result = await coordinator.fanOut(options.shardDO, {
2844
+ const result = await coordinator.fanOut(shardDO, {
2185
2845
  args: envelope.args ?? {},
2186
2846
  fanOut: envelope.fanOut,
2187
2847
  functionPath: envelope.functionPath,
@@ -2215,16 +2875,153 @@ const createWorker = (options) => {
2215
2875
  }
2216
2876
  }
2217
2877
  const shardKey = envelope.shardKey ?? defaultShard;
2218
- return dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
2878
+ const dispatch = () => dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
2879
+ if (x402Tag && options.x402Charge) {
2880
+ return options.x402Charge(request, { functionPath: envelope.functionPath, price: x402Tag.price }, dispatch);
2881
+ }
2882
+ return dispatch();
2219
2883
  }
2220
2884
  };
2885
+ const handleBatchRpc = async (request, env, context) => {
2886
+ if (request.method !== "POST") {
2887
+ throw new LunoraError("RPC batch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2888
+ }
2889
+ const text = await readBodyTextWithLimit(request);
2890
+ let body;
2891
+ try {
2892
+ body = JSON.parse(text);
2893
+ } catch {
2894
+ throw new LunoraError("RPC batch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
2895
+ }
2896
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
2897
+ throw new LunoraError("RPC batch body must be an object", { code: "BAD_REQUEST", status: 400 });
2898
+ }
2899
+ const { calls } = body;
2900
+ if (!Array.isArray(calls)) {
2901
+ throw new LunoraError("RPC batch `calls` must be an array", { code: "BAD_REQUEST", status: 400 });
2902
+ }
2903
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2904
+ const groups = groupBatchCallsByShard(calls, defaultShard);
2905
+ for (const entries of groups.values()) {
2906
+ for (const entry of entries) {
2907
+ if (options.functions?.[entry.functionPath]?.x402) {
2908
+ throw new LunoraError(
2909
+ `paid (\`.x402\`) function "${entry.functionPath}" cannot be called in a batch; dispatch it individually over ${RPC_PATH}`,
2910
+ {
2911
+ code: "BAD_REQUEST",
2912
+ status: 400
2913
+ }
2914
+ );
2915
+ }
2916
+ }
2917
+ }
2918
+ await Promise.all(
2919
+ [...groups.entries()].flatMap(
2920
+ ([shardKey, entries]) => entries.map((entry) => authorizeRpcEnvelope({ functionPath: entry.functionPath, shardKey }, identity))
2921
+ )
2922
+ );
2923
+ const { observability } = options;
2924
+ const sinkContext = context ? {
2925
+ waitUntil: (promise) => {
2926
+ context.waitUntil?.(promise);
2927
+ }
2928
+ } : void 0;
2929
+ const results = [];
2930
+ const bookmarks = [];
2931
+ const slotError = (entry, status, code, message) => {
2932
+ return { body: { error: { code, message } }, id: entry.id, status };
2933
+ };
2934
+ const failSubBatch = (entries, status, code, message, eventFor) => {
2935
+ for (const entry of entries) {
2936
+ emitRpcEvent(observability, eventFor(entry), sinkContext);
2937
+ results.push(slotError(entry, status, code, message));
2938
+ }
2939
+ };
2940
+ const emitEntryEvents = (entries, shardKey, durationMs, statusById, fallbackStatus) => {
2941
+ for (const entry of entries) {
2942
+ const status = statusById.get(entry.id) ?? fallbackStatus;
2943
+ const ok = status < 400;
2944
+ emitRpcEvent(
2945
+ observability,
2946
+ {
2947
+ durationMs,
2948
+ functionPath: entry.functionPath,
2949
+ ok,
2950
+ shardKey,
2951
+ ...ok ? {} : { error: { code: "SHARD_ERROR", message: `batched call returned ${String(status)}`, status } }
2952
+ },
2953
+ sinkContext
2954
+ );
2955
+ }
2956
+ };
2957
+ await Promise.all(
2958
+ [...groups.entries()].map(async ([shardKey, entries]) => {
2959
+ const headers = new Headers(forwardedHeaders);
2960
+ headers.set("content-type", "application/json");
2961
+ const subRequest = new Request("https://shard.internal/rpc-batch", { body: JSON.stringify({ calls: entries }), headers, method: "POST" });
2962
+ const subStartedAt = Date.now();
2963
+ let response;
2964
+ try {
2965
+ response = await forwardToShard(shardDO, shardKey, subRequest);
2966
+ } catch (error) {
2967
+ const durationMs2 = Date.now() - subStartedAt;
2968
+ const { body: errorBody } = toErrorBody(error, { fallbackCode: "SHARD_UNAVAILABLE", redactedMessage: "shard unavailable" });
2969
+ failSubBatch(
2970
+ entries,
2971
+ 502,
2972
+ errorBody.code,
2973
+ errorBody.message,
2974
+ (entry) => buildErrorEvent(entry.functionPath, durationMs2, error, { shardKey })
2975
+ );
2976
+ return;
2977
+ }
2978
+ const durationMs = Date.now() - subStartedAt;
2979
+ const bookmark = response.headers.get("x-d1-bookmark");
2980
+ if (bookmark) {
2981
+ bookmarks.push(bookmark);
2982
+ }
2983
+ let parsed;
2984
+ try {
2985
+ parsed = await response.json();
2986
+ } catch {
2987
+ const message = `shard batch returned a non-JSON response (${String(response.status)})`;
2988
+ failSubBatch(entries, response.status, "SHARD_ERROR", message, (entry) => {
2989
+ return {
2990
+ durationMs,
2991
+ error: { code: "SHARD_ERROR", message, status: response.status },
2992
+ functionPath: entry.functionPath,
2993
+ ok: false,
2994
+ shardKey
2995
+ };
2996
+ });
2997
+ return;
2998
+ }
2999
+ const entryResults = Array.isArray(parsed.results) ? parsed.results : [];
3000
+ const statusById = new Map(entryResults.map((entry) => [entry.id, entry.status ?? response.status]));
3001
+ const seenIds = new Set(entryResults.map((entry) => entry.id));
3002
+ emitEntryEvents(entries, shardKey, durationMs, statusById, response.status);
3003
+ results.push(...entryResults);
3004
+ for (const entry of entries) {
3005
+ if (!seenIds.has(entry.id)) {
3006
+ results.push(slotError(entry, response.status, "SHARD_ERROR", `shard batch omitted result for call ${String(entry.id)}`));
3007
+ }
3008
+ }
3009
+ })
3010
+ );
3011
+ const responseHeaders = { "content-type": "application/json" };
3012
+ const [onlyBookmark] = bookmarks;
3013
+ if (bookmarks.length === 1 && onlyBookmark !== void 0) {
3014
+ responseHeaders["x-d1-bookmark"] = onlyBookmark;
3015
+ }
3016
+ return Response.json({ results }, { headers: responseHeaders, status: 200 });
3017
+ };
2221
3018
  const serverQuery = async (request, env, reference, args = {}, callOptions = {}) => {
2222
3019
  try {
2223
3020
  const functionPath = reference.__lunoraRef;
2224
3021
  if (typeof functionPath !== "string") {
2225
3022
  throw new LunoraError("serverQuery: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
2226
3023
  }
2227
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
3024
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2228
3025
  await authorizeRpcEnvelope({ functionPath, shardKey: callOptions.shardKey }, identity);
2229
3026
  const shardKey = callOptions.shardKey ?? defaultShard;
2230
3027
  return await dispatchSingleShard(functionPath, args, shardKey, forwardedHeaders);
@@ -2269,43 +3066,33 @@ const createWorker = (options) => {
2269
3066
  if (!coordinator) {
2270
3067
  throw new LunoraError("scheduled backup requires a `queryCoordinator` on the worker", { code: "BACKUP_NOT_CONFIGURED", status: 500 });
2271
3068
  }
2272
- if (!options.adminToken || options.adminToken.length === 0) {
2273
- throw new LunoraError("scheduled backup requires an `adminToken` to authenticate the per-shard export gate", {
3069
+ const adminToken = effectiveAdminToken();
3070
+ if (!adminToken || adminToken.length === 0) {
3071
+ throw new LunoraError("scheduled backup requires an `adminToken` (or `env.LUNORA_ADMIN_TOKEN`) to authenticate the per-shard export gate", {
2274
3072
  code: "BACKUP_NOT_CONFIGURED",
2275
3073
  status: 500
2276
3074
  });
2277
3075
  }
2278
- const forwardedHeaders = { authorization: `Bearer ${options.adminToken}`, "content-type": "application/json" };
3076
+ const forwardedHeaders = { authorization: `Bearer ${adminToken}`, "content-type": "application/json" };
2279
3077
  const tables = options.backupTables;
2280
3078
  let rows = 0;
2281
3079
  let bytes = 0;
2282
- let streamError;
2283
- const stream = new ReadableStream({
2284
- async pull(streamController) {
2285
- const writeRow = (row) => {
2286
- const encoded = NDJSON_ENCODER.encode(`${JSON.stringify(row)}
2287
- `);
2288
- rows += 1;
2289
- bytes += encoded.byteLength;
2290
- streamController.enqueue(encoded);
2291
- };
2292
- try {
2293
- await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow);
2294
- streamController.close();
2295
- } catch (error) {
2296
- streamError = error instanceof Error ? error : new Error(String(error));
2297
- streamController.error(error);
2298
- }
2299
- }
2300
- });
3080
+ const parts = [];
3081
+ const writeRow = (row) => {
3082
+ const line = `${JSON.stringify(row)}
3083
+ `;
3084
+ rows += 1;
3085
+ bytes += NDJSON_ENCODER.encode(line).byteLength;
3086
+ parts.push(line);
3087
+ };
3088
+ await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow, shardDO);
2301
3089
  const prefix = options.backupPrefix ?? "backups/";
2302
3090
  const timestamp = new Date(controller.scheduledTime).toISOString();
2303
3091
  const fileKey = `${prefix}lunora-backup-${timestamp.replaceAll(/[.:]/gu, "-")}.ndjson`;
2304
3092
  const manifestKey = `${fileKey}.manifest.json`;
2305
- await store.put(fileKey, stream, { httpMetadata: { contentType: "application/x-ndjson" } });
2306
- if (streamError !== void 0) {
2307
- throw streamError;
2308
- }
3093
+ await store.put(fileKey, new Blob(parts, { type: "application/x-ndjson" }), {
3094
+ httpMetadata: { contentType: "application/x-ndjson" }
3095
+ });
2309
3096
  const manifest = {
2310
3097
  bytes,
2311
3098
  createdAt: timestamp,
@@ -2321,6 +3108,7 @@ const createWorker = (options) => {
2321
3108
  await pruneBackups(store, prefix);
2322
3109
  };
2323
3110
  const handleScheduled = async (controller, env, context) => {
3111
+ resolveAdminTokenFromEnv(env);
2324
3112
  const errors = [];
2325
3113
  const toError = (error) => error instanceof Error ? error : new Error(String(error));
2326
3114
  const userHandler = options.crons?.[controller.cron];
@@ -2359,7 +3147,7 @@ const createWorker = (options) => {
2359
3147
  headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
2360
3148
  method: "POST"
2361
3149
  });
2362
- await forwardToShard(options.shardDO, defaultShard, recordRequest);
3150
+ await forwardToShard(shardDO, defaultShard, recordRequest);
2363
3151
  } catch {
2364
3152
  }
2365
3153
  };
@@ -2373,16 +3161,41 @@ const createWorker = (options) => {
2373
3161
  }
2374
3162
  const basePath = options.authBasePath ?? DEFAULT_AUTH_BASE_PATH;
2375
3163
  if (isAuthAttemptPath(url.pathname, basePath)) {
2376
- context.waitUntil(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
3164
+ context.waitUntil?.(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
2377
3165
  }
2378
3166
  return authResponse;
2379
3167
  };
2380
3168
  const customRoutes = options.routes !== void 0 && Object.keys(options.routes).length > 0 ? options.routes : void 0;
2381
3169
  const internalRoutes = {
3170
+ [STATUS_PATH]: (request) => {
3171
+ if (request.method !== "GET" && request.method !== "HEAD") {
3172
+ return new Response(void 0, { headers: { allow: "GET, HEAD" }, status: 405 });
3173
+ }
3174
+ return Response.json({ ok: true }, { headers: { "cache-control": "no-store" } });
3175
+ },
2382
3176
  [WS_PATH]: (request, env, url) => handleWebSocketUpgrade(request, env, url),
2383
3177
  [RPC_PATH]: (request, env, _url, context) => handleRpc(request, env, context),
3178
+ [RPC_BATCH_PATH]: (request, env, _url, context) => handleBatchRpc(request, env, context),
2384
3179
  [SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
2385
3180
  [CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
3181
+ // Mint a short-lived HMAC-signed WS admin sub-token. Gated by the master
3182
+ // admin bearer (header) / `adminGate`; the studio then sends the minted
3183
+ // token — not the master credential — in the WS `?token=`
3184
+ // query string. Signed with the master token itself, so both isolates
3185
+ // verify statelessly and rotating `LUNORA_ADMIN_TOKEN` invalidates every
3186
+ // outstanding sub-token. `no-store` keeps the token out of caches.
3187
+ [ADMIN_WS_TOKEN_PATH]: async (request) => {
3188
+ if (request.method !== "POST") {
3189
+ throw new LunoraError("ws-token endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
3190
+ }
3191
+ assertAdminAuthorized(request);
3192
+ const signingSecret = effectiveAdminToken();
3193
+ if (signingSecret === void 0) {
3194
+ throw new LunoraError("ws-token minting requires a configured admin token", { code: "ADMIN_TOKEN_NOT_CONFIGURED", status: 400 });
3195
+ }
3196
+ const minted = await mintWsAdminToken(signingSecret);
3197
+ return Response.json(minted, { headers: { "cache-control": "no-store" } });
3198
+ },
2386
3199
  // Extracted handler clusters built above, merged in (mirroring the auth
2387
3200
  // plane below): orchestration (migrate / rank / rankpage / shard-traffic /
2388
3201
  // pitr), data-movement (export / import / sync / connector-sync / apply),
@@ -2394,13 +3207,13 @@ const createWorker = (options) => {
2394
3207
  ...workflowsAdminRoutes,
2395
3208
  ...storageAdminRoutes,
2396
3209
  ...vectorAdminRoutes,
3210
+ ...kvAdminRoutes,
2397
3211
  ...introspectionAdminRoutes,
2398
3212
  // `/_lunora/admin/auth/*` — the whole user-management plane, one route per
2399
3213
  // `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
2400
3214
  ...buildAuthAdminRoutes({
2401
3215
  assertAdmin: assertAdminAuthorized,
2402
- // eslint-disable-next-line sonarjs/deprecation -- `authIntrospector` is the intentional read-only fallback
2403
- getAuthAdmin: () => options.authAdmin ?? options.authIntrospector,
3216
+ getAuthAdmin: () => options.authAdmin,
2404
3217
  parsePaging,
2405
3218
  queryParameter,
2406
3219
  readJsonBody: readJsonBodyWithLimit
@@ -2414,11 +3227,23 @@ const createWorker = (options) => {
2414
3227
  resolvedSecurity = resolveSecurity(options.security, env ?? {});
2415
3228
  }
2416
3229
  };
3230
+ const applyAdminGate = async (request, pathname) => {
3231
+ if (options.adminGate === void 0 || !isAdminPath(pathname)) {
3232
+ return;
3233
+ }
3234
+ try {
3235
+ if (await options.adminGate(request)) {
3236
+ accessAdminGrants.add(request);
3237
+ }
3238
+ } catch {
3239
+ }
3240
+ };
2417
3241
  const handle = async (request, env, context) => {
2418
3242
  const url = new URL(request.url);
2419
3243
  if (request.method === "POST" || request.method === "PUT") {
2420
3244
  const contentLength = Number(request.headers.get("content-length") ?? "");
2421
- if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
3245
+ const maxBodyBytes = url.pathname === KV_VALUE_PATH ? KV_VALUE_MAX_BODY_BYTES : MAX_BODY_BYTES;
3246
+ if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
2422
3247
  throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
2423
3248
  }
2424
3249
  }
@@ -2435,8 +3260,12 @@ const createWorker = (options) => {
2435
3260
  }
2436
3261
  const internalRoute = internalRoutes[url.pathname];
2437
3262
  if (internalRoute) {
3263
+ await applyAdminGate(request, url.pathname);
2438
3264
  return internalRoute(request, env, url, context);
2439
3265
  }
3266
+ if (options.voiceAgents !== void 0 && url.pathname.startsWith(VOICE_PATH_PREFIX)) {
3267
+ return handleVoiceUpgrade(request, env, url);
3268
+ }
2440
3269
  const httpRouteResponse = await dispatchHttpRoute(request, env, context);
2441
3270
  if (httpRouteResponse) {
2442
3271
  return httpRouteResponse;
@@ -2446,7 +3275,7 @@ const createWorker = (options) => {
2446
3275
  return {
2447
3276
  async fetch(request, env, context) {
2448
3277
  if (options.passThroughOnException) {
2449
- context.passThroughOnException();
3278
+ context.passThroughOnException?.();
2450
3279
  }
2451
3280
  ensureSecurityResolved(env);
2452
3281
  resolveAdminTokenFromEnv(env);
@@ -2465,6 +3294,9 @@ const createWorker = (options) => {
2465
3294
  return decorateResponse(toErrorResponse(error), request, resolvedSecurity);
2466
3295
  }
2467
3296
  },
3297
+ async queue(batch, env, context) {
3298
+ await options.queue?.(batch, env, context);
3299
+ },
2468
3300
  async scheduled(controller, env, context) {
2469
3301
  await handleScheduled(controller, env, context);
2470
3302
  },
@@ -2495,10 +3327,24 @@ const withFrameworkWorker = (host, optionsInput) => {
2495
3327
  const optionsFactory = optionsInput;
2496
3328
  return {
2497
3329
  fetch: (request, env, context) => build(optionsFactory(env)).fetch(request, env, context),
3330
+ queue: (batch, env, context) => build(optionsFactory(env)).queue?.(batch, env, context) ?? Promise.resolve(),
2498
3331
  scheduled: (controller, env, context) => build(optionsFactory(env)).scheduled(controller, env, context),
2499
3332
  serverQuery: (request, env, reference, args, options) => build(optionsFactory(env)).serverQuery(request, env, reference, args, options)
2500
3333
  };
2501
3334
  };
3335
+ const resolveLunoraOptions = (options, env) => {
3336
+ if (typeof options === "function") {
3337
+ return options(env);
3338
+ }
3339
+ const shardDO = options.shardDO ?? env?.SHARD;
3340
+ if (!shardDO) {
3341
+ throw new LunoraError(
3342
+ "@lunora/runtime: no shard Durable Object namespace found. Bind `SHARD` in wrangler.jsonc, or pass `createLunoraHandler({ shardDO: env.MY_SHARD })`."
3343
+ );
3344
+ }
3345
+ return { ...options, shardDO };
3346
+ };
3347
+ const createLunoraHandler = (options = {}) => (request, env, context) => createWorker(resolveLunoraOptions(options, env)).fetch(request, env, context ?? NOOP_EXECUTION_CONTEXT);
2502
3348
  const defineRpcEnvelope = (envelope) => envelope;
2503
3349
 
2504
- export { composeWorker, createWorker, defineRpcEnvelope, withFrameworkWorker };
3350
+ export { NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, probeRelayCount, resolveLunoraOptions, withFrameworkWorker };