@lunora/runtime 1.0.0-alpha.33 → 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 CHANGED
@@ -1,6 +1,6 @@
1
- import { R2SqlClient } from '@lunora/bindings/r2sql';
2
1
  import { RankDirection, RankPageRow, DatabaseWriterLike } from '@lunora/do';
3
2
  export type { RankDirection as RankPageDirection, RankPageRowKey as RankPageKey, RankPageRow, ShardRankPageResult } from '@lunora/do';
3
+ import { R2SqlClient } from '@lunora/bindings/r2sql';
4
4
  import { WorkflowsRestClient } from '@lunora/workflow';
5
5
  import { LunoraError as LunoraError$1, ErrorBody } from '@lunora/errors';
6
6
  /**
@@ -150,6 +150,24 @@ interface ExecutionContextLike {
150
150
  * receives a valid third argument.
151
151
  */
152
152
  declare const NOOP_EXECUTION_CONTEXT: ExecutionContextLike;
153
+ /**
154
+ * Trace-sampling configuration — the `sampling` block on the worker's
155
+ * observability options.
156
+ */
157
+ interface TraceSamplingConfig {
158
+ /**
159
+ * Always keep a whole trace that produced an error span (root or any child
160
+ * `ok: false`), regardless of the head decision — the tail bias that keeps
161
+ * failures observable under aggressive head sampling. Default `true`.
162
+ */
163
+ alwaysSampleErrors?: boolean;
164
+ /**
165
+ * Fraction of traces to keep by the deterministic head decision, in `[0, 1]`.
166
+ * `1` keeps every trace (the default), `0` drops every non-error trace, `0.1`
167
+ * keeps ~10%. Values outside the range are clamped by the decision helpers.
168
+ */
169
+ headRate?: number;
170
+ }
153
171
  /** A timestamp as better-auth stores it: epoch-ms, an ISO string, or absent. */
154
172
  type AuthTimestamp = null | number | string;
155
173
  /**
@@ -426,6 +444,67 @@ interface AuthAdmin {
426
444
  userId: string;
427
445
  }) => Promise<AuthUser>;
428
446
  }
447
+ /**
448
+ * Whether the recorded operation succeeded or failed (e.g. a rejected sign-in).
449
+ * Mirrors `@lunora/auth`'s `AuthAuditOutcome`.
450
+ */
451
+ type AuthAuditOutcome = "failure" | "success";
452
+ /**
453
+ * One recorded auth/security event, structurally mirroring `@lunora/auth`'s
454
+ * `AuthAuditEntry`. Duplicated here (like {@link import("./auth-admin-routes").AuthAdmin})
455
+ * so the runtime stays free of a hard `@lunora/auth` dependency — the host wires
456
+ * a structurally-compatible reader.
457
+ */
458
+ interface AuthAuditEntry {
459
+ /** The acting user's email, when known. Absent for anonymous/pre-auth events. */
460
+ actorEmail?: string;
461
+ /** The acting user's id, when known. */
462
+ actorId?: string;
463
+ /** JSON-decoded extra context, with secrets/PII redacted at write time; absent when none was recorded. */
464
+ detail?: Record<string, unknown>;
465
+ /** Auth event type, e.g. `sign-in` / `password-change`. */
466
+ event: string;
467
+ /** Client IP the event originated from, when resolvable. */
468
+ ip?: string;
469
+ /** Whether the operation succeeded or failed. */
470
+ outcome: AuthAuditOutcome;
471
+ /** Monotonic per-database cursor — strictly increasing, never reused. */
472
+ seq: number;
473
+ /** Wall-clock millis when the event was recorded. */
474
+ ts: number;
475
+ /** Client User-Agent, when present on the request. */
476
+ userAgent?: string;
477
+ }
478
+ /**
479
+ * Filter / paging options forwarded to the reader, structurally mirroring
480
+ * `@lunora/auth`'s `ReadAuthAuditOptions`. `limit` is clamped by the reader
481
+ * (`readAuthAuditLog` bounds it to `[1, 10000]`).
482
+ */
483
+ interface ReadAuthAuditQuery {
484
+ /** Return only events for this actor id. */
485
+ actorId?: string;
486
+ /** Return only events of this type. */
487
+ event?: string;
488
+ /** Max rows to return; clamped by the reader. */
489
+ limit?: number;
490
+ /** Return only events with `seq` strictly greater than this (forward paging). */
491
+ sinceSeq?: number;
492
+ }
493
+ /**
494
+ * The auth/security audit read plane backing the studio's "Security / audit"
495
+ * page. Unlike the shard-forwarded `__lunora_admin__:*` ops, the auth audit trail
496
+ * lives in the auth D1 database (via `@lunora/auth`'s `SqlExecutor`), so it is
497
+ * served at the worker. The host wires this — typically via `@lunora/auth`'s
498
+ * `createAuthAuditReader(d1Executor(env.DB))` — closing over that D1 binding; the
499
+ * runtime stays free of a hard `@lunora/auth` dependency. Omit the option and the
500
+ * RPC responds `AUTH_AUDIT_NOT_CONFIGURED`.
501
+ *
502
+ * The reader is a trusted server-side operator surface — the RPC gates it behind
503
+ * the worker's admin-bearer check before this is ever called.
504
+ */
505
+ interface AuthAuditReader {
506
+ read: (options: ReadAuthAuditQuery) => Promise<AuthAuditEntry[]>;
507
+ }
429
508
  /**
430
509
  * A compact, transport-safe description of one function argument — the runtime
431
510
  * read of a `v.*` validator's reflection tags (`kind` + `_meta`). The runtime
@@ -445,139 +524,968 @@ interface FunctionArgumentDescriptor {
445
524
  table?: string;
446
525
  }
447
526
  /**
448
- * Identity resolved from the inbound request by `WorkerOptions.resolveIdentity`.
449
- *
450
- * The `userId` field is specialit becomes `ctx.auth.userId` inside the
451
- * Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
452
- * forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
453
- *
454
- * Return `null` to signal that the request is anonymous; the runtime will
455
- * skip both `x-lunora-userid` and `x-lunora-identity` headers, and
456
- * `ctx.auth.userId` will be `undefined` on the shard side.
527
+ * Cloudflare Durable Object jurisdictions restrict where a DO runs and persists
528
+ * data, for data-residency / compliance regimes (GDPR, FedRAMP, US data
529
+ * residency). The set is openCloudflare adds values over time — so this is a
530
+ * widening union rather than a closed enum.
531
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
457
532
  */
