@lunora/runtime 1.0.0-alpha.22 → 1.0.0-alpha.24

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 CHANGED
@@ -1643,6 +1643,19 @@ interface FunctionRegistryEntry {
1643
1643
  */
1644
1644
  kind: "action" | "mutation" | "query" | "stream";
1645
1645
  visibility?: "internal" | "public";
1646
+ /**
1647
+ * x402 payment tag set by the `.x402({ price })` builder modifier. Present
1648
+ * only on paid public procedures; the origin worker answers an unpaid RPC
1649
+ * for such a function with a real `402` challenge (via the injected
1650
+ * {@link WorkerOptions.x402Charge} gate) before dispatching, then verifies +
1651
+ * settles at the origin boundary so the shard never sees payment state.
1652
+ * Rides along on the registered function object's identity — codegen casts
1653
+ * the real `fn` into `LUNORA_FUNCTIONS`, so reading it needs no change to the
1654
+ * generated shape (same as `fn.rls`).
1655
+ */
1656
+ x402?: {
1657
+ readonly price: number | string;
1658
+ };
1646
1659
  }
1647
1660
  /**
1648
1661
  * The generated `LUNORA_FUNCTIONS` dispatch table, narrowed to what the
@@ -1650,6 +1663,25 @@ interface FunctionRegistryEntry {
1650
1663
  */
1651
1664
  type FunctionRegistryLike = Record<string, FunctionRegistryEntry>;
1652
1665
  /**
1666
+ * Injected x402 charge gate — the seam that paywalls a `.x402({ price })`-tagged
1667
+ * procedure at the origin worker without the runtime importing `@lunora/x402`
1668
+ * (which would pull viem/solana into every worker bundle). Build it with
1669
+ * `createProcedureChargeGate(config)` from `@lunora/x402/charge` and pass it as
1670
+ * {@link WorkerOptions.x402Charge}.
1671
+ *
1672
+ * Given the inbound `request`, the paid procedure's `spec` (its `functionPath` —
1673
+ * used as the x402 challenge `resource` — and USD `price`), and a `dispatch`
1674
+ * that runs the real shard forward, it returns a real `402` + `PAYMENT-REQUIRED`
1675
+ * challenge when the request is unpaid, or the dispatched response (with
1676
+ * `X-PAYMENT-RESPONSE` attached) once the client's `X-PAYMENT` is verified and
1677
+ * settled. `dispatch` runs only after payment is verified — an unpaid or
1678
+ * invalid request never reaches the shard.
1679
+ */
1680
+ type X402ChargeGate = (request: Request, spec: {
1681
+ functionPath: string;
1682
+ price: number | string;
1683
+ }, dispatch: () => Promise<Response>) => Promise<Response>;
1684
+ /**
1653
1685
  * Lists objects in the storage bucket for the admin file browser. Structurally
1654
1686
  * compatible with `@lunora/storage`'s `Storage["list"]` — the runtime stays free
1655
1687
  * of a hard dependency on the storage package.
@@ -2313,6 +2345,18 @@ interface WorkerOptions {
2313
2345
  * reports "not configured" and the studio shows the credentials empty state.
2314
2346
  */
2315
2347
  workflowsClient?: (env: unknown) => undefined | WorkflowsRestClient;
2348
+ /**
2349
+ * Injected x402 charge gate for paid (`.x402({ price })`) procedures. Build
2350
+ * it with `createProcedureChargeGate(config)` from `@lunora/x402/charge` and
2351
+ * pass it here; the runtime stays free of a hard `@lunora/x402` dependency
2352
+ * (and its viem/solana deps).
2353
+ *
2354
+ * **Required whenever any registered function is `.x402()`-tagged.** The
2355
+ * origin worker refuses to dispatch a paid procedure with a config error
2356
+ * (`500`) when this is absent, rather than serving it free — the paywall is
2357
+ * fail-closed by construction. See {@link X402ChargeGate}.
2358
+ */
2359
+ x402Charge?: X402ChargeGate;
2316
2360
  }
