@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.
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +172 -5
- package/dist/index.d.ts +172 -5
- package/dist/index.mjs +7 -6
- package/dist/packem_shared/{createDynamicShardRegistry-BpCwo_mo.mjs → DEFAULT_REGISTRY_CACHE_TTL_MS-ocax8v0n.mjs} +4 -1
- package/dist/packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs +8 -0
- package/dist/packem_shared/applyJurisdiction-BkZtTkct.mjs +20 -0
- package/dist/packem_shared/{composeWorker-BYHiNH_V.mjs → composeWorker-BNYeYQqL.mjs} +319 -41
- package/dist/packem_shared/{createQueryCoordinator-DbxC7iUz.mjs → createQueryCoordinator-Cbds9cUI.mjs} +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/resolveShard-DDkzWtrU.mjs +0 -9
- /package/dist/packem_shared/{consoleSink-DqEvrQs0.mjs → analyticsEngineSink-DqEvrQs0.mjs} +0 -0
- /package/dist/packem_shared/{emitRpcEvent-pEdtqAK8.mjs → emitLogEvent-pEdtqAK8.mjs} +0 -0
|
@@ -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 './
|
|
3
|
-
import { resolveShard } from './
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
1882
|
+
isAdmin: requestIsAdmin,
|
|
1762
1883
|
queryCoordinator: options.queryCoordinator,
|
|
1763
|
-
resolveForwardContext:
|
|
1764
|
-
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(
|
|
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 (!
|
|
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 || !
|
|
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
|
|
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:
|
|
2022
|
+
isAdmin: requestIsAdmin,
|
|
1902
2023
|
knownTables: () => collectKnownTables(),
|
|
1903
2024
|
queryCoordinator: options.queryCoordinator,
|
|
1904
|
-
resolveForwardContext:
|
|
1905
|
-
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 (!
|
|
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 (
|
|
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
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
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__"
|