458
- interface ResolvedIdentity {
459
- /** Arbitrary additional claims. Must be JSON-serialisable. */
460
- [key: string]: unknown;
533
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
534
+ /**
535
+ * Structural projection of the bits of `DurableObjectNamespace` the runtime
536
+ * needs. Real workers-types defines a much wider surface; this lets us pass
537
+ * unit-test doubles without coupling to `@cloudflare/workers-types`.
538
+ */
539
+ interface ShardNamespaceLike {
540
+ get: (id: unknown) => {
541
+ fetch: (request: Request) => Promise<Response>;
542
+ };
461
543
  /**
462
- * JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
463
- * absent), the runtime forwards it as the socket's credential expiry — the
464
- * DO drops the socket once it lapses. Used only on the WebSocket path.
544
+ * `getByName` is the friendlier API but isn't on every workers-types
545
+ * release yet. We prefer it when available and fall back to
546
+ * `idFromName` + `get` for compatibility.
465
547
  */
466
- exp?: number;
548
+ getByName?: (name: string) => {
549
+ fetch: (request: Request) => Promise<Response>;
550
+ };
551
+ idFromName: (name: string) => unknown;
467
552
  /**
468
- * Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
469
- * both are present. Forwarded as the socket's expiry on the WebSocket path
470
- * so the DO drops the socket once it lapses; omit for non-expiring sessions.
553
+ * Derive a jurisdiction-restricted subnamespace. Every ID and stub created
554
+ * from the returned namespace is pinned to `jurisdiction`. Optional because
555
+ * older workers-types releases (and unit-test doubles) may not expose it;
556
+ * {@link applyJurisdiction} fails closed when a jurisdiction is requested
557
+ * but this method is absent.
471
558
  */
472
- expiresAtMs?: number;
473
- /** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
474
- userId: string;
559
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
560
+ }
561
+ interface ResolvedShard {
562
+ fetch: (request: Request) => Promise<Response>;
475
563
  }
476
564
  /**
477
- * A verifier that turns an inbound request into a {@link ResolvedIdentity} (or
478
- * `null` for anonymous). Structurally identical to `WorkerOptions.resolveIdentity`,
479
- * so `.auth()`'s better-auth session resolver, a signed-preview-link verifier, a
480
- * per-tenant bearer check, an upstream-JWT reader, are all just `IdentityResolver`s
481
- * the identity layer is generic over every scheme, not coupled to any one.
565
+ * Return a jurisdiction-restricted view of `namespace`, or `namespace`
566
+ * unchanged when no jurisdiction is configured.
567
+ *
568
+ * Fail-closed: if a jurisdiction is requested but the binding does not expose
569
+ * `.jurisdiction()` (an older workers-types, or a misconfigured test double),
570
+ * this throws rather than silently routing to the un-pinned global namespace —
571
+ * silently dropping a residency constraint would let data land outside the
572
+ * compliance boundary the caller asked for.
482
573
  */
483
- type IdentityResolver = (request: Request, env: unknown) => Promise<ResolvedIdentity | null> | ResolvedIdentity | null;
484
- /** Error policy for {@link composeIdentityResolvers} when a participant resolver throws. */
485
- type ComposeIdentityResolversErrorMode = "fail-closed" | "skip";
486
- /** Options for {@link composeIdentityResolvers}. */
487
- interface ComposeIdentityResolversOptions {
488
- /**
489
- * What to do when a resolver throws. `"fail-closed"` (default, safe)
490
- * re-throws so a broken verifier fails the request rather than silently
491
- * falling through to a weaker one; `"skip"` swallows the error and tries the
492
- * next resolver (use only when a resolver's failure genuinely means "not my
493
- * scheme").
494
- */
495
- readonly onError?: ComposeIdentityResolversErrorMode;
574
+ declare const applyJurisdiction: (namespace: ShardNamespaceLike, jurisdiction?: DurableObjectJurisdiction) => ShardNamespaceLike;
575
+ /** Look up a shard stub by name, preferring `getByName` when present. */
576
+ declare const resolveShard: (namespace: ShardNamespaceLike, shardKey: string) => ResolvedShard;
577
+ /**
578
+ * Source of "which shard keys exist for a given table right now". Returning
579
+ * an empty array is valid — the coordinator will respond with the merge
580
+ * strategy's identity (empty array for `concat`, `0` for `sum`, etc.).
581
+ */
582
+ interface ShardRegistry {
583
+ listShardKeys: (table: string) => Promise<ReadonlyArray<string>> | ReadonlyArray<string>;
496
584
  }
497
585
  /**
498
- * Compose several {@link IdentityResolver}s into one, first-match-wins: each is
499
- * tried in order and the first that returns a non-null identity short-circuits.
500
- * Generic over every scheme — the better-auth session resolver (obtained via the
501
- * builder's `derived.resolveIdentity` escape hatch) is just one entry in the list,
502
- * so composition never means losing it.
503
- *
504
- * A resolver that throws is handled per {@link ComposeIdentityResolversOptions.onError}
505
- * (default `"fail-closed"`: the error propagates).
586
+ * Static-map implementation. Useful for tests and for small deployments
587
+ * where shard keys are known up front (e.g. a fixed set of channel IDs).
506
588
  */
507
- declare const composeIdentityResolvers: (resolvers: ReadonlyArray<IdentityResolver>, options?: ComposeIdentityResolversOptions) => IdentityResolver;
589
+ declare const createStaticShardRegistry: (table_to_keys: Readonly<Record<string, ReadonlyArray<string>>>) => ShardRegistry;
508
590
  /**
509
- * A thin, generic helper over {@link composeIdentityResolvers} for the per-route case:
510
- * pick a resolver by `new URL(request.url).pathname`. Keys are matched by longest
511
- * path prefix; `"*"` is the fallback. Still fully generic — a route→resolver map,
512
- * with no portal / preview / tenant concepts baked in (those live in the app's
513
- * own resolvers).
514
- * @example
515
- * routeIdentityResolvers({ "/admin": adminResolver, "/partner": partnerResolver, "*": sessionResolver })
591
+ * Wire-serializable merge strategy. `topK.by` is a field name on the row
592
+ * (the runtime looks it up with a string key), not a closure.
593
+ *
594
+ * Aggregate-friendly variants for cross-shard `count` / `aggregate` /
595
+ * `groupBy` fan-outs:
596
+ *
597
+ * - `sum` — `count(*)`, `aggregate({ op: "sum" })` (sums numeric per-shard payloads).
598
+ * - `max` — `aggregate({ op: "max" })`.
599
+ * - `min` — `aggregate({ op: "min" })`.
600
+ * - `groupBy` — per-shard `GroupByEntry[]` payloads, reduced into one
601
+ * entry per distinct key tuple. `op` controls how values combine across
602
+ * shards: `sum` (default — works for `COUNT(*)` and `SUM`), `max`, `min`.
603
+ *
604
+ * `avg` is intentionally absent in v1 — a correct cross-shard average
605
+ * requires shipping `(sum, count)` per shard, not the post-shard mean.
606
+ * Use two separate fan-outs (`sum` + `count`) and divide in the caller.
607
+ *
608
+ * `rank` — cross-shard `rank()` over a partition that spans shards (e.g. a
609
+ * global leaderboard `.shardBy("userId")` with `rankIndex(partitionBy: [])`).
610
+ * Each shard's `__lunora_admin__:rankBefore` returns `{before, total}` (its
611
+ * local rows strictly-before the explicit key, plus its local partition
612
+ * total); the merge sums them into `{position: Σbefore + 1, total: Σtotal}` —
613
+ * the 1-based global position and global partition size.
516
614
  */
517
- declare const routeIdentityResolvers: (routes: Record<string, IdentityResolver>) => IdentityResolver;
518
- /** The result of validating a candidate identity against an {@link IdentityContractLike}. */
519
- type IdentityValidation = {
520
- ok: true;
615
+ type MergeStrategy = {
616
+ kind: "concat";
521
617
  } | {
522
- error: string;
523
- ok: false;
618
+ by: string;
619
+ direction?: "asc" | "desc";
620
+ k: number;
621
+ kind: "topK";
622
+ } | {
623
+ kind: "first";
624
+ } | {
625
+ kind: "max";
626
+ } | {
627
+ kind: "min";
628
+ } | {
629
+ kind: "rank";
630
+ } | {
631
+ kind: "sum";
632
+ } | {
633
+ kind: "groupBy";
634
+ op?: "max" | "min" | "sum";
524
635
  };
525
636
  /**
526
- * Structural view of `@lunora/server`'s `IdentityContract` (from `defineIdentity`).
527
- * Kept structural so `@lunora/runtime` stays free of an `@lunora/server` dependency.
528
- * Keep the `onInvalid` union and `validate`/`IdentityValidation` shapes in sync with
529
- * `@lunora/server`'s `IdentityContract` — they are projected by hand, not imported.
530
- * The generated worker entry passes the app's `defineIdentity(...)` result here;
531
- * the worker validates every resolver's returned claims against it at the trust
532
- * boundary before they become `ctx.auth`.
637
+ * Convenience: build the right wire-serializable {@link MergeStrategy} for a
638
+ * given aggregate read. The reader doesn't know which op the caller chose, so
639
+ * a fan-out wrapper passes the user's op + by-keys through this to derive the
640
+ * merge.
641
+ *
642
+ * - `count` `sum`.
643
+ * - `aggregate({ op })` `sum`/`max`/`min` (or throws for `avg`).
644
+ * - `groupBy({ by, agg })` → `groupBy({ op })` (defaults to `sum` since
645
+ * `groupBy`'s default reducer is `count`).
646
+ * @returns the derived {@link MergeStrategy}.
533
647
  */
534
- interface IdentityContractLike {
535
- /** Reject policy applied when validation fails: downgrade to anonymous, or reject the request (401). */
536
- readonly onInvalid: "anonymous" | "reject";
537
- /** Validate resolver-returned claims against the declared contract. */
538
- validate: (identity: Record<string, unknown>) => IdentityValidation;
539
- }
540
- /** One KV namespace as the studio's KV browser surfaces it. */
541
- interface KvNamespaceSummary {
542
- /** The wrangler/env binding name, e.g. `"MY_KV"`. */
543
- binding: string;
544
- }
545
- /** One key entry as the KV admin browser surfaces it. */
546
- interface KvKeyEntry {
547
- /** Absolute expiration (Unix seconds), when set. */
548
- expiration?: number;
549
- /** Per-key metadata set at write time, or absent when none. */
550
- metadata?: unknown;
551
- /** The key name. */
552
- name: string;
553
- }
554
- /** A paginated page of KV keys as the admin browser returns it. */
555
- interface KvKeyListResult {
556
- /** Opaque cursor for the next page; absent when the listing is complete. */
557
- cursor?: string;
558
- /** The keys on this page. */
559
- keys: KvKeyEntry[];
560
- /** True when this is the final page. */
561
- listComplete: boolean;
562
- }
563
- /** A KV value together with its stored metadata. */
564
- interface KvValueResult {
565
- /** Per-key metadata, or `null` when none. */
566
- metadata: unknown;
567
- /** The stored value as a string, or `null` when the key is absent. */
568
- value: null | string;
648
+ declare const mergeStrategyForAggregate: (input: {
649
+ agg?: {
650
+ op?: "avg" | "count" | "max" | "min" | "sum";
651
+ };
652
+ kind: "groupBy";
653
+ } | {
654
+ kind: "count";
655
+ } | {
656
+ kind: "scalar";
657
+ op: "avg" | "count" | "max" | "min" | "sum";
658
+ }) => MergeStrategy;
659
+ interface FanOutSpec {
660
+ merge: MergeStrategy;
661
+ /** Table whose shard keys drive the fan-out. */
662
+ table: string;
569
663
  }
570
664
  /**
571
- * The introspector the worker wires for the studio's KV browser. Build it from
572
- * the env's bound KV namespaces. Omit it and the `/_lunora/admin/kv/*`
573
- * endpoints respond `KV_NOT_CONFIGURED`.
665
+ * Per-shard failure surfaced in the aggregate response's `errors` field. We
666
+ * never throw out of `fanOut` slow/failed shards are *data*, not an
667
+ * exception, so callers can decide whether to retry or surface a partial
668
+ * UI.
574
669
  */
575
- interface KvIntrospector {
576
- /** Delete a key from a namespace. No-op when the key is absent. */
577
- deleteKey: (options: {
578
- key: string;
579
- namespace: string;
580
- }) => Promise<void>;
670
+ interface ShardError {
671
+ /** Human-readable; tests assert on `.includes("timeout")` and similar. */
672
+ message: string;
673
+ shardKey: string;
674
+ /** Set when the per-shard timeout fired. */
675
+ timedOut: boolean;
676
+ }
677
+ interface FanOutResult<T = unknown> {
678
+ /** Merged value — type depends on the merge strategy. */
679
+ data: T;
680
+ errors: ReadonlyArray<ShardError>;
681
+ /** Shards that failed or timed out. */
682
+ failed: number;
683
+ /** Shards that returned successfully. */
684
+ ok: number;
685
+ }
686
+ interface QueryCoordinatorOptions {
687
+ /**
688
+ * Maximum number of shard RPCs to issue in parallel. Defaults to 16 —
689
+ * keeps the 30-second Worker CPU budget healthy when fanning out to
690
+ * dozens of shards and avoids stampeding the DO namespace.
691
+ */
692
+ maxConcurrency?: number;
693
+ /**
694
+ * Hard per-shard timeout in milliseconds. Defaults to 5000; a slow
695
+ * shard surfaces in `errors[]` rather than stalling the aggregate.
696
+ */
697
+ perShardTimeoutMs?: number;
698
+ /** Required — drives which shards to fan out to. */
699
+ registry: ShardRegistry;
700
+ }
701
+ interface FanOutRequest {
702
+ args?: Record<string, unknown>;
703
+ fanOut: FanOutSpec;
704
+ functionPath: string;
705
+ /** Forwarded to each shard fetch (auth, cookies, bookmark). */
706
+ headers?: Record<string, string>;
707
+ }
708
+ /**
709
+ * Cross-shard migration request. Unlike {@link FanOutRequest} there is no merge
710
+ * strategy — per-shard payloads are `MigrationRunResult`-shaped objects, not
711
+ * rows, so {@link QueryCoordinator.orchestrateMigration} rolls them up with the
712
+ * fixed semantics documented on {@link MigrationFanOutResult}.
713
+ *
714
+ * `functionPath` is the admin RPC to invoke on each shard
715
+ * (`__lunora_admin__:runMigration` or `:migrationStatus`); `headers` must carry
716
+ * the `Authorization` bearer header the shard's admin gate requires (the
717
+ * configured admin token), or every shard comes back as a 403 error.
718
+ */
719
+ interface MigrationFanOutRequest {
720
+ args?: Record<string, unknown>;
721
+ functionPath: string;
722
+ headers?: Record<string, string>;
723
+ /** Table whose live shard keys the migration runs across. */
724
+ table: string;
725
+ }
726
+ /** One shard's outcome: either the unwrapped admin `result` payload, or an error. */
727
+ interface ShardMigrationOutcome {
728
+ error?: {
729
+ message: string;
730
+ timedOut: boolean;
731
+ };
732
+ /** The shard's admin `result`, peeled out of the `{ result }` envelope. */
733
+ result?: unknown;
734
+ shardKey: string;
735
+ }
736
+ interface MigrationFanOutResult {
737
+ /** Summed `changed` across shards whose result carried a numeric count. */
738
+ changed: number;
739
+ /** Shards that errored or timed out. */
740
+ failed: number;
741
+ /** Shards that returned a 2xx result. */
742
+ ok: number;
743
+ /** Summed `processed` across shards whose result carried a numeric count. */
744
+ processed: number;
745
+ /** Per-shard outcomes, in registry order. */
746
+ shards: ReadonlyArray<ShardMigrationOutcome>;
747
+ /**
748
+ * Rolled-up status. `"failed"` if any shard's runner reported failure;
749
+ * `"in_progress"` if any shard is incomplete or unreachable (the run stays
750
+ * resumable); `"completed"` only when every shard finished cleanly.
751
+ */
752
+ status: "completed" | "failed" | "in_progress";
753
+ }
754
+ /**
755
+ * Cross-shard rank request. Like {@link MigrationFanOutRequest} there is no
756
+ * caller-supplied merge — per-shard payloads are `{before, total}` objects, so
757
+ * {@link QueryCoordinator.orchestrateRank} rolls them up with the fixed
758
+ * `{position: Σbefore + 1, total: Σtotal}` semantics {@link mergeRank} defines.
759
+ *
760
+ * The key tuple (`partitionKey`/`sortValues`/`rowId`) is built off the row doc
761
+ * via `@lunora/do`'s `rankKeyFromDoc(index, doc)` and forwarded verbatim to
762
+ * each shard's `__lunora_admin__:rankBefore` admin RPC; `headers` must carry
763
+ * the admin bearer the shard's admin gate requires.
764
+ */
765
+ interface RankFanOutRequest {
766
+ headers?: Record<string, string>;
767
+ /** Rank index name on `table`. */
768
+ index: string;
769
+ /** Canonical-JSON partition tuple — `encodePartitionKey(index.partitionBy, doc)`. */
770
+ partitionKey: string;
771
+ /** The `__id__` tiebreak value — `doc._id`. */
772
+ rowId: string;
773
+ /** Serialized sort-key values in `index.sortBy` order, as produced by `rankKeyFromDoc` (wire-safe + byte-matching the stored columns). */
774
+ sortValues: ReadonlyArray<unknown>;
775
+ /** Table whose live shard keys the rank fans out across. */
776
+ table: string;
777
+ }
778
+ interface RankFanOutResult {
779
+ /** Shards that errored or timed out. */
780
+ failed: number;
781
+ /** Shards that returned a 2xx `{before, total}`. */
782
+ ok: number;
783
+ /** `true` when at least one shard failed/timed out, so `position`/`total` are under-counts (failed shards' rows missing). A caller needing an exact global rank should treat this as an error, not trust the numbers. */
784
+ partial: boolean;
785
+ /** 1-based global position within the partition (`Σbefore + 1`). */
786
+ position: number;
787
+ /** Per-shard outcomes, in registry order. */
788
+ shards: ReadonlyArray<ShardRankOutcome>;
789
+ /** Global partition total (`Σtotal`). */
790
+ total: number;
791
+ }
792
+ /** One shard's rank outcome: its `{before, total}` payload, or an error. */
793
+ interface ShardRankOutcome {
794
+ error?: {
795
+ message: string;
796
+ timedOut: boolean;
797
+ };
798
+ result?: {
799
+ before: number;
800
+ total: number;
801
+ };
802
+ shardKey: string;
803
+ }
804
+ /**
805
+ * Cross-shard ranked-pagination request. Like {@link RankFanOutRequest} there's
806
+ * no caller-supplied merge — the merge is the fixed k-way merge by the rank-key
807
+ * tuple. `take` is the global page size; `cursor` is the opaque composite cursor
808
+ * from the prior page's `continueCursor` (absent → first page). `partitionKey`,
809
+ * when set, pins a single partition (`encodePartitionKey(index.partitionBy, where)`),
810
+ * forwarded so each shard scopes its local slice to that partition.
811
+ *
812
+ * `directions` is the per-sort-key direction list (`index.sortBy[i].direction`)
813
+ * the coordinator's comparator needs to break ties the same way each shard's
814
+ * `ORDER BY` does. `partitionKey` and the `__id__` tiebreak are always ascending
815
+ * (matching the shard companion's btree), so only the sort columns vary.
816
+ */
817
+ interface RankPageFanOutRequest {
818
+ /** Opaque composite cursor from the prior page's `continueCursor`. */
819
+ cursor?: null | string;
820
+ /** Per-sort-key directions, in `index.sortBy` order. Missing/short → ascending. */
821
+ directions?: ReadonlyArray<RankDirection>;
822
+ headers?: Record<string, string>;
823
+ /** Rank index name on `table`. */
824
+ index: string;
825
+ /** Optional partition pin forwarded to each shard's local `rankPage`. */
826
+ partitionKey?: string;
827
+ /** Table whose live shard keys the page fans out across. */
828
+ table: string;
829
+ /** Global page size; defaults to 100, capped at 1000 (matching the shard-local `rankPage`). */
830
+ take?: number;
831
+ }
832
+ /** One shard's `rankPage` outcome: its local ranked slice, or an error. */
833
+ interface ShardRankPageOutcome {
834
+ /** The directions the shard ordered by (`index.sortBy[i].direction`); authoritative for the merge. */
835
+ directions?: ReadonlyArray<RankDirection>;
836
+ error?: {
837
+ message: string;
838
+ timedOut: boolean;
839
+ };
840
+ hasMore?: boolean;
841
+ rows?: ReadonlyArray<RankPageRow>;
842
+ shardKey: string;
843
+ }
844
+ interface RankPageFanOutResult {
845
+ /** Opaque composite cursor for the next page, or `null` when the merge is exhausted. */
846
+ continueCursor: null | string;
847
+ /** Shards that errored or timed out. */
848
+ failed: number;
849
+ /** `true` when the global merge has no further rows. */
850
+ isDone: boolean;
851
+ /** Shards that returned a 2xx slice. */
852
+ ok: number;
853
+ /** The globally-ranked page of hydrated docs, in cross-shard rank order. */
854
+ page: ReadonlyArray<Record<string, unknown>>;
855
+ /** `true` when at least one shard failed/timed out, so the page may be missing that shard's rows. */
856
+ partial: boolean;
857
+ /** Per-shard outcomes, in registry order. */
858
+ shards: ReadonlyArray<ShardRankPageOutcome>;
859
+ }
860
+ interface QueryCoordinator {
861
+ fanOut: <T = unknown>(namespace: ShardNamespaceLike, request: FanOutRequest) => Promise<FanOutResult<T>>;
862
+ /**
863
+ * Fan the `__lunora_admin__:applyCdc` admin RPC out by forwarding each
864
+ * pre-bucketed per-shard batch of CDC changes, rolling up the applied/failed
865
+ * counts. The replay half of point-in-time recovery.
866
+ */
867
+ orchestrateApplyCdc: (namespace: ShardNamespaceLike, request: ApplyCdcFanOutRequest) => Promise<ApplyCdcFanOutResult>;
868
+ /**
869
+ * Fan the `__lunora_admin__:cdcSync` admin RPC out to every live shard,
870
+ * each resumed from its own cursor in `request.cursors` (shardKey → seq).
871
+ * Returns the per-shard change pages plus their new cursors so the caller
872
+ * can checkpoint each shard independently — the streaming-export feed.
873
+ */
874
+ orchestrateCdcSync: (namespace: ShardNamespaceLike, request: CdcSyncFanOutRequest) => Promise<CdcSyncFanOutResult>;
875
+ /**
876
+ * Fan an export admin RPC out to every live shard, returning the
877
+ * per-shard `{rows}` payloads alongside any per-shard errors. Each shard
878
+ * returns a JSON envelope (not a streaming body) so this method is the
879
+ * collector — the worker assembles the NDJSON stream.
880
+ */
881
+ orchestrateExport: (namespace: ShardNamespaceLike, request: ExportFanOutRequest) => Promise<ExportFanOutResult>;
882
+ /**
883
+ * Fan an import admin RPC out by routing each row to its owning shard. The
884
+ * shard registry resolves which shards exist; rows whose table has a
885
+ * `shardBy(field)` are bucketed using that field's value as the shard key,
886
+ * other tables fall back to the runtime's default `__root__` shard.
887
+ */
888
+ orchestrateImport: (namespace: ShardNamespaceLike, request: ImportFanOutRequest) => Promise<ImportFanOutResult>;
889
+ /** Fan a migration admin RPC out to every live shard of a table and roll up the per-shard outcomes. */
890
+ orchestrateMigration: (namespace: ShardNamespaceLike, request: MigrationFanOutRequest) => Promise<MigrationFanOutResult>;
891
+ /**
892
+ * Fan the `__lunora_admin__:rankBefore` admin RPC out to every live shard of
893
+ * a table and roll up the per-shard `{before, total}` payloads into the
894
+ * global rank (`{position: Σbefore + 1, total: Σtotal}`). The cross-shard
895
+ * `rank()` path for a partition that spans shards.
896
+ */
897
+ orchestrateRank: (namespace: ShardNamespaceLike, request: RankFanOutRequest) => Promise<RankFanOutResult>;
898
+ /**
899
+ * Page a ranked query across every live shard of a `.shardBy(...)` table.
900
+ * Fans `__lunora_admin__:rankPage` out to each shard, gathers each shard's
901
+ * local ranked slice (rows tagged with their rank-key tuple), and k-way
902
+ * merges them by that tuple into one globally-ranked page of `take` rows.
903
+ * The opaque `continueCursor` is a composite of per-shard cursors so the
904
+ * next page resumes each shard strictly-after the last row the global page
905
+ * consumed from it — pages never drop or duplicate a row at a shard
906
+ * boundary. The cross-shard `rankPage()` path (PLAN5 §7.1 / PLAN2 #3).
907
+ */
908
+ orchestrateRankPage: (namespace: ShardNamespaceLike, request: RankPageFanOutRequest) => Promise<RankPageFanOutResult>;
909
+ /**
910
+ * Fan the `__lunora_admin__:getMetrics` admin RPC out to every live shard of
911
+ * a table and collect each shard's lifetime `requests` total into a per-shard
912
+ * `{ shardKey, requests }` distribution. The feed the studio's `hot_shard`
913
+ * advisor lint needs: a single shard's snapshot can't reveal cross-shard
914
+ * skew, so this fans the cheap metrics read out and returns the whole shard
915
+ * set's request volumes (a failed shard surfaces as `requests: 0`).
916
+ */
917
+ orchestrateShardTraffic: (namespace: ShardNamespaceLike, request: ShardTrafficFanOutRequest) => Promise<ShardTrafficFanOutResult>;
918
+ readonly registry: ShardRegistry;
919
+ }
920
+ /**
921
+ * Cross-shard export request. `tables` is the union of every table the caller
922
+ * wants exported (shard-local **or** global); `headers` carries the admin
923
+ * bearer the per-shard gate expects. Shard registries are queried for the
924
+ * complete set of live shards across all listed shard-local tables.
925
+ */
926
+ interface ExportFanOutRequest {
927
+ args?: Record<string, unknown>;
928
+ headers?: Record<string, string>;
929
+ /**
930
+ * Tables driving the fan-out. Shards are derived from the union of each
931
+ * table's live shard keys — so an export of `["users","messages"]` reaches
932
+ * every shard that holds either table. Globals are skipped here; the
933
+ * worker reads them from D1 directly.
934
+ */
935
+ tables: ReadonlyArray<string>;
936
+ }
937
+ /** Per-shard export outcome. */
938
+ interface ShardExportOutcome {
939
+ error?: {
940
+ message: string;
941
+ timedOut: boolean;
942
+ };
943
+ /** Rows from this shard, or undefined when an error occurred. */
944
+ rows?: ReadonlyArray<{
945
+ doc: Record<string, unknown>;
946
+ table: string;
947
+ }>;
948
+ shardKey: string;
949
+ }
950
+ interface ExportFanOutResult {
951
+ failed: number;
952
+ ok: number;
953
+ shards: ReadonlyArray<ShardExportOutcome>;
954
+ }
955
+ /**
956
+ * Cross-shard change-data-capture request. `tables` drives shard discovery (the
957
+ * union of their live shard keys, like export); `cursors` maps each shard key
958
+ * to the `seq` it was last read through (absent → from the beginning). `limit`
959
+ * caps each shard's page.
960
+ */
961
+ interface CdcSyncFanOutRequest {
962
+ cursors?: Record<string, number>;
963
+ headers?: Record<string, string>;
964
+ limit?: number;
965
+ tables: ReadonlyArray<string>;
966
+ }
967
+ /** Per-shard CDC page: the changes plus the new cursor to resume this shard from. */
968
+ interface ShardCdcOutcome {
969
+ changes?: ReadonlyArray<Record<string, unknown>>;
970
+ /** New per-shard cursor; on error it echoes the shard's prior cursor so a retry resumes cleanly. */
971
+ cursor: number;
972
+ error?: {
973
+ message: string;
974
+ timedOut: boolean;
975
+ };
976
+ shardKey: string;
977
+ }
978
+ interface CdcSyncFanOutResult {
979
+ failed: number;
980
+ ok: number;
981
+ shards: ReadonlyArray<ShardCdcOutcome>;
982
+ }
983
+ /**
984
+ * Cross-shard import request. Rows have already been bucketed by the runtime
985
+ * into one batch per shard key — the coordinator's job is to forward each
986
+ * batch and roll up the per-shard insert counts + errors.
987
+ */
988
+ interface ImportFanOutRequest {
989
+ /**
990
+ * Per-shard batches keyed by shard key. Each entry will be POSTed as the
991
+ * `rows` arg of `__lunora_admin__:importShard`. The shard's
992
+ * starting-line-number for error attribution is carried in `startLine`.
993
+ */
994
+ batches: ReadonlyArray<{
995
+ rows: ReadonlyArray<{
996
+ doc: Record<string, unknown>;
997
+ table: string;
998
+ }>;
999
+ shardKey: string;
1000
+ startLine?: number;
1001
+ }>;
1002
+ headers?: Record<string, string>;
1003
+ }
1004
+ interface ShardImportOutcome {
1005
+ error?: {
1006
+ message: string;
1007
+ timedOut: boolean;
1008
+ };
1009
+ result?: {
1010
+ conflicts: number;
1011
+ errors: ReadonlyArray<{
1012
+ code: string;
1013
+ line: number;
1014
+ message: string;
1015
+ table: string;
1016
+ }>;
1017
+ inserted: Record<string, number>;
1018
+ };
1019
+ shardKey: string;
1020
+ }
1021
+ interface ImportFanOutResult {
1022
+ /** Total conflicts (skipped `_id`s) across shards. */
1023
+ conflicts: number;
1024
+ /** Errors merged across all per-shard outcomes. */
1025
+ errors: ReadonlyArray<{
1026
+ code: string;
1027
+ line: number;
1028
+ message: string;
1029
+ table: string;
1030
+ }>;
1031
+ failed: number;
1032
+ /** Per-table summed insert counts. */
1033
+ inserted: Record<string, number>;
1034
+ ok: number;
1035
+ shards: ReadonlyArray<ShardImportOutcome>;
1036
+ }
1037
+ /**
1038
+ * Cross-shard CDC replay request (point-in-time recovery). Changes are
1039
+ * pre-bucketed by the runtime into one batch per shard key — the coordinator
1040
+ * forwards each batch to `__lunora_admin__:applyCdc` and rolls up the counts.
1041
+ */
1042
+ interface ApplyCdcFanOutRequest {
1043
+ batches: ReadonlyArray<{
1044
+ changes: ReadonlyArray<Record<string, unknown>>;
1045
+ shardKey: string;
1046
+ }>;
1047
+ headers?: Record<string, string>;
1048
+ }
1049
+ interface ApplyCdcFanOutResult {
1050
+ /** Total changes applied across shards. */
1051
+ applied: number;
1052
+ failed: number;
1053
+ ok: number;
1054
+ }
1055
+ /**
1056
+ * Cross-shard traffic request. Like {@link MigrationFanOutRequest} there is no
1057
+ * caller-supplied merge — each shard's `__lunora_admin__:getMetrics` payload
1058
+ * carries its own lifetime `requests` total, and {@link rollUpShardTraffic}
1059
+ * collects them into one `{ shardKey, requests }` entry per shard. `headers`
1060
+ * must carry the admin bearer the per-shard `getMetrics` gate requires.
1061
+ *
1062
+ * `table` drives shard discovery: the registry's live shard keys for the table
1063
+ * are the shards fanned out to. This is the feed the studio's `hot_shard`
1064
+ * runtime advisor consumes to compute cross-shard skew — a single shard's
1065
+ * snapshot can't, so the panel fans this out on demand.
1066
+ */
1067
+ interface ShardTrafficFanOutRequest {
1068
+ headers?: Record<string, string>;
1069
+ /** Table whose live shard keys the traffic fan-out runs across. */
1070
+ table: string;
1071
+ }
1072
+ /** One shard's traffic total, mirroring the advisor's `AdvisorShardTraffic` (sans the optional `group`). */
1073
+ interface ShardTrafficEntry {
1074
+ /** Lifetime request count read off the shard's `getMetrics` snapshot; `0` for a shard that failed/timed out. */
1075
+ requests: number;
1076
+ /** The shard key (the DO id name); `""` for the unnamed root shard. */
1077
+ shardKey: string;
1078
+ }
1079
+ interface ShardTrafficFanOutResult {
1080
+ /** Shards that errored or timed out (their `requests` are reported as `0`). */
1081
+ failed: number;
1082
+ /** Shards that returned a 2xx `getMetrics` snapshot. */
1083
+ ok: number;
1084
+ /**
1085
+ * Per-shard request totals, in registry order. Shaped to plug straight into
1086
+ * the advisor's `LintContext.shardTraffic` so the `hot_shard` lint can
1087
+ * compute the cross-shard share. A failed shard still appears (with
1088
+ * `requests: 0`) so callers see the full shard set.
1089
+ */
1090
+ shards: ReadonlyArray<ShardTrafficEntry>;
1091
+ }
1092
+ declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => QueryCoordinator;
1093
+ /**
1094
+ * One change in the export stream — a clean projection of the raw op-log CDC
1095
+ * record that preserves `seq` (for ordering / idempotency downstream) and the
1096
+ * post-image `doc`. A delete carries no `doc`; the primary key survives in `id`.
1097
+ */
1098
+ interface ExportChange {
1099
+ doc?: Record<string, unknown>;
1100
+ id?: string;
1101
+ op: "delete" | "insert" | "update" | "upsert";
1102
+ seq?: number;
1103
+ table: string;
1104
+ ts?: number;
1105
+ }
1106
+ /** One shard's batch handed to a sink. `cursor` is the new watermark this batch advances the shard to on ack. */
1107
+ interface ExportBatch {
1108
+ changes: ReadonlyArray<ExportChange>;
1109
+ cursor: number;
1110
+ shardKey: string;
1111
+ sink: string;
1112
+ }
1113
+ /**
1114
+ * An export sink. `deliver` MUST reject (throw) when the batch was not durably
1115
+ * accepted downstream — a resolved promise is treated as an acknowledgement and
1116
+ * advances the cursor. Build one with {@link defineExportSink}, or use the
1117
+ * built-in {@link webhookExportSink} / {@link r2Sink}.
1118
+ */
1119
+ interface ExportSink {
1120
+ deliver: (batch: ExportBatch) => Promise<void>;
1121
+ name: string;
1122
+ }
1123
+ /**
1124
+ * Durable per-shard cursor store, keyed by sink name. Mirrors the
1125
+ * `__lunora_source_cursor` watermark from CDC-in: the last op-log `seq` each
1126
+ * shard was delivered through. Injected so the tap stays testable and workerd-safe
1127
+ * — {@link createMemoryCursorStore} for tests, {@link createKvCursorStore} for a
1128
+ * deployment.
1129
+ */
1130
+ interface ExportCursorStore {
1131
+ read: (sink: string) => Promise<Record<string, number>>;
1132
+ write: (sink: string, cursors: Record<string, number>) => Promise<void>;
1133
+ }
1134
+ /** A shard the tap could not drain this pass (sink failure or shard error); its cursor was left un-advanced for retry. */
1135
+ interface ExportTapFailure {
1136
+ error: string;
1137
+ shardKey: string;
1138
+ }
1139
+ /** Outcome of one drain pass. */
1140
+ interface ExportTapResult {
1141
+ /** The persisted per-shard cursor map after this pass. */
1142
+ cursors: Record<string, number>;
1143
+ /** Total changes acknowledged by the sink this pass. */
1144
+ delivered: number;
1145
+ /** Shards left un-advanced (retry pending). Their presence does not stall other shards or the shard's writes. */
1146
+ failures: ReadonlyArray<ExportTapFailure>;
1147
+ /** `true` when any shard returned a full page (more changes likely remain) or any shard failed — the caller should schedule another pass. */
1148
+ hasMore: boolean;
1149
+ /** Number of shards inspected this pass. */
1150
+ shards: number;
1151
+ }
1152
+ /** Options for one {@link runExportTap} drain pass. */
1153
+ interface RunExportTapOptions {
1154
+ /** Cross-shard coordinator providing the op-log change feed. */
1155
+ coordinator: QueryCoordinator;
1156
+ /** Durable cursor store (per-shard watermark). */
1157
+ cursorStore: ExportCursorStore;
1158
+ /** Headers forwarded to each shard (identity / admin bearer). */
1159
+ headers?: Record<string, string>;
1160
+ /** Base backoff in ms for the first retry (doubles each attempt, capped at `maxBackoffMs`). Defaults to `100`. */
1161
+ initialBackoffMs?: number;
1162
+ /** Per-shard page size. */
1163
+ limit?: number;
1164
+ /** Cap on the exponential backoff delay. Defaults to `5000`. */
1165
+ maxBackoffMs?: number;
1166
+ /** Retries after the first delivery attempt before a shard is left for the next pass. Defaults to `3`. */
1167
+ maxRetries?: number;
1168
+ /** The shard DO namespace to fan the feed across. */
1169
+ shardDO: ShardNamespaceLike;
1170
+ /** The sink to deliver to. */
1171
+ sink: ExportSink;
1172
+ /** Injected sleep (defaults to a real timer) so tests drive backoff deterministically. */
1173
+ sleep?: (ms: number) => Promise<void>;
1174
+ /** Tables driving shard discovery (union of their live shard keys). */
1175
+ tables: ReadonlyArray<string>;
1176
+ }
1177
+ /**
1178
+ * Project a raw op-log CDC record (`{ id, op, seq, table, ts, doc? }`) into a
1179
+ * clean {@link ExportChange}. Mirrors `./connector-cdc`'s `flattenCdcChange` but
1180
+ * PRESERVES `seq` / `id` / `ts` so a downstream warehouse can order and dedupe.
1181
+ */
1182
+ declare const sanitizeChange: (raw: Record<string, unknown>) => ExportChange;
1183
+ /**
1184
+ * Run one drain pass of the export tap for a single sink. Reads the durable
1185
+ * cursor, pulls the op-log change feed, delivers each shard's ordered batch (with
1186
+ * retry/backoff), advances only the cursors of shards the sink acknowledged, and
1187
+ * persists the merged cursor map. Idempotent to schedule repeatedly (cron / admin
1188
+ * poke); `hasMore` signals whether another pass is warranted immediately.
1189
+ */
1190
+ declare const runExportTap: (options: RunExportTapOptions) => Promise<ExportTapResult>;
1191
+ /**
1192
+ * Define a custom export sink. A thin identity wrapper that validates the shape
1193
+ * and gives call sites a named factory symmetric with `defineExportSink` in the
1194
+ * plan. The `deliver` contract: resolve on durable acceptance, reject otherwise.
1195
+ */
1196
+ declare const defineExportSink: (config: ExportSink) => ExportSink;
1197
+ /** `fetch`-like signature so the webhook sink is testable without a real network. */
1198
+ type FetchLike = (input: string, init: {
1199
+ body: string;
1200
+ headers: Record<string, string>;
1201
+ method: string;
1202
+ }) => Promise<{
1203
+ ok: boolean;
1204
+ status: number;
1205
+ }>;
1206
+ /**
1207
+ * Built-in webhook sink: POST the shard's changes as an NDJSON body to `url`. A
1208
+ * non-2xx response rejects, so the tap retries + applies backpressure. Idempotency
1209
+ * headers (`x-lunora-sink`, `x-lunora-shard`, `x-lunora-cursor`) let the receiver
1210
+ * dedupe an at-least-once replay.
1211
+ */
1212
+ declare const webhookExportSink: (config: {
1213
+ fetchImpl?: FetchLike;
1214
+ headers?: Record<string, string>;
1215
+ name: string;
1216
+ url: string;
1217
+ }) => ExportSink;
1218
+ /** Minimal R2 bucket surface the sink needs (structurally compatible with an `R2Bucket` binding). */
1219
+ interface R2PutLike {
1220
+ put: (key: string, value: string, options?: {
1221
+ httpMetadata?: {
1222
+ contentType?: string;
1223
+ };
1224
+ }) => Promise<unknown>;
1225
+ }
1226
+ /**
1227
+ * Built-in R2 sink: write each shard's changes as an NDJSON object under
1228
+ * `&lt;prefix>/&lt;shardKey>/&lt;cursor>.ndjson`. The cursor in the key makes each object
1229
+ * content-addressed by watermark, so an at-least-once replay overwrites the same
1230
+ * key rather than duplicating (idempotent at the object level). A `put` rejection
1231
+ * propagates so the tap retries.
1232
+ */
1233
+ declare const r2Sink: (config: {
1234
+ bucket: R2PutLike;
1235
+ name: string;
1236
+ prefix?: string;
1237
+ }) => ExportSink;
1238
+ /** In-memory cursor store for tests. `snapshot` exposes the persisted cursors for assertions. */
1239
+ declare const createMemoryCursorStore: () => ExportCursorStore & {
1240
+ snapshot: () => Record<string, Record<string, number>>;
1241
+ };
1242
+ /** Minimal KV surface the cursor store needs (structurally compatible with a `KVNamespace` binding). */
1243
+ interface KvLike {
1244
+ get: (key: string, type: "json") => Promise<unknown>;
1245
+ put: (key: string, value: string) => Promise<unknown>;
1246
+ }
1247
+ /**
1248
+ * KV-backed durable cursor store. The watermark key mirrors the CDC-in
1249
+ * convention (`__lunora_source_cursor`): `__lunora_source_cursor:export:&lt;sink>`.
1250
+ * A missing / malformed value reads as the empty map (drain from the beginning),
1251
+ * so a fresh sink or a corrupted key can never crash the pass.
1252
+ */
1253
+ declare const createKvCursorStore: (kv: KvLike, options?: {
1254
+ keyPrefix?: string;
1255
+ }) => ExportCursorStore;
1256
+ declare const HEALTH_PATH = "/_lunora/health";
1257
+ declare const HEALTH_READY_PATH = "/_lunora/health/ready";
1258
+ /** Which probe(s) a check participates in. `both` (the default) runs on the aggregate probe and the readiness gate. */
1259
+ type HealthProbeKind = "both" | "liveness" | "readiness";
1260
+ /** The verdict a single probe returns. `message` is runtime-authored and must never echo a secret, env value, or user binding name. */
1261
+ interface HealthProbeResult {
1262
+ healthy: boolean;
1263
+ /** Optional runtime-authored detail (e.g. "binding unreachable"). Included only in the `admin` posture. */
1264
+ message?: string;
1265
+ }
1266
+ /** One registered health check over a binding or subsystem. */
1267
+ interface HealthProbe {
1268
+ /**
1269
+ * The async probe. It must be cheap and self-contained; a thrown error is
1270
+ * treated as an unhealthy result (fail-closed) so a probe bug never 500s the
1271
+ * endpoint.
1272
+ */
1273
+ check: () => Promise<HealthProbeResult> | HealthProbeResult;
1274
+ /**
1275
+ * A critical dependency flips the aggregate `/_lunora/health` probe to `503`
1276
+ * when unhealthy. A non-critical one only degrades the reported status.
1277
+ */
1278
+ critical?: boolean;
1279
+ /** Which probe(s) this check runs on. Defaults to `"both"`. */
1280
+ kind?: HealthProbeKind;
1281
+ /** Stable check name surfaced in the report (e.g. `"durable-object"`, `"d1"`). Not a secret. */
1282
+ name: string;
1283
+ }
1284
+ /** Auth posture for the health endpoints. `"public"` (default) is unauthenticated + message-redacted; `"admin"` requires a valid admin bearer. */
1285
+ type HealthAuthPosture = "admin" | "public";
1286
+ /** One check's line in the response body. */
1287
+ interface HealthCheckReport {
1288
+ critical: boolean;
1289
+ /** Present only in the `admin` posture. */
1290
+ message?: string;
1291
+ name: string;
1292
+ status: "down" | "up";
1293
+ }
1294
+ /** The health response body. Deliberately minimal — status, per-check up/down, and static app metadata only. */
1295
+ interface HealthBody {
1296
+ appName: string;
1297
+ appVersion: string;
1298
+ checks: HealthCheckReport[];
1299
+ status: "degraded" | "healthy" | "unhealthy";
1300
+ timestamp: string;
1301
+ }
1302
+ /** Injected dependencies for the health routes. Probes are resolved per-request so they can read the invocation `env` (bindings only exist at request time). */
1303
+ interface HealthRouteDeps {
1304
+ /** Static application name surfaced in the body. Not a secret. */
1305
+ appName?: string;
1306
+ /** Static application version surfaced in the body. Not a secret. */
1307
+ appVersion?: string;
1308
+ /** Auth posture. Defaults to `"public"`. */
1309
+ auth?: HealthAuthPosture;
1310
+ /**
1311
+ * Cache the last computed report for this many ms so an orchestrator polling
1312
+ * every few seconds does not hammer the bindings. Defaults to `0` (no cache).
1313
+ */
1314
+ cacheTtlMs?: number;
1315
+ /** Admin-bearer predicate, consulted only when `auth === "admin"`. */
1316
+ isAdmin: (request: Request) => boolean;
1317
+ /**
1318
+ * Resolve the probes for this invocation from its `env`. Called once per
1319
+ * request; the returned probes are registered on a fresh `HealthCheck`
1320
+ * registry so the report reflects the live bindings.
1321
+ */
1322
+ resolveProbes: (env: unknown) => ReadonlyArray<HealthProbe>;
1323
+ }
1324
+ /** Build the health + readiness route map merged into the worker's internal route table. */
1325
+ declare const buildHealthRoutes: (deps: HealthRouteDeps) => Record<string, (request: Request, env: unknown) => Promise<Response>>;
1326
+ /**
1327
+ * Structural probe of a Durable Object namespace's reachability: resolve the
1328
+ * default shard stub and issue a cheap request. ANY response (even a `404` for
1329
+ * an unknown path) proves the DO answered; only a thrown error means the object
1330
+ * is unreachable. Never inspects the response body, so it cannot leak state.
1331
+ */
1332
+ declare const durableObjectProbe: (name: string, namespace: {
1333
+ get: (id: unknown) => {
1334
+ fetch: (request: Request) => Promise<Response>;
1335
+ };
1336
+ idFromName: (id: string) => unknown;
1337
+ }, shardKey: string) => HealthProbe;
1338
+ /**
1339
+ * Active probe of a D1 database: run `SELECT 1`. Healthy when it resolves. The
1340
+ * binding is passed structurally (only `.prepare().first()` is used) so the
1341
+ * runtime stays free of a hard `@cloudflare/workers-types` dependency.
1342
+ */
1343
+ declare const d1Probe: (name: string, database: {
1344
+ prepare: (sql: string) => {
1345
+ first: () => Promise<unknown>;
1346
+ };
1347
+ }) => HealthProbe;
1348
+ /**
1349
+ * Presence check for a binding whose remote health cannot be probed cheaply (R2,
1350
+ * queues, Hyperdrive). A bound, well-shaped value reports healthy; the check does
1351
+ * NOT perform a billable remote op. Non-critical by default: a presence gap
1352
+ * degrades the status without forcing a `503`.
1353
+ */
1354
+ declare const presenceProbe: (name: string, bound: boolean) => HealthProbe;
1355
+ /**
1356
+ * Identity resolved from the inbound request by `WorkerOptions.resolveIdentity`.
1357
+ *
1358
+ * The `userId` field is special — it becomes `ctx.auth.userId` inside the
1359
+ * Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
1360
+ * forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
1361
+ *
1362
+ * Return `null` to signal that the request is anonymous; the runtime will
1363
+ * skip both `x-lunora-userid` and `x-lunora-identity` headers, and
1364
+ * `ctx.auth.userId` will be `undefined` on the shard side.
1365
+ */
1366
+ interface ResolvedIdentity {
1367
+ /** Arbitrary additional claims. Must be JSON-serialisable. */
1368
+ [key: string]: unknown;
1369
+ /**
1370
+ * JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
1371
+ * absent), the runtime forwards it as the socket's credential expiry — the
1372
+ * DO drops the socket once it lapses. Used only on the WebSocket path.
1373
+ */
1374
+ exp?: number;
1375
+ /**
1376
+ * Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
1377
+ * both are present. Forwarded as the socket's expiry on the WebSocket path
1378
+ * so the DO drops the socket once it lapses; omit for non-expiring sessions.
1379
+ */
1380
+ expiresAtMs?: number;
1381
+ /** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
1382
+ userId: string;
1383
+ }
1384
+ /**
1385
+ * A verifier that turns an inbound request into a {@link ResolvedIdentity} (or
1386
+ * `null` for anonymous). Structurally identical to `WorkerOptions.resolveIdentity`,
1387
+ * so `.auth()`'s better-auth session resolver, a signed-preview-link verifier, a
1388
+ * per-tenant bearer check, an upstream-JWT reader, … are all just `IdentityResolver`s
1389
+ * — the identity layer is generic over every scheme, not coupled to any one.
1390
+ */
1391
+ type IdentityResolver = (request: Request, env: unknown) => Promise<ResolvedIdentity | null> | ResolvedIdentity | null;
1392
+ /** Error policy for {@link composeIdentityResolvers} when a participant resolver throws. */
1393
+ type ComposeIdentityResolversErrorMode = "fail-closed" | "skip";
1394
+ /** Options for {@link composeIdentityResolvers}. */
1395
+ interface ComposeIdentityResolversOptions {
1396
+ /**
1397
+ * What to do when a resolver throws. `"fail-closed"` (default, safe)
1398
+ * re-throws so a broken verifier fails the request rather than silently
1399
+ * falling through to a weaker one; `"skip"` swallows the error and tries the
1400
+ * next resolver (use only when a resolver's failure genuinely means "not my
1401
+ * scheme").
1402
+ */
1403
+ readonly onError?: ComposeIdentityResolversErrorMode;
1404
+ }
1405
+ /**
1406
+ * Compose several {@link IdentityResolver}s into one, first-match-wins: each is
1407
+ * tried in order and the first that returns a non-null identity short-circuits.
1408
+ * Generic over every scheme — the better-auth session resolver (obtained via the
1409
+ * builder's `derived.resolveIdentity` escape hatch) is just one entry in the list,
1410
+ * so composition never means losing it.
1411
+ *
1412
+ * A resolver that throws is handled per {@link ComposeIdentityResolversOptions.onError}
1413
+ * (default `"fail-closed"`: the error propagates).
1414
+ */
1415
+ declare const composeIdentityResolvers: (resolvers: ReadonlyArray<IdentityResolver>, options?: ComposeIdentityResolversOptions) => IdentityResolver;
1416
+ /**
1417
+ * A thin, generic helper over {@link composeIdentityResolvers} for the per-route case:
1418
+ * pick a resolver by `new URL(request.url).pathname`. Keys are matched by longest
1419
+ * path prefix; `"*"` is the fallback. Still fully generic — a route→resolver map,
1420
+ * with no portal / preview / tenant concepts baked in (those live in the app's
1421
+ * own resolvers).
1422
+ * @example
1423
+ * routeIdentityResolvers({ "/admin": adminResolver, "/partner": partnerResolver, "*": sessionResolver })
1424
+ */
1425
+ declare const routeIdentityResolvers: (routes: Record<string, IdentityResolver>) => IdentityResolver;
1426
+ /** The result of validating a candidate identity against an {@link IdentityContractLike}. */
1427
+ type IdentityValidation = {
1428
+ ok: true;
1429
+ } | {
1430
+ error: string;
1431
+ ok: false;
1432
+ };
1433
+ /**
1434
+ * Structural view of `@lunora/server`'s `IdentityContract` (from `defineIdentity`).
1435
+ * Kept structural so `@lunora/runtime` stays free of an `@lunora/server` dependency.
1436
+ * Keep the `onInvalid` union and `validate`/`IdentityValidation` shapes in sync with
1437
+ * `@lunora/server`'s `IdentityContract` — they are projected by hand, not imported.
1438
+ * The generated worker entry passes the app's `defineIdentity(...)` result here;
1439
+ * the worker validates every resolver's returned claims against it at the trust
1440
+ * boundary before they become `ctx.auth`.
1441
+ */
1442
+ interface IdentityContractLike {
1443
+ /** Reject policy applied when validation fails: downgrade to anonymous, or reject the request (401). */
1444
+ readonly onInvalid: "anonymous" | "reject";
1445
+ /** Validate resolver-returned claims against the declared contract. */
1446
+ validate: (identity: Record<string, unknown>) => IdentityValidation;
1447
+ }
1448
+ /** One KV namespace as the studio's KV browser surfaces it. */
1449
+ interface KvNamespaceSummary {
1450
+ /** The wrangler/env binding name, e.g. `"MY_KV"`. */
1451
+ binding: string;
1452
+ }
1453
+ /** One key entry as the KV admin browser surfaces it. */
1454
+ interface KvKeyEntry {
1455
+ /** Absolute expiration (Unix seconds), when set. */
1456
+ expiration?: number;
1457
+ /** Per-key metadata set at write time, or absent when none. */
1458
+ metadata?: unknown;
1459
+ /** The key name. */
1460
+ name: string;
1461
+ }
1462
+ /** A paginated page of KV keys as the admin browser returns it. */
1463
+ interface KvKeyListResult {
1464
+ /** Opaque cursor for the next page; absent when the listing is complete. */
1465
+ cursor?: string;
1466
+ /** The keys on this page. */
1467
+ keys: KvKeyEntry[];
1468
+ /** True when this is the final page. */
1469
+ listComplete: boolean;
1470
+ }
1471
+ /** A KV value together with its stored metadata. */
1472
+ interface KvValueResult {
1473
+ /** Per-key metadata, or `null` when none. */
1474
+ metadata: unknown;
1475
+ /** The stored value as a string, or `null` when the key is absent. */
1476
+ value: null | string;
1477
+ }
1478
+ /**
1479
+ * The introspector the worker wires for the studio's KV browser. Build it from
1480
+ * the env's bound KV namespaces. Omit it and the `/_lunora/admin/kv/*`
1481
+ * endpoints respond `KV_NOT_CONFIGURED`.
1482
+ */
1483
+ interface KvIntrospector {
1484
+ /** Delete a key from a namespace. No-op when the key is absent. */
1485
+ deleteKey: (options: {
1486
+ key: string;
1487
+ namespace: string;
1488
+ }) => Promise<void>;
581
1489
  /** Read a value (as text) and its metadata from a namespace key. */
582
1490
  getValue: (options: {
583
1491
  key: string;
@@ -648,974 +1556,548 @@ interface LogEvent {
648
1556
  /** Function path that emitted the line, e.g. `"messages:list"`. */
649
1557
  functionPath: string;
650
1558
  /** Severity the line was logged at. */
651
- level: ContextLogLevel;
652
- /** Display string — the message, or the console-style args rendered and space-joined. */
653
- message: string;
654
- /** Shard key for single-shard calls; absent for the unnamed root DO. */
655
- shardKey?: string;
656
- /** Span id of the RPC this line was emitted under (trace correlation), or absent. */
657
- spanId?: string;
658
- /** Trace id this line belongs to (from the inbound `traceparent`), or absent. */
659
- traceId?: string;
660
- /** Wall-clock millis when the line was emitted. */
661
- ts: number;
662
- /** Acting userId, or absent when anonymous. */
663
- userId?: string;
664
- }
665
- /**
666
- * The written-column contract: every field `pipelineLogSink` emits, mapped to the
667
- * column it is stored under by default (the identity mapping). Also the source of
668
- * truth for the {@link PipelineLogField} union. Mirrors the record built in
669
- * `pipelineLogSink` — the read side of the same contract.
670
- */
671
- declare const DEFAULT_COLUMNS: {
672
- readonly fields: "fields";
673
- readonly functionPath: "functionPath";
674
- readonly level: "level";
675
- readonly message: "message";
676
- readonly shardKey: "shardKey";
677
- readonly spanId: "spanId";
678
- readonly traceId: "traceId";
679
- readonly ts: "ts";
680
- readonly userId: "userId";
681
- };
682
- /**
683
- * The canonical field names of one persisted log record — the keys
684
- * `pipelineLogSink` writes. Used as the {@link PipelineLogColumnMap} keys and the
685
- * {@link PipelineLogRow} shape, so the reader stays decoupled from whatever
686
- * physical column names the operator's Iceberg table happens to use.
687
- */
688
- type PipelineLogField = keyof typeof DEFAULT_COLUMNS;
689
- /**
690
- * Field-to-column-name map. Defaults to the identity mapping (each field stored
691
- * under its own name, matching what `pipelineLogSink` writes). Override per-field
692
- * when the Iceberg schema renames a column; unspecified fields keep their
693
- * default. This is the single knob that lets one reader serve differently shaped
694
- * Data Catalog tables.
695
- */
696
- type PipelineLogColumnMap = Partial<Record<PipelineLogField, string>>;
697
- /** An opaque keyset cursor: the `ts` of the row after the last one returned. */
698
- interface PipelineLogCursor {
699
- /** Epoch-millis boundary; the next page is every row strictly older than this. */
700
- ts: number;
701
- }
702
- /** Filters for one {@link PipelineLogReader} query. Every value is inlined safely (`lit`/`sql`). */
703
- interface PipelineLogQuery {
704
- /** Continue after a previous page (keyset on `ts DESC`). Combined with the other filters. */
705
- cursor?: PipelineLogCursor;
706
- /** Match only this exact function path's records. Prefer `functionPathPrefix` for a namespace sweep. */
707
- functionPath?: string;
708
- /** Match records whose `functionPath` starts with this string (rendered as a `LIKE 'prefix%'`). */
709
- functionPathPrefix?: string;
710
- /** Match only this exact severity. When set, `minLevel` is ignored for the same field. */
711
- level?: ContextLogLevel;
712
- /** Max rows to return. Clamped to `[1, 10000]`; defaults to {@link DEFAULT_LOG_LIMIT}. */
713
- limit?: number;
714
- /** Severity floor: keep every level at or above this in {@link LOG_LEVEL_ORDER} (e.g. `warn` keeps warn, error, fatal). */
715
- minLevel?: ContextLogLevel;
716
- /** Match only this shard key. */
717
- shardKey?: string;
718
- /** Lower time bound, inclusive (`ts` at or after this), epoch-millis. */
719
- sinceTs?: number;
720
- /** Match only this trace id. */
721
- traceId?: string;
722
- /** Upper time bound, inclusive (`ts` at or before this), epoch-millis. */
723
- untilTs?: number;
724
- /** Match only this acting user id. */
725
- userId?: string;
726
- }
727
- /**
728
- * One decoded log record. Always keyed by the canonical {@link PipelineLogField}
729
- * names regardless of the physical columns (the reader remaps via `columnMap`),
730
- * so consumers never see the operator's storage names.
731
- */
732
- interface PipelineLogRow {
733
- /**
734
- * Structured fields, when the record carried them. A `serializeFields` sink
735
- * stores these as a JSON string, which the reader parses back to an object;
736
- * a plain string that is not valid JSON is returned verbatim.
737
- */
738
- fields?: unknown;
739
- /** Function path that emitted the line, e.g. `"messages:list"`. */
740
- functionPath: string;
741
- /** Severity the line was logged at. */
742
- level: ContextLogLevel;
743
- /** Rendered message. */
744
- message: string;
745
- /** Shard key for single-shard calls, when present. */
746
- shardKey?: string;
747
- /** Span id the line was emitted under, when present. */
748
- spanId?: string;
749
- /** Trace id the line belongs to, when present. */
750
- traceId?: string;
751
- /** Epoch-millis the line was emitted. */
752
- ts: number;
753
- /** Acting user id, when present. */
754
- userId?: string;
755
- }
756
- /** One page of {@link PipelineLogRow}s, newest first, plus the cursor for the next page (absent means last page). */
757
- interface PipelineLogPage {
758
- /** The cursor to pass as {@link PipelineLogQuery} `cursor` for the following page; absent when this is the last page. */
759
- nextCursor?: PipelineLogCursor;
760
- /** The rows, ordered `ts DESC` (newest first). At most `limit` of them. */
761
- rows: PipelineLogRow[];
762
- }
763
- /** Options for {@link createPipelineLogReader}. */
764
- interface PipelineLogReaderOptions {
765
- /**
766
- * Override any physical column name that diverges from the default (identity)
767
- * mapping. Unspecified fields keep their {@link DEFAULT_LOG_COLUMNS} name.
768
- */
769
- columnMap?: PipelineLogColumnMap;
770
- /**
771
- * The Iceberg namespace the `table` lives in (R2 Data Catalog database).
772
- * Combined as `namespace.table` in the `FROM` clause; omit when `table`
773
- * already carries its namespace.
774
- */
775
- namespace?: string;
776
- /** The Iceberg table name the Pipeline writes log records to (e.g. `"logs"`). */
777
- table: string;
778
- }
779
- /** The reader surface: a single keyset-paginated {@link PipelineLogPage} query. */
780
- interface PipelineLogReader {
781
- /** Run one filtered, keyset-paginated read and return a {@link PipelineLogPage}. */
782
- query: (query?: PipelineLogQuery) => Promise<PipelineLogPage>;
783
- }
784
- /** The written-column contract exposed publicly: canonical field to default physical column name. */
785
- declare const DEFAULT_LOG_COLUMNS: Readonly<Record<PipelineLogField, string>>;
786
- /** Default page size when a query omits `limit`. */
787
- declare const DEFAULT_LOG_LIMIT: number;
788
- /**
789
- * Build a durable-log reader over one R2 Data Catalog (Iceberg) table.
790
- *
791
- * The returned {@link PipelineLogReader} compiles each call to a safe
792
- * `SELECT ... WHERE ... ORDER BY ts DESC LIMIT n` (over-fetching by one) and
793
- * decodes the rows back to the canonical {@link PipelineLogRow} shape. All value
794
- * filters are escaped through `@lunora/bindings/r2sql`'s `sql`/`lit`; column
795
- * names come from `options.columnMap` (operator config), spliced with `raw`.
796
- * @param client An {@link R2SqlClient} (`createR2Sql({ accountId, apiToken, bucket })`).
797
- * @param options The target `table` (plus optional `namespace`) and any `columnMap` overrides.
798
- */
799
- declare const createPipelineLogReader: (client: R2SqlClient, options: PipelineLogReaderOptions) => PipelineLogReader;
800
- /**
801
- * Wire constants for the durable log archive, shared between the server route
802
- * (`@lunora/runtime`'s `log-archive-admin-routes`) and the Studio Archive feed
803
- * (`@lunora/studio`). Kept here — not in `@lunora/runtime` — because the studio
804
- * is a browser bundle that must not import a runtime *value* (which would drag
805
- * the DO/R2-SQL runtime into the browser). This file is dependency-free and
806
- * bundler-inlined into each consumer, so both sides share one source of truth
807
- * with no dependency edge.
808
- */
809
- /**
810
- * The error `code` the archive route returns (400) when the operator has wired
811
- * no archive table (`logArchive`) or the `R2_SQL_*` credentials are missing. The
812
- * Studio keys its "not configured" empty state off this exact value.
813
- */
814
- declare const LOG_ARCHIVE_NOT_CONFIGURED = "LOG_ARCHIVE_NOT_CONFIGURED";
815
- /** The route the studio's `queryLogArchive` client method POSTs to. */
816
- declare const LOG_ARCHIVE_PATH = "/_lunora/admin/logs/archive";
817
- /**
818
- * The app-level archive config the worker passes through: which Data Catalog
819
- * table the Pipeline writes log records to (the read-side of `pipelineLogSink`),
820
- * plus optional namespace / physical-column overrides. The R2 SQL *credentials*
821
- * are NOT here — they live on `env` (`R2_SQL_*`), read per request.
822
- */
823
- interface LogArchiveConfig {
824
- /** Override any physical column name that diverges from the {@link createPipelineLogReader} default mapping. */
825
- columnMap?: PipelineLogColumnMap;
826
- /** The Iceberg namespace (R2 Data Catalog database) the table lives in; omit when `table` already carries it. */
827
- namespace?: string;
828
- /** The Iceberg table the Pipeline writes log records to (e.g. `"logs"`). */
829
- table: string;
830
- }
831
- /**
832
- * Build the {@link LogArchiveConfig} from `env` for the generated worker entry —
833
- * the zero-config seam the codegen `createWorker({ logArchive })` wiring uses,
834
- * mirroring how the R2 SQL *credentials* already come from `env` (`R2_SQL_*`).
835
- *
836
- * Returns `undefined` unless `LUNORA_LOG_ARCHIVE_TABLE` is set, so the studio
837
- * Archive feed stays "not configured" until the operator opts in by naming the
838
- * Data Catalog table (+ optional `LUNORA_LOG_ARCHIVE_NAMESPACE`). `columnMap`
839
- * overrides aren't env-expressible — a hand-written worker passes `logArchive`
840
- * to `createWorker` directly for those.
841
- */
842
- declare const resolveLogArchiveFromEnv: (environment: unknown) => LogArchiveConfig | undefined;
843
- /**
844
- * What kind of instrument produced a measurement, which decides how a collector
845
- * aggregates it:
846
- *
847
- * - `counter` — a monotonic delta to add up (requests, retries, bytes sent).
848
- * - `gauge` — a point-in-time reading that replaces the last one (queue depth,
849
- * cache size).
850
- * - `histogram` — a value whose *distribution* matters (latency, payload size),
851
- * giving percentiles rather than just a mean.
852
- */
853
- type MetricKind = "counter" | "gauge" | "histogram";
854
- /**
855
- * One measurement recorded from a function handler.
856
- *
857
- * Each `ctx.metrics.*` call produces exactly one of these — the runtime does no
858
- * pre-aggregation, so counters carry **delta** temporality and a collector sums
859
- * them. That keeps the sink model identical to logs and spans (one event, one
860
- * export) at the cost of chattiness in a hot loop, where the handler should sum
861
- * locally and record once.
862
- */
863
- interface MetricEvent {
864
- /**
865
- * Structured attributes the caller attached, normalized to a fresh bag of
866
- * JSON-safe primitives (see `shared/log-fields.ts`). These are the metric's
867
- * dimensions — keep them low-cardinality; an id-valued attribute creates a
868
- * distinct time series per id.
869
- *
870
- * Caller-controlled, so they MAY contain user input and they DO egress to
871
- * whatever destination the sink ships to — the same caveat as a log line's
872
- * `fields` and a span's `error.message`. Scrub upstream if that matters.
873
- */
874
- attributes?: LogFields;
875
- /** Function path that recorded the measurement, e.g. `"orders:checkout"`. */
876
- functionPath: string;
877
- /** Instrument kind; see {@link MetricKind}. */
878
- kind: MetricKind;
879
- /** Instrument name, e.g. `"orders.placed"`. */
880
- name: string;
1559
+ level: ContextLogLevel;
1560
+ /** Display string — the message, or the console-style args rendered and space-joined. */
1561
+ message: string;
881
1562
  /** Shard key for single-shard calls; absent for the unnamed root DO. */
882
1563
  shardKey?: string;
883
- /** Wall-clock millis when the measurement was recorded. */
1564
+ /** Span id of the RPC this line was emitted under (trace correlation), or absent. */
1565
+ spanId?: string;
1566
+ /** Trace id this line belongs to (from the inbound `traceparent`), or absent. */
1567
+ traceId?: string;
1568
+ /** Wall-clock millis when the line was emitted. */
884
1569
  ts: number;
885
- /**
886
- * The measured value: the increment for a `counter`, the current reading for
887
- * a `gauge`, the observed sample for a `histogram`.
888
- */
889
- value: number;
1570
+ /** Acting userId, or absent when anonymous. */
1571
+ userId?: string;
890
1572
  }
891
1573
  /**
892
- * One span produced by a `ctx.trace(name, fn)` call, or the synthetic root span
893
- * the shard records for the dispatch itself so a waterfall has a bar to hang
894
- * its children under.
895
- *
896
- * Ids are the same lowercase-hex form the OTLP encoders and `traceparent` use
897
- * (32-hex trace, 16-hex span), so a `SpanEvent` composes into an OTLP span with
898
- * no reformatting.
1574
+ * The written-column contract: every field `pipelineLogSink` emits, mapped to the
1575
+ * column it is stored under by default (the identity mapping). Also the source of
1576
+ * truth for the {@link PipelineLogField} union. Mirrors the record built in
1577
+ * `pipelineLogSink` — the read side of the same contract.
899
1578
  */
900
- interface SpanEvent {
901
- /**
902
- * Structured attributes the caller attached, already normalized to a fresh
903
- * bag of JSON-safe primitives (see `shared/log-fields.ts`) exactly like a log
904
- * line's `fields`. Absent when the caller passed none.
905
- */
906
- attributes?: LogFields;
907
- /** Wall-clock duration of the span body, in milliseconds. */
908
- durationMs: number;
909
- /**
910
- * Populated when the span body threw. `type` is the error's constructor name
911
- * (or its `LunoraError` code); `message` is the human-readable string and may
912
- * include user input, so sinks shipping to third parties should scrub it.
913
- */
914
- error?: {
915
- message: string;
916
- type: string;
917
- };
918
- /**
919
- * Function path the span was created under, e.g. `"messages:list"`. A span
920
- * created inside a function invoked via `ctx.runQuery`/`runMutation`/
921
- * `runAction` carries the OUTER entrypoint's path, since the composed call
922
- * reuses its context the same attribution rule `ctx.log` follows.
923
- */
924
- functionPath: string;
925
- /** Caller-supplied span name, e.g. `"stripe.charge"`. */
926
- name: string;
927
- /** True when the span body returned without throwing. */
928
- ok: boolean;
929
- /**
930
- * Span id of the enclosing span — the parent `ctx.trace` when nested, else
931
- * the dispatch's own RPC span (from the inbound `traceparent`). A span with
932
- * no inbound trace context is parented to a locally-minted root, so this is
933
- * always set for a `ctx.trace` span; only the synthetic `dispatch` span below
934
- * carries `""`, meaning "nothing above me in this trace".
935
- */
936
- parentSpanId: string;
937
- /**
938
- * True for the synthetic span representing the **dispatch itself**, which the
939
- * shard records so a waterfall has a bar for the request to hang its
940
- * `ctx.trace` spans under.
941
- *
942
- * Named for what it is rather than "root": it is not the root of the
943
- * collector-side trace the worker's own RPC span sits above it and it is
944
- * never exported to a sink, because the runtime already emits that dispatch
945
- * via `onRpc` and a collector would otherwise show it twice. Locally it *is*
946
- * the outermost span, which is why the fold prefers it as a trace's anchor.
947
- */
948
- dispatch?: boolean;
949
- /** Shard key for single-shard calls; absent for the unnamed root DO. */
1579
+ declare const DEFAULT_COLUMNS: {
1580
+ readonly fields: "fields";
1581
+ readonly functionPath: "functionPath";
1582
+ readonly level: "level";
1583
+ readonly message: "message";
1584
+ readonly shardKey: "shardKey";
1585
+ readonly spanId: "spanId";
1586
+ readonly traceId: "traceId";
1587
+ readonly ts: "ts";
1588
+ readonly userId: "userId";
1589
+ };
1590
+ /**
1591
+ * The canonical field names of one persisted log record the keys
1592
+ * `pipelineLogSink` writes. Used as the {@link PipelineLogColumnMap} keys and the
1593
+ * {@link PipelineLogRow} shape, so the reader stays decoupled from whatever
1594
+ * physical column names the operator's Iceberg table happens to use.
1595
+ */
1596
+ type PipelineLogField = keyof typeof DEFAULT_COLUMNS;
1597
+ /**
1598
+ * Field-to-column-name map. Defaults to the identity mapping (each field stored
1599
+ * under its own name, matching what `pipelineLogSink` writes). Override per-field
1600
+ * when the Iceberg schema renames a column; unspecified fields keep their
1601
+ * default. This is the single knob that lets one reader serve differently shaped
1602
+ * Data Catalog tables.
1603
+ */
1604
+ type PipelineLogColumnMap = Partial<Record<PipelineLogField, string>>;
1605
+ /** An opaque keyset cursor: the `ts` of the row after the last one returned. */
1606
+ interface PipelineLogCursor {
1607
+ /** Epoch-millis boundary; the next page is every row strictly older than this. */
1608
+ ts: number;
1609
+ }
1610
+ /** Filters for one {@link PipelineLogReader} query. Every value is inlined safely (`lit`/`sql`). */
1611
+ interface PipelineLogQuery {
1612
+ /** Continue after a previous page (keyset on `ts DESC`). Combined with the other filters. */
1613
+ cursor?: PipelineLogCursor;
1614
+ /** Match only this exact function path's records. Prefer `functionPathPrefix` for a namespace sweep. */
1615
+ functionPath?: string;
1616
+ /** Match records whose `functionPath` starts with this string (rendered as a `LIKE 'prefix%'`). */
1617
+ functionPathPrefix?: string;
1618
+ /** Match only this exact severity. When set, `minLevel` is ignored for the same field. */
1619
+ level?: ContextLogLevel;
1620
+ /** Max rows to return. Clamped to `[1, 10000]`; defaults to {@link DEFAULT_LOG_LIMIT}. */
1621
+ limit?: number;
1622
+ /** Severity floor: keep every level at or above this in {@link LOG_LEVEL_ORDER} (e.g. `warn` keeps warn, error, fatal). */
1623
+ minLevel?: ContextLogLevel;
1624
+ /** Match only this shard key. */
950
1625
  shardKey?: string;
951
- /** This span's own id (16-hex). */
952
- spanId: string;
953
- /** Wall-clock millis when the span started. */
954
- startTs: number;
955
- /** Trace this span belongs to (32-hex) shared with the dispatch's logs. */
956
- traceId: string;
957
- /** Acting userId, or absent when anonymous. */
1626
+ /** Lower time bound, inclusive (`ts` at or after this), epoch-millis. */
1627
+ sinceTs?: number;
1628
+ /** Match only this trace id. */
1629
+ traceId?: string;
1630
+ /** Upper time bound, inclusive (`ts` at or before this), epoch-millis. */
1631
+ untilTs?: number;
1632
+ /** Match only this acting user id. */
958
1633
  userId?: string;
959
1634
  }
960
1635
  /**
961
- * Per-RPC dispatch event. Single-shard calls set `shardKey`; cross-shard
962
- * fan-outs set `fanOut` with the table being aggregated, shard count, and
963
- * per-shard failure count.
1636
+ * One decoded log record. Always keyed by the canonical {@link PipelineLogField}
1637
+ * names regardless of the physical columns (the reader remaps via `columnMap`),
1638
+ * so consumers never see the operator's storage names.
964
1639
  */
965
- interface ObservabilityEvent {
966
- /** Wall-clock duration of the dispatch, in milliseconds. */
967
- durationMs: number;
968
- /**
969
- * Populated on `ok === false`. `code`/`status` mirror the LunoraError
970
- * taxonomy; `message` is the human-readable string (may include user
971
- * input — sinks that ship to third parties should scrub it).
972
- */
973
- error?: {
974
- code: string;
975
- message: string;
976
- status: number;
977
- };
1640
+ interface PipelineLogRow {
978
1641
  /**
979
- * Populated for fan-out dispatches.
980
- * `shards` is the total fan-out cardinality; `failed` counts shards that
981
- * timed out or returned an error (the same `errors[]` the response body
982
- * carries to the caller).
1642
+ * Structured fields, when the record carried them. A `serializeFields` sink
1643
+ * stores these as a JSON string, which the reader parses back to an object;
1644
+ * a plain string that is not valid JSON is returned verbatim.
983
1645
  */
984
- fanOut?: {
985
- failed: number;
986
- shards: number;
987
- table: string;
988
- };
989
- /** Function path being invoked, e.g. `"messages:list"`. */
1646
+ fields?: unknown;
1647
+ /** Function path that emitted the line, e.g. `"messages:list"`. */
990
1648
  functionPath: string;
991
- /** True when the dispatch completed without throwing. */
992
- ok: boolean;
993
- /** Shard key for single-shard calls; absent for fan-outs. */
1649
+ /** Severity the line was logged at. */
1650
+ level: ContextLogLevel;
1651
+ /** Rendered message. */
1652
+ message: string;
1653
+ /** Shard key for single-shard calls, when present. */
994
1654
  shardKey?: string;
995
- /**
996
- * W3C trace context for this dispatch, generated once at dispatch entry (32-
997
- * and 16-hex). A sink (e.g. `otlpSink`) reuses these for the dispatch's span
998
- * instead of minting fresh ids, and the runtime propagates them to the shard
999
- * as a `traceparent` so a container the handler calls can stitch its spans
1000
- * under the same trace. Absent on paths that don't originate a trace (a sink
1001
- * falls back to random ids).
1002
- */
1655
+ /** Span id the line was emitted under, when present. */
1003
1656
  spanId?: string;
1657
+ /** Trace id the line belongs to, when present. */
1004
1658
  traceId?: string;
1659
+ /** Epoch-millis the line was emitted. */
1660
+ ts: number;
1661
+ /** Acting user id, when present. */
1662
+ userId?: string;
1005
1663
  }
1006
- /**
1007
- * The `ctx.log` observability contract lives in `shared/` (inlined into each
1008
- * `dist`) so the DO that builds the events and the runtime sink that consumes
1009
- * them agree by construction rather than by hand-mirrored duplication. Re-exported
1010
- * here under the runtime's historical names.
1011
- *
1012
- * `LogLevel` is the canonical `ctx.log` severity union (the five console tiers
1013
- * plus `trace`/`fatal`); `ObservabilitySinkContext` is the shared per-event sink
1014
- * context (a `waitUntil` to keep a background send alive past the response).
1015
- */
1016
- type LogLevel = ContextLogLevel;
1017
- type ObservabilitySinkContext = LogSinkContext;
1018
- /**
1019
- * The hook contract. Methods are optional so a sink can opt into only the
1020
- * events it cares about; the runtime no-ops the others.
1021
- */
1022
- interface ObservabilitySink {
1023
- /** Invoked once per `ctx.log.*` call from a function handler. */
1024
- onLog?: (event: LogEvent, context?: ObservabilitySinkContext) => void;
1025
- /**
1026
- * Invoked once per `ctx.metrics.*` measurement. No pre-aggregation happens
1027
- * upstream, so counter values are deltas for the destination to sum.
1028
- */
1029
- onMetric?: (event: MetricEvent, context?: ObservabilitySinkContext) => void;
1030
- /** Invoked once per dispatched RPC (single-shard or fan-out). */
1031
- onRpc?: (event: ObservabilityEvent, context?: ObservabilitySinkContext) => void;
1032
- /**
1033
- * Invoked once per `ctx.trace(name, fn)` span, when the span body settles.
1034
- * Distinct from `onRpc`: that is the one SERVER span per dispatch, this is the
1035
- * INTERNAL spans a handler creates beneath it.
1036
- */
1037
- onSpan?: (event: SpanEvent, context?: ObservabilitySinkContext) => void;
1664
+ /** One page of {@link PipelineLogRow}s, newest first, plus the cursor for the next page (absent means last page). */
1665
+ interface PipelineLogPage {
1666
+ /** The cursor to pass as {@link PipelineLogQuery} `cursor` for the following page; absent when this is the last page. */
1667
+ nextCursor?: PipelineLogCursor;
1668
+ /** The rows, ordered `ts DESC` (newest first). At most `limit` of them. */
1669
+ rows: PipelineLogRow[];
1038
1670
  }
1039
- /**
1040
- * Invoke `sink.onRpc` with the given event, swallowing any error the sink
1041
- * throws. Use at the dispatch boundary; the runtime should never see a
1042
- * sink-originating throw bubble up past this point. `context.waitUntil`, when
1043
- * supplied, lets a network sink keep its send alive past the response.
1044
- */
1045
- declare const emitRpcEvent: (sink: ObservabilitySink | undefined, event: ObservabilityEvent, context?: ObservabilitySinkContext) => void;
1046
- /**
1047
- * Invoke `sink.onLog` with the given log event, swallowing any error the sink
1048
- * throws. The same failure model as {@link emitRpcEvent}: a buggy log sink must
1049
- * never break the handler that emitted the line.
1050
- */
1051
- declare const emitLogEvent: (sink: ObservabilitySink | undefined, event: LogEvent, context?: ObservabilitySinkContext) => void;
1052
- /**
1053
- * Cloudflare Durable Object jurisdictions restrict where a DO runs and persists
1054
- * data, for data-residency / compliance regimes (GDPR, FedRAMP, US data
1055
- * residency). The set is open — Cloudflare adds values over time — so this is a
1056
- * widening union rather than a closed enum.
1057
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1058
- */
1059
- type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
1060
- /**
1061
- * Structural projection of the bits of `DurableObjectNamespace` the runtime
1062
- * needs. Real workers-types defines a much wider surface; this lets us pass
1063
- * unit-test doubles without coupling to `@cloudflare/workers-types`.
1064
- */
1065
- interface ShardNamespaceLike {
1066
- get: (id: unknown) => {
1067
- fetch: (request: Request) => Promise<Response>;
1068
- };
1671
+ /** Options for {@link createPipelineLogReader}. */
1672
+ interface PipelineLogReaderOptions {
1069
1673
  /**
1070
- * `getByName` is the friendlier API but isn't on every workers-types
1071
- * release yet. We prefer it when available and fall back to
1072
- * `idFromName` + `get` for compatibility.
1674
+ * Override any physical column name that diverges from the default (identity)
1675
+ * mapping. Unspecified fields keep their {@link DEFAULT_LOG_COLUMNS} name.
1073
1676
  */
1074
- getByName?: (name: string) => {
1075
- fetch: (request: Request) => Promise<Response>;
1076
- };
1077
- idFromName: (name: string) => unknown;
1677
+ columnMap?: PipelineLogColumnMap;
1078
1678
  /**
1079
- * Derive a jurisdiction-restricted subnamespace. Every ID and stub created
1080
- * from the returned namespace is pinned to `jurisdiction`. Optional because
1081
- * older workers-types releases (and unit-test doubles) may not expose it;
1082
- * {@link applyJurisdiction} fails closed when a jurisdiction is requested
1083
- * but this method is absent.
1679
+ * The Iceberg namespace the `table` lives in (R2 Data Catalog database).
1680
+ * Combined as `namespace.table` in the `FROM` clause; omit when `table`
1681
+ * already carries its namespace.
1084
1682
  */
1085
- jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
1683
+ namespace?: string;
1684
+ /** The Iceberg table name the Pipeline writes log records to (e.g. `"logs"`). */
1685
+ table: string;
1086
1686
  }
1087
- interface ResolvedShard {
1088
- fetch: (request: Request) => Promise<Response>;
1687
+ /** The reader surface: a single keyset-paginated {@link PipelineLogPage} query. */
1688
+ interface PipelineLogReader {
1689
+ /** Run one filtered, keyset-paginated read and return a {@link PipelineLogPage}. */
1690
+ query: (query?: PipelineLogQuery) => Promise<PipelineLogPage>;
1089
1691
  }
1692
+ /** The written-column contract exposed publicly: canonical field to default physical column name. */
1693
+ declare const DEFAULT_LOG_COLUMNS: Readonly<Record<PipelineLogField, string>>;
1694
+ /** Default page size when a query omits `limit`. */
1695
+ declare const DEFAULT_LOG_LIMIT: number;
1090
1696
  /**
1091
- * Return a jurisdiction-restricted view of `namespace`, or `namespace`
1092
- * unchanged when no jurisdiction is configured.
1697
+ * Build a durable-log reader over one R2 Data Catalog (Iceberg) table.
1093
1698
  *
1094
- * Fail-closed: if a jurisdiction is requested but the binding does not expose
1095
- * `.jurisdiction()` (an older workers-types, or a misconfigured test double),
1096
- * this throws rather than silently routing to the un-pinned global namespace
1097
- * silently dropping a residency constraint would let data land outside the
1098
- * compliance boundary the caller asked for.
1699
+ * The returned {@link PipelineLogReader} compiles each call to a safe
1700
+ * `SELECT ... WHERE ... ORDER BY ts DESC LIMIT n` (over-fetching by one) and
1701
+ * decodes the rows back to the canonical {@link PipelineLogRow} shape. All value
1702
+ * filters are escaped through `@lunora/bindings/r2sql`'s `sql`/`lit`; column
1703
+ * names come from `options.columnMap` (operator config), spliced with `raw`.
1704
+ * @param client An {@link R2SqlClient} (`createR2Sql({ accountId, apiToken, bucket })`).
1705
+ * @param options The target `table` (plus optional `namespace`) and any `columnMap` overrides.
1099
1706
  */
1100
- declare const applyJurisdiction: (namespace: ShardNamespaceLike, jurisdiction?: DurableObjectJurisdiction) => ShardNamespaceLike;
1101
- /** Look up a shard stub by name, preferring `getByName` when present. */
1102
- declare const resolveShard: (namespace: ShardNamespaceLike, shardKey: string) => ResolvedShard;
1707
+ declare const createPipelineLogReader: (client: R2SqlClient, options: PipelineLogReaderOptions) => PipelineLogReader;
1103
1708
  /**
1104
- * Source of "which shard keys exist for a given table right now". Returning
1105
- * an empty array is valid — the coordinator will respond with the merge
1106
- * strategy's identity (empty array for `concat`, `0` for `sum`, etc.).
1709
+ * Wire constants for the durable log archive, shared between the server route
1710
+ * (`@lunora/runtime`'s `log-archive-admin-routes`) and the Studio Archive feed
1711
+ * (`@lunora/studio`). Kept here not in `@lunora/runtime` because the studio
1712
+ * is a browser bundle that must not import a runtime *value* (which would drag
1713
+ * the DO/R2-SQL runtime into the browser). This file is dependency-free and
1714
+ * bundler-inlined into each consumer, so both sides share one source of truth
1715
+ * with no dependency edge.
1107
1716
  */
1108
- interface ShardRegistry {
1109
- listShardKeys: (table: string) => Promise<ReadonlyArray<string>> | ReadonlyArray<string>;
1110
- }
1111
1717
  /**
1112
- * Static-map implementation. Useful for tests and for small deployments
1113
- * where shard keys are known up front (e.g. a fixed set of channel IDs).
1718
+ * The error `code` the archive route returns (400) when the operator has wired
1719
+ * no archive table (`logArchive`) or the `R2_SQL_*` credentials are missing. The
1720
+ * Studio keys its "not configured" empty state off this exact value.
1114
1721
  */
1115
- declare const createStaticShardRegistry: (table_to_keys: Readonly<Record<string, ReadonlyArray<string>>>) => ShardRegistry;
1722
+ declare const LOG_ARCHIVE_NOT_CONFIGURED = "LOG_ARCHIVE_NOT_CONFIGURED";
1723
+ /** The route the studio's `queryLogArchive` client method POSTs to. */
1724
+ declare const LOG_ARCHIVE_PATH = "/_lunora/admin/logs/archive";
1116
1725
  /**
1117
- * Wire-serializable merge strategy. `topK.by` is a field name on the row
1118
- * (the runtime looks it up with a string key), not a closure.
1119
- *
1120
- * Aggregate-friendly variants for cross-shard `count` / `aggregate` /
1121
- * `groupBy` fan-outs:
1122
- *
1123
- * - `sum` `count(*)`, `aggregate({ op: "sum" })` (sums numeric per-shard payloads).
1124
- * - `max` — `aggregate({ op: "max" })`.
1125
- * - `min` `aggregate({ op: "min" })`.
1126
- * - `groupBy` — per-shard `GroupByEntry[]` payloads, reduced into one
1127
- * entry per distinct key tuple. `op` controls how values combine across
1128
- * shards: `sum` (default — works for `COUNT(*)` and `SUM`), `max`, `min`.
1129
- *
1130
- * `avg` is intentionally absent in v1 — a correct cross-shard average
1131
- * requires shipping `(sum, count)` per shard, not the post-shard mean.
1132
- * Use two separate fan-outs (`sum` + `count`) and divide in the caller.
1726
+ * The app-level archive config the worker passes through: which Data Catalog
1727
+ * table the Pipeline writes log records to (the read-side of `pipelineLogSink`),
1728
+ * plus optional namespace / physical-column overrides. The R2 SQL *credentials*
1729
+ * are NOT here they live on `env` (`R2_SQL_*`), read per request.
1730
+ */
1731
+ interface LogArchiveConfig {
1732
+ /** Override any physical column name that diverges from the {@link createPipelineLogReader} default mapping. */
1733
+ columnMap?: PipelineLogColumnMap;
1734
+ /** The Iceberg namespace (R2 Data Catalog database) the table lives in; omit when `table` already carries it. */
1735
+ namespace?: string;
1736
+ /** The Iceberg table the Pipeline writes log records to (e.g. `"logs"`). */
1737
+ table: string;
1738
+ }
1739
+ /**
1740
+ * Build the {@link LogArchiveConfig} from `env` for the generated worker entry —
1741
+ * the zero-config seam the codegen `createWorker({ logArchive })` wiring uses,
1742
+ * mirroring how the R2 SQL *credentials* already come from `env` (`R2_SQL_*`).
1133
1743
  *
1134
- * `rank` cross-shard `rank()` over a partition that spans shards (e.g. a
1135
- * global leaderboard `.shardBy("userId")` with `rankIndex(partitionBy: [])`).
1136
- * Each shard's `__lunora_admin__:rankBefore` returns `{before, total}` (its
1137
- * local rows strictly-before the explicit key, plus its local partition
1138
- * total); the merge sums them into `{position: Σbefore + 1, total: Σtotal}` —
1139
- * the 1-based global position and global partition size.
1744
+ * Returns `undefined` unless `LUNORA_LOG_ARCHIVE_TABLE` is set, so the studio
1745
+ * Archive feed stays "not configured" until the operator opts in by naming the
1746
+ * Data Catalog table (+ optional `LUNORA_LOG_ARCHIVE_NAMESPACE`). `columnMap`
1747
+ * overrides aren't env-expressible a hand-written worker passes `logArchive`
1748
+ * to `createWorker` directly for those.
1140
1749
  */
1141
- type MergeStrategy = {
1142
- kind: "concat";
1143
- } | {
1144
- by: string;
1145
- direction?: "asc" | "desc";
1146
- k: number;
1147
- kind: "topK";
1148
- } | {
1149
- kind: "first";
1150
- } | {
1151
- kind: "max";
1152
- } | {
1153
- kind: "min";
1154
- } | {
1155
- kind: "rank";
1156
- } | {
1157
- kind: "sum";
1158
- } | {
1159
- kind: "groupBy";
1160
- op?: "max" | "min" | "sum";
1161
- };
1750
+ declare const resolveLogArchiveFromEnv: (environment: unknown) => LogArchiveConfig | undefined;
1162
1751
  /**
1163
- * Convenience: build the right wire-serializable {@link MergeStrategy} for a
1164
- * given aggregate read. The reader doesn't know which op the caller chose, so
1165
- * a fan-out wrapper passes the user's op + by-keys through this to derive the
1166
- * merge.
1752
+ * What kind of instrument produced a measurement, which decides how a collector
1753
+ * aggregates it:
1167
1754
  *
1168
- * - `count` `sum`.
1169
- * - `aggregate({ op })` `sum`/`max`/`min` (or throws for `avg`).
1170
- * - `groupBy({ by, agg })` → `groupBy({ op })` (defaults to `sum` since
1171
- * `groupBy`'s default reducer is `count`).
1172
- * @returns the derived {@link MergeStrategy}.
1755
+ * - `counter` a monotonic delta to add up (requests, retries, bytes sent).
1756
+ * - `gauge` a point-in-time reading that replaces the last one (queue depth,
1757
+ * cache size).
1758
+ * - `histogram` a value whose *distribution* matters (latency, payload size),
1759
+ * giving percentiles rather than just a mean.
1173
1760
  */
1174
- declare const mergeStrategyForAggregate: (input: {
1175
- agg?: {
1176
- op?: "avg" | "count" | "max" | "min" | "sum";
1177
- };
1178
- kind: "groupBy";
1179
- } | {
1180
- kind: "count";
1181
- } | {
1182
- kind: "scalar";
1183
- op: "avg" | "count" | "max" | "min" | "sum";
1184
- }) => MergeStrategy;
1185
- interface FanOutSpec {
1186
- merge: MergeStrategy;
1187
- /** Table whose shard keys drive the fan-out. */
1188
- table: string;
1189
- }
1761
+ type MetricKind = "counter" | "gauge" | "histogram";
1190
1762
  /**
1191
- * Per-shard failure surfaced in the aggregate response's `errors` field. We
1192
- * never throw out of `fanOut` — slow/failed shards are *data*, not an
1193
- * exception, so callers can decide whether to retry or surface a partial
1194
- * UI.
1763
+ * One measurement recorded from a function handler.
1764
+ *
1765
+ * Each `ctx.metrics.*` call produces exactly one of these the runtime does no
1766
+ * pre-aggregation, so counters carry **delta** temporality and a collector sums
1767
+ * them. That keeps the sink model identical to logs and spans (one event, one
1768
+ * export) at the cost of chattiness in a hot loop, where the handler should sum
1769
+ * locally and record once.
1195
1770
  */
1196
- interface ShardError {
1197
- /** Human-readable; tests assert on `.includes("timeout")` and similar. */
1198
- message: string;
1199
- shardKey: string;
1200
- /** Set when the per-shard timeout fired. */
1201
- timedOut: boolean;
1202
- }
1203
- interface FanOutResult<T = unknown> {
1204
- /** Merged value — type depends on the merge strategy. */
1205
- data: T;
1206
- errors: ReadonlyArray<ShardError>;
1207
- /** Shards that failed or timed out. */
1208
- failed: number;
1209
- /** Shards that returned successfully. */
1210
- ok: number;
1211
- }
1212
- interface QueryCoordinatorOptions {
1771
+ interface MetricEvent {
1213
1772
  /**
1214
- * Maximum number of shard RPCs to issue in parallel. Defaults to 16 —
1215
- * keeps the 30-second Worker CPU budget healthy when fanning out to
1216
- * dozens of shards and avoids stampeding the DO namespace.
1773
+ * Structured attributes the caller attached, normalized to a fresh bag of
1774
+ * JSON-safe primitives (see `shared/log-fields.ts`). These are the metric's
1775
+ * dimensions keep them low-cardinality; an id-valued attribute creates a
1776
+ * distinct time series per id.
1777
+ *
1778
+ * Caller-controlled, so they MAY contain user input and they DO egress to
1779
+ * whatever destination the sink ships to — the same caveat as a log line's
1780
+ * `fields` and a span's `error.message`. Scrub upstream if that matters.
1217
1781
  */
1218
- maxConcurrency?: number;
1782
+ attributes?: LogFields;
1783
+ /** Function path that recorded the measurement, e.g. `"orders:checkout"`. */
1784
+ functionPath: string;
1785
+ /** Instrument kind; see {@link MetricKind}. */
1786
+ kind: MetricKind;
1787
+ /** Instrument name, e.g. `"orders.placed"`. */
1788
+ name: string;
1789
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
1790
+ shardKey?: string;
1219
1791
  /**
1220
- * Hard per-shard timeout in milliseconds. Defaults to 5000; a slow
1221
- * shard surfaces in `errors[]` rather than stalling the aggregate.
1792
+ * Trace id of the dispatch that recorded this measurement, when it ran inside
1793
+ * one the measurement's **exemplar**, letting a consumer jump from a metric
1794
+ * point to a trace that produced it (OpenTelemetry's exemplar model). Stamped
1795
+ * by the shard from the current request's trace context, not by the caller.
1222
1796
  */
1223
- perShardTimeoutMs?: number;
1224
- /** Required drives which shards to fan out to. */
1225
- registry: ShardRegistry;
1226
- }
1227
- interface FanOutRequest {
1228
- args?: Record<string, unknown>;
1229
- fanOut: FanOutSpec;
1230
- functionPath: string;
1231
- /** Forwarded to each shard fetch (auth, cookies, bookmark). */
1232
- headers?: Record<string, string>;
1233
- }
1234
- /**
1235
- * Cross-shard migration request. Unlike {@link FanOutRequest} there is no merge
1236
- * strategy — per-shard payloads are `MigrationRunResult`-shaped objects, not
1237
- * rows, so {@link QueryCoordinator.orchestrateMigration} rolls them up with the
1238
- * fixed semantics documented on {@link MigrationFanOutResult}.
1239
- *
1240
- * `functionPath` is the admin RPC to invoke on each shard
1241
- * (`__lunora_admin__:runMigration` or `:migrationStatus`); `headers` must carry
1242
- * the `Authorization` bearer header the shard's admin gate requires (the
1243
- * configured admin token), or every shard comes back as a 403 error.
1244
- */
1245
- interface MigrationFanOutRequest {
1246
- args?: Record<string, unknown>;
1247
- functionPath: string;
1248
- headers?: Record<string, string>;
1249
- /** Table whose live shard keys the migration runs across. */
1250
- table: string;
1797
+ traceId?: string;
1798
+ /** Wall-clock millis when the measurement was recorded. */
1799
+ ts: number;
1800
+ /**
1801
+ * The measured value: the increment for a `counter`, the current reading for
1802
+ * a `gauge`, the observed sample for a `histogram`.
1803
+ */
1804
+ value: number;
1251
1805
  }
1252
- /** One shard's outcome: either the unwrapped admin `result` payload, or an error. */
1253
- interface ShardMigrationOutcome {
1806
+ interface SpanEvent {
1807
+ /**
1808
+ * Structured attributes the caller attached, already normalized to a fresh
1809
+ * bag of JSON-safe primitives (see `shared/log-fields.ts`) exactly like a log
1810
+ * line's `fields`. Absent when the caller passed none.
1811
+ */
1812
+ attributes?: LogFields;
1813
+ /** Wall-clock duration of the span body, in milliseconds. */
1814
+ durationMs: number;
1815
+ /**
1816
+ * Populated when the span body threw. `type` is the error's constructor name
1817
+ * (or its `LunoraError` code); `message` is the human-readable string and may
1818
+ * include user input, so sinks shipping to third parties should scrub it.
1819
+ */
1254
1820
  error?: {
1255
1821
  message: string;
1256
- timedOut: boolean;
1822
+ type: string;
1257
1823
  };
1258
- /** The shard's admin `result`, peeled out of the `{ result }` envelope. */
1259
- result?: unknown;
1260
- shardKey: string;
1261
- }
1262
- interface MigrationFanOutResult {
1263
- /** Summed `changed` across shards whose result carried a numeric count. */
1264
- changed: number;
1265
- /** Shards that errored or timed out. */
1266
- failed: number;
1267
- /** Shards that returned a 2xx result. */
1268
- ok: number;
1269
- /** Summed `processed` across shards whose result carried a numeric count. */
1270
- processed: number;
1271
- /** Per-shard outcomes, in registry order. */
1272
- shards: ReadonlyArray<ShardMigrationOutcome>;
1273
1824
  /**
1274
- * Rolled-up status. `"failed"` if any shard's runner reported failure;
1275
- * `"in_progress"` if any shard is incomplete or unreachable (the run stays
1276
- * resumable); `"completed"` only when every shard finished cleanly.
1825
+ * Function path the span was created under, e.g. `"messages:list"`. A span
1826
+ * created inside a function invoked via `ctx.runQuery`/`runMutation`/
1827
+ * `runAction` carries the OUTER entrypoint's path, since the composed call
1828
+ * reuses its context — the same attribution rule `ctx.log` follows.
1277
1829
  */
1278
- status: "completed" | "failed" | "in_progress";
1830
+ functionPath: string;
1831
+ /** Caller-supplied span name, e.g. `"stripe.charge"`. */
1832
+ name: string;
1833
+ /** True when the span body returned without throwing. */
1834
+ ok: boolean;
1835
+ /**
1836
+ * Span id of the enclosing span — the parent `ctx.trace` when nested, else
1837
+ * the dispatch's own RPC span (from the inbound `traceparent`). A span with
1838
+ * no inbound trace context is parented to a locally-minted root, so this is
1839
+ * always set for a `ctx.trace` span; only the synthetic `dispatch` span below
1840
+ * carries `""`, meaning "nothing above me in this trace".
1841
+ */
1842
+ parentSpanId: string;
1843
+ /**
1844
+ * True for the synthetic span representing the **dispatch itself**, which the
1845
+ * shard records so a waterfall has a bar for the request to hang its
1846
+ * `ctx.trace` spans under.
1847
+ *
1848
+ * Named for what it is rather than "root": it is not the root of the
1849
+ * collector-side trace — the worker's own RPC span sits above it — and it is
1850
+ * never exported to a sink, because the runtime already emits that dispatch
1851
+ * via `onRpc` and a collector would otherwise show it twice. Locally it *is*
1852
+ * the outermost span, which is why the fold prefers it as a trace's anchor.
1853
+ */
1854
+ dispatch?: boolean;
1855
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
1856
+ shardKey?: string;
1857
+ /** This span's own id (16-hex). */
1858
+ spanId: string;
1859
+ /** Wall-clock millis when the span started. */
1860
+ startTs: number;
1861
+ /** Trace this span belongs to (32-hex) — shared with the dispatch's logs. */
1862
+ traceId: string;
1863
+ /** Acting userId, or absent when anonymous. */
1864
+ userId?: string;
1279
1865
  }
1280
1866
  /**
1281
- * Cross-shard rank request. Like {@link MigrationFanOutRequest} there is no
1282
- * caller-supplied merge per-shard payloads are `{before, total}` objects, so
1283
- * {@link QueryCoordinator.orchestrateRank} rolls them up with the fixed
1284
- * `{position: Σbefore + 1, total: Σtotal}` semantics {@link mergeRank} defines.
1285
- *
1286
- * The key tuple (`partitionKey`/`sortValues`/`rowId`) is built off the row doc
1287
- * via `@lunora/do`'s `rankKeyFromDoc(index, doc)` and forwarded verbatim to
1288
- * each shard's `__lunora_admin__:rankBefore` admin RPC; `headers` must carry
1289
- * the admin bearer the shard's admin gate requires.
1867
+ * Per-RPC dispatch event. Single-shard calls set `shardKey`; cross-shard
1868
+ * fan-outs set `fanOut` with the table being aggregated, shard count, and
1869
+ * per-shard failure count.
1290
1870
  */
1291
- interface RankFanOutRequest {
1292
- headers?: Record<string, string>;
1293
- /** Rank index name on `table`. */
1294
- index: string;
1295
- /** Canonical-JSON partition tuple `encodePartitionKey(index.partitionBy, doc)`. */
1296
- partitionKey: string;
1297
- /** The `__id__` tiebreak value `doc._id`. */
1298
- rowId: string;
1299
- /** Serialized sort-key values in `index.sortBy` order, as produced by `rankKeyFromDoc` (wire-safe + byte-matching the stored columns). */
1300
- sortValues: ReadonlyArray<unknown>;
1301
- /** Table whose live shard keys the rank fans out across. */
1302
- table: string;
1303
- }
1304
- interface RankFanOutResult {
1305
- /** Shards that errored or timed out. */
1306
- failed: number;
1307
- /** Shards that returned a 2xx `{before, total}`. */
1308
- ok: number;
1309
- /** `true` when at least one shard failed/timed out, so `position`/`total` are under-counts (failed shards' rows missing). A caller needing an exact global rank should treat this as an error, not trust the numbers. */
1310
- partial: boolean;
1311
- /** 1-based global position within the partition (`Σbefore + 1`). */
1312
- position: number;
1313
- /** Per-shard outcomes, in registry order. */
1314
- shards: ReadonlyArray<ShardRankOutcome>;
1315
- /** Global partition total (`Σtotal`). */
1316
- total: number;
1317
- }
1318
- /** One shard's rank outcome: its `{before, total}` payload, or an error. */
1319
- interface ShardRankOutcome {
1871
+ interface ObservabilityEvent {
1872
+ /** Wall-clock duration of the dispatch, in milliseconds. */
1873
+ durationMs: number;
1874
+ /**
1875
+ * Populated on `ok === false`. `code`/`status` mirror the LunoraError
1876
+ * taxonomy; `message` is the human-readable string (may include user
1877
+ * input sinks that ship to third parties should scrub it).
1878
+ */
1320
1879
  error?: {
1880
+ code: string;
1321
1881
  message: string;
1322
- timedOut: boolean;
1882
+ status: number;
1323
1883
  };
1324
- result?: {
1325
- before: number;
1326
- total: number;
1884
+ /**
1885
+ * Populated for fan-out dispatches.
1886
+ * `shards` is the total fan-out cardinality; `failed` counts shards that
1887
+ * timed out or returned an error (the same `errors[]` the response body
1888
+ * carries to the caller).
1889
+ */
1890
+ fanOut?: {
1891
+ failed: number;
1892
+ shards: number;
1893
+ table: string;
1327
1894
  };
1328
- shardKey: string;
1895
+ /** Function path being invoked, e.g. `"messages:list"`. */
1896
+ functionPath: string;
1897
+ /** True when the dispatch completed without throwing. */
1898
+ ok: boolean;
1899
+ /** Shard key for single-shard calls; absent for fan-outs. */
1900
+ shardKey?: string;
1901
+ /**
1902
+ * W3C trace context for this dispatch, generated once at dispatch entry (32-
1903
+ * and 16-hex). A sink (e.g. `otlpSink`) reuses these for the dispatch's span
1904
+ * instead of minting fresh ids, and the runtime propagates them to the shard
1905
+ * as a `traceparent` so a container the handler calls can stitch its spans
1906
+ * under the same trace. Absent on paths that don't originate a trace (a sink
1907
+ * falls back to random ids).
1908
+ */
1909
+ spanId?: string;
1910
+ traceId?: string;
1329
1911
  }
1330
1912
  /**
1331
- * Cross-shard ranked-pagination request. Like {@link RankFanOutRequest} there's
1332
- * no caller-supplied merge the merge is the fixed k-way merge by the rank-key
1333
- * tuple. `take` is the global page size; `cursor` is the opaque composite cursor
1334
- * from the prior page's `continueCursor` (absent → first page). `partitionKey`,
1335
- * when set, pins a single partition (`encodePartitionKey(index.partitionBy, where)`),
1336
- * forwarded so each shard scopes its local slice to that partition.
1913
+ * The `ctx.log` observability contract lives in `shared/` (inlined into each
1914
+ * `dist`) so the DO that builds the events and the runtime sink that consumes
1915
+ * them agree by construction rather than by hand-mirrored duplication. Re-exported
1916
+ * here under the runtime's historical names.
1337
1917
  *
1338
- * `directions` is the per-sort-key direction list (`index.sortBy[i].direction`)
1339
- * the coordinator's comparator needs to break ties the same way each shard's
1340
- * `ORDER BY` does. `partitionKey` and the `__id__` tiebreak are always ascending
1341
- * (matching the shard companion's btree), so only the sort columns vary.
1918
+ * `LogLevel` is the canonical `ctx.log` severity union (the five console tiers
1919
+ * plus `trace`/`fatal`); `ObservabilitySinkContext` is the shared per-event sink
1920
+ * context (a `waitUntil` to keep a background send alive past the response).
1342
1921
  */
1343
- interface RankPageFanOutRequest {
1344
- /** Opaque composite cursor from the prior page's `continueCursor`. */
1345
- cursor?: null | string;
1346
- /** Per-sort-key directions, in `index.sortBy` order. Missing/short ascending. */
1347
- directions?: ReadonlyArray<RankDirection>;
1348
- headers?: Record<string, string>;
1349
- /** Rank index name on `table`. */
1350
- index: string;
1351
- /** Optional partition pin forwarded to each shard's local `rankPage`. */
1352
- partitionKey?: string;
1353
- /** Table whose live shard keys the page fans out across. */
1354
- table: string;
1355
- /** Global page size; defaults to 100, capped at 1000 (matching the shard-local `rankPage`). */
1356
- take?: number;
1357
- }
1358
- /** One shard's `rankPage` outcome: its local ranked slice, or an error. */
1359
- interface ShardRankPageOutcome {
1360
- /** The directions the shard ordered by (`index.sortBy[i].direction`); authoritative for the merge. */
1361
- directions?: ReadonlyArray<RankDirection>;
1362
- error?: {
1363
- message: string;
1364
- timedOut: boolean;
1365
- };
1366
- hasMore?: boolean;
1367
- rows?: ReadonlyArray<RankPageRow>;
1368
- shardKey: string;
1369
- }
1370
- interface RankPageFanOutResult {
1371
- /** Opaque composite cursor for the next page, or `null` when the merge is exhausted. */
1372
- continueCursor: null | string;
1373
- /** Shards that errored or timed out. */
1374
- failed: number;
1375
- /** `true` when the global merge has no further rows. */
1376
- isDone: boolean;
1377
- /** Shards that returned a 2xx slice. */
1378
- ok: number;
1379
- /** The globally-ranked page of hydrated docs, in cross-shard rank order. */
1380
- page: ReadonlyArray<Record<string, unknown>>;
1381
- /** `true` when at least one shard failed/timed out, so the page may be missing that shard's rows. */
1382
- partial: boolean;
1383
- /** Per-shard outcomes, in registry order. */
1384
- shards: ReadonlyArray<ShardRankPageOutcome>;
1385
- }
1386
- interface QueryCoordinator {
1387
- fanOut: <T = unknown>(namespace: ShardNamespaceLike, request: FanOutRequest) => Promise<FanOutResult<T>>;
1388
- /**
1389
- * Fan the `__lunora_admin__:applyCdc` admin RPC out by forwarding each
1390
- * pre-bucketed per-shard batch of CDC changes, rolling up the applied/failed
1391
- * counts. The replay half of point-in-time recovery.
1392
- */
1393
- orchestrateApplyCdc: (namespace: ShardNamespaceLike, request: ApplyCdcFanOutRequest) => Promise<ApplyCdcFanOutResult>;
1394
- /**
1395
- * Fan the `__lunora_admin__:cdcSync` admin RPC out to every live shard,
1396
- * each resumed from its own cursor in `request.cursors` (shardKey → seq).
1397
- * Returns the per-shard change pages plus their new cursors so the caller
1398
- * can checkpoint each shard independently — the streaming-export feed.
1399
- */
1400
- orchestrateCdcSync: (namespace: ShardNamespaceLike, request: CdcSyncFanOutRequest) => Promise<CdcSyncFanOutResult>;
1401
- /**
1402
- * Fan an export admin RPC out to every live shard, returning the
1403
- * per-shard `{rows}` payloads alongside any per-shard errors. Each shard
1404
- * returns a JSON envelope (not a streaming body) so this method is the
1405
- * collector — the worker assembles the NDJSON stream.
1406
- */
1407
- orchestrateExport: (namespace: ShardNamespaceLike, request: ExportFanOutRequest) => Promise<ExportFanOutResult>;
1408
- /**
1409
- * Fan an import admin RPC out by routing each row to its owning shard. The
1410
- * shard registry resolves which shards exist; rows whose table has a
1411
- * `shardBy(field)` are bucketed using that field's value as the shard key,
1412
- * other tables fall back to the runtime's default `__root__` shard.
1413
- */
1414
- orchestrateImport: (namespace: ShardNamespaceLike, request: ImportFanOutRequest) => Promise<ImportFanOutResult>;
1415
- /** Fan a migration admin RPC out to every live shard of a table and roll up the per-shard outcomes. */
1416
- orchestrateMigration: (namespace: ShardNamespaceLike, request: MigrationFanOutRequest) => Promise<MigrationFanOutResult>;
1922
+ type LogLevel = ContextLogLevel;
1923
+ type ObservabilitySinkContext = LogSinkContext;
1924
+ /**
1925
+ * The hook contract. Methods are optional so a sink can opt into only the
1926
+ * events it cares about; the runtime no-ops the others.
1927
+ */
1928
+ interface ObservabilitySink {
1417
1929
  /**
1418
- * Fan the `__lunora_admin__:rankBefore` admin RPC out to every live shard of
1419
- * a table and roll up the per-shard `{before, total}` payloads into the
1420
- * global rank (`{position: Σbefore + 1, total: Σtotal}`). The cross-shard
1421
- * `rank()` path for a partition that spans shards.
1930
+ * **Opt-in, EXPERIMENTAL, default `false`.** When `true`, each `ctx.trace`
1931
+ * span the Durable Object records is ALSO emitted as a Cloudflare **custom
1932
+ * span** (`tracing.enterSpan` from `cloudflare:workers`, GA 2026-06-16) so it
1933
+ * nests inside CF's native binding/fetch/handler trace tree on the hosted
1934
+ * path — a deeper waterfall in Cloudflare's own trace viewer.
1935
+ *
1936
+ * Capability-probed: a safe no-op off-Cloudflare, on a compat date predating
1937
+ * custom spans, or when the trace is unsampled. This ONLY ADDS a CF-side span;
1938
+ * it never replaces {@link ObservabilitySink.onSpan}, which stays the source
1939
+ * of truth and drives the local studio waterfall.
1940
+ *
1941
+ * **Workerd-validated (partial).** The `tracing.enterSpan` bridge is confirmed
1942
+ * available and side-effect-free inside a real Durable Object under
1943
+ * `@cloudflare/vitest-pool-workers` — the body runs without throwing,
1944
+ * `span.isTraced` is a real boolean, and `onSpan`'s recorded tree is byte-for-byte
1945
+ * identical with the flag on vs off. Still EXPERIMENTAL because the harness is
1946
+ * unsampled (`isTraced === false`), so CF's own EXPORTED parent-linking of the
1947
+ * custom span under the DO's ambient span is not yet observable there.
1948
+ *
1949
+ * **Double-export caveat.** Leave this off unless you understand the trade:
1950
+ * with it on, a deployment that also ships `onSpan` to a collector via
1951
+ * `otlpSink` AND lets Cloudflare export its trace tree will emit the same
1952
+ * logical span down two pipelines. Enable it only when you want the CF-native
1953
+ * nesting and have accounted for that overlap.
1954
+ *
1955
+ * Pass this on the SAME sink object you give both `createWorker` and
1956
+ * `createShardDO` — the DO reads the flag when building `ctx.trace`.
1422
1957
  */
1423
- orchestrateRank: (namespace: ShardNamespaceLike, request: RankFanOutRequest) => Promise<RankFanOutResult>;
1958
+ fuseCloudflareTraces?: boolean;
1959
+ /** Invoked once per `ctx.log.*` call from a function handler. */
1960
+ onLog?: (event: LogEvent, context?: ObservabilitySinkContext) => void;
1424
1961
  /**
1425
- * Page a ranked query across every live shard of a `.shardBy(...)` table.
1426
- * Fans `__lunora_admin__:rankPage` out to each shard, gathers each shard's
1427
- * local ranked slice (rows tagged with their rank-key tuple), and k-way
1428
- * merges them by that tuple into one globally-ranked page of `take` rows.
1429
- * The opaque `continueCursor` is a composite of per-shard cursors so the
1430
- * next page resumes each shard strictly-after the last row the global page
1431
- * consumed from it — pages never drop or duplicate a row at a shard
1432
- * boundary. The cross-shard `rankPage()` path (PLAN5 §7.1 / PLAN2 #3).
1962
+ * Invoked once per `ctx.metrics.*` measurement. No pre-aggregation happens
1963
+ * upstream, so counter values are deltas for the destination to sum.
1433
1964
  */
1434
- orchestrateRankPage: (namespace: ShardNamespaceLike, request: RankPageFanOutRequest) => Promise<RankPageFanOutResult>;
1965
+ onMetric?: (event: MetricEvent, context?: ObservabilitySinkContext) => void;
1966
+ /** Invoked once per dispatched RPC (single-shard or fan-out). */
1967
+ onRpc?: (event: ObservabilityEvent, context?: ObservabilitySinkContext) => void;
1435
1968
  /**
1436
- * Fan the `__lunora_admin__:getMetrics` admin RPC out to every live shard of
1437
- * a table and collect each shard's lifetime `requests` total into a per-shard
1438
- * `{ shardKey, requests }` distribution. The feed the studio's `hot_shard`
1439
- * advisor lint needs: a single shard's snapshot can't reveal cross-shard
1440
- * skew, so this fans the cheap metrics read out and returns the whole shard
1441
- * set's request volumes (a failed shard surfaces as `requests: 0`).
1969
+ * Invoked once per `ctx.trace(name, fn)` span, when the span body settles.
1970
+ * Distinct from `onRpc`: that is the one SERVER span per dispatch, this is the
1971
+ * INTERNAL spans a handler creates beneath it.
1442
1972
  */
1443
- orchestrateShardTraffic: (namespace: ShardNamespaceLike, request: ShardTrafficFanOutRequest) => Promise<ShardTrafficFanOutResult>;
1444
- readonly registry: ShardRegistry;
1973
+ onSpan?: (event: SpanEvent, context?: ObservabilitySinkContext) => void;
1445
1974
  }
1446
1975
  /**
1447
- * Cross-shard export request. `tables` is the union of every table the caller
1448
- * wants exported (shard-local **or** global); `headers` carries the admin
1449
- * bearer the per-shard gate expects. Shard registries are queried for the
1450
- * complete set of live shards across all listed shard-local tables.
1976
+ * Invoke `sink.onRpc` with the given event, swallowing any error the sink
1977
+ * throws. Use at the dispatch boundary; the runtime should never see a
1978
+ * sink-originating throw bubble up past this point. `context.waitUntil`, when
1979
+ * supplied, lets a network sink keep its send alive past the response.
1980
+ *
1981
+ * `sampling` applies the trace-sampling verdict to this dispatch's SERVER span:
1982
+ * the event is dropped unless the trace was head-sampled or (with errors
1983
+ * force-kept) this dispatch errored — the tail bias. A dispatch with no
1984
+ * `traceId` (a fan-out aggregation, which mints none) is always kept, and an
1985
+ * absent `sampling` keeps everything, so both are backward-compatible.
1451
1986
  */
1452
- interface ExportFanOutRequest {
1453
- args?: Record<string, unknown>;
1454
- headers?: Record<string, string>;
1455
- /**
1456
- * Tables driving the fan-out. Shards are derived from the union of each
1457
- * table's live shard keys — so an export of `["users","messages"]` reaches
1458
- * every shard that holds either table. Globals are skipped here; the
1459
- * worker reads them from D1 directly.
1460
- */
1461
- tables: ReadonlyArray<string>;
1462
- }
1463
- /** Per-shard export outcome. */
1464
- interface ShardExportOutcome {
1465
- error?: {
1466
- message: string;
1467
- timedOut: boolean;
1468
- };
1469
- /** Rows from this shard, or undefined when an error occurred. */
1470
- rows?: ReadonlyArray<{
1471
- doc: Record<string, unknown>;
1472
- table: string;
1473
- }>;
1474
- shardKey: string;
1475
- }
1476
- interface ExportFanOutResult {
1477
- failed: number;
1478
- ok: number;
1479
- shards: ReadonlyArray<ShardExportOutcome>;
1480
- }
1987
+ declare const emitRpcEvent: (sink: ObservabilitySink | undefined, event: ObservabilityEvent, context?: ObservabilitySinkContext, sampling?: TraceSamplingConfig) => void;
1481
1988
  /**
1482
- * Cross-shard change-data-capture request. `tables` drives shard discovery (the
1483
- * union of their live shard keys, like export); `cursors` maps each shard key
1484
- * to the `seq` it was last read through (absent → from the beginning). `limit`
1485
- * caps each shard's page.
1989
+ * Invoke `sink.onLog` with the given log event, swallowing any error the sink
1990
+ * throws. The same failure model as {@link emitRpcEvent}: a buggy log sink must
1991
+ * never break the handler that emitted the line.
1486
1992
  */
1487
- interface CdcSyncFanOutRequest {
1488
- cursors?: Record<string, number>;
1489
- headers?: Record<string, string>;
1490
- limit?: number;
1491
- tables: ReadonlyArray<string>;
1492
- }
1493
- /** Per-shard CDC page: the changes plus the new cursor to resume this shard from. */
1494
- interface ShardCdcOutcome {
1495
- changes?: ReadonlyArray<Record<string, unknown>>;
1496
- /** New per-shard cursor; on error it echoes the shard's prior cursor so a retry resumes cleanly. */
1497
- cursor: number;
1498
- error?: {
1499
- message: string;
1500
- timedOut: boolean;
1501
- };
1502
- shardKey: string;
1503
- }
1504
- interface CdcSyncFanOutResult {
1505
- failed: number;
1506
- ok: number;
1507
- shards: ReadonlyArray<ShardCdcOutcome>;
1508
- }
1993
+ declare const emitLogEvent: (sink: ObservabilitySink | undefined, event: LogEvent, context?: ObservabilitySinkContext) => void;
1994
+ /** Procedure kinds that can be exposed over REST (`stream` cannot — it is a WebSocket surface). */
1995
+ type RestFunctionKind = "action" | "mutation" | "query";
1509
1996
  /**
1510
- * Cross-shard import request. Rows have already been bucketed by the runtime
1511
- * into one batch per shard key the coordinator's job is to forward each
1512
- * batch and roll up the per-shard insert counts + errors.
1997
+ * The `.expose({ rest: true })` tag stamped onto a registered procedure (runtime,
1998
+ * as `fn.expose`) or discovered from its builder chain (codegen, onto the
1999
+ * `FunctionIR`). Presence of `rest === true` is the ONLY thing that opts a
2000
+ * procedure into the surface — everything is default-closed.
1513
2001
  */
1514
- interface ImportFanOutRequest {
1515
- /**
1516
- * Per-shard batches keyed by shard key. Each entry will be POSTed as the
1517
- * `rows` arg of `__lunora_admin__:importShard`. The shard's
1518
- * starting-line-number for error attribution is carried in `startLine`.
1519
- */
1520
- batches: ReadonlyArray<{
1521
- rows: ReadonlyArray<{
1522
- doc: Record<string, unknown>;
1523
- table: string;
1524
- }>;
1525
- shardKey: string;
1526
- startLine?: number;
1527
- }>;
1528
- headers?: Record<string, string>;
2002
+ interface RestExposure {
2003
+ rest?: boolean;
1529
2004
  }
1530
- interface ShardImportOutcome {
1531
- error?: {
1532
- message: string;
1533
- timedOut: boolean;
1534
- };
1535
- result?: {
1536
- conflicts: number;
1537
- errors: ReadonlyArray<{
1538
- code: string;
1539
- line: number;
1540
- message: string;
1541
- table: string;
1542
- }>;
1543
- inserted: Record<string, number>;
1544
- };
1545
- shardKey: string;
1546
- }
1547
- interface ImportFanOutResult {
1548
- /** Total conflicts (skipped `_id`s) across shards. */
1549
- conflicts: number;
1550
- /** Errors merged across all per-shard outcomes. */
1551
- errors: ReadonlyArray<{
1552
- code: string;
1553
- line: number;
1554
- message: string;
1555
- table: string;
1556
- }>;
1557
- failed: number;
1558
- /** Per-table summed insert counts. */
1559
- inserted: Record<string, number>;
1560
- ok: number;
1561
- shards: ReadonlyArray<ShardImportOutcome>;
2005
+ /** One resolved REST endpoint: the transport method + URL path a procedure is reachable at. */
2006
+ interface RestSurfaceEntry {
2007
+ functionPath: string;
2008
+ kind: RestFunctionKind;
2009
+ method: "GET" | "POST";
2010
+ name: string;
2011
+ namespace: string;
2012
+ path: string;
1562
2013
  }
1563
2014
  /**
1564
- * Cross-shard CDC replay request (point-in-time recovery). Changes are
1565
- * pre-bucketed by the runtime into one batch per shard key the coordinator
1566
- * forwards each batch to `__lunora_admin__:applyCdc` and rolls up the counts.
2015
+ * Resolve the full REST surface from a list of procedures, filtering to the ones
2016
+ * opted in via `.expose({ rest: true })`. The single source of truth both the
2017
+ * runtime router and the OpenAPI emitter derive from — a `stream` procedure or a
2018
+ * malformed path is skipped. Ordered by path for stable enumeration.
1567
2019
  */
1568
- interface ApplyCdcFanOutRequest {
1569
- batches: ReadonlyArray<{
1570
- changes: ReadonlyArray<Record<string, unknown>>;
1571
- shardKey: string;
1572
- }>;
1573
- headers?: Record<string, string>;
2020
+ declare const describeRestSurface: (procedures: ReadonlyArray<{
2021
+ exposure?: RestExposure;
2022
+ functionPath: string;
2023
+ kind: "action" | "mutation" | "query" | "stream";
2024
+ }>) => RestSurfaceEntry[];
2025
+ /** The bits of a registered function the REST router reads: its kind and its `.expose` tag. */
2026
+ interface RestRegistryEntry {
2027
+ expose?: RestExposure;
2028
+ kind: "action" | "mutation" | "query" | "stream";
1574
2029
  }
1575
- interface ApplyCdcFanOutResult {
1576
- /** Total changes applied across shards. */
1577
- applied: number;
1578
- failed: number;
1579
- ok: number;
2030
+ /** Registry map (structurally the generated `LUNORA_FUNCTIONS`, narrowed to what REST needs). */
2031
+ type RestRegistryLike = Record<string, RestRegistryEntry>;
2032
+ /** Dispatch one exposed procedure through the shared RPC path (auth + RLS + validators enforced at the shard). Returns the shard `Response`. */
2033
+ type RestInvoke = (parameters: {
2034
+ args: Record<string, unknown>;
2035
+ env: unknown;
2036
+ functionPath: string;
2037
+ request: Request;
2038
+ shardKey?: string;
2039
+ }) => Promise<Response>;
2040
+ /**
2041
+ * Optional per-request rate-limit gate for the public surface. Returns a `429`
2042
+ * `Response` when the request is limited (the router returns it verbatim), or
2043
+ * `undefined` to let the call through. Built in `create-worker` over
2044
+ * `@lunora/ratelimit`.
2045
+ */
2046
+ type RestRateLimit = (request: Request, functionPath: string) => Promise<Response | undefined> | Response | undefined;
2047
+ interface RestRouteDeps {
2048
+ /** The generated function registry — the source of which procedures are exposed. */
2049
+ functions: RestRegistryLike;
2050
+ /** The shared RPC dispatch (bound in `create-worker`). */
2051
+ invoke: RestInvoke;
2052
+ /** Optional rate-limit gate for the public surface. */
2053
+ rateLimit?: RestRateLimit;
2054
+ /** JSON body reader with the shared size cap. */
2055
+ readJsonBody: (request: Request) => Promise<Record<string, unknown>>;
2056
+ }
2057
+ /**
2058
+ * The resolved REST surface for a registry — the ordered list of exposed
2059
+ * `{ functionPath, method, path, kind }`. Exported so a contract test can assert
2060
+ * the runtime surface equals the published OpenAPI (both derive from the same
2061
+ * `shared/rest-surface` helper).
2062
+ */
2063
+ declare const restSurfaceFromRegistry: (functions: RestRegistryLike) => ReturnType<typeof describeRestSurface>;
2064
+ /** Read `shardKey` from `?shardKey=` or the `x-lunora-shard-key` header; `undefined` routes to the default shard. */
2065
+ declare const readShardKey: (url: URL, request: Request) => string | undefined;
2066
+ /**
2067
+ * Decode GET args from the query string. Each value is parsed as JSON when it
2068
+ * looks like a JSON scalar/array/object (so `?limit=10` → number, `?ids=[1,2]` →
2069
+ * array), else kept as a string. `shardKey` is reserved for routing and excluded.
2070
+ */
2071
+ declare const argsFromQuery: (url: URL) => Record<string, unknown>;
2072
+ /**
2073
+ * Build the REST route map merged into the worker's internal route table. One
2074
+ * exact-path entry per exposed procedure — so the surface is closed by
2075
+ * construction. A `query` handler accepts `GET` (args from the query string) and
2076
+ * `POST` (args from a JSON body); a `mutation` / `action` accepts `POST` only.
2077
+ */
2078
+ declare const buildRestRoutes: (deps: RestRouteDeps) => Record<string, (request: Request, env: unknown) => Promise<Response>>;
2079
+ /** Structural view of a `@lunora/ratelimit` `RateLimiter` — only the `.limit()` call, so the runtime needs no hard dependency. */
2080
+ interface RateLimiterLike {
2081
+ limit: (name: string, args?: {
2082
+ key?: string;
2083
+ }) => Promise<{
2084
+ ok: boolean;
2085
+ retryAfter: number;
2086
+ }>;
1580
2087
  }
1581
2088
  /**
1582
- * Cross-shard traffic request. Like {@link MigrationFanOutRequest} there is no
1583
- * caller-supplied merge each shard's `__lunora_admin__:getMetrics` payload
1584
- * carries its own lifetime `requests` total, and {@link rollUpShardTraffic}
1585
- * collects them into one `{ shardKey, requests }` entry per shard. `headers`
1586
- * must carry the admin bearer the per-shard `getMetrics` gate requires.
1587
- *
1588
- * `table` drives shard discovery: the registry's live shard keys for the table
1589
- * are the shards fanned out to. This is the feed the studio's `hot_shard`
1590
- * runtime advisor consumes to compute cross-shard skew — a single shard's
1591
- * snapshot can't, so the panel fans this out on demand.
2089
+ * Adapt a `@lunora/ratelimit` limiter into a {@link RestRateLimit} gate for the
2090
+ * public REST surface (plan 167). Pass the limiter and the rate name to charge;
2091
+ * `key` isolates the limit per caller (IP / user / API key — defaults to the
2092
+ * `cf-connecting-ip` header, else a shared bucket). A denied request becomes a
2093
+ * `429` with a `Retry-After` header (seconds, ceil of the limiter's ms). The
2094
+ * runtime imports nothing from `@lunora/ratelimit` — build the limiter in the
2095
+ * worker entry and pass it here.
1592
2096
  */
1593
- interface ShardTrafficFanOutRequest {
1594
- headers?: Record<string, string>;
1595
- /** Table whose live shard keys the traffic fan-out runs across. */
1596
- table: string;
1597
- }
1598
- /** One shard's traffic total, mirroring the advisor's `AdvisorShardTraffic` (sans the optional `group`). */
1599
- interface ShardTrafficEntry {
1600
- /** Lifetime request count read off the shard's `getMetrics` snapshot; `0` for a shard that failed/timed out. */
1601
- requests: number;
1602
- /** The shard key (the DO id name); `""` for the unnamed root shard. */
1603
- shardKey: string;
1604
- }
1605
- interface ShardTrafficFanOutResult {
1606
- /** Shards that errored or timed out (their `requests` are reported as `0`). */
1607
- failed: number;
1608
- /** Shards that returned a 2xx `getMetrics` snapshot. */
1609
- ok: number;
1610
- /**
1611
- * Per-shard request totals, in registry order. Shaped to plug straight into
1612
- * the advisor's `LintContext.shardTraffic` so the `hot_shard` lint can
1613
- * compute the cross-shard share. A failed shard still appears (with
1614
- * `requests: 0`) so callers see the full shard set.
1615
- */
1616
- shards: ReadonlyArray<ShardTrafficEntry>;
1617
- }
1618
- declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => QueryCoordinator;
2097
+ declare const createRestRateLimit: (limiter: RateLimiterLike, options: {
2098
+ key?: (request: Request, functionPath: string) => string | undefined;
2099
+ name: string;
2100
+ }) => RestRateLimit;
1619
2101
  /** Per-header overrides for {@link SecurityHeadersOptions}. `false` omits the header. */
1620
2102
  interface SecurityHeadersOptions {
1621
2103
  /**
@@ -1917,6 +2399,17 @@ interface FunctionDescriptor {
1917
2399
  interface FunctionRegistryEntry {
1918
2400
  /** The function's `v.*` args validator map; read structurally for the signature view. */
1919
2401
  args?: unknown;
2402
+ /**
2403
+ * Opt-in public-surface tag set by the `.expose({ rest: true })` builder
2404
+ * modifier (plan 167). Present only on procedures deliberately published over
2405
+ * REST; the runtime builds a `/_lunora/rest/&lt;namespace>/&lt;fn>` route for each,
2406
+ * routing THROUGH the procedure so auth/RLS/validators are enforced. Rides
2407
+ * along on the registered function's identity (like `fn.x402` / `fn.rls`), so
2408
+ * reading it needs no change to the generated registry shape.
2409
+ */
2410
+ expose?: {
2411
+ readonly rest?: boolean;
2412
+ };
1920
2413
  /**
1921
2414
  * The generated registry carries `"stream"` alongside query/mutation/action;
1922
2415
  * the discovery endpoint surfaces the latter three only (a `stream` function
@@ -2206,6 +2699,81 @@ interface BackupManifest {
2206
2699
  scheduledTime: number;
2207
2700
  tables?: string;
2208
2701
  }
2702
+ /**
2703
+ * Health / readiness probe configuration (plan 177). Everything is optional; the
2704
+ * runtime always registers its default binding probes, so the endpoints work
2705
+ * with `health: {}` (or the field omitted). Nothing here is a secret — `appName`
2706
+ * / `appVersion` are the only strings echoed in the body, and per-check messages
2707
+ * are surfaced only under the `"admin"` posture.
2708
+ */
2709
+ interface HealthOptions {
2710
+ /** Application name surfaced in the health body. Defaults to `"lunora"`. */
2711
+ appName?: string;
2712
+ /** Application version surfaced in the health body. Defaults to `"0.0.0"`. */
2713
+ appVersion?: string;
2714
+ /**
2715
+ * Auth posture. `"public"` (default) serves the probe unauthenticated with
2716
+ * per-check messages redacted; `"admin"` requires a valid admin bearer and
2717
+ * includes the (runtime-authored) messages.
2718
+ */
2719
+ auth?: "admin" | "public";
2720
+ /** Cache the computed report for this many ms so a frequent poller does not re-run every probe. Defaults to `0`. */
2721
+ cacheTtlMs?: number;
2722
+ /** Skip the auto-registered D1 / R2 / queue / Hyperdrive binding probes (keep only the DO probe + `probes`). Defaults to `false`. */
2723
+ disableBindingProbes?: boolean;
2724
+ /** Extra bespoke probes appended to the auto-registered set (e.g. a downstream API reachability check). */
2725
+ probes?: ReadonlyArray<HealthProbe>;
2726
+ }
2727
+ /**
2728
+ * One registered device subscription as surfaced by the gated
2729
+ * `__lunora_admin__:listPushSubscriptions` admin RPC (backing the Studio
2730
+ * Notifications page). Structurally mirrors `@lunora/notify`'s
2731
+ * `PushSubscriptionDevice` — the runtime carries NO `@lunora/notify` dependency,
2732
+ * so the shape is declared here and matched by duck typing (the studio reuses the
2733
+ * canonical `@lunora/notify` type). Delivery secrets (Web Push `keys`, FCM
2734
+ * `token`) are never part of this shape.
2735
+ */
2736
+ interface NotifySubscriptionDevice {
2737
+ /** Unix-ms creation time. */
2738
+ createdAt: number;
2739
+ /** Web Push service endpoint URL (web-push only). */
2740
+ endpoint?: string;
2741
+ /** Stable identifier used as the store key. */
2742
+ id: string;
2743
+ /** The delivery channel this subscription targets (`"web-push"` / `"fcm"`). */
2744
+ kind: string;
2745
+ /** Last delivery error message, when `lastStatus` is `failed`/`expired`. */
2746
+ lastError?: string;
2747
+ /** Unix-ms time of the most recent register/send touch. */
2748
+ lastSeenAt: number;
2749
+ /** Last-known delivery outcome (`"ok"` / `"failed"` / `"expired"`). */
2750
+ lastStatus?: string;
2751
+ /** Arbitrary app metadata (device name, locale, topics, …). */
2752
+ metadata?: Record<string, unknown>;
2753
+ /** Owning user id, or `null`/absent when anonymous. */
2754
+ userId?: null | string;
2755
+ }
2756
+ /**
2757
+ * The minimal read surface the worker needs off an `@lunora/notify` subscription
2758
+ * store to serve `__lunora_admin__:listPushSubscriptions`: just `list`. Codegen
2759
+ * binds this from the app's `defineNotify({ store })` (`store(env)`), so the
2760
+ * worker reads registered devices through the very store the handlers write to.
2761
+ * Structural (not a `@lunora/notify` import) to keep the runtime dependency-free.
2762
+ */
2763
+ interface NotifySubscriptionStoreLike {
2764
+ /**
2765
+ * List every stored subscription. Declared with NO parameter so a concrete
2766
+ * `@lunora/notify` `SubscriptionStore` — whose `list(filter?)` narrows `kind`
2767
+ * to the `"web-push" | "fcm"` union — assigns cleanly under
2768
+ * `strictFunctionTypes` (an extra optional parameter on the source is fine).
2769
+ * The RPC handler applies the `{ kind, userId }` filter in-memory, so no typed
2770
+ * filter needs to cross this dependency-free structural boundary.
2771
+ */
2772
+ list: () => Promise<ReadonlyArray<NotifySubscriptionDevice & {
2773
+ keys?: unknown;
2774
+ token?: unknown;
2775
+ }>>;
2776
+ }
2209
2777
  interface WorkerOptions {
2210
2778
  /**
2211
2779
  * An additional, async authorization gate for the `/_lunora/admin/*` plane
@@ -2265,6 +2833,17 @@ interface WorkerOptions {
2265
2833
  * every `/auth/*` endpoint responds `AUTH_NOT_CONFIGURED`.
2266
2834
  */
2267
2835
  authAdmin?: AuthAdmin;
2836
+ /**
2837
+ * The auth/security audit read plane backing the studio's "Security / audit"
2838
+ * page (the `__lunora_admin__:getAuthAuditLog` admin RPC). The audit trail
2839
+ * lives in the auth D1 database (via `@lunora/auth`'s `SqlExecutor`), not in a
2840
+ * shard's DO SQLite, so — unlike the other `__lunora_admin__:*` ops — the RPC
2841
+ * is served here at the worker, admin-gated, through this reader. Wire it with
2842
+ * `@lunora/auth`'s `createAuthAuditReader(d1Executor(env.DB))`. Omit it and the
2843
+ * RPC responds `AUTH_AUDIT_NOT_CONFIGURED`; a caller without a valid admin
2844
+ * bearer always gets `ADMIN_FORBIDDEN` first (default-closed).
2845
+ */
2846
+ authAuditReader?: AuthAuditReader;
2268
2847
  /**
2269
2848
  * Base path the auth routes are mounted under (default `/api/auth`). Used
2270
2849
  * to classify which inbound paths are auth ATTEMPTS for the app-level
@@ -2377,11 +2956,27 @@ interface WorkerOptions {
2377
2956
  d1?: unknown;
2378
2957
  /** Default shard key used when an envelope omits one. */
2379
2958
  defaultShardKey?: string;
2959
+ /**
2960
+ * Durable per-shard cursor store for the continuous CDC export tap (plan 170),
2961
+ * mirroring the CDC-in `__lunora_source_cursor` watermark. Build a KV-backed
2962
+ * one with `createKvCursorStore(env.CDC_CURSORS)`. Required (alongside
2963
+ * {@link WorkerOptions.exportSinks}) for the `POST /_lunora/admin/export-tap/run`
2964
+ * drain route; absent → the route reports `EXPORT_TAP_NOT_CONFIGURED`.
2965
+ */
2966
+ exportCursorStore?: ExportCursorStore;
2380
2967
  /**
2381
2968
  * Stream `.global()` rows for the admin export endpoint. When omitted,
2382
2969
  * the export endpoint covers only shard-local tables.
2383
2970
  */
2384
2971
  exportGlobals?: GlobalExportFunction;
2972
+ /**
2973
+ * Named continuous-export sinks (plan 170) the CDC tap drains the op-log change
2974
+ * feed to. Build with `webhookSink({...})`, `r2Sink({...})`, or a custom
2975
+ * `defineExportSink({...})`. Paired with {@link WorkerOptions.exportCursorStore}
2976
+ * to enable the `POST /_lunora/admin/export-tap/run` drain route (at-least-once,
2977
+ * ordered per shard, resumable). Absent / empty → the route reports not-configured.
2978
+ */
2979
+ exportSinks?: Record<string, ExportSink>;
2385
2980
  /**
2386
2981
  * The generated `LUNORA_FUNCTIONS` map (from `_generated/functions.ts`). When
2387
2982
  * set, the worker exposes the admin-gated `GET /_lunora/admin/functions`
@@ -2398,6 +2993,17 @@ interface WorkerOptions {
2398
2993
  * respond `GLOBALS_NOT_CONFIGURED`.
2399
2994
  */
2400
2995
  globalIntrospector?: GlobalIntrospector;
2996
+ /**
2997
+ * Health / readiness probe configuration (plan 177). When present (or left as
2998
+ * the default — probes are always registered), the worker serves
2999
+ * `GET /_lunora/health` (aggregate; `503` when a critical dependency is down)
3000
+ * and `GET /_lunora/health/ready` (readiness gate). The runtime auto-registers
3001
+ * probes for the shard Durable Object (reachability, critical), any D1 binding
3002
+ * (`SELECT 1`, critical), and R2 / queue / Hyperdrive bindings (presence,
3003
+ * non-critical); `probes` adds bespoke checks. The body never leaks secrets —
3004
+ * see {@link HealthOptions}.
3005
+ */
3006
+ health?: HealthOptions;
2401
3007
  /**
2402
3008
  * Router for HTTP actions (`httpRouter()` from `@lunora/server`, a hono app).
2403
3009
  * Consulted for requests that miss the explicit {@link WorkerOptions.routes}
@@ -2462,6 +3068,17 @@ interface WorkerOptions {
2462
3068
  * overrides). Absent → the Archive feed reports "not configured".
2463
3069
  */
2464
3070
  logArchive?: LogArchiveConfig;
3071
+ /**
3072
+ * The `@lunora/notify` device-subscription store, bound from the request
3073
+ * `env` by codegen from the app's `lunora/notify.ts` `defineNotify({ store })`.
3074
+ * Backs the gated `__lunora_admin__:listPushSubscriptions` admin RPC (the
3075
+ * Studio Notifications page): the worker reads registered devices — endpoint /
3076
+ * kind / last-send status / delivery errors — through the SAME store the
3077
+ * handlers register into. Delivery secrets (Web Push keys, FCM token) are
3078
+ * stripped before the devices leave the worker. Absent (no store configured)
3079
+ * ⇒ the RPC returns an empty device list rather than erroring.
3080
+ */
3081
+ notifySubscriptionStore?: NotifySubscriptionStoreLike;
2465
3082
  /**
2466
3083
  * Optional telemetry sink. When supplied, the worker emits one
2467
3084
  * `onRpc` event per dispatched RPC (single-shard forward or fan-out)
@@ -2553,6 +3170,16 @@ interface WorkerOptions {
2553
3170
  * bucket rows; when omitted, every row routes to the default shard.
2554
3171
  */
2555
3172
  resolveTableSharding?: AdminTableResolver;
3173
+ /**
3174
+ * Optional per-request rate-limit gate for the opt-in public REST surface
3175
+ * (plan 167). Invoked with the inbound request + the target `functionPath`
3176
+ * BEFORE the procedure is dispatched; return a `429` `Response` to reject
3177
+ * (returned verbatim, `Retry-After` included) or `undefined` to allow. Build it
3178
+ * over `@lunora/ratelimit` in the worker entry — the runtime stays free of a
3179
+ * hard `@lunora/ratelimit` dependency. Only consulted for REST calls; typed RPC
3180
+ * is unaffected.
3181
+ */
3182
+ restRateLimit?: RestRateLimit;
2556
3183
  /**
2557
3184
  * Map of routes for custom HTTP handlers (auth callbacks etc.). Keys can
2558
3185
  * be either `"METHOD path"` (e.g. `"GET /healthz"`) or just `"path"`
@@ -2560,6 +3187,25 @@ interface WorkerOptions {
2560
3187
  * first.
2561
3188
  */
2562
3189
  routes?: Record<string, Route>;
3190
+ /**
3191
+ * Trace-sampling policy for the observability pipeline, mirroring Cloudflare
3192
+ * Workers' `head_sampling_rate`. Governs only trace spans (the per-dispatch
3193
+ * SERVER span and the `ctx.trace` INTERNAL spans beneath it) — never metrics
3194
+ * or `ctx.log` lines.
3195
+ *
3196
+ * The decision is deterministic per trace: a stable value derived from the
3197
+ * `traceId` is compared to `headRate`, so the same trace is kept or dropped
3198
+ * as a whole on the worker and on every shard/container it fans out to (no
3199
+ * half traces). The head decision is propagated to shards via the
3200
+ * `traceparent` sampled flag, so they drop the matching `ctx.trace` spans
3201
+ * coherently.
3202
+ *
3203
+ * With `alwaysSampleErrors` (default `true`), a trace that produced an error
3204
+ * span is kept whole regardless of the head decision — the tail bias, so
3205
+ * failures are never sampled away even at an aggressive `headRate`. Omit the
3206
+ * option (or leave `headRate` at its default `1`) to keep every trace.
3207
+ */
3208
+ sampling?: TraceSamplingConfig;
2563
3209
  /**
2564
3210
  * Namespace binding for the `SchedulerDO` (typically `env.SCHEDULER`). When
2565
3211
  * set, the worker exposes the admin-gated `/_lunora/admin/scheduled`
@@ -3225,4 +3871,4 @@ declare const otlpSink: (options: OtlpSinkOptions) => ObservabilitySink;
3225
3871
  */
3226
3872
  declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
3227
3873
  declare const VERSION: string;
3228
- export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_LOG_COLUMNS, DEFAULT_LOG_LIMIT, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, LOG_ARCHIVE_NOT_CONFIGURED, LOG_ARCHIVE_PATH, type ListAuthUsersOptions, type LogArchiveConfig, type LogEvent, type LogFields, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MetricEvent, type MetricKind, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type PipelineLike, type PipelineLogColumnMap, type PipelineLogCursor, type PipelineLogField, type PipelineLogPage, type PipelineLogQuery, type PipelineLogReader, type PipelineLogReaderOptions, type PipelineLogRow, type PipelineLogSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type SpanEvent, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createPipelineLogReader, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, pipelineLogSink, resolveLogArchiveFromEnv, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
3874
+ export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_LOG_COLUMNS, DEFAULT_LOG_LIMIT, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportBatch, type ExportChange, type ExportCursorStore, type ExportFanOutRequest, type ExportFanOutResult, type ExportSink, type ExportTapFailure, type ExportTapResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, HEALTH_PATH, HEALTH_READY_PATH, type HealthAuthPosture, type HealthBody, type HealthCheckReport, type HealthProbe, type HealthProbeKind, type HealthProbeResult, type HealthRouteDeps, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, LOG_ARCHIVE_NOT_CONFIGURED, LOG_ARCHIVE_PATH, type ListAuthUsersOptions, type LogArchiveConfig, type LogEvent, type LogFields, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MetricEvent, type MetricKind, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type NotifySubscriptionDevice, type NotifySubscriptionStoreLike, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type PipelineLike, type PipelineLogColumnMap, type PipelineLogCursor, type PipelineLogField, type PipelineLogPage, type PipelineLogQuery, type PipelineLogReader, type PipelineLogReaderOptions, type PipelineLogRow, type PipelineLogSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type RateLimiterLike, type ResolvedSecurity, type ResolvedShard, type RestInvoke, type RestRateLimit, type RestRegistryEntry, type RestRegistryLike, type RestRouteDeps, type Route, type RpcContext, type RpcEnvelope, type RunExportTapOptions, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type SpanEvent, type StorageListFunction as StorageListFn, type StorageObject, type TraceSamplingConfig, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, argsFromQuery, buildHealthRoutes, buildRestRoutes, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createKvCursorStore, createLunoraHandler, createMemoryCursorStore, createPipelineLogReader, createQueryCoordinator, createRestRateLimit, createStaticShardRegistry, createWorker, d1Probe, decorateResponse, defineExportSink, defineRpcEnvelope, durableObjectProbe, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, pipelineLogSink, presenceProbe, r2Sink, readShardKey, resolveLogArchiveFromEnv, resolveLunoraOptions, resolveSecurity, resolveShard, restSurfaceFromRegistry, routeIdentityResolvers, runExportTap, sanitizeChange, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookExportSink, webhookSink, withFrameworkWorker };