@lunora/runtime 1.0.0-alpha.34 → 1.0.0-alpha.35
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/dist/index.d.mts +1647 -1001
- package/dist/index.d.ts +1647 -1001
- package/dist/index.mjs +6 -3
- package/dist/packem_shared/HEALTH_PATH-e5J_NHBx.mjs +150 -0
- package/dist/packem_shared/{analyticsEngineSink-DE48mQA9.mjs → analyticsEngineSink-DgL64WVC.mjs} +1 -1
- package/dist/packem_shared/argsFromQuery-BqPiQTPc.mjs +114 -0
- package/dist/packem_shared/{composeWorker-DAwO9LLs.mjs → composeWorker-UwHY2r8O.mjs} +282 -69
- package/dist/packem_shared/createKvCursorStore-g8aA6B4L.mjs +165 -0
- package/dist/packem_shared/emitLogEvent-BlGMnKZK.mjs +1 -0
- package/dist/packem_shared/method-guard-Qzw99aCj.mjs +3 -0
- package/dist/packem_shared/observability--NOFYBFc.mjs +47 -0
- package/dist/packem_shared/{otlp-DOLuy1Aj.mjs → otlp-DKZJCkdD.mjs} +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/emitLogEvent-pEdtqAK8.mjs +0 -20
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { isLunoraError, toErrorBody } from '@lunora/errors';
|
|
2
2
|
import { NOOP_EXECUTION_CONTEXT } from './NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
|
|
3
|
-
import { o as otlpRandomHex, b as buildTraceparent } from './otlp-
|
|
3
|
+
import { o as otlpRandomHex, b as buildTraceparent } from './otlp-DKZJCkdD.mjs';
|
|
4
|
+
import { r as resolveTraceSampling, a as emitRpcEvent } from './observability--NOFYBFc.mjs';
|
|
4
5
|
import { LunoraError, toErrorResponse } from './LunoraError-Bpb9EFJ3.mjs';
|
|
6
|
+
import { runExportTap } from './createKvCursorStore-g8aA6B4L.mjs';
|
|
7
|
+
import { m as methodGuard } from './method-guard-Qzw99aCj.mjs';
|
|
8
|
+
import { buildHealthRoutes, durableObjectProbe, d1Probe, presenceProbe } from './HEALTH_PATH-e5J_NHBx.mjs';
|
|
5
9
|
import { wrapResolverWithContract } from './composeIdentityResolvers-XGjO7V1J.mjs';
|
|
6
10
|
export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-XGjO7V1J.mjs';
|
|
7
11
|
import { buildLogArchiveAdminRoutes } from './LOG_ARCHIVE_PATH-CNs0bznX.mjs';
|
|
8
|
-
import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
|
|
9
12
|
import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
|
|
13
|
+
import { buildRestRoutes } from './argsFromQuery-BqPiQTPc.mjs';
|
|
10
14
|
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-DRWQFNhF.mjs';
|
|
11
15
|
|
|
12
16
|
const evictOldestEntry = (map, capacity) => {
|
|
@@ -545,6 +549,52 @@ const buildAuthAdminRoutes = (deps) => {
|
|
|
545
549
|
return routes;
|
|
546
550
|
};
|
|
547
551
|
|
|
552
|
+
const GET_AUTH_AUDIT_LOG_OP = "__lunora_admin__:getAuthAuditLog";
|
|
553
|
+
const stringArgument = (value) => typeof value === "string" && value !== "" ? value : void 0;
|
|
554
|
+
const numericArgument = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
555
|
+
const buildGetAuthAuditLog = (deps) => {
|
|
556
|
+
const handle = async (request, args) => {
|
|
557
|
+
deps.assertAdmin(request);
|
|
558
|
+
const reader = deps.getReader();
|
|
559
|
+
if (reader === void 0) {
|
|
560
|
+
throw new LunoraError("the auth/security audit endpoint requires an `authAuditReader` on the worker", {
|
|
561
|
+
code: "AUTH_AUDIT_NOT_CONFIGURED",
|
|
562
|
+
status: 400
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const query = {};
|
|
566
|
+
const actorId = stringArgument(args["actorId"]);
|
|
567
|
+
const event = stringArgument(args["event"]);
|
|
568
|
+
const sinceSeq = numericArgument(args["sinceSeq"]);
|
|
569
|
+
const limit = numericArgument(args["limit"]);
|
|
570
|
+
if (actorId !== void 0) {
|
|
571
|
+
query.actorId = actorId;
|
|
572
|
+
}
|
|
573
|
+
if (event !== void 0) {
|
|
574
|
+
query.event = event;
|
|
575
|
+
}
|
|
576
|
+
if (sinceSeq !== void 0) {
|
|
577
|
+
query.sinceSeq = sinceSeq;
|
|
578
|
+
}
|
|
579
|
+
if (limit !== void 0) {
|
|
580
|
+
query.limit = limit;
|
|
581
|
+
}
|
|
582
|
+
let entries;
|
|
583
|
+
try {
|
|
584
|
+
entries = await reader.read(query);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error instanceof LunoraError) {
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
console.error("[lunora] auth audit read failed:", error);
|
|
590
|
+
throw new LunoraError("auth audit read failed", { code: "AUTH_AUDIT_READ_FAILED", status: 500 });
|
|
591
|
+
}
|
|
592
|
+
const result = { entries };
|
|
593
|
+
return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
|
|
594
|
+
};
|
|
595
|
+
return handle;
|
|
596
|
+
};
|
|
597
|
+
|
|
548
598
|
const MAX_BATCH_ENTRIES = 500;
|
|
549
599
|
|
|
550
600
|
const normalizeBatchCall = (raw, index, defaultShard) => {
|
|
@@ -711,6 +761,7 @@ const IMPORT_PATH = "/_lunora/admin/import";
|
|
|
711
761
|
const SYNC_PATH = "/_lunora/admin/sync";
|
|
712
762
|
const CONNECTOR_SYNC_PATH = "/_lunora/admin/connector/sync";
|
|
713
763
|
const APPLY_PATH = "/_lunora/admin/apply";
|
|
764
|
+
const EXPORT_TAP_RUN_PATH = "/_lunora/admin/export-tap/run";
|
|
714
765
|
const NDJSON_ENCODER$1 = new TextEncoder();
|
|
715
766
|
const parseExportBody = async (request) => {
|
|
716
767
|
let body;
|
|
@@ -740,18 +791,28 @@ const parseExportBody = async (request) => {
|
|
|
740
791
|
return { tables };
|
|
741
792
|
};
|
|
742
793
|
const buildDataMovementAdminRoutes = (deps) => {
|
|
743
|
-
const {
|
|
794
|
+
const {
|
|
795
|
+
applyGlobals,
|
|
796
|
+
exportCursorStore,
|
|
797
|
+
exportSinks,
|
|
798
|
+
knownTables,
|
|
799
|
+
queryCoordinator,
|
|
800
|
+
requireAdminOption,
|
|
801
|
+
resolveForwardContext,
|
|
802
|
+
shardDO,
|
|
803
|
+
streamExportRows,
|
|
804
|
+
streamingImport,
|
|
805
|
+
syncGlobals
|
|
806
|
+
} = deps;
|
|
744
807
|
const handleExport = async (request, env) => {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (!isAdmin(request)) {
|
|
749
|
-
throw new LunoraError("admin export endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
750
|
-
}
|
|
751
|
-
const coordinator = queryCoordinator;
|
|
752
|
-
if (!coordinator) {
|
|
753
|
-
throw new LunoraError("Export endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
|
|
808
|
+
const wrongMethod = methodGuard(request, ["POST"]);
|
|
809
|
+
if (wrongMethod) {
|
|
810
|
+
return wrongMethod;
|
|
754
811
|
}
|
|
812
|
+
const coordinator = requireAdminOption(request, queryCoordinator, {
|
|
813
|
+
code: "BAD_REQUEST",
|
|
814
|
+
message: "Export endpoint requires a `queryCoordinator` on the worker"
|
|
815
|
+
});
|
|
755
816
|
const body = await parseExportBody(request);
|
|
756
817
|
const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
|
|
757
818
|
const stream = new ReadableStream({
|
|
@@ -771,16 +832,14 @@ const buildDataMovementAdminRoutes = (deps) => {
|
|
|
771
832
|
return new Response(stream, { headers: { "content-type": "application/x-ndjson" }, status: 200 });
|
|
772
833
|
};
|
|
773
834
|
const handleCdcSync = async (request, env) => {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (!isAdmin(request)) {
|
|
778
|
-
throw new LunoraError("admin sync endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
779
|
-
}
|
|
780
|
-
const coordinator = queryCoordinator;
|
|
781
|
-
if (!coordinator) {
|
|
782
|
-
throw new LunoraError("Sync endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
|
|
835
|
+
const wrongMethod = methodGuard(request, ["POST"]);
|
|
836
|
+
if (wrongMethod) {
|
|
837
|
+
return wrongMethod;
|
|
783
838
|
}
|
|
839
|
+
const coordinator = requireAdminOption(request, queryCoordinator, {
|
|
840
|
+
code: "BAD_REQUEST",
|
|
841
|
+
message: "Sync endpoint requires a `queryCoordinator` on the worker"
|
|
842
|
+
});
|
|
784
843
|
const raw = await readJsonBodyWithLimit(request);
|
|
785
844
|
const cursors = typeof raw["cursors"] === "object" && raw["cursors"] !== null ? raw["cursors"] : {};
|
|
786
845
|
const limit = typeof raw["limit"] === "number" ? raw["limit"] : void 0;
|
|
@@ -798,16 +857,14 @@ const buildDataMovementAdminRoutes = (deps) => {
|
|
|
798
857
|
return Response.json({ global, shards: shardResult.shards }, { status: 200 });
|
|
799
858
|
};
|
|
800
859
|
const handleConnectorSync = async (request, env) => {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (!isAdmin(request)) {
|
|
805
|
-
throw new LunoraError("admin connector sync endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
806
|
-
}
|
|
807
|
-
const coordinator = queryCoordinator;
|
|
808
|
-
if (!coordinator) {
|
|
809
|
-
throw new LunoraError("Connector sync endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
|
|
860
|
+
const wrongMethod = methodGuard(request, ["POST"]);
|
|
861
|
+
if (wrongMethod) {
|
|
862
|
+
return wrongMethod;
|
|
810
863
|
}
|
|
864
|
+
const coordinator = requireAdminOption(request, queryCoordinator, {
|
|
865
|
+
code: "BAD_REQUEST",
|
|
866
|
+
message: "Connector sync endpoint requires a `queryCoordinator` on the worker"
|
|
867
|
+
});
|
|
811
868
|
const raw = await readJsonBodyWithLimit(request);
|
|
812
869
|
const state = decodeConnectorCursor(raw["cursor"]);
|
|
813
870
|
const limit = typeof raw["limit"] === "number" && raw["limit"] > 0 ? raw["limit"] : void 0;
|
|
@@ -838,16 +895,14 @@ const buildDataMovementAdminRoutes = (deps) => {
|
|
|
838
895
|
return Response.json(page, { status: 200 });
|
|
839
896
|
};
|
|
840
897
|
const handleApplyCdc = async (request, env) => {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if (!isAdmin(request)) {
|
|
845
|
-
throw new LunoraError("admin apply endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
846
|
-
}
|
|
847
|
-
const coordinator = queryCoordinator;
|
|
848
|
-
if (!coordinator) {
|
|
849
|
-
throw new LunoraError("Apply endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
|
|
898
|
+
const wrongMethod = methodGuard(request, ["POST"]);
|
|
899
|
+
if (wrongMethod) {
|
|
900
|
+
return wrongMethod;
|
|
850
901
|
}
|
|
902
|
+
const coordinator = requireAdminOption(request, queryCoordinator, {
|
|
903
|
+
code: "BAD_REQUEST",
|
|
904
|
+
message: "Apply endpoint requires a `queryCoordinator` on the worker"
|
|
905
|
+
});
|
|
851
906
|
const raw = await readJsonBodyWithLimit(request);
|
|
852
907
|
const rawBatches = Array.isArray(raw["batches"]) ? raw["batches"] : [];
|
|
853
908
|
const batches = rawBatches.map((batch) => batch).filter(
|
|
@@ -865,15 +920,11 @@ const buildDataMovementAdminRoutes = (deps) => {
|
|
|
865
920
|
return Response.json({ applied: shardResult.applied + globalApplied, failed: shardResult.failed, ok: shardResult.ok }, { status: 200 });
|
|
866
921
|
};
|
|
867
922
|
const handleImport = async (request, env) => {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (!isAdmin(request)) {
|
|
872
|
-
throw new LunoraError("admin import endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
873
|
-
}
|
|
874
|
-
if (!queryCoordinator) {
|
|
875
|
-
throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
|
|
923
|
+
const wrongMethod = methodGuard(request, ["POST"]);
|
|
924
|
+
if (wrongMethod) {
|
|
925
|
+
return wrongMethod;
|
|
876
926
|
}
|
|
927
|
+
requireAdminOption(request, queryCoordinator, { code: "BAD_REQUEST", message: "Import endpoint requires a `queryCoordinator` on the worker" });
|
|
877
928
|
const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
|
|
878
929
|
const result = await streamingImport(request, forwardedHeaders);
|
|
879
930
|
return Response.json(result, {
|
|
@@ -881,10 +932,50 @@ const buildDataMovementAdminRoutes = (deps) => {
|
|
|
881
932
|
status: 200
|
|
882
933
|
});
|
|
883
934
|
};
|
|
935
|
+
const handleExportTapRun = async (request, env) => {
|
|
936
|
+
const wrongMethod = methodGuard(request, ["POST"]);
|
|
937
|
+
if (wrongMethod) {
|
|
938
|
+
return wrongMethod;
|
|
939
|
+
}
|
|
940
|
+
const coordinator = requireAdminOption(request, queryCoordinator, {
|
|
941
|
+
code: "BAD_REQUEST",
|
|
942
|
+
message: "Export-tap endpoint requires a `queryCoordinator` on the worker"
|
|
943
|
+
});
|
|
944
|
+
if (exportSinks === void 0 || Object.keys(exportSinks).length === 0 || exportCursorStore === void 0) {
|
|
945
|
+
throw new LunoraError("Export-tap endpoint requires `exportSinks` + `exportCursorStore` on the worker", {
|
|
946
|
+
code: "EXPORT_TAP_NOT_CONFIGURED",
|
|
947
|
+
status: 400
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
const raw = await readJsonBodyWithLimit(request);
|
|
951
|
+
const sinkName = typeof raw["sink"] === "string" ? raw["sink"] : void 0;
|
|
952
|
+
const limit = typeof raw["limit"] === "number" && raw["limit"] > 0 ? raw["limit"] : void 0;
|
|
953
|
+
const requestedTables = Array.isArray(raw["tables"]) ? raw["tables"].filter((table) => typeof table === "string") : void 0;
|
|
954
|
+
if (sinkName === void 0) {
|
|
955
|
+
throw new LunoraError("Export-tap `sink` must name a configured sink", { code: "BAD_REQUEST", status: 400 });
|
|
956
|
+
}
|
|
957
|
+
const sink = exportSinks[sinkName];
|
|
958
|
+
if (sink === void 0) {
|
|
959
|
+
throw new LunoraError(`Export-tap sink "${sinkName}" is not configured`, { code: "NOT_FOUND", status: 404 });
|
|
960
|
+
}
|
|
961
|
+
const { headers: forwardedHeaders } = await resolveForwardContext(request, env);
|
|
962
|
+
const probeTables = requestedTables ?? knownTables();
|
|
963
|
+
const result = await runExportTap({
|
|
964
|
+
coordinator,
|
|
965
|
+
cursorStore: exportCursorStore,
|
|
966
|
+
headers: forwardedHeaders,
|
|
967
|
+
limit,
|
|
968
|
+
shardDO,
|
|
969
|
+
sink,
|
|
970
|
+
tables: probeTables
|
|
971
|
+
});
|
|
972
|
+
return Response.json(result, { headers: { "content-type": "application/json" }, status: 200 });
|
|
973
|
+
};
|
|
884
974
|
return {
|
|
885
975
|
[APPLY_PATH]: handleApplyCdc,
|
|
886
976
|
[CONNECTOR_SYNC_PATH]: handleConnectorSync,
|
|
887
977
|
[EXPORT_PATH]: handleExport,
|
|
978
|
+
[EXPORT_TAP_RUN_PATH]: handleExportTapRun,
|
|
888
979
|
[IMPORT_PATH]: handleImport,
|
|
889
980
|
[SYNC_PATH]: handleCdcSync
|
|
890
981
|
};
|
|
@@ -2009,6 +2100,7 @@ const readForwardedIdentity = (request) => {
|
|
|
2009
2100
|
};
|
|
2010
2101
|
const DEFAULT_AUTH_BASE_PATH = "/api/auth";
|
|
2011
2102
|
const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
|
|
2103
|
+
const LIST_PUSH_SUBSCRIPTIONS_OP = "__lunora_admin__:listPushSubscriptions";
|
|
2012
2104
|
const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
|
|
2013
2105
|
const isAuthAttemptPath = (pathname, basePath) => {
|
|
2014
2106
|
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
@@ -2276,6 +2368,25 @@ const checkAdminWsToken = async (request, expected, requireEphemeral) => {
|
|
|
2276
2368
|
}
|
|
2277
2369
|
return constantTimeEqual(expected, supplied);
|
|
2278
2370
|
};
|
|
2371
|
+
const detectBindingProbe = (key, value) => {
|
|
2372
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") {
|
|
2373
|
+
return void 0;
|
|
2374
|
+
}
|
|
2375
|
+
const shape = value;
|
|
2376
|
+
if (typeof shape["prepare"] === "function" && typeof shape["batch"] === "function" && typeof shape["dump"] === "function") {
|
|
2377
|
+
return d1Probe(`d1:${key}`, value);
|
|
2378
|
+
}
|
|
2379
|
+
if (typeof shape["list"] === "function" && typeof shape["head"] === "function" && typeof shape["createMultipartUpload"] === "function") {
|
|
2380
|
+
return presenceProbe(`r2:${key}`, true);
|
|
2381
|
+
}
|
|
2382
|
+
if (typeof shape["send"] === "function" && typeof shape["sendBatch"] === "function" && typeof shape["get"] !== "function") {
|
|
2383
|
+
return presenceProbe(`queue:${key}`, true);
|
|
2384
|
+
}
|
|
2385
|
+
if (typeof shape["connectionString"] === "string") {
|
|
2386
|
+
return presenceProbe(`hyperdrive:${key}`, true);
|
|
2387
|
+
}
|
|
2388
|
+
return void 0;
|
|
2389
|
+
};
|
|
2279
2390
|
const createWorker = (options) => {
|
|
2280
2391
|
const defaultShard = options.defaultShardKey ?? "__root__";
|
|
2281
2392
|
const publicResolveIdentity = wrapResolverWithContract(options.resolveIdentity, options.identity);
|
|
@@ -2488,17 +2599,6 @@ const createWorker = (options) => {
|
|
|
2488
2599
|
await releasePoolSlot(candidate);
|
|
2489
2600
|
return response;
|
|
2490
2601
|
};
|
|
2491
|
-
const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
|
|
2492
|
-
applyGlobals: options.applyGlobals,
|
|
2493
|
-
isAdmin: requestIsAdmin,
|
|
2494
|
-
knownTables: () => collectKnownTables(),
|
|
2495
|
-
queryCoordinator: options.queryCoordinator,
|
|
2496
|
-
resolveForwardContext: resolveAdminForwardContext,
|
|
2497
|
-
shardDO,
|
|
2498
|
-
streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow, shardDO),
|
|
2499
|
-
streamingImport: (request, headers) => streamingImport(request, options, headers, shardDO),
|
|
2500
|
-
syncGlobals: options.syncGlobals
|
|
2501
|
-
});
|
|
2502
2602
|
const assertAdminAuthorized = (request) => {
|
|
2503
2603
|
if (!requestIsAdmin(request)) {
|
|
2504
2604
|
throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
@@ -2511,6 +2611,49 @@ const createWorker = (options) => {
|
|
|
2511
2611
|
}
|
|
2512
2612
|
return value;
|
|
2513
2613
|
};
|
|
2614
|
+
const getAuthAuditLog = buildGetAuthAuditLog({
|
|
2615
|
+
assertAdmin: assertAdminAuthorized,
|
|
2616
|
+
getReader: () => options.authAuditReader
|
|
2617
|
+
});
|
|
2618
|
+
const listPushSubscriptions = async (request, args) => {
|
|
2619
|
+
assertAdminAuthorized(request);
|
|
2620
|
+
const store = options.notifySubscriptionStore;
|
|
2621
|
+
if (store === void 0) {
|
|
2622
|
+
return Response.json({ subscriptions: [] }, { headers: { "content-type": "application/json" }, status: 200 });
|
|
2623
|
+
}
|
|
2624
|
+
const rawKind = args?.["kind"];
|
|
2625
|
+
const rawUserId = args?.["userId"];
|
|
2626
|
+
const kindFilter = typeof rawKind === "string" && rawKind !== "" ? rawKind : void 0;
|
|
2627
|
+
const userIdFilter = typeof rawUserId === "string" && rawUserId !== "" ? rawUserId : void 0;
|
|
2628
|
+
const stored = await store.list();
|
|
2629
|
+
const subscriptions = stored.filter((device) => (kindFilter === void 0 || device.kind === kindFilter) && (userIdFilter === void 0 || device.userId === userIdFilter)).map(({ keys: _keys, token: _token, ...device }) => device);
|
|
2630
|
+
return Response.json({ subscriptions }, { headers: { "content-type": "application/json" }, status: 200 });
|
|
2631
|
+
};
|
|
2632
|
+
const serveReservedWorkerRpc = async (request, envelope) => {
|
|
2633
|
+
if (envelope.fanOut) {
|
|
2634
|
+
return void 0;
|
|
2635
|
+
}
|
|
2636
|
+
if (envelope.functionPath === GET_AUTH_AUDIT_LOG_OP) {
|
|
2637
|
+
return getAuthAuditLog(request, envelope.args ?? {});
|
|
2638
|
+
}
|
|
2639
|
+
if (envelope.functionPath === LIST_PUSH_SUBSCRIPTIONS_OP) {
|
|
2640
|
+
return listPushSubscriptions(request, envelope.args);
|
|
2641
|
+
}
|
|
2642
|
+
return void 0;
|
|
2643
|
+
};
|
|
2644
|
+
const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
|
|
2645
|
+
applyGlobals: options.applyGlobals,
|
|
2646
|
+
exportCursorStore: options.exportCursorStore,
|
|
2647
|
+
exportSinks: options.exportSinks,
|
|
2648
|
+
knownTables: () => collectKnownTables(),
|
|
2649
|
+
queryCoordinator: options.queryCoordinator,
|
|
2650
|
+
requireAdminOption,
|
|
2651
|
+
resolveForwardContext: resolveAdminForwardContext,
|
|
2652
|
+
shardDO,
|
|
2653
|
+
streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow, shardDO),
|
|
2654
|
+
streamingImport: (request, headers) => streamingImport(request, options, headers, shardDO),
|
|
2655
|
+
syncGlobals: options.syncGlobals
|
|
2656
|
+
});
|
|
2514
2657
|
const queryParameter = (url, name) => {
|
|
2515
2658
|
const value = url.searchParams.get(name);
|
|
2516
2659
|
return value === null || value === "" ? void 0 : value;
|
|
@@ -2588,6 +2731,32 @@ const createWorker = (options) => {
|
|
|
2588
2731
|
queryParameter,
|
|
2589
2732
|
requireAdminOption
|
|
2590
2733
|
});
|
|
2734
|
+
const resolveHealthProbes = (env) => {
|
|
2735
|
+
const probes = [];
|
|
2736
|
+
const namespace = shardDO ?? env?.SHARD;
|
|
2737
|
+
if (namespace !== void 0) {
|
|
2738
|
+
probes.push(durableObjectProbe("durable-object", namespace, defaultShard));
|
|
2739
|
+
}
|
|
2740
|
+
if (options.health?.disableBindingProbes !== true) {
|
|
2741
|
+
for (const [key, value] of Object.entries(env ?? {})) {
|
|
2742
|
+
const probe = detectBindingProbe(key, value);
|
|
2743
|
+
if (probe !== void 0) {
|
|
2744
|
+
probes.push(probe);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
for (const probe of options.health?.probes ?? []) {
|
|
2749
|
+
probes.push(probe);
|
|
2750
|
+
}
|
|
2751
|
+
return probes;
|
|
2752
|
+
};
|
|
2753
|
+
const healthRoutes = buildHealthRoutes({
|
|
2754
|
+
appName: options.health?.appName,
|
|
2755
|
+
appVersion: options.health?.appVersion,
|
|
2756
|
+
auth: options.health?.auth ?? "public",
|
|
2757
|
+
isAdmin: requestIsAdmin,
|
|
2758
|
+
resolveProbes: resolveHealthProbes
|
|
2759
|
+
});
|
|
2591
2760
|
const buildHttpActionContext = async (request, env, context) => {
|
|
2592
2761
|
const { claims, headers, userId } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2593
2762
|
const run = async (reference, args = {}) => {
|
|
@@ -2777,12 +2946,17 @@ const createWorker = (options) => {
|
|
|
2777
2946
|
};
|
|
2778
2947
|
const dispatchSingleShard = async (functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
|
|
2779
2948
|
const rpcStartedAt = Date.now();
|
|
2780
|
-
const { observability } = options;
|
|
2949
|
+
const { observability, sampling } = options;
|
|
2781
2950
|
const traceId = otlpRandomHex(16);
|
|
2782
2951
|
const spanId = otlpRandomHex(8);
|
|
2952
|
+
const decision = resolveTraceSampling(sampling, traceId);
|
|
2783
2953
|
const forwarded = new Request(`https://shard.internal/rpc`, {
|
|
2784
2954
|
body: JSON.stringify({ args, functionPath }),
|
|
2785
|
-
headers: {
|
|
2955
|
+
headers: {
|
|
2956
|
+
...forwardedHeaders,
|
|
2957
|
+
traceparent: buildTraceparent(traceId, spanId, decision.isTraced),
|
|
2958
|
+
"x-lunora-sample-errors": decision.keepErrors ? "1" : "0"
|
|
2959
|
+
},
|
|
2786
2960
|
method: "POST"
|
|
2787
2961
|
});
|
|
2788
2962
|
try {
|
|
@@ -2798,20 +2972,21 @@ const createWorker = (options) => {
|
|
|
2798
2972
|
traceId,
|
|
2799
2973
|
...response.ok ? {} : { error: { code: "SHARD_ERROR", message: `shard returned ${String(response.status)}`, status: response.status } }
|
|
2800
2974
|
},
|
|
2801
|
-
sinkContext
|
|
2975
|
+
sinkContext,
|
|
2976
|
+
sampling
|
|
2802
2977
|
);
|
|
2803
2978
|
return response;
|
|
2804
2979
|
} catch (error) {
|
|
2805
|
-
emitRpcEvent(
|
|
2980
|
+
emitRpcEvent(
|
|
2981
|
+
observability,
|
|
2982
|
+
{ ...buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey }), spanId, traceId },
|
|
2983
|
+
sinkContext,
|
|
2984
|
+
sampling
|
|
2985
|
+
);
|
|
2806
2986
|
throw error;
|
|
2807
2987
|
}
|
|
2808
2988
|
};
|
|
2809
|
-
const
|
|
2810
|
-
if (request.method !== "POST") {
|
|
2811
|
-
throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
2812
|
-
}
|
|
2813
|
-
const envelope = await parseEnvelope(request);
|
|
2814
|
-
logRpcDebug(env, envelope);
|
|
2989
|
+
const assertDispatchableEnvelope = (envelope) => {
|
|
2815
2990
|
if (envelope.fanOut && envelope.shardKey) {
|
|
2816
2991
|
throw new LunoraError("RPC envelope cannot set both `shardKey` and `fanOut`", { code: "BAD_REQUEST", status: 400 });
|
|
2817
2992
|
}
|
|
@@ -2827,6 +3002,18 @@ const createWorker = (options) => {
|
|
|
2827
3002
|
status: 400
|
|
2828
3003
|
});
|
|
2829
3004
|
}
|
|
3005
|
+
};
|
|
3006
|
+
const handleRpc = async (request, env, context) => {
|
|
3007
|
+
if (request.method !== "POST") {
|
|
3008
|
+
throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
3009
|
+
}
|
|
3010
|
+
const envelope = await parseEnvelope(request);
|
|
3011
|
+
logRpcDebug(env, envelope);
|
|
3012
|
+
assertDispatchableEnvelope(envelope);
|
|
3013
|
+
const reservedResponse = await serveReservedWorkerRpc(request, envelope);
|
|
3014
|
+
if (reservedResponse !== void 0) {
|
|
3015
|
+
return reservedResponse;
|
|
3016
|
+
}
|
|
2830
3017
|
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2831
3018
|
await authorizeRpcEnvelope(envelope, identity);
|
|
2832
3019
|
const x402Tag = resolveX402Charge(envelope, options);
|
|
@@ -3171,6 +3358,24 @@ const createWorker = (options) => {
|
|
|
3171
3358
|
}
|
|
3172
3359
|
return authResponse;
|
|
3173
3360
|
};
|
|
3361
|
+
const invokeExposed = async ({ args, env, functionPath, request, shardKey }) => {
|
|
3362
|
+
const envelope = { functionPath, ...shardKey === void 0 ? {} : { shardKey } };
|
|
3363
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
3364
|
+
await authorizeRpcEnvelope(envelope, identity);
|
|
3365
|
+
const resolvedShardKey = shardKey ?? defaultShard;
|
|
3366
|
+
const dispatch = () => dispatchSingleShard(functionPath, args, resolvedShardKey, forwardedHeaders);
|
|
3367
|
+
const x402Tag = resolveX402Charge(envelope, options);
|
|
3368
|
+
if (x402Tag && options.x402Charge) {
|
|
3369
|
+
return options.x402Charge(request, { functionPath, price: x402Tag.price }, dispatch);
|
|
3370
|
+
}
|
|
3371
|
+
return dispatch();
|
|
3372
|
+
};
|
|
3373
|
+
const restRoutes = buildRestRoutes({
|
|
3374
|
+
functions: options.functions ?? {},
|
|
3375
|
+
invoke: invokeExposed,
|
|
3376
|
+
readJsonBody: readJsonBodyWithLimit,
|
|
3377
|
+
...options.restRateLimit ? { rateLimit: options.restRateLimit } : {}
|
|
3378
|
+
});
|
|
3174
3379
|
const customRoutes = options.routes !== void 0 && Object.keys(options.routes).length > 0 ? options.routes : void 0;
|
|
3175
3380
|
const internalRoutes = {
|
|
3176
3381
|
[STATUS_PATH]: (request) => {
|
|
@@ -3216,6 +3421,14 @@ const createWorker = (options) => {
|
|
|
3216
3421
|
...kvAdminRoutes,
|
|
3217
3422
|
...logArchiveAdminRoutes,
|
|
3218
3423
|
...introspectionAdminRoutes,
|
|
3424
|
+
// `/_lunora/health` + `/_lunora/health/ready` — public (or admin-gated)
|
|
3425
|
+
// liveness/readiness probes; not under the admin prefix, so `applyAdminGate`
|
|
3426
|
+
// never runs on them (the handler self-gates when `auth: "admin"`).
|
|
3427
|
+
...healthRoutes,
|
|
3428
|
+
// `/_lunora/rest/<namespace>/<fn>` — the opt-in public REST surface, one
|
|
3429
|
+
// route per `.expose({ rest: true })` procedure (default-closed). Routes
|
|
3430
|
+
// THROUGH the procedure, so auth/RLS/validators are enforced at the shard.
|
|
3431
|
+
...restRoutes,
|
|
3219
3432
|
// `/_lunora/admin/auth/*` — the whole user-management plane, one route per
|
|
3220
3433
|
// `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
|
|
3221
3434
|
...buildAuthAdminRoutes({
|
|
@@ -3354,4 +3567,4 @@ const resolveLunoraOptions = (options, env) => {
|
|
|
3354
3567
|
const createLunoraHandler = (options = {}) => (request, env, context) => createWorker(resolveLunoraOptions(options, env)).fetch(request, env, context ?? NOOP_EXECUTION_CONTEXT);
|
|
3355
3568
|
const defineRpcEnvelope = (envelope) => envelope;
|
|
3356
3569
|
|
|
3357
|
-
export { NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, probeRelayCount, resolveLunoraOptions, withFrameworkWorker };
|
|
3570
|
+
export { GET_AUTH_AUDIT_LOG_OP, NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, probeRelayCount, resolveLunoraOptions, withFrameworkWorker };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
2
|
+
setTimeout(resolve, ms);
|
|
3
|
+
});
|
|
4
|
+
const sanitizeChange = (raw) => {
|
|
5
|
+
const table = typeof raw["table"] === "string" ? raw["table"] : "";
|
|
6
|
+
const rawOp = typeof raw["op"] === "string" ? raw["op"] : "";
|
|
7
|
+
const op = rawOp === "delete" || rawOp === "insert" || rawOp === "update" ? rawOp : "upsert";
|
|
8
|
+
const id = typeof raw["id"] === "string" ? raw["id"] : void 0;
|
|
9
|
+
const documentRow = raw["doc"] && typeof raw["doc"] === "object" ? raw["doc"] : void 0;
|
|
10
|
+
const seq = typeof raw["seq"] === "number" && Number.isFinite(raw["seq"]) ? raw["seq"] : void 0;
|
|
11
|
+
const ts = typeof raw["ts"] === "number" && Number.isFinite(raw["ts"]) ? raw["ts"] : void 0;
|
|
12
|
+
return {
|
|
13
|
+
op,
|
|
14
|
+
table,
|
|
15
|
+
...documentRow === void 0 ? {} : { doc: documentRow },
|
|
16
|
+
...id === void 0 ? {} : { id },
|
|
17
|
+
...seq === void 0 ? {} : { seq },
|
|
18
|
+
...ts === void 0 ? {} : { ts }
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const deliverWithRetry = async (sink, batch, maxRetries, initialBackoffMs, maxBackoffMs, sleep) => {
|
|
22
|
+
let attempt = 0;
|
|
23
|
+
while (true) {
|
|
24
|
+
try {
|
|
25
|
+
await sink.deliver(batch);
|
|
26
|
+
return;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (attempt >= maxRetries) {
|
|
29
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
30
|
+
}
|
|
31
|
+
const delay = Math.min(initialBackoffMs * 2 ** attempt, maxBackoffMs);
|
|
32
|
+
await sleep(delay);
|
|
33
|
+
attempt += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const runExportTap = async (options) => {
|
|
38
|
+
const {
|
|
39
|
+
coordinator,
|
|
40
|
+
cursorStore,
|
|
41
|
+
headers,
|
|
42
|
+
initialBackoffMs = 100,
|
|
43
|
+
limit,
|
|
44
|
+
maxBackoffMs = 5e3,
|
|
45
|
+
maxRetries = 3,
|
|
46
|
+
shardDO,
|
|
47
|
+
sink,
|
|
48
|
+
sleep = defaultSleep,
|
|
49
|
+
tables
|
|
50
|
+
} = options;
|
|
51
|
+
const priorCursors = await cursorStore.read(sink.name);
|
|
52
|
+
const feed = await coordinator.orchestrateCdcSync(shardDO, { cursors: priorCursors, headers, limit, tables });
|
|
53
|
+
const nextCursors = { ...priorCursors };
|
|
54
|
+
const failures = [];
|
|
55
|
+
let delivered = 0;
|
|
56
|
+
let hasMore = false;
|
|
57
|
+
for (const shard of feed.shards) {
|
|
58
|
+
if (shard.error) {
|
|
59
|
+
failures.push({ error: shard.error.message, shardKey: shard.shardKey });
|
|
60
|
+
hasMore = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const rawChanges = shard.changes ?? [];
|
|
64
|
+
if (rawChanges.length === 0) {
|
|
65
|
+
nextCursors[shard.shardKey] = shard.cursor;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const changes = rawChanges.map((change) => sanitizeChange(change));
|
|
69
|
+
const batch = { changes, cursor: shard.cursor, shardKey: shard.shardKey, sink: sink.name };
|
|
70
|
+
try {
|
|
71
|
+
await deliverWithRetry(sink, batch, maxRetries, initialBackoffMs, maxBackoffMs, sleep);
|
|
72
|
+
nextCursors[shard.shardKey] = shard.cursor;
|
|
73
|
+
delivered += changes.length;
|
|
74
|
+
if (limit !== void 0 && rawChanges.length >= limit) {
|
|
75
|
+
hasMore = true;
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
failures.push({ error: error instanceof Error ? error.message : String(error), shardKey: shard.shardKey });
|
|
79
|
+
hasMore = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await cursorStore.write(sink.name, nextCursors);
|
|
83
|
+
return { cursors: nextCursors, delivered, failures, hasMore, shards: feed.shards.length };
|
|
84
|
+
};
|
|
85
|
+
const defineExportSink = (config) => {
|
|
86
|
+
if (typeof config.name !== "string" || config.name.length === 0) {
|
|
87
|
+
throw new Error("defineExportSink: `name` must be a non-empty string");
|
|
88
|
+
}
|
|
89
|
+
if (typeof config.deliver !== "function") {
|
|
90
|
+
throw new TypeError("defineExportSink: `deliver` must be a function");
|
|
91
|
+
}
|
|
92
|
+
return { deliver: config.deliver, name: config.name };
|
|
93
|
+
};
|
|
94
|
+
const encodeNdjson = (changes) => `${changes.map((change) => JSON.stringify(change)).join("\n")}
|
|
95
|
+
`;
|
|
96
|
+
const webhookExportSink = (config) => {
|
|
97
|
+
const fetchImpl = config.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
98
|
+
return defineExportSink({
|
|
99
|
+
deliver: async (batch) => {
|
|
100
|
+
const response = await fetchImpl(config.url, {
|
|
101
|
+
body: encodeNdjson(batch.changes),
|
|
102
|
+
headers: {
|
|
103
|
+
"content-type": "application/x-ndjson",
|
|
104
|
+
"x-lunora-cursor": String(batch.cursor),
|
|
105
|
+
"x-lunora-shard": batch.shardKey,
|
|
106
|
+
"x-lunora-sink": batch.sink,
|
|
107
|
+
...config.headers
|
|
108
|
+
},
|
|
109
|
+
method: "POST"
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`webhook export sink "${config.name}" returned ${String(response.status)}`);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
name: config.name
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const r2Sink = (config) => {
|
|
119
|
+
let prefix = config.prefix ?? "cdc";
|
|
120
|
+
while (prefix.endsWith("/")) {
|
|
121
|
+
prefix = prefix.slice(0, -1);
|
|
122
|
+
}
|
|
123
|
+
return defineExportSink({
|
|
124
|
+
deliver: async (batch) => {
|
|
125
|
+
const key = `${prefix}/${batch.shardKey}/${String(batch.cursor)}.ndjson`;
|
|
126
|
+
await config.bucket.put(key, encodeNdjson(batch.changes), { httpMetadata: { contentType: "application/x-ndjson" } });
|
|
127
|
+
},
|
|
128
|
+
name: config.name
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const createMemoryCursorStore = () => {
|
|
132
|
+
const state = {};
|
|
133
|
+
return {
|
|
134
|
+
read: (sink) => Promise.resolve({ ...state[sink] }),
|
|
135
|
+
snapshot: () => structuredClone(state),
|
|
136
|
+
write: (sink, cursors) => {
|
|
137
|
+
state[sink] = { ...cursors };
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const createKvCursorStore = (kv, options) => {
|
|
143
|
+
const keyPrefix = options?.keyPrefix ?? "__lunora_source_cursor:export";
|
|
144
|
+
const keyFor = (sink) => `${keyPrefix}:${sink}`;
|
|
145
|
+
return {
|
|
146
|
+
read: async (sink) => {
|
|
147
|
+
const raw = await kv.get(keyFor(sink), "json");
|
|
148
|
+
if (raw === null || typeof raw !== "object") {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
const cursors = {};
|
|
152
|
+
for (const [shardKey, value] of Object.entries(raw)) {
|
|
153
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
154
|
+
cursors[shardKey] = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return cursors;
|
|
158
|
+
},
|
|
159
|
+
write: async (sink, cursors) => {
|
|
160
|
+
await kv.put(keyFor(sink), JSON.stringify(cursors));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export { createKvCursorStore, createMemoryCursorStore, defineExportSink, r2Sink, runExportTap, sanitizeChange, webhookExportSink };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { e as emitLogEvent, a as emitRpcEvent } from './observability--NOFYBFc.mjs';
|