2317
2361
  interface RpcContext {
2318
2362
  ctx: ExecutionContextLike;
@@ -2321,6 +2365,12 @@ interface RpcContext {
2321
2365
  shardKey: string;
2322
2366
  }
2323
2367
  /**
2368
+ * Ask the owner how many relays to spread new connections across for `shardKey`
2369
+ * (plan 075 Phase 2), cached per isolate so a promoted shard doesn't add a
2370
+ * round-trip to every WS upgrade. Fails closed to `0` (owner-served) on any error,
2371
+ * so a relay-probe hiccup can never break a connection.
2372
+ */
2373
+ /**
2324
2374
  * The composed Lunora worker. `fetch` / `scheduled` are the standard Cloudflare
2325
2375
  * module-worker entrypoints (so the object can be re-exported directly as
2326
2376
  * `export default createWorker(...)`). `serverQuery` is the in-process fast-path
package/dist/index.d.ts CHANGED
@@ -1643,6 +1643,19 @@ interface FunctionRegistryEntry {
1643
1643
  */
1644
1644
  kind: "action" | "mutation" | "query" | "stream";
1645
1645
  visibility?: "internal" | "public";
1646
+ /**
1647
+ * x402 payment tag set by the `.x402({ price })` builder modifier. Present
1648
+ * only on paid public procedures; the origin worker answers an unpaid RPC
1649
+ * for such a function with a real `402` challenge (via the injected
1650
+ * {@link WorkerOptions.x402Charge} gate) before dispatching, then verifies +
1651
+ * settles at the origin boundary so the shard never sees payment state.
1652
+ * Rides along on the registered function object's identity — codegen casts
1653
+ * the real `fn` into `LUNORA_FUNCTIONS`, so reading it needs no change to the
1654
+ * generated shape (same as `fn.rls`).
1655
+ */
1656
+ x402?: {
1657
+ readonly price: number | string;
1658
+ };
1646
1659
  }
1647
1660
  /**
1648
1661
  * The generated `LUNORA_FUNCTIONS` dispatch table, narrowed to what the
@@ -1650,6 +1663,25 @@ interface FunctionRegistryEntry {
1650
1663
  */
1651
1664
  type FunctionRegistryLike = Record<string, FunctionRegistryEntry>;
1652
1665
  /**
1666
+ * Injected x402 charge gate — the seam that paywalls a `.x402({ price })`-tagged
1667
+ * procedure at the origin worker without the runtime importing `@lunora/x402`
1668
+ * (which would pull viem/solana into every worker bundle). Build it with
1669
+ * `createProcedureChargeGate(config)` from `@lunora/x402/charge` and pass it as
1670
+ * {@link WorkerOptions.x402Charge}.
1671
+ *
1672
+ * Given the inbound `request`, the paid procedure's `spec` (its `functionPath` —
1673
+ * used as the x402 challenge `resource` — and USD `price`), and a `dispatch`
1674
+ * that runs the real shard forward, it returns a real `402` + `PAYMENT-REQUIRED`
1675
+ * challenge when the request is unpaid, or the dispatched response (with
1676
+ * `X-PAYMENT-RESPONSE` attached) once the client's `X-PAYMENT` is verified and
1677
+ * settled. `dispatch` runs only after payment is verified — an unpaid or
1678
+ * invalid request never reaches the shard.
1679
+ */
1680
+ type X402ChargeGate = (request: Request, spec: {
1681
+ functionPath: string;
1682
+ price: number | string;
1683
+ }, dispatch: () => Promise<Response>) => Promise<Response>;
1684
+ /**
1653
1685
  * Lists objects in the storage bucket for the admin file browser. Structurally
1654
1686
  * compatible with `@lunora/storage`'s `Storage["list"]` — the runtime stays free
1655
1687
  * of a hard dependency on the storage package.
@@ -2313,6 +2345,18 @@ interface WorkerOptions {
2313
2345
  * reports "not configured" and the studio shows the credentials empty state.
2314
2346
  */
2315
2347
  workflowsClient?: (env: unknown) => undefined | WorkflowsRestClient;
