@lunora/runtime 1.0.0-alpha.1 → 1.0.0-alpha.11

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,8 +1,12 @@
1
+ import { NOOP_EXECUTION_CONTEXT } from './NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
1
2
  import { LunoraError, toErrorResponse, isStructuralLunoraError, isStructuralConflictError } from './LunoraError-CL0aOtpo.mjs';
2
- import { emitRpcEvent } from './emitRpcEvent-pEdtqAK8.mjs';
3
- import { resolveShard } from './resolveShard-DDkzWtrU.mjs';
3
+ import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
4
+ import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
4
5
  import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-DbISh_Wi.mjs';
5
6
 
7
+ const RELAY_NAME_INFIX = "::relay::";
8
+ const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
9
+
6
10
  const AUTH_BASE = "/_lunora/admin/auth";
7
11
  const AUTH_ADMIN_ERROR_STATUS = {
8
12
  PASSWORD_TOO_LONG: 400,
@@ -278,6 +282,45 @@ const buildAuthAdminRoutes = (deps) => {
278
282
  return routes;
279
283
  };
280
284
 
285
+ const MAX_BATCH_ENTRIES = 500;
286
+
287
+ const normalizeBatchCall = (raw, index, defaultShard) => {
288
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
289
+ throw new LunoraError("each batch call must be an object", { code: "BAD_REQUEST", status: 400 });
290
+ }
291
+ const call = raw;
292
+ if (typeof call.functionPath !== "string") {
293
+ throw new LunoraError("each batch call needs a string `functionPath`", { code: "BAD_REQUEST", status: 400 });
294
+ }
295
+ if (call.functionPath.startsWith("__lunora_relation__:") || call.functionPath.startsWith("__lunora_admin__")) {
296
+ throw new LunoraError("reserved function path cannot be batched", { code: "FORBIDDEN", status: 403 });
297
+ }
298
+ return {
299
+ entry: {
300
+ args: call.args === void 0 ? {} : call.args,
301
+ clientId: typeof call.clientId === "string" ? call.clientId : void 0,
302
+ clientSeq: typeof call.clientSeq === "number" ? call.clientSeq : void 0,
303
+ functionPath: call.functionPath,
304
+ id: typeof call.id === "number" ? call.id : index,
305
+ mutationId: typeof call.mutationId === "string" ? call.mutationId : void 0
306
+ },
307
+ shardKey: typeof call.shardKey === "string" ? call.shardKey : defaultShard
308
+ };
309
+ };
310
+ const groupBatchCallsByShard = (calls, defaultShard) => {
311
+ if (calls.length > MAX_BATCH_ENTRIES) {
312
+ throw new LunoraError(`RPC batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit`, { code: "BAD_REQUEST", status: 400 });
313
+ }
314
+ const groups = /* @__PURE__ */ new Map();
315
+ for (const [index, raw] of calls.entries()) {
316
+ const { entry, shardKey } = normalizeBatchCall(raw, index, defaultShard);
317
+ const group = groups.get(shardKey) ?? [];
318
+ group.push(entry);
319
+ groups.set(shardKey, group);
320
+ }
321
+ return groups;
322
+ };
323
+
281
324
  const MAX_BODY_BYTES = 1048576;
282
325
  const readBodyTextWithLimit = async (request, limit = MAX_BODY_BYTES) => {
283
326
  if (!request.body) {
@@ -597,14 +640,14 @@ const partitionExportTables = (options, tables) => {
597
640
  }
598
641
  return { globalTables, shardLocalTables };
599
642
  };
600
- const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow) => {
643
+ const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace) => {
601
644
  if (tables !== void 0 && shardLocalTables.length === 0) {
602
645
  return;
603
646
  }
604
647
  const exportTables = tables === void 0 ? [] : shardLocalTables;
605
648
  const probeFallback = tables === void 0 ? collectKnownTables() : [];
606
649
  const probeTables = exportTables.length > 0 ? exportTables : probeFallback;
607
- const result = await coordinator.orchestrateExport(options.shardDO, {
650
+ const result = await coordinator.orchestrateExport(namespace, {
608
651
  args: { tables: exportTables },
609
652
  headers: forwardedHeaders,
610
653
  tables: probeTables
@@ -618,9 +661,9 @@ const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tabl
618
661
  }
619
662
  }
620
663
  };
621
- const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow) => {
664
+ const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow, namespace) => {
622
665
  const { globalTables, shardLocalTables } = partitionExportTables(options, tables);
623
- await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow);
666
+ await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace);
624
667
  const exportGlobalsFunction = options.exportGlobals;
