@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 +1647 -1001
- package/dist/index.d.ts +1647 -1001
- package/dist/index.mjs +6 -3
- package/dist/packem_shared/HEALTH_PATH-e5J_NHBx.mjs +150 -0
- package/dist/packem_shared/{analyticsEngineSink-Bn7p0URe.mjs → analyticsEngineSink-DgL64WVC.mjs} +8 -2
- package/dist/packem_shared/argsFromQuery-BqPiQTPc.mjs +114 -0
- package/dist/packem_shared/{composeWorker-DAwO9LLs.mjs → composeWorker-UwHY2r8O.mjs} +282 -69
- package/dist/packem_shared/createKvCursorStore-g8aA6B4L.mjs +165 -0
- package/dist/packem_shared/emitLogEvent-BlGMnKZK.mjs +1 -0
- package/dist/packem_shared/method-guard-Qzw99aCj.mjs +3 -0
- package/dist/packem_shared/observability--NOFYBFc.mjs +47 -0
- package/dist/packem_shared/{otlp-DOLuy1Aj.mjs → otlp-DKZJCkdD.mjs} +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/emitLogEvent-pEdtqAK8.mjs +0 -20
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
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* The
|
|
451
|
-
*
|
|
452
|
-
*
|
|
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 open — Cloudflare 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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
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
|
-
|
|
548
|
+
getByName?: (name: string) => {
|
|
549
|
+
fetch: (request: Request) => Promise<Response>;
|
|
550
|
+
};
|
|
551
|
+
idFromName: (name: string) => unknown;
|
|
467
552
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
559
|
+
jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
|
|
560
|
+
}
|
|
561
|
+
interface ResolvedShard {
|
|
562
|
+
fetch: (request: Request) => Promise<Response>;
|
|
475
563
|
}
|
|
476
564
|
/**
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
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
|
-
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
*
|
|
499
|
-
*
|
|
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
|
|
589
|
+
declare const createStaticShardRegistry: (table_to_keys: Readonly<Record<string, ReadonlyArray<string>>>) => ShardRegistry;
|
|
508
590
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
type IdentityValidation = {
|
|
520
|
-
ok: true;
|
|
615
|
+
type MergeStrategy = {
|
|
616
|
+
kind: "concat";
|
|
521
617
|
} | {
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
|
|
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
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
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
|
|
576
|
-
/**
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
+
* `<prefix>/<shardKey>/<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:<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
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
/**
|
|
952
|
-
|
|
953
|
-
/**
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
|
|
957
|
-
/**
|
|
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
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
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
|
|
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
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
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
|
-
|
|
985
|
-
|
|
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
|
-
/**
|
|
992
|
-
|
|
993
|
-
/**
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1071
|
-
*
|
|
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
|
-
|
|
1075
|
-
fetch: (request: Request) => Promise<Response>;
|
|
1076
|
-
};
|
|
1077
|
-
idFromName: (name: string) => unknown;
|
|
1677
|
+
columnMap?: PipelineLogColumnMap;
|
|
1078
1678
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
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
|
-
|
|
1683
|
+
namespace?: string;
|
|
1684
|
+
/** The Iceberg table name the Pipeline writes log records to (e.g. `"logs"`). */
|
|
1685
|
+
table: string;
|
|
1086
1686
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
*
|
|
1092
|
-
* unchanged when no jurisdiction is configured.
|
|
1697
|
+
* Build a durable-log reader over one R2 Data Catalog (Iceberg) table.
|
|
1093
1698
|
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
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
|
-
*
|
|
1113
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
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
|
-
* `
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1164
|
-
*
|
|
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
|
-
* - `
|
|
1169
|
-
* - `
|
|
1170
|
-
*
|
|
1171
|
-
* `
|
|
1172
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1221
|
-
*
|
|
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
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
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
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
-
|
|
1882
|
+
status: number;
|
|
1323
1883
|
};
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
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
|
-
* `
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
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
|
-
|
|
1958
|
+
fuseCloudflareTraces?: boolean;
|
|
1959
|
+
/** Invoked once per `ctx.log.*` call from a function handler. */
|
|
1960
|
+
onLog?: (event: LogEvent, context?: ObservabilitySinkContext) => void;
|
|
1424
1961
|
/**
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
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
|
-
|
|
1444
|
-
readonly registry: ShardRegistry;
|
|
1973
|
+
onSpan?: (event: SpanEvent, context?: ObservabilitySinkContext) => void;
|
|
1445
1974
|
}
|
|
1446
1975
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
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
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
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
|
|
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
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
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
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
-
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
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
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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/<namespace>/<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 };
|