2348
+ /**
2349
+ * Injected x402 charge gate for paid (`.x402({ price })`) procedures. Build
2350
+ * it with `createProcedureChargeGate(config)` from `@lunora/x402/charge` and
2351
+ * pass it here; the runtime stays free of a hard `@lunora/x402` dependency
2352
+ * (and its viem/solana deps).
2353
+ *
2354
+ * **Required whenever any registered function is `.x402()`-tagged.** The
2355
+ * origin worker refuses to dispatch a paid procedure with a config error
2356
+ * (`500`) when this is absent, rather than serving it free — the paywall is
2357
+ * fail-closed by construction. See {@link X402ChargeGate}.
2358
+ */
2359
+ x402Charge?: X402ChargeGate;
2316
2360
  }
2317
2361
  interface RpcContext {
2318
2362
  ctx: ExecutionContextLike;
@@ -2321,6 +2365,12 @@ interface RpcContext {
2321
2365
  shardKey: string;
2322
2366
  }
2323
2367
  /**
2368
+ * Ask the owner how many relays to spread new connections across for `shardKey`
2369
+ * (plan 075 Phase 2), cached per isolate so a promoted shard doesn't add a
2370
+ * round-trip to every WS upgrade. Fails closed to `0` (owner-served) on any error,
2371
+ * so a relay-probe hiccup can never break a connection.
2372
+ */
2373
+ /**
2324
2374
  * The composed Lunora worker. `fetch` / `scheduled` are the standard Cloudflare
2325
2375
  * module-worker entrypoints (so the object can be re-exported directly as
2326
2376
  * `export default createWorker(...)`). `serverQuery` is the in-process fast-path
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
2
- export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-Dpw9d1s5.mjs';
2
+ export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-CxwkPZUl.mjs';
3
3
  export { createCrossShardRelationCapabilities } from './packem_shared/createCrossShardRelationCapabilities-CbcWjkAn.mjs';
4
4
  export { DEFAULT_REGISTRY_CACHE_TTL_MS, SHARD_REGISTRY_DO_NAME, createDynamicShardRegistry } from './packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-B3pA7aXp.mjs';
5
5
  export { LunoraError, toErrorResponse } from './packem_shared/LunoraError-Bpb9EFJ3.mjs';
@@ -7,7 +7,7 @@ export { emitLogEvent, emitRpcEvent } from './packem_shared/emitLogEvent-pEdtqAK
7
7
  export { analyticsEngineSink, combineSinks, consoleSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-DqEvrQs0.mjs';
8
8
  export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggregate } from './packem_shared/createQueryCoordinator-DNCJzOZE.mjs';
9
9
  export { applyJurisdiction, resolveShard } from './packem_shared/applyJurisdiction-BkZtTkct.mjs';
10
- export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-CsZc49QC.mjs';
10
+ export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-DRWQFNhF.mjs';
11
11
  export { NOOP_EXECUTION_CONTEXT } from './packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
12
12
  export { composeIdentityResolvers, routeIdentityResolvers } from './packem_shared/composeIdentityResolvers-XGjO7V1J.mjs';
13
13
 
@@ -5,7 +5,17 @@ import { wrapResolverWithContract } from './composeIdentityResolvers-XGjO7V1J.mj
5
5
  export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-XGjO7V1J.mjs';
6
6
  import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
7
7
  import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
8
- import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-CsZc49QC.mjs';
8
+ import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-DRWQFNhF.mjs';
9
+
10
+ const evictOldestEntry = (map, capacity) => {
11
+ if (map.size < capacity) {
12
+ return;
13
+ }
14
+ const oldest = map.keys().next().value;
15
+ if (oldest !== void 0) {
16
+ map.delete(oldest);
17
+ }
18
+ };
9
19
 
10
20
  const RELAY_NAME_INFIX = "::relay::";
11
21
  const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
@@ -477,6 +487,9 @@ const normalizeBatchCall = (raw, index, defaultShard) => {
477
487
  if (call.functionPath.startsWith("__lunora_relation__:") || call.functionPath.startsWith("__lunora_admin__")) {
478
488
  throw new LunoraError("reserved function path cannot be batched", { code: "FORBIDDEN", status: 403 });
479
489
  }
490
+ if (call.args !== void 0 && (typeof call.args !== "object" || call.args === null || Array.isArray(call.args))) {
491
+ throw new LunoraError("each batch call `args` must be an object", { code: "BAD_REQUEST", status: 400 });
492
+ }
480
493
  return {
481
494
  entry: {
482
495
  args: call.args === void 0 ? {} : call.args,
@@ -2028,6 +2041,22 @@ const logRpcDebug = (env, envelope) => {
2028
2041
  }
2029
2042
  console.warn(`[lunora:rpc] ${envelope.fanOut ? "fan-out" : `shard=${envelope.shardKey ?? "(root)"}`} ${envelope.functionPath}`);
2030
2043
  };
2044
+ const resolveX402Charge = (envelope, options) => {
2045
+ const x402Tag = options.functions?.[envelope.functionPath]?.x402;
2046
+ if (!x402Tag) {
2047
+ return void 0;
2048
+ }
2049
+ if (envelope.fanOut) {
2050
+ throw new LunoraError("a paid (`.x402`) function cannot be fanned out", { code: "BAD_REQUEST", status: 400 });
2051
+ }
2052
+ if (!options.x402Charge) {
2053
+ throw new LunoraError(`function "${envelope.functionPath}" is marked paid (.x402) but no x402Charge gate is configured on the worker`, {
2054
+ code: "MISCONFIGURED",
2055
+ status: 500
2056
+ });
2057
+ }
2058
+ return x402Tag;
2059
+ };
2031
2060
  const parseEnvelope = async (request) => {
2032
2061
  const text = await readBodyTextWithLimit(request);
2033
2062
  let body;
@@ -2069,12 +2098,16 @@ const forwardToShard = async (namespace, shardKey, request) => {
2069
2098
  };
2070
2099
  const relayProbeCache = /* @__PURE__ */ new Map();
2071
2100
  const RELAY_PROBE_TTL_MS = 5e3;
2101
+ const RELAY_PROBE_MAX_ENTRIES = 4096;
2072
2102
  const probeRelayCount = async (namespace, shardKey) => {
2073
2103
  const now = Date.now();
2074
2104
  const cached = relayProbeCache.get(shardKey);
2075
2105
  if (cached !== void 0 && cached.expiresMs > now) {
2076
2106
  return cached.relayCount;
2077
2107
  }
2108
+ if (cached !== void 0) {
2109
+ relayProbeCache.delete(shardKey);
2110
+ }
2078
2111
  let relayCount = 0;
2079
2112
  try {
2080
2113
  const response = await resolveShard(namespace, shardKey).fetch(new Request("https://shard.internal/_lunora/route"));
@@ -2088,6 +2121,7 @@ const probeRelayCount = async (namespace, shardKey) => {
2088
2121
  } catch {
2089
2122
  relayCount = 0;
2090
2123
  }
2124
+ evictOldestEntry(relayProbeCache, RELAY_PROBE_MAX_ENTRIES);
2091
2125
  relayProbeCache.set(shardKey, { expiresMs: now + RELAY_PROBE_TTL_MS, relayCount });
2092
2126
  return relayCount;
2093
2127
  };
@@ -2499,9 +2533,12 @@ const createWorker = (options) => {
2499
2533
  guardUnauthenticatedShardAccess("shard");
2500
2534
  }
2501
2535
  const upgradeHeaders = new Headers(request.headers);
2502
- upgradeHeaders.delete("x-lunora-userid");
2503
- upgradeHeaders.delete("x-lunora-identity");
2504
- upgradeHeaders.delete("x-lunora-identity-exp");
2536
+ const clientHeaderNames = [...upgradeHeaders.keys()];
2537
+ for (const name of clientHeaderNames) {
2538
+ if (name.startsWith("x-lunora-")) {
2539
+ upgradeHeaders.delete(name);
2540
+ }
2541
+ }
2505
2542
  const forwardedUserId = forwardedHeaders["x-lunora-userid"];
2506
2543
  const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
2507
2544
  const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
@@ -2586,12 +2623,6 @@ const createWorker = (options) => {
2586
2623
  },
2587
2624
  sinkContext
2588
2625
  );
2589
- const responseBookmark = response.headers.get("x-d1-bookmark");
2590
- if (responseBookmark) {
2591
- const headers = new Headers(response.headers);
2592
- headers.set("x-d1-bookmark", responseBookmark);
2593
- return new Response(response.body, { headers, status: response.status });
2594
- }
2595
2626
  return response;
2596
2627
  } catch (error) {
2597
2628
  emitRpcEvent(observability, buildErrorEvent(functionPath, Date.now() - rpcStartedAt, error, { shardKey }), sinkContext);
@@ -2621,6 +2652,7 @@ const createWorker = (options) => {
2621
2652
  }
2622
2653
  const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2623
2654
  await authorizeRpcEnvelope(envelope, identity);
2655
+ const x402Tag = resolveX402Charge(envelope, options);
2624
2656
  {
2625
2657
  const rpcStartedAt = Date.now();
2626
2658
  const { observability } = options;
@@ -2672,7 +2704,11 @@ const createWorker = (options) => {
2672
2704
  }
2673
2705
  }
2674
2706
  const shardKey = envelope.shardKey ?? defaultShard;
2675
- return dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
2707
+ const dispatch = () => dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
2708
+ if (x402Tag && options.x402Charge) {
2709
+ return options.x402Charge(request, { functionPath: envelope.functionPath, price: x402Tag.price }, dispatch);
2710
+ }
2711
+ return dispatch();
2676
2712
  }
