@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.
@@ -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-DOLuy1Aj.mjs';
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 { applyGlobals, isAdmin, knownTables, queryCoordinator, resolveForwardContext, shardDO, streamExportRows, streamingImport, syncGlobals } = deps;
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
- if (request.method !== "POST") {
746
- throw new LunoraError("Export endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
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
- if (request.method !== "POST") {
775
- throw new LunoraError("Sync endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
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
- if (request.method !== "POST") {
802
- throw new LunoraError("Connector sync endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
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
- if (request.method !== "POST") {
842
- throw new LunoraError("Apply endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
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
- if (request.method !== "POST") {
869
- throw new LunoraError("Import endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
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: { ...forwardedHeaders, traceparent: buildTraceparent(traceId, spanId) },
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(observability, { ...buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey }), spanId, traceId }, sinkContext);
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 handleRpc = async (request, env, context) => {
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';
@@ -0,0 +1,3 @@
1
+ const methodGuard = (request, allowed) => allowed.includes(request.method) ? void 0 : new Response(void 0, { headers: { allow: allowed.join(", ") }, status: 405 });
2
+
3
+ export { methodGuard as m };