625
668
  const wantGlobals = tables === void 0 || globalTables.length > 0;
626
669
  if (wantGlobals && exportGlobalsFunction) {
@@ -740,7 +783,7 @@ const mergeImportResult = (totals, result) => {
740
783
  }
741
784
  totals.conflicts += result.conflicts;
742
785
  };
743
- const streamingImport = async (request, options, forwardedHeaders) => {
786
+ const streamingImport = async (request, options, forwardedHeaders, namespace) => {
744
787
  const defaultShard = options.defaultShardKey ?? "__root__";
745
788
  const { errors, globalRows, perShard } = await bucketImportStream(request, options, defaultShard);
746
789
  const totals = { conflicts: 0, errors, inserted: {} };
@@ -749,7 +792,7 @@ const streamingImport = async (request, options, forwardedHeaders) => {
749
792
  if (!coordinator) {
750
793
  throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
751
794
  }
752
- const result = await coordinator.orchestrateImport(options.shardDO, {
795
+ const result = await coordinator.orchestrateImport(namespace, {
753
796
  batches: [...perShard.values()],
754
797
  headers: forwardedHeaders
755
798
  });
@@ -966,7 +1009,7 @@ const buildIntrospectionAdminRoutes = (deps) => {
966
1009
  };
967
1010
  };
968
1011
 
969
- const MIGRATE_PATH = "/_lunora/migrate";
1012
+ const MIGRATE_PATH$1 = "/_lunora/migrate";
970
1013
  const PITR_PATH = "/_lunora/admin/pitr";
971
1014
  const RANK_PATH = "/_lunora/admin/rank";
972
1015
  const RANKPAGE_PATH = "/_lunora/admin/rankpage";
@@ -1223,7 +1266,7 @@ const buildOrchestrationAdminRoutes = (deps) => {
1223
1266
  return forwardToShard(shardDO, pitr.shardKey ?? defaultShard, forwarded);
1224
1267
  };
1225
1268
  return {
1226
- [MIGRATE_PATH]: handleMigrate,
1269
+ [MIGRATE_PATH$1]: handleMigrate,
1227
1270
  [PITR_PATH]: handlePitr,
1228
1271
  [RANK_PATH]: handleRank,
1229
1272
  [RANKPAGE_PATH]: handleRankPage,
@@ -1502,7 +1545,7 @@ const buildWorkflowsAdminRoutes = (deps) => {
1502
1545
  assertAdmin(request);
1503
1546
  const client = resolveWorkflowsClient(env);
1504
1547
  if (!client) {
1505
- return throwNotConfigured();
1548
+ return Response.json({ configured: false, instances: [], page: 1, perPage: 0, totalCount: 0 });
1506
1549
  }
1507
1550
  const workflowName = requireQuery(url, "name");
1508
1551
  const status = toInstanceStatus(url.searchParams.get("status"));
@@ -1554,9 +1597,13 @@ const buildWorkflowsAdminRoutes = (deps) => {
1554
1597
 
1555
1598
  const NDJSON_ENCODER = new TextEncoder();
1556
1599
  const RPC_PATH = "/_lunora/rpc";
1600
+ const RPC_BATCH_PATH = "/_lunora/rpc-batch";
1557
1601
  const WS_PATH = "/_lunora/ws";
1558
1602
  const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
1559
1603
  const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
1604
+ const ADMIN_PATH_PREFIX = "/_lunora/admin/";
1605
+ const MIGRATE_PATH = "/_lunora/migrate";
1606
+ const isAdminPath = (pathname) => pathname.startsWith(ADMIN_PATH_PREFIX) || pathname === MIGRATE_PATH;
1560
1607
  const DEFAULT_AUTH_BASE_PATH = "/api/auth";
1561
1608
  const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
1562
1609
  const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
@@ -1598,6 +1645,8 @@ const resolveForwardContext = async (request, env, resolveIdentity) => {
1598
1645
  const cookie = request.headers.get("cookie");
1599
1646
  const bookmark = request.headers.get("x-d1-bookmark");
1600
1647
  const mutationId = request.headers.get("x-lunora-mutation-id");
1648
+ const clientId = request.headers.get("x-lunora-client-id");
1649
+ const clientSeq = request.headers.get("x-lunora-client-seq");
1601
1650
  if (authorization) {
1602
1651
  headers["authorization"] = authorization;
1603
1652
  }
@@ -1610,6 +1659,12 @@ const resolveForwardContext = async (request, env, resolveIdentity) => {
1610
1659
  if (mutationId) {
1611
1660
  headers["x-lunora-mutation-id"] = mutationId;
1612
1661
  }
1662
+ if (clientId) {
1663
+ headers["x-lunora-client-id"] = clientId;
1664
+ }
1665
+ if (clientSeq) {
1666
+ headers["x-lunora-client-seq"] = clientSeq;
1667
+ }
1613
1668
  const clientIp = request.headers.get("cf-connecting-ip");
1614
1669
  if (clientIp) {
1615
1670
  headers["x-lunora-client-ip"] = clientIp;
@@ -1662,6 +1717,12 @@ const validateFanOut = (fanOut) => {
1662
1717
  }
1663
1718
  return spec;
1664
1719
  };
1720
+ const logRpcDebug = (env, envelope) => {
1721
+ if (!env?.LUNORA_DEBUG_RPC) {
1722
+ return;
1723
+ }
1724
+ console.warn(`[lunora:rpc] ${envelope.fanOut ? "fan-out" : `shard=${envelope.shardKey ?? "(root)"}`} ${envelope.functionPath}`);
1725
+ };
1665
1726
  const parseEnvelope = async (request) => {
1666
1727
  const text = await readBodyTextWithLimit(request);
1667
1728
  let body;
@@ -1692,6 +1753,41 @@ const forwardToShard = async (namespace, shardKey, request) => {
1692
1753
  const stub = resolveShard(namespace, shardKey);
1693
1754
  return stub.fetch(request);
1694
1755
  };
1756
+ const relayProbeCache = /* @__PURE__ */ new Map();
1757
+ const RELAY_PROBE_TTL_MS = 5e3;
1758
+ const probeRelayCount = async (namespace, shardKey) => {
1759
+ const now = Date.now();
1760
+ const cached = relayProbeCache.get(shardKey);
1761
+ if (cached !== void 0 && cached.expiresMs > now) {
1762
+ return cached.relayCount;
1763
+ }
1764
+ let relayCount = 0;
1765
+ try {
1766
+ const response = await resolveShard(namespace, shardKey).fetch(new Request("https://shard.internal/_lunora/route"));
1767
+ if (response.ok) {
1768
+ const body = await response.json();
1769
+ const reported = body.relayCount;
1770
+ if (typeof reported === "number" && reported > 0) {
1771
+ relayCount = Math.floor(reported);
1772
+ }
1773
+ }
1774
+ } catch {
1775
+ relayCount = 0;
1776
+ }
1777
+ relayProbeCache.set(shardKey, { expiresMs: now + RELAY_PROBE_TTL_MS, relayCount });
1778
+ return relayCount;
1779
+ };
1780
+ const resolveShardBindingName = (env, namespace) => {
1781
+ if (env === null || typeof env !== "object") {
1782
+ return void 0;
1783
+ }
1784
+ for (const [key, value] of Object.entries(env)) {
1785
+ if (value === namespace) {
1786
+ return key;
1787
+ }
1788
+ }
1789
+ return void 0;
1790
+ };
1695
1791
  const constantTimeEqual = (expected, supplied) => {
1696
1792
  const max = Math.max(expected.length, supplied.length);
1697
1793
  let diff = expected.length ^ supplied.length;
@@ -1740,6 +1836,31 @@ const checkAdminWsToken = (request, expected) => {
1740
1836
  };
1741
1837
  const createWorker = (options) => {
1742
1838
  const defaultShard = options.defaultShardKey ?? "__root__";
1839
+ const shardDO = applyJurisdiction(options.shardDO, options.jurisdiction);
1840
+ const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
1841
+ let envAdminToken;
1842
+ const effectiveAdminToken = () => options.adminToken ?? envAdminToken;
1843
+ const resolveAdminTokenFromEnv = (env) => {
1844
+ if (envAdminToken !== void 0 || options.adminToken !== void 0) {
1845
+ return;
1846
+ }
1847
+ const value = (env ?? {})["LUNORA_ADMIN_TOKEN"];
1848
+ if (typeof value === "string" && value.length > 0) {
1849
+ envAdminToken = value;
1850
+ }
1851
+ };
1852
+ const accessAdminGrants = /* @__PURE__ */ new WeakSet();
1853
+ const requestIsAdmin = (request) => checkAdminAuth(request, effectiveAdminToken()) || accessAdminGrants.has(request);
1854
+ const resolveAdminForwardContext = async (request, env) => {
1855
+ const context = await resolveForwardContext(request, env, options.resolveIdentity);
1856
+ if (accessAdminGrants.has(request) && context.headers["authorization"] === void 0) {
1857
+ const token = effectiveAdminToken();
1858
+ if (token !== void 0) {
1859
+ context.headers["authorization"] = `Bearer ${token}`;
1860
+ }
1861
+ }
1862
+ return context;
1863
+ };
1743
1864
  const hasAnyShardAuth = Boolean(options.authorizeShard) || Boolean(options.authorizeFanOut);
1744
1865
  let warnedUnauthenticatedShardAccess = false;
1745
1866
  const warnUnauthenticatedShardAccessOnce = (kind) => {
@@ -1758,10 +1879,10 @@ const createWorker = (options) => {
1758
1879
  const orchestrationAdminRoutes = buildOrchestrationAdminRoutes({
1759
1880
  defaultShard,
1760
1881
  forwardToShard,
1761
- isAdmin: (request) => checkAdminAuth(request, options.adminToken),
1882
+ isAdmin: requestIsAdmin,
1762
1883
  queryCoordinator: options.queryCoordinator,
1763
- resolveForwardContext: (request, env) => resolveForwardContext(request, env, options.resolveIdentity),
1764
- shardDO: options.shardDO
1884
+ resolveForwardContext: resolveAdminForwardContext,
1885
+ shardDO
1765
1886
  });
1766
1887
  const dispatchToShard = async (functionPath, args, shardKey) => {
1767
1888
  if (options.authorizeShard) {
@@ -1779,7 +1900,7 @@ const createWorker = (options) => {
1779
1900
  headers: { "content-type": "application/json", "x-lunora-system": "1" },
1780
1901
  method: "POST"
1781
1902
  });
1782
- return forwardToShard(options.shardDO, shardKey, forwarded);
1903
+ return forwardToShard(shardDO, shardKey, forwarded);
1783
1904
  };
1784
1905
  const startCronWorkflow = async (binding, job, env) => {
1785
1906
  const candidate = env?.[binding];
@@ -1824,7 +1945,7 @@ const createWorker = (options) => {
1824
1945
  }
1825
1946
  };
1826
1947
  const handleRunCronJob = async (request, env) => {
1827
- if (!checkAdminAuth(request, options.adminToken)) {
1948
+ if (!requestIsAdmin(request)) {
1828
1949
  throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1829
1950
  }
1830
1951
  if (request.method !== "POST") {
@@ -1847,12 +1968,12 @@ const createWorker = (options) => {
1847
1968
  };
1848
1969
  const releasePoolSlot = async (candidate) => {
1849
1970
  const pool = typeof candidate.pool === "string" && candidate.pool.length > 0 ? candidate.pool : void 0;
1850
- if (!pool || !options.schedulerDO || typeof candidate.id !== "string") {
1971
+ if (!pool || !schedulerDO || typeof candidate.id !== "string") {
1851
1972
  return;
1852
1973
  }
1853
1974
  const instanceName = typeof candidate.instanceName === "string" && candidate.instanceName.length > 0 ? candidate.instanceName : "default";
1854
1975
  try {
1855
- await options.schedulerDO.get(options.schedulerDO.idFromName(instanceName)).fetch(
1976
+ await schedulerDO.get(schedulerDO.idFromName(instanceName)).fetch(
1856
1977
  new Request("https://scheduler.internal/complete", {
1857
1978
  body: JSON.stringify({ id: candidate.id, pool }),
1858
1979
  headers: { "content-type": "application/json" },
@@ -1898,17 +2019,17 @@ const createWorker = (options) => {
1898
2019
  };
1899
2020
  const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
1900
2021
  applyGlobals: options.applyGlobals,
1901
- isAdmin: (request) => checkAdminAuth(request, options.adminToken),
2022
+ isAdmin: requestIsAdmin,
1902
2023
  knownTables: () => collectKnownTables(),
1903
2024
  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),
2025
+ resolveForwardContext: resolveAdminForwardContext,
2026
+ shardDO,
2027
+ streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow, shardDO),
2028
+ streamingImport: (request, headers) => streamingImport(request, options, headers, shardDO),
1908
2029
  syncGlobals: options.syncGlobals
1909
2030
  });
1910
2031
  const assertAdminAuthorized = (request) => {
1911
- if (!checkAdminAuth(request, options.adminToken)) {
2032
+ if (!requestIsAdmin(request)) {
1912
2033
  throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
1913
2034
  }
1914
2035
  };
@@ -1935,17 +2056,17 @@ const createWorker = (options) => {
1935
2056
  };
1936
2057
  };
1937
2058
  const requireSchedulerNamespace = () => {
1938
- if (options.schedulerDO === void 0) {
2059
+ if (schedulerDO === void 0) {
1939
2060
  throw new LunoraError("scheduled endpoints require a `schedulerDO` namespace on the worker", { code: "SCHEDULER_NOT_CONFIGURED", status: 400 });
1940
2061
  }
1941
- return options.schedulerDO;
2062
+ return schedulerDO;
1942
2063
  };
1943
2064
  const resolveSchedulerStub = (request) => {
1944
2065
  assertAdminAuthorized(request);
1945
2066
  return resolveShard(requireSchedulerNamespace(), options.schedulerInstanceName ?? "default");
1946
2067
  };
1947
2068
  const scheduledAdminRoutes = buildScheduledAdminRoutes({
1948
- checkWsAdmin: (request) => checkAdminAuth(request, options.adminToken) || checkAdminWsToken(request, options.adminToken),
2069
+ checkWsAdmin: (request) => requestIsAdmin(request) || checkAdminWsToken(request, effectiveAdminToken()),
1949
2070
  requireSchedulerNamespace,
1950
2071
  resolveSchedulerStub,
1951
2072
  schedulerInstanceName: options.schedulerInstanceName ?? "default"
@@ -1998,7 +2119,7 @@ const createWorker = (options) => {
1998
2119
  headers,
1999
2120
  method: "POST"
2000
2121
  });
2001
- const response = await forwardToShard(options.shardDO, defaultShard, forwarded);
2122
+ const response = await forwardToShard(shardDO, defaultShard, forwarded);
2002
2123
  const payload = await response.json();
2003
2124
  if (payload.error) {
2004
2125
  throw new LunoraError(payload.error.message ?? "shard RPC failed", {
@@ -2061,7 +2182,16 @@ const createWorker = (options) => {
2061
2182
  if (forwardedExp !== void 0) {
2062
2183
  upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
2063
2184
  }
2064
- return forwardToShard(options.shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
2185
+ const binding = resolveShardBindingName(env, options.shardDO);
2186
+ if (binding !== void 0) {
2187
+ upgradeHeaders.set("x-lunora-shard-binding", binding);
2188
+ const relayCount = await probeRelayCount(shardDO, shardKey);
2189
+ if (relayCount > 0) {
2190
+ const target = relayName(shardKey, Math.floor(Math.random() * relayCount));
2191
+ return forwardToShard(shardDO, target, new Request(request, { headers: upgradeHeaders }));
2192
+ }
2193
+ }
2194
+ return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
2065
2195
  };
2066
2196
  const authorizeRpcEnvelope = async (envelope, identity) => {
2067
2197
  if (envelope.fanOut) {
@@ -2107,7 +2237,7 @@ const createWorker = (options) => {
2107
2237
  method: "POST"
2108
2238
  });
2109
2239
  try {
2110
- const response = await forwardToShard(options.shardDO, shardKey, forwarded);
2240
+ const response = await forwardToShard(shardDO, shardKey, forwarded);
2111
2241
  emitRpcEvent(
2112
2242
  observability,
2113
2243
  {
@@ -2136,6 +2266,7 @@ const createWorker = (options) => {
2136
2266
  throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2137
2267
  }
2138
2268
  const envelope = await parseEnvelope(request);
2269
+ logRpcDebug(env, envelope);
2139
2270
  if (envelope.fanOut && envelope.shardKey) {
2140
2271
  throw new LunoraError("RPC envelope cannot set both `shardKey` and `fanOut`", { code: "BAD_REQUEST", status: 400 });
2141
2272
  }
@@ -2158,7 +2289,7 @@ const createWorker = (options) => {
2158
2289
  const { observability } = options;
2159
2290
  const sinkContext = context ? {
2160
2291
  waitUntil: (promise) => {
2161
- context.waitUntil(promise);
2292
+ context.waitUntil?.(promise);
2162
2293
  }
2163
2294
  } : void 0;
2164
2295
  if (envelope.fanOut) {
@@ -2170,7 +2301,7 @@ const createWorker = (options) => {
2170
2301
  });
2171
2302
  }
2172
2303
  try {
2173
- const result = await coordinator.fanOut(options.shardDO, {
2304
+ const result = await coordinator.fanOut(shardDO, {
2174
2305
  args: envelope.args ?? {},
2175
2306
  fanOut: envelope.fanOut,
2176
2307
  functionPath: envelope.functionPath,
@@ -2207,6 +2338,119 @@ const createWorker = (options) => {
2207
2338
  return dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
2208
2339
  }
2209
2340
  };
2341
+ const handleBatchRpc = async (request, env, context) => {
2342
+ if (request.method !== "POST") {
2343
+ throw new LunoraError("RPC batch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
2344
+ }
2345
+ const text = await readBodyTextWithLimit(request);
2346
+ let body;
2347
+ try {
2348
+ body = JSON.parse(text);
2349
+ } catch {
2350
+ throw new LunoraError("RPC batch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
2351
+ }
2352
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
2353
+ throw new LunoraError("RPC batch body must be an object", { code: "BAD_REQUEST", status: 400 });
2354
+ }
2355
+ const { calls } = body;
2356
+ if (!Array.isArray(calls)) {
2357
+ throw new LunoraError("RPC batch `calls` must be an array", { code: "BAD_REQUEST", status: 400 });
2358
+ }
2359
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2360
+ const groups = groupBatchCallsByShard(calls, defaultShard);
2361
+ await Promise.all(
2362
+ [...groups.entries()].flatMap(
2363
+ ([shardKey, entries]) => entries.map((entry) => authorizeRpcEnvelope({ functionPath: entry.functionPath, shardKey }, identity))
2364
+ )
2365
+ );
2366
+ const { observability } = options;
2367
+ const sinkContext = context ? {
2368
+ waitUntil: (promise) => {
2369
+ context.waitUntil?.(promise);
2370
+ }
2371
+ } : void 0;
2372
+ const results = [];
2373
+ let latestBookmark;
2374
+ const slotError = (entry, status, code, message) => {
2375
+ return { body: { error: { code, message } }, id: entry.id, status };
2376
+ };
2377
+ const failSubBatch = (entries, status, code, message, eventFor) => {
2378
+ for (const entry of entries) {
2379
+ emitRpcEvent(observability, eventFor(entry), sinkContext);
2380
+ results.push(slotError(entry, status, code, message));
2381
+ }
2382
+ };
2383
+ const emitEntryEvents = (entries, shardKey, durationMs, statusById, fallbackStatus) => {
2384
+ for (const entry of entries) {
2385
+ const status = statusById.get(entry.id) ?? fallbackStatus;
2386
+ const ok = status < 400;
2387
+ emitRpcEvent(
2388
+ observability,
2389
+ {
2390
+ durationMs,
2391
+ functionPath: entry.functionPath,
2392
+ ok,
2393
+ shardKey,
2394
+ ...ok ? {} : { error: { code: "SHARD_ERROR", message: `batched call returned ${String(status)}`, status } }
2395
+ },
2396
+ sinkContext
2397
+ );
2398
+ }
2399
+ };
2400
+ await Promise.all(
2401
+ [...groups.entries()].map(async ([shardKey, entries]) => {
2402
+ const headers = new Headers(forwardedHeaders);
2403
+ headers.set("content-type", "application/json");
2404
+ const subRequest = new Request("https://shard.internal/rpc-batch", { body: JSON.stringify({ calls: entries }), headers, method: "POST" });
2405
+ const subStartedAt = Date.now();
2406
+ let response;
2407
+ try {
2408
+ response = await forwardToShard(shardDO, shardKey, subRequest);
2409
+ } catch (error) {
2410
+ const durationMs2 = Date.now() - subStartedAt;
2411
+ const message = error instanceof Error ? error.message : String(error);
2412
+ failSubBatch(entries, 502, "SHARD_UNAVAILABLE", message, (entry) => buildErrorEvent(entry.functionPath, durationMs2, error, { shardKey }));
2413
+ return;
2414
+ }
2415
+ const durationMs = Date.now() - subStartedAt;
2416
+ const bookmark = response.headers.get("x-d1-bookmark");
2417
+ if (bookmark) {
2418
+ latestBookmark = bookmark;
2419
+ }
2420
+ let parsed;
2421
+ try {
2422
+ parsed = await response.json();
2423
+ } catch {
2424
+ const message = `shard batch returned a non-JSON response (${String(response.status)})`;
2425
+ failSubBatch(entries, response.status, "SHARD_ERROR", message, (entry) => {
2426
+ return {
2427
+ durationMs,
2428
+ error: { code: "SHARD_ERROR", message, status: response.status },
2429
+ functionPath: entry.functionPath,
2430
+ ok: false,
2431
+ shardKey
2432
+ };
2433
+ });
2434
+ return;
2435
+ }
2436
+ const entryResults = Array.isArray(parsed.results) ? parsed.results : [];
2437
+ const statusById = new Map(entryResults.map((entry) => [entry.id, entry.status ?? response.status]));
2438
+ const seenIds = new Set(entryResults.map((entry) => entry.id));
2439
+ emitEntryEvents(entries, shardKey, durationMs, statusById, response.status);
2440
+ results.push(...entryResults);
2441
+ for (const entry of entries) {
2442
+ if (!seenIds.has(entry.id)) {
2443
+ results.push(slotError(entry, response.status, "SHARD_ERROR", `shard batch omitted result for call ${String(entry.id)}`));
2444
+ }
2445
+ }
2446
+ })
2447
+ );
2448
+ const responseHeaders = { "content-type": "application/json" };
2449
+ if (latestBookmark !== void 0) {
2450
+ responseHeaders["x-d1-bookmark"] = latestBookmark;
2451
+ }
2452
+ return Response.json({ results }, { headers: responseHeaders, status: 200 });
2453
+ };
2210
2454
  const serverQuery = async (request, env, reference, args = {}, callOptions = {}) => {
2211
2455
  try {
2212
2456
  const functionPath = reference.__lunoraRef;
@@ -2279,7 +2523,7 @@ const createWorker = (options) => {
2279
2523
  streamController.enqueue(encoded);
2280
2524
  };
2281
2525
  try {
2282
- await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow);
2526
+ await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow, shardDO);
2283
2527
  streamController.close();
2284
2528
  } catch (error) {
2285
2529
  streamError = error instanceof Error ? error : new Error(String(error));
@@ -2348,7 +2592,7 @@ const createWorker = (options) => {
2348
2592
  headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
2349
2593
  method: "POST"
2350
2594
  });
2351
- await forwardToShard(options.shardDO, defaultShard, recordRequest);
2595
+ await forwardToShard(shardDO, defaultShard, recordRequest);
2352
2596
  } catch {
2353
2597
  }
2354
2598
  };
@@ -2362,13 +2606,15 @@ const createWorker = (options) => {
2362
2606
  }
2363
2607
  const basePath = options.authBasePath ?? DEFAULT_AUTH_BASE_PATH;
2364
2608
  if (isAuthAttemptPath(url.pathname, basePath)) {
2365
- context.waitUntil(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
2609
+ context.waitUntil?.(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
2366
2610
  }
2367
2611
  return authResponse;
2368
2612
  };
2613
+ const customRoutes = options.routes !== void 0 && Object.keys(options.routes).length > 0 ? options.routes : void 0;
2369
2614
  const internalRoutes = {
2370
2615
  [WS_PATH]: (request, env, url) => handleWebSocketUpgrade(request, env, url),
2371
2616
  [RPC_PATH]: (request, env, _url, context) => handleRpc(request, env, context),
2617
+ [RPC_BATCH_PATH]: (request, env, _url, context) => handleBatchRpc(request, env, context),
2372
2618
  [SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
2373
2619
  [CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
2374
2620
  // Extracted handler clusters built above, merged in (mirroring the auth
@@ -2402,6 +2648,17 @@ const createWorker = (options) => {
2402
2648
  resolvedSecurity = resolveSecurity(options.security, env ?? {});
2403
2649
  }
2404
2650
  };
2651
+ const applyAdminGate = async (request, pathname) => {
2652
+ if (options.adminGate === void 0 || !isAdminPath(pathname)) {
2653
+ return;
2654
+ }
2655
+ try {
2656
+ if (await options.adminGate(request)) {
2657
+ accessAdminGrants.add(request);
2658
+ }
2659
+ } catch {
2660
+ }
2661
+ };
2405
2662
  const handle = async (request, env, context) => {
2406
2663
  const url = new URL(request.url);
2407
2664
  if (request.method === "POST" || request.method === "PUT") {
@@ -2414,13 +2671,16 @@ const createWorker = (options) => {
2414
2671
  if (authResponse) {
2415
2672
  return authResponse;
2416
2673
  }
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);
2674
+ if (customRoutes) {
2675
+ const methodAndPath = `${request.method} ${url.pathname}`;
2676
+ const route = customRoutes[methodAndPath] ?? customRoutes[url.pathname];
2677
+ if (route) {
2678
+ return route(request, env, context);
2679
+ }
2421
2680
  }
2422
2681
  const internalRoute = internalRoutes[url.pathname];
2423
2682
  if (internalRoute) {
2683
+ await applyAdminGate(request, url.pathname);
2424
2684
  return internalRoute(request, env, url, context);
2425
2685
  }
2426
2686
  const httpRouteResponse = await dispatchHttpRoute(request, env, context);
@@ -2432,9 +2692,10 @@ const createWorker = (options) => {
2432
2692
  return {
2433
2693
  async fetch(request, env, context) {
2434
2694
  if (options.passThroughOnException) {
2435
- context.passThroughOnException();
2695
+ context.passThroughOnException?.();
2436
2696
  }
2437
2697
  ensureSecurityResolved(env);
2698
+ resolveAdminTokenFromEnv(env);
2438
2699
  const preflight = handleCorsPreflight(request, resolvedSecurity);
2439
2700
  if (preflight) {
2440
2701
  return preflight;
@@ -2450,6 +2711,9 @@ const createWorker = (options) => {
2450
2711
  return decorateResponse(toErrorResponse(error), request, resolvedSecurity);
2451
2712
  }
2452
2713
  },
2714
+ async queue(batch, env, context) {
2715
+ await options.queue?.(batch, env, context);
2716
+ },
2453
2717
  async scheduled(controller, env, context) {
2454
2718
  await handleScheduled(controller, env, context);
2455
2719
  },
@@ -2480,10 +2744,24 @@ const withFrameworkWorker = (host, optionsInput) => {
2480
2744
  const optionsFactory = optionsInput;
2481
2745
  return {
2482
2746
  fetch: (request, env, context) => build(optionsFactory(env)).fetch(request, env, context),
2747
+ queue: (batch, env, context) => build(optionsFactory(env)).queue?.(batch, env, context) ?? Promise.resolve(),
2483
2748
  scheduled: (controller, env, context) => build(optionsFactory(env)).scheduled(controller, env, context),
2484
2749
  serverQuery: (request, env, reference, args, options) => build(optionsFactory(env)).serverQuery(request, env, reference, args, options)
2485
2750
  };
2486
2751
  };
2752
+ const resolveLunoraOptions = (options, env) => {
2753
+ if (typeof options === "function") {
2754
+ return options(env);
2755
+ }
2756
+ const shardDO = options.shardDO ?? env?.SHARD;
2757
+ if (!shardDO) {
2758
+ throw new Error(
2759
+ "@lunora/runtime: no shard Durable Object namespace found. Bind `SHARD` in wrangler.jsonc, or pass `createLunoraHandler({ shardDO: env.MY_SHARD })`."
2760
+ );
2761
+ }
2762
+ return { ...options, shardDO };
2763
+ };
2764
+ const createLunoraHandler = (options = {}) => (request, env, context) => createWorker(resolveLunoraOptions(options, env)).fetch(request, env, context ?? NOOP_EXECUTION_CONTEXT);
2487
2765
  const defineRpcEnvelope = (envelope) => envelope;
2488
2766
 
2489
- export { composeWorker, createWorker, defineRpcEnvelope, withFrameworkWorker };
2767
+ export { NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker };
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from './LunoraError-CL0aOtpo.mjs';
2
- import { resolveShard } from './resolveShard-DDkzWtrU.mjs';
2
+ import { resolveShard } from './applyJurisdiction-BkZtTkct.mjs';
3
3
 
4
4
  const createStaticShardRegistry = (table_to_keys) => {
5
5
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.11",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/runtime"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "README.md",
30
30
  "LICENSE.md",
31
31
  "__assets__"
@@ -1,9 +0,0 @@
1
- const resolveShard = (namespace, shardKey) => {
2
- if (typeof namespace.getByName === "function") {
3
- return namespace.getByName(shardKey);
4
- }
5
- const id = namespace.idFromName(shardKey);
6
- return namespace.get(id);
7
- };
8
-
9
- export { resolveShard };