2677
2713
  };
2678
2714
  const handleBatchRpc = async (request, env, context) => {
@@ -2693,8 +2729,21 @@ const createWorker = (options) => {
2693
2729
  if (!Array.isArray(calls)) {
2694
2730
  throw new LunoraError("RPC batch `calls` must be an array", { code: "BAD_REQUEST", status: 400 });
2695
2731
  }
2696
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2732
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2697
2733
  const groups = groupBatchCallsByShard(calls, defaultShard);
2734
+ for (const entries of groups.values()) {
2735
+ for (const entry of entries) {
2736
+ if (options.functions?.[entry.functionPath]?.x402) {
2737
+ throw new LunoraError(
2738
+ `paid (\`.x402\`) function "${entry.functionPath}" cannot be called in a batch; dispatch it individually over ${RPC_PATH}`,
2739
+ {
2740
+ code: "BAD_REQUEST",
2741
+ status: 400
2742
+ }
2743
+ );
2744
+ }
2745
+ }
2746
+ }
2698
2747
  await Promise.all(
2699
2748
  [...groups.entries()].flatMap(
2700
2749
  ([shardKey, entries]) => entries.map((entry) => authorizeRpcEnvelope({ functionPath: entry.functionPath, shardKey }, identity))
@@ -2707,7 +2756,7 @@ const createWorker = (options) => {
2707
2756
  }
2708
2757
  } : void 0;
2709
2758
  const results = [];
2710
- let latestBookmark;
2759
+ const bookmarks = [];
2711
2760
  const slotError = (entry, status, code, message) => {
2712
2761
  return { body: { error: { code, message } }, id: entry.id, status };
2713
2762
  };
@@ -2758,7 +2807,7 @@ const createWorker = (options) => {
2758
2807
  const durationMs = Date.now() - subStartedAt;
2759
2808
  const bookmark = response.headers.get("x-d1-bookmark");
2760
2809
  if (bookmark) {
2761
- latestBookmark = bookmark;
2810
+ bookmarks.push(bookmark);
2762
2811
  }
2763
2812
  let parsed;
2764
2813
  try {
@@ -2789,8 +2838,9 @@ const createWorker = (options) => {
2789
2838
  })
2790
2839
  );
2791
2840
  const responseHeaders = { "content-type": "application/json" };
2792
- if (latestBookmark !== void 0) {
2793
- responseHeaders["x-d1-bookmark"] = latestBookmark;
2841
+ const [onlyBookmark] = bookmarks;
2842
+ if (bookmarks.length === 1 && onlyBookmark !== void 0) {
2843
+ responseHeaders["x-d1-bookmark"] = onlyBookmark;
2794
2844
  }
2795
2845
  return Response.json({ results }, { headers: responseHeaders, status: 200 });
2796
2846
  };
@@ -2845,13 +2895,14 @@ const createWorker = (options) => {
2845
2895
  if (!coordinator) {
2846
2896
  throw new LunoraError("scheduled backup requires a `queryCoordinator` on the worker", { code: "BACKUP_NOT_CONFIGURED", status: 500 });
2847
2897
  }
2848
- if (!options.adminToken || options.adminToken.length === 0) {
2849
- throw new LunoraError("scheduled backup requires an `adminToken` to authenticate the per-shard export gate", {
2898
+ const adminToken = effectiveAdminToken();
2899
+ if (!adminToken || adminToken.length === 0) {
2900
+ throw new LunoraError("scheduled backup requires an `adminToken` (or `env.LUNORA_ADMIN_TOKEN`) to authenticate the per-shard export gate", {
2850
2901
  code: "BACKUP_NOT_CONFIGURED",
2851
2902
  status: 500
2852
2903
  });
2853
2904
  }
2854
- const forwardedHeaders = { authorization: `Bearer ${options.adminToken}`, "content-type": "application/json" };
2905
+ const forwardedHeaders = { authorization: `Bearer ${adminToken}`, "content-type": "application/json" };
2855
2906
  const tables = options.backupTables;
2856
2907
  let rows = 0;
2857
2908
  let bytes = 0;
@@ -2897,6 +2948,7 @@ const createWorker = (options) => {
2897
2948
  await pruneBackups(store, prefix);
2898
2949
  };
2899
2950
  const handleScheduled = async (controller, env, context) => {
2951
+ resolveAdminTokenFromEnv(env);
2900
2952
  const errors = [];
2901
2953
  const toError = (error) => error instanceof Error ? error : new Error(String(error));
2902
2954
  const userHandler = options.crons?.[controller.cron];
@@ -3013,7 +3065,8 @@ const createWorker = (options) => {
3013
3065
  const url = new URL(request.url);
3014
3066
  if (request.method === "POST" || request.method === "PUT") {
3015
3067
  const contentLength = Number(request.headers.get("content-length") ?? "");
3016
- if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
3068
+ const maxBodyBytes = url.pathname === KV_VALUE_PATH ? KV_VALUE_MAX_BODY_BYTES : MAX_BODY_BYTES;
3069
+ if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
3017
3070
  throw new LunoraError("Body too large", { code: "PAYLOAD_TOO_LARGE", status: 413 });
3018
3071
  }
3019
3072
  }
@@ -3114,4 +3167,4 @@ const resolveLunoraOptions = (options, env) => {
3114
3167
  const createLunoraHandler = (options = {}) => (request, env, context) => createWorker(resolveLunoraOptions(options, env)).fetch(request, env, context ?? NOOP_EXECUTION_CONTEXT);
3115
3168
  const defineRpcEnvelope = (envelope) => envelope;
3116
3169
 
3117
- export { NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker };
3170
+ export { NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, probeRelayCount, resolveLunoraOptions, withFrameworkWorker };
@@ -202,7 +202,14 @@ const handleCorsPreflight = (request, resolved) => {
202
202
  const headers = corsResponseHeaders(origin, resolved.cors);
203
203
  const requested = request.headers.get("access-control-request-headers");
204
204
  headers.set("access-control-allow-methods", resolved.cors.allowedMethods.join(", "));
205
- headers.set("access-control-allow-headers", requested ?? resolved.cors.allowedHeaders.join(", "));
205
+ let allowedHeaders;
206
+ if (requested === null) {
207
+ allowedHeaders = resolved.cors.allowedHeaders.join(", ");
208
+ } else {
209
+ const permitted = new Set(resolved.cors.allowedHeaders.map((name) => name.toLowerCase()));
210
+ allowedHeaders = requested.split(",").map((name) => name.trim()).filter((name) => name.length > 0 && permitted.has(name.toLowerCase())).join(", ");
211
+ }
212
+ headers.set("access-control-allow-headers", allowedHeaders);
206
213
  headers.set("access-control-max-age", String(resolved.cors.maxAge));
207
214
  return new Response(null, { headers, status: 204 });
208
215
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.22",
3
+ "version": "1.0.0-alpha.24",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.3"
49
+ "@lunora/errors": "1.0.0-alpha.4"
50
50
  },
51
51
  "engines": {
52
52
  "node": "^22.15.0 || >=24.11.0"