@voltro/runtime 0.33.0 → 0.34.0

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +1801 -0
  2. package/dist/index.d.ts +1159 -66
  3. package/dist/index.js +2530 -1508
  4. package/package.json +7 -7
package/dist/index.d.ts CHANGED
@@ -1,9 +1,14 @@
1
+ import { AnyApprovalPolicy } from '@voltro/protocol';
1
2
  import { AnyCheckSpec } from '@voltro/protocol';
3
+ import { AnyGuardSpec } from '@voltro/protocol';
4
+ import { AuthMiddleware } from '@voltro/protocol';
2
5
  import { AuthStrategy } from '@voltro/protocol';
3
6
  import { CaughtUpVerdict } from '@voltro/database';
4
7
  import { ChangeEvent } from '@voltro/database';
5
8
  import { clearRetentions } from '@voltro/database';
9
+ import { ConnectionCredential } from '@voltro/protocol';
6
10
  import { ConnectionInfo } from '@voltro/protocol';
11
+ import { ConnectionInfoMiddleware } from '@voltro/protocol';
7
12
  import { ConnectionKind } from '@voltro/protocol';
8
13
  import { ConnectionState } from '@voltro/protocol';
9
14
  import { Context } from 'effect';
@@ -38,10 +43,12 @@ import { inspectWorkflow } from '@voltro/workflow';
38
43
  import { Kv } from '@voltro/kv';
39
44
  import { KvStoreShape } from '@voltro/kv';
40
45
  import { Layer } from 'effect';
46
+ import { listRetentionConflicts } from '@voltro/database';
41
47
  import { listRetentions } from '@voltro/database';
42
48
  import { Metric } from 'effect';
43
49
  import { MetricBoundaries } from 'effect';
44
50
  import { MetricReader } from '@opentelemetry/sdk-metrics';
51
+ import { PendingApproval } from '@voltro/protocol';
45
52
  import { PluginHttpRoute } from '@voltro/protocol';
46
53
  import { PluginRefOrphanPolicy } from '@voltro/database';
47
54
  import { Predicate } from '@voltro/database';
@@ -50,6 +57,8 @@ import { QueryDescriptor } from '@voltro/database';
50
57
  import { Redis } from 'ioredis';
51
58
  import { registerRetention } from '@voltro/database';
52
59
  import { RestRouteDescriptor } from '@voltro/protocol/rest';
60
+ import { RetentionConflict } from '@voltro/database';
61
+ import { RetentionSource } from '@voltro/database';
53
62
  import { RetentionSpec } from '@voltro/database';
54
63
  import { retentionTtlMsFromEnv } from '@voltro/database';
55
64
  import { Row } from '@voltro/database';
@@ -66,6 +75,7 @@ import { spawn } from 'node:child_process';
66
75
  import { SqlClient } from '@effect/sql';
67
76
  import { Stream } from 'effect';
68
77
  import { Subject } from '@voltro/protocol';
78
+ import { SubjectResolution } from '@voltro/protocol';
69
79
  import { SubjectService } from '@voltro/protocol';
70
80
  import { SubscriptionEvent } from '@voltro/protocol';
71
81
  import { sweepRetention } from '@voltro/database';
@@ -527,26 +537,24 @@ export declare type AnalyticsMetric = 'count' | 'unique' | {
527
537
  readonly avg: string;
528
538
  };
529
539
 
530
- /**
531
- * A single reactive-table change to mirror into the warehouse. Mirrors
532
- * the framework's internal `ChangeEvent` but flattened to the shape a
533
- * sink needs: the table name, the operation, and the row. For `insert`
534
- * / `update` the post-image is in `row`; for `delete` the pre-image is
535
- * in `row` (so the sink knows which primary key to remove).
536
- */
537
- export declare interface AnalyticsMirrorChange {
538
- readonly table: string;
539
- readonly op: 'insert' | 'update' | 'delete';
540
- /** Post-image for insert/update; pre-image for delete. */
541
- readonly row: Readonly<Record<string, unknown>>;
542
- }
543
-
544
540
  export declare interface AnalyticsMirrorHandle {
545
541
  /** Tables actually being mirrored (the sink's `mirror.tables`). Empty
546
542
  * when the sink declares no mirror. */
547
543
  readonly tables: ReadonlyArray<string>;
548
- /** Detach the `onChange` subscription. */
544
+ /** Detach the `onChange` subscription and stop the repair loop. */
549
545
  readonly detach: () => void;
546
+ /** Settle every queued + in-flight write. Resolves when the mirror has
547
+ * nothing left to apply (or has queued it for repair). */
548
+ readonly flush: () => Promise<void>;
549
+ /**
550
+ * Re-drive every key awaiting repair: re-read its CURRENT row from the
551
+ * store and re-apply it (upsert when present, remove when gone) under
552
+ * a fresh version. Runs on the `repairIntervalMs` timer; exposed so an
553
+ * operator or a shutdown hook can force a pass. Resolves with the
554
+ * number of keys that landed.
555
+ */
556
+ readonly repair: () => Promise<number>;
557
+ readonly stats: () => AnalyticsMirrorStats;
550
558
  }
551
559
 
552
560
  /**
@@ -561,6 +569,13 @@ export declare interface AnalyticsMirrorHandle {
561
569
  * MUST be idempotent — the same change may be re-delivered after a
562
570
  * reconnect, and the upsert is keyed on `primaryKey` so re-applying a
563
571
  * row is a no-op-equivalent overwrite.
572
+ *
573
+ * Idempotent is not enough on its own: re-delivery can also arrive OUT
574
+ * OF ORDER, so both methods additionally receive a {@link MirrorVersion}
575
+ * and MUST ignore a write whose version is not greater than the version
576
+ * already stored for that key (ClickHouse gets this from
577
+ * `ReplacingMergeTree(version)`; DuckDB from an `ON CONFLICT … WHERE
578
+ * excluded.version > version` guard).
564
579
  */
565
580
  export declare interface AnalyticsMirrorImpl {
566
581
  /** Reactive tables this sink mirrors. The framework only forwards
@@ -569,10 +584,81 @@ export declare interface AnalyticsMirrorImpl {
569
584
  /** Primary-key column the sink upserts/deletes by. Defaults to `'id'`
570
585
  * when omitted. */
571
586
  readonly primaryKey?: string;
572
- /** Apply an insert/update. Idempotent upsert keyed on `primaryKey`. */
573
- readonly upsert: (table: string, row: Readonly<Record<string, unknown>>) => Effect.Effect<void, AnalyticsFailure>;
574
- /** Apply a delete. Idempotent — removing an absent key is a no-op. */
575
- readonly remove: (table: string, primaryKeyValue: unknown) => Effect.Effect<void, AnalyticsFailure>;
587
+ /** Apply an insert/update. Idempotent upsert keyed on `primaryKey`,
588
+ * version-guarded by `write.version`. */
589
+ readonly upsert: (write: AnalyticsMirrorUpsert) => Effect.Effect<void, AnalyticsFailure>;
590
+ /** Apply a delete. Idempotent removing an absent key is a no-op —
591
+ * and version-guarded by `write.version`. */
592
+ readonly remove: (write: AnalyticsMirrorRemove) => Effect.Effect<void, AnalyticsFailure>;
593
+ }
594
+
595
+ /** One delete to apply to the mirror. */
596
+ export declare interface AnalyticsMirrorRemove {
597
+ readonly table: string;
598
+ /** Primary-key value of the removed row. */
599
+ readonly primaryKeyValue: unknown;
600
+ /** See {@link MirrorVersion}. A delete is ordered against upserts of
601
+ * the same key by exactly this number. */
602
+ readonly version: MirrorVersion;
603
+ }
604
+
605
+ /** Counters a caller (boot log, inspect endpoint, test) can read. */
606
+ export declare interface AnalyticsMirrorStats {
607
+ /** Writes the sink accepted. */
608
+ readonly forwarded: number;
609
+ /** Keys with a queued-but-not-yet-applied write. */
610
+ readonly pending: number;
611
+ /** Keys awaiting repair (retries exhausted). */
612
+ readonly awaitingRepair: number;
613
+ /** Changes provably lost — repair-queue overflow. */
614
+ readonly dropped: number;
615
+ }
616
+
617
+ /**
618
+ * Every number the mirror picks on the app's behalf, with its default.
619
+ * Each is also readable from the environment so an operator can tune a
620
+ * running deployment without a code change.
621
+ */
622
+ export declare interface AnalyticsMirrorTunables {
623
+ /**
624
+ * Total attempts for one mirror write (1 = no retry).
625
+ * Default `5`. Env: `VOLTRO_ANALYTICS_MIRROR_RETRY_ATTEMPTS`.
626
+ */
627
+ readonly retryAttempts: number;
628
+ /**
629
+ * First backoff delay; doubles per attempt.
630
+ * Default `100`ms. Env: `VOLTRO_ANALYTICS_MIRROR_RETRY_BASE_MS`.
631
+ */
632
+ readonly retryBaseDelayMs: number;
633
+ /**
634
+ * Ceiling for the doubling backoff.
635
+ * Default `30_000`ms. Env: `VOLTRO_ANALYTICS_MIRROR_RETRY_MAX_MS`.
636
+ */
637
+ readonly retryMaxDelayMs: number;
638
+ /**
639
+ * How often the repair loop re-drives changes whose retries ran out.
640
+ * `0` disables the loop (the queue then only drains via an explicit
641
+ * `handle.repair()`).
642
+ * Default `60_000`ms. Env: `VOLTRO_ANALYTICS_MIRROR_REPAIR_INTERVAL_MS`.
643
+ */
644
+ readonly repairIntervalMs: number;
645
+ /**
646
+ * Maximum keys held for repair. Past this, the oldest queued key is
647
+ * dropped, counted and logged at error level — a bounded queue that
648
+ * says so beats an unbounded one that ends the process.
649
+ * Default `10_000`. Env: `VOLTRO_ANALYTICS_MIRROR_REPAIR_QUEUE_LIMIT`.
650
+ */
651
+ readonly repairQueueLimit: number;
652
+ }
653
+
654
+ /** One insert/update to apply to the mirror. */
655
+ export declare interface AnalyticsMirrorUpsert {
656
+ readonly table: string;
657
+ /** Post-image of the changed row. */
658
+ readonly row: Readonly<Record<string, unknown>>;
659
+ /** See {@link MirrorVersion} — persist it, compare against it, never
660
+ * replace it with a local clock reading. */
661
+ readonly version: MirrorVersion;
576
662
  }
577
663
 
578
664
  /**
@@ -680,6 +766,9 @@ declare type AnyRow = Record<string, unknown>;
680
766
 
681
767
  declare type AnyRow_2 = Record<string, unknown>;
682
768
 
769
+ /** Locked-down policy for an API response. Nothing loads, nothing frames it. */
770
+ export declare const API_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
771
+
683
772
  export declare const API_KEYS_TABLE = "_voltro_api_keys";
684
773
 
685
774
  /** Admin-gated management routes (issue / list / revoke) for the built-in keys.
@@ -924,6 +1013,21 @@ export declare interface AppContext {
924
1013
  readonly loadMany: DataLoader['loadMany'];
925
1014
  }
926
1015
 
1016
+ /**
1017
+ * The connection's headers as the auth chain should see them: the transport's
1018
+ * own headers with the bound credential patched over them.
1019
+ *
1020
+ * Returns the input UNCHANGED when nothing is bound — the overwhelmingly common
1021
+ * case is a connection that never re-authenticated, and it must not pay for a
1022
+ * clone.
1023
+ *
1024
+ * Header names are lowercased on the patched keys because the rest of the chain
1025
+ * reads them lowercase (`headers['cookie']`, `headers['authorization']`), and a
1026
+ * patch written as `Authorization` that lands beside an existing `authorization`
1027
+ * is a credential that is present and invisible.
1028
+ */
1029
+ export declare const applyConnectionCredential: (clientId: number, headers: Record<string, string | undefined>) => Record<string, string | undefined>;
1030
+
927
1031
  /**
928
1032
  * Pure helper backing `ctx.store.applyDefined`. Picks the listed keys from
929
1033
  * `input` whose value is not `undefined`, so a PATCH sets exactly the fields
@@ -987,6 +1091,72 @@ export declare const applyUndoInvocation: (input: {
987
1091
  readonly ok: boolean;
988
1092
  }>;
989
1093
 
1094
+ /** What the dispatch spine hands the gate. */
1095
+ export declare interface ApprovalAdmissionInput {
1096
+ readonly procedure: string;
1097
+ readonly kind: 'mutation' | 'action';
1098
+ readonly policy: AnyApprovalPolicy;
1099
+ readonly subject: Subject | null;
1100
+ readonly input: unknown;
1101
+ readonly traceId?: string | undefined;
1102
+ /** Distinguishes a deliberate repeat of an identical request. */
1103
+ readonly nonce?: string | undefined;
1104
+ }
1105
+
1106
+ /** The slice of AppContext the approval executors need. */
1107
+ export declare interface ApprovalExecutorCtx {
1108
+ readonly store: DataStore;
1109
+ readonly request?: {
1110
+ readonly subject?: Subject | null;
1111
+ } | undefined;
1112
+ }
1113
+
1114
+ export declare interface ApprovalGateApi {
1115
+ /**
1116
+ * ADMIT or REFUSE one approval-requiring call.
1117
+ *
1118
+ * Resolves when the call may proceed (an approval was found and CONSUMED, so
1119
+ * it cannot be spent twice). REJECTS with one of the typed approval errors
1120
+ * otherwise — the caller re-throws it and the rpc layer matches it against the
1121
+ * descriptor's `error:` union.
1122
+ */
1123
+ readonly admit: (input: ApprovalAdmissionInput) => Promise<void>;
1124
+ }
1125
+
1126
+ /**
1127
+ * The content-addressed identity of an intent.
1128
+ *
1129
+ * LENGTH-PREFIXED, not separator-joined, for the reason `promptVersionDigest`
1130
+ * records: no printable separator is injective over arbitrary prose, and the
1131
+ * repo's rule forbids the NUL that would be. A procedure named `a` with
1132
+ * requester `b:c` must not digest the same as `a:b` with requester `c`.
1133
+ */
1134
+ export declare const approvalIntentKey: (input: {
1135
+ readonly procedure: string;
1136
+ readonly requestedBy: string | null;
1137
+ readonly tenantId: string | null;
1138
+ readonly payload: string;
1139
+ /** Distinguishes two DELIBERATELY identical requests. The caller states it;
1140
+ * the framework never invents one (that would defeat the whole idea). */
1141
+ readonly nonce?: string | undefined;
1142
+ }) => string;
1143
+
1144
+ /**
1145
+ * Mint a nonce for a DELIBERATE repeat of an identical request.
1146
+ *
1147
+ * Exported for callers that genuinely need two identical intents in flight (the
1148
+ * same refund, twice, on purpose). Never called by the framework: inventing one
1149
+ * per attempt is exactly the too-fine identity the header rejects.
1150
+ */
1151
+ export declare const approvalNonce: () => string;
1152
+
1153
+ export declare const APPROVALS_TABLE = "_voltro_approvals";
1154
+
1155
+ /** `__voltro.approvals.pending` — the calling subject's approval work. */
1156
+ export declare const approvalsPendingExecutor: (input: {
1157
+ readonly limit?: number;
1158
+ }, ctx: ApprovalExecutorCtx) => Promise<ReadonlyArray<PendingApproval>>;
1159
+
990
1160
  /**
991
1161
  * Controls the lifecycle of the app process the orchestrator fronts.
992
1162
  * Both `start` and `stop` are idempotent so the orchestrator can call
@@ -1021,6 +1191,14 @@ export declare const assertCan: (subject: RebacSubject, action: string, resource
1021
1191
  */
1022
1192
  export declare const assertConnectionCipherConfigured: (definitions: ReadonlyArray<ConnectionDefinition>) => void;
1023
1193
 
1194
+ /**
1195
+ * Boot gate: every mounted incoming-webhook route must carry a verification
1196
+ * declaration. Throws `UnverifiedWebhookRoute` for the first that does not.
1197
+ */
1198
+ export declare const assertWebhookRoutesDeclareVerification: (routes: ReadonlyMap<string, {
1199
+ readonly handle: unknown;
1200
+ }> | undefined) => void;
1201
+
1024
1202
  /**
1025
1203
  * Assign a subject to a variant — STABLE (same subject ⇒ same variant, always)
1026
1204
  * and BALANCED (proportions converge to the weights over many subjects). The
@@ -1092,12 +1270,10 @@ export declare interface AsyncKv {
1092
1270
  * Wire the sink's CDC-mirror to the store's change stream. No-op (and
1093
1271
  * returns an empty handle) when the sink declares no `mirror` — so the
1094
1272
  * caller can always call this unconditionally.
1095
- *
1096
- * Returns the set of mirrored tables (for boot logging) + a detach.
1097
1273
  */
1098
1274
  export declare const attachAnalyticsMirror: (options: AttachAnalyticsMirrorOptions) => AnalyticsMirrorHandle;
1099
1275
 
1100
- export declare interface AttachAnalyticsMirrorOptions {
1276
+ export declare interface AttachAnalyticsMirrorOptions extends Partial<AnalyticsMirrorTunables> {
1101
1277
  readonly store: DataStore;
1102
1278
  readonly sink: AnalyticsSinkImpl;
1103
1279
  readonly log: SyncLogger;
@@ -1193,6 +1369,10 @@ export declare interface AuditableQuery {
1193
1369
  readonly source?: string | ReadonlyArray<string> | undefined;
1194
1370
  }
1195
1371
 
1372
+ export declare interface AuthMiddlewareLayerOptions {
1373
+ readonly resolveSubject: ResolveSubjectFn;
1374
+ }
1375
+
1196
1376
  /**
1197
1377
  * An Effect that is ALSO awaitable.
1198
1378
  *
@@ -1241,7 +1421,18 @@ export declare interface BeginOAuthResult {
1241
1421
  readonly state: string;
1242
1422
  }
1243
1423
 
1244
- export declare const bindConnectionSubject: (clientId: number, subject: Subject) => void;
1424
+ /**
1425
+ * Present a new credential on an ALREADY-OPEN connection.
1426
+ *
1427
+ * Called from a handler that has just authenticated the caller — `auth.signin`,
1428
+ * a tenant switch — with the credential it minted (typically the session cookie
1429
+ * it is about to `Set-Cookie`). Every subsequent call on this connection is
1430
+ * resolved by the FULL auth chain against these headers, so the caller gets
1431
+ * exactly what a reconnect would give them and nothing more.
1432
+ *
1433
+ * Idempotent; a second bind replaces the first.
1434
+ */
1435
+ export declare const bindConnectionCredential: (clientId: number, credential: ConnectionCredential) => void;
1245
1436
 
1246
1437
  /**
1247
1438
  * Bind one client's subscription to a declared event.
@@ -1522,6 +1713,14 @@ export declare interface CandidateShape {
1522
1713
  }
1523
1714
 
1524
1715
  /* Excluded from this release type: canonicalize */
1716
+
1717
+ /**
1718
+ * Canonical JSON — object keys sorted recursively, so two structurally equal
1719
+ * inputs that differ only in key order digest identically. A retry from a
1720
+ * different client build must not mint a second approval.
1721
+ */
1722
+ export declare const canonicalJson: (value: unknown) => string;
1723
+
1525
1724
  export { CaughtUpVerdict }
1526
1725
 
1527
1726
  /** A CDC change event, structurally — what the reactive engine already emits
@@ -1632,6 +1831,17 @@ export declare const CLAIM_RETENTION_BUCKETS = 64;
1632
1831
  */
1633
1832
  export declare const claimGraceMs: (bucketWidthMs: number | undefined) => number;
1634
1833
 
1834
+ /**
1835
+ * Decide whether a state-changing request may proceed.
1836
+ *
1837
+ * `headers` keys must already be lowercased (the rpc server lowercases once per
1838
+ * request and passes the same bag to every guard).
1839
+ */
1840
+ export declare const classifyRequestOrigin: (input: {
1841
+ readonly headers: Readonly<Record<string, string>>;
1842
+ readonly config: OriginGuardConfig;
1843
+ }) => OriginDecision;
1844
+
1635
1845
  /**
1636
1846
  * Decide whether a candidate matches a maintainable shape. Conservative by
1637
1847
  * design — anything not provably maintainable is rejected (→ full recompute),
@@ -1639,6 +1849,9 @@ export declare const claimGraceMs: (bucketWidthMs: number | undefined) => number
1639
1849
  */
1640
1850
  export declare const classifyShape: (c: CandidateShape) => ShapeClassification;
1641
1851
 
1852
+ /** Test seam. */
1853
+ export declare const clearApprovalGate: () => void;
1854
+
1642
1855
  /** Test seam — the in-flight map is module state; a test that asserts
1643
1856
  * single-flight must be able to start from empty. */
1644
1857
  export declare const clearConnectionRefreshFlights: () => void;
@@ -1651,6 +1864,14 @@ export { clearRetentions }
1651
1864
  /** Clear the process-wide handle (test teardown). */
1652
1865
  export declare const clearSystemStoreHandle: () => void;
1653
1866
 
1867
+ export declare interface ClientAddressInput {
1868
+ /** `socket.remoteAddress` — the only unforgeable value in the request. */
1869
+ readonly socketAddress: string | undefined;
1870
+ /** Raw `x-forwarded-for` header, if present. */
1871
+ readonly xForwardedFor: string | undefined;
1872
+ readonly config: TrustedProxyConfig;
1873
+ }
1874
+
1654
1875
  /**
1655
1876
  * Collect the declared rules from the discovered tables.
1656
1877
  *
@@ -1867,13 +2088,6 @@ export declare interface ConnectionsFacadeDeps {
1867
2088
  /** Test-only — number of streams currently registered for a client. */
1868
2089
  export declare const _connectionStreamCount: (clientId: number) => number;
1869
2090
 
1870
- /** Snapshot for diagnostics / dashboard. */
1871
- export declare const connectionSubjectsSnapshot: () => ReadonlyArray<{
1872
- clientId: number;
1873
- subjectType: string;
1874
- tenantId: string | null;
1875
- }>;
1876
-
1877
2091
  /** A resolved token set. `expiresAt` is an absolute epoch-ms deadline. */
1878
2092
  export declare interface ConnectionTokens {
1879
2093
  readonly accessToken: string;
@@ -2049,6 +2263,8 @@ export declare interface CostBudgetDefinition {
2049
2263
  /** Resolved tumbling-window length in ms, or `null` for a cumulative budget. */
2050
2264
  readonly windowMs: number | null;
2051
2265
  readonly severity: CostBudgetSeverity;
2266
+ /** Resolved breach behaviour — `'observe'` unless the app opted in. */
2267
+ readonly onExceeded: CostBudgetOnExceeded;
2052
2268
  readonly description?: string;
2053
2269
  }
2054
2270
 
@@ -2073,14 +2289,50 @@ export declare interface CostBudgetDefinitionInput {
2073
2289
  readonly window?: string;
2074
2290
  /** Alerting priority carried on the breach signal. Default `'warn'`. */
2075
2291
  readonly severity?: CostBudgetSeverity;
2292
+ /**
2293
+ * What a breach DOES. Default `'observe'` — signal only, the behaviour this
2294
+ * module shipped with.
2295
+ *
2296
+ * `'suspend'` opts the budget into stopping the next spend: a durable run
2297
+ * reaching a pre-spend gate for this budget parks on a
2298
+ * `_voltro_budget_holds` row and resumes when the budget recovers. Opt-in,
2299
+ * because parking a run is a change in what the app DOES and a default that
2300
+ * silently stalls work is worse than one that silently allows it.
2301
+ */
2302
+ readonly onExceeded?: CostBudgetOnExceeded;
2076
2303
  /** Human-facing note surfaced in devtools / the breach message. */
2077
2304
  readonly description?: string;
2078
2305
  }
2079
2306
 
2307
+ /**
2308
+ * What a breach DOES, as opposed to how loudly it is reported.
2309
+ *
2310
+ * This file used to state flatly that "the framework never BLOCKS compute on a
2311
+ * budget … an observability-grade signal over work that already happened", and
2312
+ * that was accurate: a cost event arrives after its compute, so a budget here
2313
+ * genuinely cannot refuse the work that produced it.
2314
+ *
2315
+ * What it can do — and now does — is stop the NEXT one. `'suspend'` marks the
2316
+ * budget as one that parks durable runs: a run reaching a pre-spend gate for
2317
+ * this budget SUSPENDS on a `_voltro_budget_holds` row (`@voltro/workflow`'s
2318
+ * `suspendForBudget`) instead of proceeding. The suspension is therefore still
2319
+ * not retroactive, and saying so is the honest version: it bounds future spend,
2320
+ * and the work already metered is already paid for.
2321
+ *
2322
+ * This flag is a DECLARATION, not a wiring. Nothing here subscribes the
2323
+ * accountant's `recovered` signal to `releaseBudgetHolds` — whether a recovered
2324
+ * compute budget should wake a run held on a DIFFERENT (e.g. AI-USD) budget is
2325
+ * an app decision, and a hold re-checks on its own durable clock regardless, so
2326
+ * no run is stranded by the absence. An app that wants the immediate release
2327
+ * subscribes: `registry.subscribe((s) => s.kind === 'recovered' && …)`.
2328
+ *
2329
+ * `'observe'` (default) keeps the previous behaviour exactly — signal only.
2330
+ */
2331
+ export declare type CostBudgetOnExceeded = 'observe' | 'suspend';
2332
+
2080
2333
  /** How loud a budget breach is. A classification carried on the signal for
2081
- * alerting priority — the framework never BLOCKS compute on a budget here
2082
- * (that is a caller's choice, the way `requireAiBudget` fails a call); a cost
2083
- * budget is an observability-grade signal over work that already happened. */
2334
+ * alerting priority — orthogonal to {@link CostBudgetOnExceeded}, which decides
2335
+ * whether anything STOPS. */
2084
2336
  export declare type CostBudgetSeverity = 'info' | 'warn' | 'critical';
2085
2337
 
2086
2338
  /** Emitted when a budget crosses a threshold for a tenant. `warn` / `exceeded`
@@ -2107,6 +2359,9 @@ export declare interface CostBudgetState {
2107
2359
  readonly warnThreshold: number;
2108
2360
  /** The unit this budget meters, or `null` for a total-across-units budget. */
2109
2361
  readonly unit: string | null;
2362
+ /** What a breach of this budget DOES — carried on the state so an operator
2363
+ * view can tell an observed breach from one that is holding runs. */
2364
+ readonly onExceeded: CostBudgetOnExceeded;
2110
2365
  /** When the current window opened (windowed budgets), else `null`. */
2111
2366
  readonly windowStartedAt: number | null;
2112
2367
  /** When this budget last entered its current `status`. */
@@ -2222,10 +2477,14 @@ export declare const crud: {
2222
2477
  readonly id: string;
2223
2478
  }, ctx: AppContext) => Promise<Row | null>;
2224
2479
  /** Insert the input as a new row (id / tenant / audit auto-stamped). The echoed
2225
- * row is redacted. Guard the DESCRIPTOR this does not gate. */
2480
+ * row is redacted, and an input that sets a `.serverOnly()` column is REFUSED
2481
+ * with `ServerOnlyColumnWrite`. Guard the DESCRIPTOR — this does not gate. */
2226
2482
  create: (table: string, options?: CrudWriteOptions) => (input: Row, ctx: AppContext) => Promise<Row>;
2227
2483
  /** Patch a row by id (`{ id, ...patch }`); returns the updated row or `null`.
2228
- * Redacted. Guard the DESCRIPTOR. */
2484
+ * Redacted, and a patch that sets a `.serverOnly()` column is REFUSED with
2485
+ * `ServerOnlyColumnWrite`. On a `tenant()` table the keyed write resolves the
2486
+ * row inside the caller's tenant (`TenantRowNotFound` otherwise — see
2487
+ * `storeMiddleware`). Guard the DESCRIPTOR. */
2229
2488
  update: (table: string, options?: CrudWriteOptions) => (input: {
2230
2489
  readonly id: string;
2231
2490
  } & Record<string, unknown>, ctx: AppContext) => Promise<Row | null>;
@@ -2362,6 +2621,9 @@ export declare interface CrudWriteOptions {
2362
2621
  readonly redact?: ReadonlyArray<string>;
2363
2622
  }
2364
2623
 
2624
+ /** Every raw read recorded in the current scope; empty outside one. */
2625
+ export declare const currentRawReads: () => ReadonlyArray<RawReadObservation>;
2626
+
2365
2627
  /**
2366
2628
  * The current request's loader, or `undefined` outside a request.
2367
2629
  *
@@ -2404,6 +2666,37 @@ export declare const dataStoreIdempotencyStore: (store: DataStore) => Idempotenc
2404
2666
  /** Build a durable `KvStoreShape` over a raw `DataStore`. */
2405
2667
  export declare const dataStoreKvStore: (store: DataStore) => KvStoreShape;
2406
2668
 
2669
+ /**
2670
+ * Approve or reject ONE intent, applying every refusal in a fixed order.
2671
+ *
2672
+ * The order is asserted in the tests and is not arbitrary: identity checks come
2673
+ * before authority checks so a requester with the approver scope is told they
2674
+ * cannot approve their OWN request (the accurate reason) rather than being
2675
+ * silently allowed through on a scope they do hold.
2676
+ */
2677
+ export declare const decideApproval: (store: DataStore, input: DecideApprovalInput) => Promise<{
2678
+ readonly approvalId: string;
2679
+ readonly status: "approved" | "rejected";
2680
+ }>;
2681
+
2682
+ export declare interface DecideApprovalInput {
2683
+ readonly approvalId: string;
2684
+ readonly decision: 'approve' | 'reject';
2685
+ readonly note?: string | undefined;
2686
+ readonly approver: Subject | null;
2687
+ readonly now?: () => number;
2688
+ }
2689
+
2690
+ /** `__voltro.approvals.decide` — approve or reject one pending intent. */
2691
+ export declare const decideApprovalInvocation: (input: {
2692
+ readonly approvalId: string;
2693
+ readonly decision: "approve" | "reject";
2694
+ readonly note?: string;
2695
+ }, ctx: ApprovalExecutorCtx) => Promise<{
2696
+ readonly approvalId: string;
2697
+ readonly status: "approved" | "rejected";
2698
+ }>;
2699
+
2407
2700
  /** Decrypt a value produced by `encryptField` / an `.encrypted()` column. A
2408
2701
  * value that is NOT ciphertext (`enc:v1:…`) is returned unchanged — so a
2409
2702
  * raw-SQL read path can be switched to encryption while pre-existing plaintext
@@ -2411,10 +2704,35 @@ export declare const dataStoreKvStore: (store: DataStore) => KvStoreShape;
2411
2704
  * cipher is registered (a genuine ciphertext with a wrong key throws GCM). */
2412
2705
  export declare const decryptField: (value: string) => string;
2413
2706
 
2707
+ export declare const DEFAULT_ANALYTICS_MIRROR_TUNABLES: AnalyticsMirrorTunables;
2708
+
2709
+ /** Default life of a pending intent when neither the descriptor nor the app
2710
+ * says otherwise. 24 h: long enough to survive a weekend handoff being missed
2711
+ * by a few hours, short enough that a forgotten queue empties itself. */
2712
+ export declare const DEFAULT_APPROVAL_EXPIRY_MS: number;
2713
+
2414
2714
  /** Assumed distance between buckets when a caller passes none — the cron
2415
2715
  * engine's finest useful cadence. */
2416
2716
  export declare const DEFAULT_CLAIM_BUCKET_MS = 60000;
2417
2717
 
2718
+ /**
2719
+ * How many subscribers one change event is delivered to CONCURRENTLY.
2720
+ *
2721
+ * 8 rather than 1 (fully serial, what this replaced) or unbounded. Serial makes
2722
+ * one slow guard's round-trip the prefix of every later subscriber's latency —
2723
+ * measured at 50 subscribers × a 5 ms guard, the last delivery landed 699 ms
2724
+ * after the write. Unbounded would open one DB round-trip per subscriber at the
2725
+ * same instant, which on a 10-connection pool is slower than serial AND starves
2726
+ * the request path that shares it. 8 sits under a default pool and still
2727
+ * collapses the serial chain by roughly its own factor.
2728
+ */
2729
+ export declare const DEFAULT_DELIVERY_CONCURRENCY = 8;
2730
+
2731
+ /** 180 days + subdomains. Deliberately NOT `preload`: preload is effectively
2732
+ * irreversible for a domain, so it must be a deployment decision, never a
2733
+ * framework default. */
2734
+ export declare const DEFAULT_HSTS = "max-age=15552000; includeSubDomains";
2735
+
2418
2736
  /** Ceiling the idle backoff climbs to. Deliberately short enough to be a
2419
2737
  * FLOOR under a missed wake rather than a substitute for one. */
2420
2738
  export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
@@ -2423,6 +2741,16 @@ export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
2423
2741
  * small enough to stop a pathological body being buffered into memory. */
2424
2742
  export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
2425
2743
 
2744
+ /**
2745
+ * How many raw-SQL reads one request records for the undeclared-`dependsOn`
2746
+ * diagnostic. A bound rather than none: the recording lives for as long as the
2747
+ * async context that opened it, and a long-lived non-request context (a
2748
+ * subscriber runner, a boot seed) would otherwise accumulate one entry per raw
2749
+ * read forever. Reads beyond the bound are dropped, not remembered — this is a
2750
+ * diagnostic, and a diagnostic that leaks memory is worse than a missing one.
2751
+ */
2752
+ export declare const DEFAULT_RAW_READ_TRACKING_LIMIT = 32;
2753
+
2426
2754
  export declare const DEFAULT_REFRESH_SKEW_MS = 60000;
2427
2755
 
2428
2756
  /**
@@ -2693,7 +3021,33 @@ export declare class Dispatcher {
2693
3021
  */
2694
3022
  private readonly computedSubs;
2695
3023
  private readonly computedByTable;
3024
+ /** Resolved delivery tunables. Read once here so `voltro dev` and
3025
+ * `voltro serve` cannot disagree about them — see `reactiveConfig.ts`. */
3026
+ private readonly reactive;
3027
+ /** Query labels already warned about for an undeclared raw read, so a hot
3028
+ * subscription logs once rather than once per subscriber. */
3029
+ private readonly rawReadWarned;
2696
3030
  constructor(deps: DispatcherDependencies);
3031
+ /**
3032
+ * REL-14 — a raw read that nothing can invalidate, said out loud.
3033
+ *
3034
+ * `store.raw(...)` is opaque to every mechanism that makes a query live: the
3035
+ * matcher reads a predicate, the dependency graph reads an eager spec, and a
3036
+ * raw fragment is a string neither can parse. `dependsOn` is how an author
3037
+ * tells us; omitting it produces a subscription that opens, delivers once and
3038
+ * then never updates. Nothing throws, nothing logs, and the bug looks like
3039
+ * ours.
3040
+ *
3041
+ * Wired HERE, in the dispatcher, and called from BOTH `subscribe` and
3042
+ * `subscribeComputed` — the one place every reactive subscription passes
3043
+ * through, on both boot paths. `voltro dev` and `voltro serve` build their own
3044
+ * rpc bridges and have drifted before; a check that lives in one of them is
3045
+ * not a check. There is no second place to put this, which is the point.
3046
+ *
3047
+ * Once per query label. A per-subscriber warning on a hot query is its own
3048
+ * kind of useless.
3049
+ */
3050
+ private warnUndeclaredRawReads;
2697
3051
  /**
2698
3052
  * Register a logical subscription. The caller's `emit` receives:
2699
3053
  * - exactly one `snapshot` event with the initial query result,
@@ -2817,6 +3171,13 @@ export declare interface DispatcherDependencies {
2817
3171
  * present → subscriptions whose descriptor opted in (a `cacheBinding`
2818
3172
  * is passed to `subscribe`) share + cache their initial snapshot. */
2819
3173
  readonly cache?: SnapshotCache;
3174
+ /**
3175
+ * Tunables of the delivery loop — see `reactiveConfig.ts`. Absent → the
3176
+ * documented defaults. Resolved ONCE, in the constructor, so the two boot
3177
+ * paths cannot each derive their own answer; an env override wins over
3178
+ * whatever is passed here.
3179
+ */
3180
+ readonly reactive?: ReactiveConfigInput;
2820
3181
  }
2821
3182
 
2822
3183
  /** Bookkeeping tenant key for the `_voltro_wakeups` rows that dormant schedules
@@ -2882,6 +3243,25 @@ export declare interface DrainResult {
2882
3243
  readonly failed: number;
2883
3244
  readonly dead: number;
2884
3245
  readonly skipped: number;
3246
+ /**
3247
+ * How many rows this pass had to look at. `0` means the queue is EMPTY, which
3248
+ * a caller cannot otherwise tell from "everything delivered" — both leave
3249
+ * `delivered` at whatever it was, and only one of them means the poll has
3250
+ * nothing left to do.
3251
+ */
3252
+ readonly scanned: number;
3253
+ /**
3254
+ * When the earliest NOT-yet-due pending row comes due, as an epoch ms.
3255
+ *
3256
+ * `undefined` means nothing in the read window is waiting. A LOWER bound (the
3257
+ * window is capped by `batchSize`), which is the safe direction: understating
3258
+ * it costs one early pass, overstating it delays a delivery.
3259
+ *
3260
+ * This is what lets the runner arm for a backoff instead of polling until it
3261
+ * expires — the one thing in this loop that genuinely needs a clock, since a
3262
+ * retry becoming due is not a write anybody can be notified about.
3263
+ */
3264
+ readonly nextAttemptAt?: number;
2885
3265
  }
2886
3266
 
2887
3267
  /**
@@ -2926,6 +3306,23 @@ export declare const enableGraphObservation: () => void;
2926
3306
  * columns use). Throws if no cipher is registered. */
2927
3307
  export declare const encryptField: (plaintext: string) => string;
2928
3308
 
3309
+ /**
3310
+ * The dispatch-spine entry point. A no-op for a descriptor with no policy; a
3311
+ * REFUSAL when a policy exists and no gate was installed.
3312
+ *
3313
+ * Fail-closed is the whole point: "the approvals store is not wired, so run the
3314
+ * refund" is precisely the hole the declaration exists to close, and it is the
3315
+ * shape a boot-path parity miss would produce.
3316
+ */
3317
+ export declare const enforceApproval: (input: {
3318
+ readonly procedure: string;
3319
+ readonly kind: "mutation" | "action";
3320
+ readonly policy: AnyApprovalPolicy | undefined;
3321
+ readonly subject: unknown;
3322
+ readonly input: unknown;
3323
+ readonly traceId?: string | undefined;
3324
+ }) => Promise<void>;
3325
+
2929
3326
  export declare interface EnqueueOptions {
2930
3327
  /** Drop this enqueue if an undelivered row already carries the same key. */
2931
3328
  readonly idempotencyKey?: string;
@@ -3577,8 +3974,12 @@ export declare interface ExperimentDefinition {
3577
3974
  readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3578
3975
  };
3579
3976
  /** Resolved to a function: reads the subject and normalises it to a non-empty
3580
- * string, or `null` when the row carries no subject. */
3581
- readonly subject: (row: Readonly<Record<string, unknown>>) => string | null;
3977
+ * string, or `null` when the row carries no subject. `null` for a
3978
+ * `variantFrom` experiment, which assigns nothing. */
3979
+ readonly subject: ((row: Readonly<Record<string, unknown>>) => string | null) | null;
3980
+ /** Resolved arm reader for a `variantFrom` experiment; `null` when the
3981
+ * experiment assigns its own arms. Exactly one of these two is non-null. */
3982
+ readonly variantFrom: ((row: Readonly<Record<string, unknown>>) => string | null) | null;
3582
3983
  readonly variants: ReadonlyArray<ExperimentVariant>;
3583
3984
  /** Resolved holdout fraction in [0, 1); `0` when none. */
3584
3985
  readonly holdout: number;
@@ -3599,8 +4000,33 @@ export declare interface ExperimentDefinitionInput {
3599
4000
  readonly table: string;
3600
4001
  readonly where?: (row: Readonly<Record<string, unknown>>) => boolean;
3601
4002
  };
3602
- /** The stable assignment key extractor. */
3603
- readonly subject: ExperimentSubject;
4003
+ /**
4004
+ * The stable assignment key extractor — the experiment ASSIGNS.
4005
+ *
4006
+ * Exactly one of `subject` / `variantFrom` is required. They are the two ways
4007
+ * an experiment can know which arm a row belongs to, and they are mutually
4008
+ * exclusive by construction: declaring both would mean two answers to one
4009
+ * question, which is the defect this exclusion exists to prevent.
4010
+ */
4011
+ readonly subject?: ExperimentSubject;
4012
+ /**
4013
+ * READ the arm off the row instead of assigning it — for measuring an
4014
+ * assignment something ELSE already made and persisted.
4015
+ *
4016
+ * This is what makes `plugin-flags` + `defineExperiment` one product rather
4017
+ * than two that happen to sit in the same app. A flag serves a variant by its
4018
+ * own deterministic bucketing (FNV-1a over the flag key); an experiment in
4019
+ * `subject` mode buckets independently (FNV-1a over the EXPERIMENT name). Both
4020
+ * are stable, both are uniform, and they DISAGREE — so measuring a flag's
4021
+ * rollout with a `subject`-mode experiment reports the uplift of an arm
4022
+ * assignment nobody was ever served. Reading the arm the flag actually served
4023
+ * (write `flagVariant(ctx, f)` onto the row, point `variantFrom` at that
4024
+ * column) makes the served arm and the measured arm the same fact.
4025
+ *
4026
+ * Persisting the arm is also the only version that survives a weight change:
4027
+ * a re-hash at read time silently re-labels every historical row.
4028
+ */
4029
+ readonly variantFrom?: ExperimentVariantSource;
3604
4030
  /** The arms. At least two (a control + one treatment). */
3605
4031
  readonly variants: ReadonlyArray<ExperimentVariantSpec>;
3606
4032
  /** Fraction (0..1) of subjects held out ENTIRELY — assigned to the reserved
@@ -3630,9 +4056,23 @@ export declare class ExperimentEvaluator {
3630
4056
  private readonly now;
3631
4057
  private seeded;
3632
4058
  private lastUpdatedAt;
4059
+ /** The declared arm names — the admissible set in `variantFrom` mode. */
4060
+ private readonly declaredVariants;
3633
4061
  constructor(def: ExperimentDefinition, deps: ExperimentEvaluatorDeps);
3634
- /** The variant an in-scope row is assigned to, or `null` when the row is out
3635
- * of the `where` population or carries no subject. */
4062
+ /**
4063
+ * The variant an in-scope row belongs to, or `null` when the row is out of the
4064
+ * `where` population, carries no subject, or (in `variantFrom` mode) carries
4065
+ * an arm this experiment does not declare.
4066
+ *
4067
+ * The two modes meet here and only here — the maintainers behind this are
4068
+ * identical either way, which is the point: an experiment that MEASURES an
4069
+ * arm somebody else assigned rides the same per-write IVM aggregate as one
4070
+ * that assigns its own.
4071
+ *
4072
+ * An unrecognised arm name is EXCLUDED, never folded into a neighbour. A typo
4073
+ * that silently landed in the baseline would corrupt the one number every
4074
+ * other row's lift is divided by.
4075
+ */
3636
4076
  private variantOf;
3637
4077
  private inScope;
3638
4078
  /** Seed both maintainers from the current base rows. Never emits (there is no
@@ -3778,6 +4218,17 @@ export declare interface ExperimentVariantResult {
3778
4218
  readonly diff: number | null;
3779
4219
  }
3780
4220
 
4221
+ /**
4222
+ * Where the ARM comes from when the experiment did not assign it.
4223
+ *
4224
+ * A column NAME whose value IS the variant, or a function returning it.
4225
+ * `null` / `undefined` / a name that is not one of the declared variants ⇒ the
4226
+ * row is UNASSIGNED and excluded from every arm — an unrecognised arm name is
4227
+ * never folded into a neighbour, because a typo that silently lands in
4228
+ * `control` corrupts the baseline the whole result is measured against.
4229
+ */
4230
+ declare type ExperimentVariantSource = string | ((row: Readonly<Record<string, unknown>>) => string | null | undefined);
4231
+
3781
4232
  /** A named arm of the experiment. `weight` skews the split (default 1 = equal);
3782
4233
  * a subject is assigned in proportion to its weight over the total. */
3783
4234
  export declare interface ExperimentVariantSpec {
@@ -3980,12 +4431,15 @@ export declare const gauge: (name: string, description?: string) => Metric.Metri
3980
4431
  /** Generate a fresh token `<prefix><40-char-base64url>`. */
3981
4432
  export declare const generateApiKeyToken: (prefix?: string) => string;
3982
4433
 
4434
+ export declare const getApprovalGate: () => ApprovalGateApi | undefined;
4435
+
4436
+ /** The credential bound to a connection, if any. Exposed for diagnostics and
4437
+ * for `applyConnectionCredential`; the auth chain goes through the latter. */
4438
+ export declare const getConnectionCredential: (clientId: number) => ConnectionCredential | undefined;
4439
+
3983
4440
  /** The registered resolver, if the app declared any connections. */
3984
4441
  export declare const getConnectionResolver: () => ConnectionResolver | undefined;
3985
4442
 
3986
- /** Look up the override for a connection. Returns `undefined` if no override is set. */
3987
- export declare const getConnectionSubject: (clientId: number) => Subject | undefined;
3988
-
3989
4443
  export declare const getCredentialExpiry: (clientId: number) => number | undefined;
3990
4444
 
3991
4445
  /** The registered field cipher, if any. The store middleware reads this. */
@@ -4058,6 +4512,10 @@ export declare const historyProvenance: (history: ReadonlyArray<RowHistoryEntry>
4058
4512
  * this name when `holdout > 0`. */
4059
4513
  export declare const HOLDOUT_VARIANT = "holdout";
4060
4514
 
4515
+ /** Safe-for-any-page policy: closes clickjacking + base-tag injection + plugin
4516
+ * embedding without constraining what the page itself loads. */
4517
+ export declare const HTML_CSP = "frame-ancestors 'none'; base-uri 'none'; object-src 'none'";
4518
+
4061
4519
  /** A secrets backend over a plain JSON HTTP API — no SDK. Wraps fetch + cache.
4062
4520
  * Vault (`/v1/secret/data/...`), Doppler, and the cloud control-plane all fit. */
4063
4521
  export declare const httpSecretsBackend: (opts: HttpSecretsOptions) => SecretsBackend;
@@ -4255,6 +4713,8 @@ export declare interface InspectStream {
4255
4713
 
4256
4714
  export { inspectWorkflow }
4257
4715
 
4716
+ export declare const installApprovalGate: (gate: ApprovalGateApi) => void;
4717
+
4258
4718
  /**
4259
4719
  * Install the resolver that answers `guards: [{ action, resourceType }]`.
4260
4720
  *
@@ -4341,6 +4801,9 @@ export declare type InverseOp = {
4341
4801
  * inverse needs (e.g. a hard-delete with no captured `prev`). */
4342
4802
  export declare const invertChange: (c: ForwardChange) => InverseOp | null;
4343
4803
 
4804
+ /** Parse an already-normalized address into its bytes (4 for v4, 16 for v6). */
4805
+ export declare const ipToBytes: (ip: string) => Uint8Array | undefined;
4806
+
4344
4807
  export declare const isAggregateDefinition: (value: unknown) => value is AggregateDefinition;
4345
4808
 
4346
4809
  export declare const isCostBudgetDefinition: (value: unknown) => value is CostBudgetDefinition;
@@ -4367,6 +4830,34 @@ export declare const isInfraError: (e: unknown) => boolean;
4367
4830
 
4368
4831
  export declare const isObservingGraph: () => boolean;
4369
4832
 
4833
+ /**
4834
+ * Shadow an INHERITED scope with a fresh one; do nothing when there is none.
4835
+ *
4836
+ * Two decisions here, and both were measured rather than reasoned.
4837
+ *
4838
+ * WHY IT ISOLATES INSTEAD OF REUSING. Nothing stops a non-request caller from
4839
+ * building a handler-facing store on a long-lived context — a boot seed,
4840
+ * `runAsSystem`, a subscriber runner. A scope opened there is found by every
4841
+ * request that runs underneath it (async-local lookup walks outward), so reusing
4842
+ * it would collect every request's raw reads into one bag and let a warning name
4843
+ * a query that never issued the read. A diagnostic that names the WRONG query is
4844
+ * worse than a missing one.
4845
+ *
4846
+ * WHY IT DOES NOTHING WHEN THERE IS NO SCOPE, rather than opening one eagerly.
4847
+ * `enterWith` is not free after it returns: each live frame taxes every
4848
+ * subsequent `await` in that context. Measured — a bare `await Promise.resolve()`
4849
+ * costs 0.165 µs with no frame, 0.256 µs with one, and 0.524 µs with six; they
4850
+ * STACK. Opening a scope for every request would put that on the request path in
4851
+ * exchange for nothing on the overwhelming majority of requests, which never
4852
+ * touch raw SQL at all. `recordRawRead` opens one on demand instead, so the cost
4853
+ * lands only on a request that already paid for a database round-trip.
4854
+ *
4855
+ * What this costs is a raw read inside `store.transactional(...)`: the
4856
+ * transaction's re-wrap isolates, so the read lands in the transaction's scope
4857
+ * and the subscribe never sees it. A missed warning, not a wrong one.
4858
+ */
4859
+ export declare const isolateRawReadScope: () => void;
4860
+
4370
4861
  /**
4371
4862
  * A subject that AUTHENTICATED but resolved with NO active org — a `user` (not
4372
4863
  * `anonymous`/`system`) carrying the empty-string tenant sentinel. Every
@@ -4415,6 +4906,10 @@ export declare const isScheduleDefinition: (v: unknown) => v is BrandedScheduleD
4415
4906
  */
4416
4907
  export declare const isSoftDelete: (change: ChangeEvent) => boolean;
4417
4908
 
4909
+ /** The methods that can change state. A GET cannot be a CSRF write, and the WS
4910
+ * upgrade is a GET — so the upgrade is guarded by PATH, not by method. */
4911
+ export declare const isStateChangingMethod: (method: string) => boolean;
4912
+
4418
4913
  /** A freshly issued key — `token` is shown ONCE and never stored in clear. */
4419
4914
  export declare interface IssuedApiKey {
4420
4915
  readonly id: string;
@@ -4442,6 +4937,9 @@ export declare interface IssueInput {
4442
4937
  readonly metadata?: Readonly<Record<string, unknown>> | null;
4443
4938
  }
4444
4939
 
4940
+ /** Is `address` one of the peers we are willing to take a forwarded chain from? */
4941
+ export declare const isTrustedProxy: (address: string | undefined, config: TrustedProxyConfig) => boolean;
4942
+
4445
4943
  export declare const isWorkflowEventTriggerDefinition: (value: unknown) => value is WorkflowEventTriggerDefinition;
4446
4944
 
4447
4945
  /** A maintainable aggregate op. (countDistinct / window funcs are NOT here —
@@ -4551,6 +5049,8 @@ export declare const listConnectionStates: (store: VaultStore, registry: Connect
4551
5049
 
4552
5050
  export declare const listResourcePolicies: () => ReadonlyArray<ResourcePolicy>;
4553
5051
 
5052
+ export { listRetentionConflicts }
5053
+
4554
5054
  export { listRetentions }
4555
5055
 
4556
5056
  export declare interface LoaderDeps {
@@ -4704,10 +5204,44 @@ export declare const makeApiKeyUsageFlusher: (store: ApiKeyStore) => (id: string
4704
5204
  * the test-context factory so `ctx.access` can't drift between them. */
4705
5205
  export declare const makeAppAccess: (subject: Subject) => AppAccess;
4706
5206
 
5207
+ /**
5208
+ * Build the gate over a live store. ONE construction, called by the shared cli
5209
+ * builder both boot paths use — the dev/serve parity rule applied to a control
5210
+ * whose absence is a silent open door rather than a crash.
5211
+ */
5212
+ export declare const makeApprovalGate: (options: MakeApprovalGateOptions) => ApprovalGateApi;
5213
+
5214
+ export declare interface MakeApprovalGateOptions {
5215
+ readonly store: DataStore;
5216
+ /** App-level default expiry (the `approvals.expiresIn` tunable), already
5217
+ * resolved to ms by the caller. Falls back to 24 h. */
5218
+ readonly defaultExpiryMs?: number;
5219
+ readonly now?: () => number;
5220
+ }
5221
+
4707
5222
  /** Build the `ctx.kv` facade. `KV_BACKEND` picks the backend; unknown values
4708
5223
  * fall back to `database`. */
4709
5224
  export declare const makeAsyncKv: ({ store, env }: KvFacadeOptions) => Promise<KvFacade>;
4710
5225
 
5226
+ /**
5227
+ * The `AuthMiddleware` + `ConnectionInfoMiddleware` pair every rpc surface
5228
+ * needs, wired to one app's strategy chain.
5229
+ *
5230
+ * Per call, in this order:
5231
+ *
5232
+ * 1. patch the connection's soft-reauth credential over the transport headers
5233
+ * (a no-op for a connection that never re-authenticated);
5234
+ * 2. run the FULL chain on the result — strategies, session revocation,
5235
+ * `resolveScopes`, the scope cache;
5236
+ * 3. record what the strategy verified about the credential's lifetime, so
5237
+ * `ConnectionInfo.credentialExpiresAt` reports the token's own `exp` rather
5238
+ * than a cookie derivation that only ever saw one auth shape.
5239
+ *
5240
+ * Step 1 is the only thing a re-authenticated connection gets. It changes WHICH
5241
+ * credential is presented and nothing about how it is judged.
5242
+ */
5243
+ export declare const makeAuthMiddlewareLayers: (options: AuthMiddlewareLayerOptions) => Layer.Layer<AuthMiddleware | ConnectionInfoMiddleware>;
5244
+
4711
5245
  /**
4712
5246
  * Detect a serial jump per origin, and report it as a proven count.
4713
5247
  *
@@ -4838,6 +5372,35 @@ export declare const makeMutationRunner: (deps: MutationRunnerDeps) => (mutation
4838
5372
  */
4839
5373
  export declare const makeOneShotQueryRunner: <D>(deps: OneShotQueryRunnerDeps<D>) => (query: MutationLike, input: unknown, requestContext: ServeRequestContext) => Promise<unknown>;
4840
5374
 
5375
+ /**
5376
+ * Build the per-boot "is this path outside the origin check" predicate.
5377
+ *
5378
+ * Everything not named here is guarded. The three inputs, and why each is a
5379
+ * different KIND of exemption:
5380
+ *
5381
+ * - `INSPECT_PREFIX` — structural, above.
5382
+ * - `webhookPaths` — the `*.webhook.tsx` mounts. Their caller is a third
5383
+ * party and `assertWebhookRoutesDeclareVerification` refuses to boot one
5384
+ * that has not declared how it authenticates that caller, so the signature
5385
+ * — not the cookie — is the authority. (Most send no `Origin` and would
5386
+ * pass anyway; naming them means a provider that DOES send one is not a
5387
+ * mystery 403 in production.)
5388
+ * - `exemptRoutePaths` — DECLARED, per route, via
5389
+ * `PluginHttpRoute.originGuard: 'exempt'`. The claim and the test to apply
5390
+ * are documented on that field.
5391
+ *
5392
+ * A path shared by SEVERAL routes is exempt only when EVERY route on it
5393
+ * declares the exemption — the pre-routing check cannot know which route in the
5394
+ * group will end up owning the method, so the strictest member decides.
5395
+ */
5396
+ export declare const makeOriginGuardExemption: (input: {
5397
+ readonly webhookPaths?: Iterable<string> | undefined;
5398
+ readonly httpRoutes?: Iterable<{
5399
+ readonly path: string;
5400
+ readonly originGuard?: "exempt" | undefined;
5401
+ }> | undefined;
5402
+ }) => ((path: string) => boolean);
5403
+
4841
5404
  export declare const makeOutboxFacade: (deps: OutboxFacadeDeps) => OutboxFacade;
4842
5405
 
4843
5406
  export declare const makePostCommitWorkflowFacade: (base: WorkflowsAppContext, afterCommit: (work: () => Promise<unknown>) => void) => WorkflowsAppContext;
@@ -5137,6 +5700,17 @@ declare interface MemoryRywOptions {
5137
5700
  */
5138
5701
  export declare const memoryRywPositionStore: (options?: MemoryRywOptions) => RywPositionStore;
5139
5702
 
5703
+ /**
5704
+ * Merge a name→value map into a `Cookie` header string, replacing same-named
5705
+ * pairs and preserving every other cookie the connection carries.
5706
+ *
5707
+ * Replacing the whole header would be simpler and wrong: a rebinder knows its
5708
+ * own cookie and nothing about the CSRF token, the locale, or the second app on
5709
+ * the same host. Values are percent-encoded on the way in because `readCookie`
5710
+ * percent-decodes on the way out, and the round trip has to be lossless.
5711
+ */
5712
+ export declare const mergeCookieHeader: (existing: string | undefined, patch: Readonly<Record<string, string>>) => string;
5713
+
5140
5714
  export { MetricBoundaries }
5141
5715
 
5142
5716
  export declare interface MetricBucketPoint {
@@ -5174,6 +5748,23 @@ export declare type MetricSampleType = 'counter' | 'gauge' | 'histogram' | 'summ
5174
5748
  * into the OTel MeterProvider when a `metricReader` is configured. */
5175
5749
  export declare type MetricsMode = 'off' | 'console' | 'otlp' | 'memory';
5176
5750
 
5751
+ /**
5752
+ * Ordering token for one mirrored write.
5753
+ *
5754
+ * **The sink MUST persist this and MUST NOT invent its own.** It is
5755
+ * assigned by the framework at the moment the change leaves the store's
5756
+ * change channel — i.e. in COMMIT order — so a write with a lower
5757
+ * version is, by construction, an older image of that row. Stamping a
5758
+ * version at sink-call time instead (a `Date.now()` inside `upsert`)
5759
+ * makes a late-arriving STALE image outrank the fresh one and win
5760
+ * permanently; that is the bug this type exists to make unrepresentable.
5761
+ *
5762
+ * Monotonically increasing within a process, and wall-clock-derived
5763
+ * (microseconds since the epoch, bumped by one on collision) so it also
5764
+ * increases across a restart.
5765
+ */
5766
+ export declare type MirrorVersion = number;
5767
+
5177
5768
  export declare interface MutationLifecycle {
5178
5769
  readonly afterCommit: (work: () => Promise<unknown>) => void;
5179
5770
  }
@@ -5183,6 +5774,9 @@ export declare interface MutationLike {
5183
5774
  readonly name: string;
5184
5775
  readonly source?: string | ReadonlyArray<string> | undefined;
5185
5776
  readonly guards?: ReadonlyArray<AnyCheckSpec> | undefined;
5777
+ /** The second-human control (mutation/action only). Enforced by
5778
+ * `enforceApprovalGate`, immediately after `enforceGuards`. */
5779
+ readonly requiresApproval?: AnyApprovalPolicy | undefined;
5186
5780
  readonly target?: {
5187
5781
  readonly table: string;
5188
5782
  } | ReadonlyArray<{
@@ -5278,6 +5872,9 @@ export declare const nextWakeup: (store: DataStore) => Promise<Wakeup | null>;
5278
5872
  * registered, or for a system subject. */
5279
5873
  export declare const NO_ROW_FILTER: RowFilterScope;
5280
5874
 
5875
+ /** Nothing trusted: the socket address is the client address. The default. */
5876
+ export declare const NO_TRUSTED_PROXIES: TrustedProxyConfig;
5877
+
5281
5878
  /**
5282
5879
  * The real single-node supervisor: `spawn`s the app command and considers
5283
5880
  * it healthy once `healthUrl` returns 2xx. Stopping sends `stopSignal`
@@ -5346,6 +5943,15 @@ export declare const noopAnalyticsSpec: AnalyticsSinkSpec;
5346
5943
  * misses, writes are dropped. Mirrors the cache's `noopCache`. */
5347
5944
  export declare const noopKv: AsyncKv;
5348
5945
 
5946
+ /** Strip the decorations a real-world address string arrives with:
5947
+ * `[::1]:443` → `::1`, `::ffff:10.0.0.1` → `10.0.0.1`, `fe80::1%eth0` → `fe80::1`. */
5948
+ export declare const normalizeIp: (raw: string) => string | undefined;
5949
+
5950
+ /** `HTTPS://Example.com:443/x` → `https://example.com`. Returns the lowercased
5951
+ * input when it is not a parseable absolute URL, so an unparseable Origin can
5952
+ * never accidentally compare equal to a real one. */
5953
+ export declare const normalizeOrigin: (raw: string) => string;
5954
+
5349
5955
  /**
5350
5956
  * `.one()` matched a number of rows other than exactly one.
5351
5957
  *
@@ -5442,14 +6048,6 @@ export declare interface ObservedProcedure {
5442
6048
  */
5443
6049
  export declare const observeStore: <S extends object>(store: S) => S;
5444
6050
 
5445
- /**
5446
- * Listen for re-bind events. Returns an unsubscribe function. Used by
5447
- * the dispatcher to re-scope active subscriptions when a connection's
5448
- * subject changes (e.g. anonymous → authenticated user on a different
5449
- * tenant — subscriptions filtered by tenantId need re-evaluation).
5450
- */
5451
- export declare const onBindConnectionSubject: (listener: (clientId: number, subject: Subject) => void) => () => void;
5452
-
5453
6051
  export declare interface OneShotQueryRunnerDeps<D> extends QueryProducerDeps<D> {
5454
6052
  /**
5455
6053
  * Execute a FINALIZED (tenant-scoped) descriptor → rows. On the WS path the
@@ -5499,6 +6097,26 @@ export declare interface OrchestratorTickDeps {
5499
6097
  readonly log?: WakeOrchestratorLogger;
5500
6098
  }
5501
6099
 
6100
+ export declare type OriginDecision = {
6101
+ readonly allowed: true;
6102
+ readonly reason: 'guard-off' | 'same-origin' | 'loopback' | 'allowlisted' | 'no-browser-origin';
6103
+ } | {
6104
+ readonly allowed: false;
6105
+ readonly reason: 'cross-origin' | 'opaque-origin' | 'cross-site-metadata';
6106
+ /** What the request claimed, for the log line. Never echoed to the client. */
6107
+ readonly origin: string;
6108
+ };
6109
+
6110
+ export declare interface OriginGuardConfig {
6111
+ readonly mode: OriginGuardMode;
6112
+ /** Extra origins to accept, normalised to `scheme://host[:port]`. Needed for
6113
+ * a split deployment where the web app and the api sit on different hosts. */
6114
+ readonly allowedOrigins: ReadonlyArray<string>;
6115
+ }
6116
+
6117
+ /** `off` disables the check entirely; `same-origin` (default) enforces it. */
6118
+ export declare type OriginGuardMode = 'off' | 'same-origin';
6119
+
5502
6120
  /**
5503
6121
  * Map an `import('@effect/opentelemetry')` rejection to what the reader needs.
5504
6122
  *
@@ -5648,6 +6266,13 @@ declare interface P2COptions {
5648
6266
  readonly random?: () => number;
5649
6267
  }
5650
6268
 
6269
+ /**
6270
+ * Parse an interval string (`'30m'`, `'4h'`, `'7d'`) to ms, or `null`.
6271
+ * A local copy for the same reason `finops.ts` keeps one — this module must not
6272
+ * grow a dependency for five lines of regex.
6273
+ */
6274
+ export declare const parseApprovalInterval: (spec: string) => number | null;
6275
+
5651
6276
  /** Parse a `traceparent` header. Returns null when malformed or when
5652
6277
  * the all-zero invalid ids are present (per the spec those are
5653
6278
  * treated as "no parent"). */
@@ -5683,6 +6308,19 @@ export declare const payloadPropertyNames: (schema: Schema.Schema.Any) => {
5683
6308
  readonly optional: ReadonlyArray<string>;
5684
6309
  };
5685
6310
 
6311
+ /**
6312
+ * The calling subject's approval work: intents they may DECIDE, plus intents
6313
+ * they REQUESTED that are still live.
6314
+ *
6315
+ * Subject-scoped in here rather than by a descriptor guard — see the descriptor
6316
+ * comment for why. An anonymous caller gets an empty list, which is the correct
6317
+ * answer and not a refusal: they have no work.
6318
+ */
6319
+ export declare const pendingApprovalsFor: (store: DataStore, subject: Subject | null, options?: {
6320
+ readonly limit?: number | undefined;
6321
+ readonly now?: () => number;
6322
+ }) => Promise<ReadonlyArray<PendingApproval>>;
6323
+
5686
6324
  /** One resolved rule: "when <targetTable> loses a row, do <policy> to <table.column>". */
5687
6325
  export declare interface PluginRefRule {
5688
6326
  readonly table: string;
@@ -5893,6 +6531,27 @@ export declare interface QuerySubscriberDeps<D> extends QueryProducerDeps<D> {
5893
6531
  readonly subscribeComputed: (computed: ComputedQuery, emit: (event: unknown) => void, context: ServeRequestContext) => Promise<() => void>;
5894
6532
  }
5895
6533
 
6534
+ /** One `store.raw(...)` call, as the reactive engine needs to see it. */
6535
+ export declare interface RawReadObservation {
6536
+ /** The fragment's STATIC text, joined with `?` where a value was bound.
6537
+ * Never the values — they are user data and this string reaches a log. */
6538
+ readonly sql: string;
6539
+ /** The tables the caller declared, from `opts.dependsOn` or the fragment.
6540
+ * Empty is the whole point of this module. */
6541
+ readonly dependsOn: ReadonlyArray<string>;
6542
+ }
6543
+
6544
+ /**
6545
+ * The static text of a captured fragment, with every bound value rendered as
6546
+ * `?`.
6547
+ *
6548
+ * The values are deliberately NOT included. This string is written to a log and
6549
+ * a raw fragment's values are the caller's data — a customer email, a token
6550
+ * being looked up. The static text is what identifies the query to its author,
6551
+ * which is all the warning needs.
6552
+ */
6553
+ export declare const rawSqlPreview: (strings: ReadonlyArray<string>) => string;
6554
+
5896
6555
  /** What a reaction does when it fires — run an agent or start a workflow. Both
5897
6556
  * identified by name; the serve layer resolves + runs them as `agentActor`. */
5898
6557
  export declare type ReactionAct = {
@@ -6026,6 +6685,32 @@ export declare interface ReactionRunDeps {
6026
6685
  readonly now?: () => number;
6027
6686
  }
6028
6687
 
6688
+ /** The reactive tunables a caller may declare. Every field optional; every
6689
+ * default above. */
6690
+ export declare interface ReactiveConfigInput {
6691
+ /**
6692
+ * How many subscribers a single change event is delivered to at once.
6693
+ *
6694
+ * Raise it when deliveries are dominated by I/O the framework performs on the
6695
+ * subscriber's behalf — a `guards:` resource resolver or a row-filter loader
6696
+ * that hits the database once per subscriber. Lower it (to `1`, fully serial)
6697
+ * only if you have measured your connection pool being starved by reactive
6698
+ * traffic; the delivery loop shares the pool with the request path.
6699
+ */
6700
+ readonly deliveryConcurrency?: number;
6701
+ /**
6702
+ * How many raw-SQL reads one request records for the `dependsOn` diagnostic.
6703
+ * Only worth raising if a handler issues many raw reads and you want the
6704
+ * warning to name a later one.
6705
+ */
6706
+ readonly rawReadTrackingLimit?: number;
6707
+ }
6708
+
6709
+ export declare interface ReactiveEnv {
6710
+ readonly VOLTRO_REACTIVE_DELIVERY_CONCURRENCY?: string;
6711
+ readonly VOLTRO_RAW_READ_TRACKING_LIMIT?: string;
6712
+ }
6713
+
6029
6714
  /** A reactive query returns a builder descriptor instead of a value; the store
6030
6715
  * runs it and streams the rows. Only queries may do this. */
6031
6716
  export declare type ReactiveReturn<D extends ExecutorDescriptor> = D extends {
@@ -6136,6 +6821,21 @@ export declare const recordEventPublished: (event: string) => void;
6136
6821
  /** Subscriber attached (+1) or detached (-1). */
6137
6822
  export declare const recordEventSubscribers: (event: string, delta: number) => void;
6138
6823
 
6824
+ /**
6825
+ * Record a raw read, opening a scope for the rest of this execution context if
6826
+ * none is open yet.
6827
+ *
6828
+ * Opening it HERE is what keeps the request path free of an async-local frame it
6829
+ * would almost never use — see `isolateRawReadScope`. The scope then lives for
6830
+ * the remainder of this context, which is the request, which is exactly long
6831
+ * enough for the subscribe that follows to read it.
6832
+ *
6833
+ * Bounded by `rawReadTrackingLimit`, and DEDUPED by sql text: a handler that
6834
+ * raw-reads in a loop must not turn a diagnostic into a leak, and the tenth
6835
+ * identical read tells the reader nothing the first did not.
6836
+ */
6837
+ export declare const recordRawRead: (observation: RawReadObservation) => void;
6838
+
6139
6839
  /**
6140
6840
  * Record one framework sample into the Effect metric registry. Synchronous —
6141
6841
  * metric updates are pure, so `runSync` is cheap and safe to call from the
@@ -6366,6 +7066,11 @@ export declare interface RegistryTableLike {
6366
7066
  readonly validatePatchSchema?: Schema.Schema.Any;
6367
7067
  }
6368
7068
 
7069
+ /** Rebuild checkable guards from the row. The stored resource id becomes a
7070
+ * constant extractor, so the decision is scoped to the SAME resource the
7071
+ * request was. */
7072
+ export declare const rehydrateGuards: (serialized: ReadonlyArray<SerializedGuard>) => ReadonlyArray<AnyGuardSpec>;
7073
+
6369
7074
  /** A relationship tuple — `subjectId` has `relation` on `<resourceType>:<resourceId>`.
6370
7075
  * In practice these are rows in a relation table; the engine takes them as data. */
6371
7076
  export declare interface RelationTuple {
@@ -6548,8 +7253,8 @@ export declare interface ResendOptions {
6548
7253
  /** Exported for tests — the warn-once set is process-global by design. */
6549
7254
  export declare const resetComputedQueryCacheWarnings: () => void;
6550
7255
 
6551
- /** Test/dev-only — clear ALL overrides. Don't call from app code. */
6552
- export declare const _resetConnectionSubjectsForTest: () => void;
7256
+ /** Test/dev-only — clear ALL per-connection state. Don't call from app code. */
7257
+ export declare const _resetConnectionCredentialsForTest: () => void;
6553
7258
 
6554
7259
  /** Test seams for the tag-cache guard — the cache is module-private otherwise. */
6555
7260
  export declare const resetEventMetricTagCacheForTests: () => void;
@@ -6560,6 +7265,23 @@ export declare const resetObservedGraph: () => void;
6560
7265
  /** Reset to the env default (tests). */
6561
7266
  export declare const resetSecretsBackend: () => void;
6562
7267
 
7268
+ /**
7269
+ * The client address to rate-limit, geo-block and audit by.
7270
+ *
7271
+ * The rules, in the order they fire:
7272
+ *
7273
+ * 1. No `x-forwarded-for` → the socket address. (Nothing to decide.)
7274
+ * 2. `trustAll` → the leftmost forwarded token. The pre-fix behaviour, now an
7275
+ * explicit opt-in for an ingress that OVERWRITES the header.
7276
+ * 3. Hop mode (`trustedProxies: ['2']`) → walk `n` hops in from the right of
7277
+ * `[...forwarded, socket]`.
7278
+ * 4. Otherwise the immediate peer must itself be trusted; if it is not, the
7279
+ * chain is a client-supplied string and is ignored entirely.
7280
+ * 5. With a trusted peer, walk the chain right-to-left past every trusted hop;
7281
+ * the first address that is NOT a configured proxy is the client.
7282
+ */
7283
+ export declare const resolveClientAddress: (input: ClientAddressInput) => string | undefined;
7284
+
6563
7285
  /**
6564
7286
  * Resolve the calling subject's credential for one connection, refreshed.
6565
7287
  *
@@ -6656,6 +7378,11 @@ export declare interface ResolvedConnection {
6656
7378
  */
6657
7379
  export declare const resolveDependentTables: (descriptor: QueryDescriptor) => ReadonlySet<string>;
6658
7380
 
7381
+ export declare interface ResolvedReactiveConfig {
7382
+ readonly deliveryConcurrency: number;
7383
+ readonly rawReadTrackingLimit: number;
7384
+ }
7385
+
6659
7386
  /**
6660
7387
  * A `WorkflowCallerContext` after the engine has filled in what a workflow
6661
7388
  * always needs. A run started by a request carries the caller's subject; a
@@ -6678,8 +7405,39 @@ export declare interface ResolvedWorkflowCallerContext extends WorkflowCallerCon
6678
7405
  readonly traceId: string;
6679
7406
  }
6680
7407
 
7408
+ /**
7409
+ * Resolve the guard's configuration.
7410
+ *
7411
+ * Precedence: explicit `app.config.ts` values (threaded through
7412
+ * `RpcServerOptions.security`) → env override → safe default. An unrecognised
7413
+ * mode falls back to enforcement rather than to `off`: a typo in a security
7414
+ * knob must not silently disable it.
7415
+ */
7416
+ export declare const resolveOriginGuardConfig: (configured?: {
7417
+ readonly mode?: OriginGuardMode | undefined;
7418
+ readonly allowedOrigins?: ReadonlyArray<string> | undefined;
7419
+ } | undefined, env?: Readonly<Record<string, string | undefined>>) => OriginGuardConfig;
7420
+
7421
+ export declare const resolveReactiveConfig: (config?: ReactiveConfigInput | undefined, env?: ReactiveEnv) => ResolvedReactiveConfig;
7422
+
6681
7423
  export declare const resolveRedirectUri: (definition: OAuth2ConnectionDefinition, publicUrl: string) => string;
6682
7424
 
7425
+ /**
7426
+ * Whether the request reached the app over TLS.
7427
+ *
7428
+ * `x-forwarded-proto` is believed only from a trusted proxy, for exactly the
7429
+ * reason `x-forwarded-for` is: it is a client-writable header. Getting this
7430
+ * wrong in the permissive direction would let any client make the framework
7431
+ * emit HSTS (harmless) — and, more importantly, would let it claim `https` in
7432
+ * an audit record, so it is gated the same way.
7433
+ */
7434
+ export declare const resolveRequestIsHttps: (input: {
7435
+ readonly socketEncrypted: boolean;
7436
+ readonly socketAddress: string | undefined;
7437
+ readonly xForwardedProto: string | undefined;
7438
+ readonly config: TrustedProxyConfig;
7439
+ }) => boolean;
7440
+
6683
7441
  /** Rebuild the flagged groups from the (already-committed) base rows + merge —
6684
7442
  * the caller's resolution of a min/max rescan signal. `baseRows` is the full
6685
7443
  * current table (or at least every row in the rescan groups). */
@@ -6739,6 +7497,21 @@ export declare const resolveSecretsBackend: (config: SecretsBackendConfig | unde
6739
7497
  * await (e.g. deriving a permission host from a key). Bypasses remote backends. */
6740
7498
  export declare const resolveSecretSync: (key: string) => string | undefined;
6741
7499
 
7500
+ /**
7501
+ * Resolve the header configuration.
7502
+ *
7503
+ * Precedence: explicit `app.config.ts` values → env override → defaults. Env
7504
+ * knobs: `VOLTRO_SECURITY_HEADERS` (`off|default|strict`), `VOLTRO_CSP`,
7505
+ * `VOLTRO_CSP_HTML`, `VOLTRO_HSTS` — each accepting `off` to drop just that one.
7506
+ */
7507
+ export declare const resolveSecurityHeaders: (configured?: SecurityHeadersOptions | undefined, env?: Readonly<Record<string, string | undefined>>) => SecurityHeadersConfig;
7508
+
7509
+ /** The app's composed strategy chain — `buildResolveSubject`'s return value. */
7510
+ export declare type ResolveSubjectFn = (input: {
7511
+ headers: Record<string, string | undefined>;
7512
+ clientId: number;
7513
+ }) => Promise<SubjectResolution>;
7514
+
6742
7515
  /**
6743
7516
  * Resolve the recording mode. `VOLTRO_TIMELINE=off|interesting|all` (also
6744
7517
  * on/1 = interesting, 0 = off) overrides; default `interesting` outside
@@ -6748,6 +7521,18 @@ export declare const resolveSecretSync: (key: string) => string | undefined;
6748
7521
  */
6749
7522
  export declare const resolveTimelineMode: (env?: Record<string, string | undefined>) => TimelineMode;
6750
7523
 
7524
+ /**
7525
+ * Resolve the trusted-proxy configuration.
7526
+ *
7527
+ * Precedence: explicit config (from `app.config.ts`, threaded through
7528
+ * `RpcServerOptions.security.trustedProxies`) beats the `VOLTRO_TRUSTED_PROXIES`
7529
+ * env override, which beats the safe default of trusting nothing. The env var
7530
+ * exists so an operator can correct a misconfigured deployment without a
7531
+ * rebuild; the config field exists because it is a tunable and tunables belong
7532
+ * in `app.config.ts`.
7533
+ */
7534
+ export declare const resolveTrustedProxies: (configured?: ReadonlyArray<string> | undefined, env?: Readonly<Record<string, string | undefined>>) => TrustedProxyConfig;
7535
+
6751
7536
  /** Per-resource-type policy. `actions` maps an action to the relations that
6752
7537
  * grant it (ANY-of). `implies` is relation implication (owner ⇒ editor ⇒
6753
7538
  * viewer), applied as a transitive closure before the action check. */
@@ -6760,6 +7545,10 @@ export declare interface ResourcePolicy {
6760
7545
  /** The full ordered list of result variants for a definition, holdout last. */
6761
7546
  export declare const resultVariantNames: (def: ExperimentDefinition) => ReadonlyArray<string>;
6762
7547
 
7548
+ export { RetentionConflict }
7549
+
7550
+ export { RetentionSource }
7551
+
6763
7552
  export { RetentionSpec }
6764
7553
 
6765
7554
  export { retentionTtlMsFromEnv }
@@ -7001,15 +7790,36 @@ export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
7001
7790
  readonly ping?: () => Promise<boolean>;
7002
7791
  };
7003
7792
  /**
7004
- * Max `Content-Length` (bytes) accepted on the buffered `POST /rpc` JSON
7005
- * endpoint. A JSON rpc mutation/action/query envelope is never legitimately
7006
- * large, so an oversized body is a memory-DoS attempt — rejected with 413
7007
- * BEFORE the body is buffered. Applies ONLY to `/rpc` (JSON); file uploads
7008
- * ride separate plugin routes with their own `limits.maxBytes`, and WS frames
7009
- * are capped by the `ws` library default (100 MiB). Per-IP rate limiting +
7010
- * the primary body cap belong at the ingress. Default {@link DEFAULT_MAX_RPC_BODY_BYTES}.
7793
+ * Max body size (bytes) accepted on the buffered `POST /rpc` JSON endpoint. A
7794
+ * JSON rpc mutation/action/query envelope is never legitimately large, so an
7795
+ * oversized body is a memory-DoS attempt — refused with `413` and never
7796
+ * buffered past the cap. Applies ONLY to `/rpc` (JSON); file uploads ride
7797
+ * separate plugin routes with their own `limits.maxBytes`, and WS frames are
7798
+ * capped by the `ws` library default (100 MiB). Per-IP rate limiting + the
7799
+ * primary body cap belong at the ingress. Default
7800
+ * {@link DEFAULT_MAX_RPC_BODY_BYTES}.
7801
+ *
7802
+ * **Both shapes end in a `413`, and the byte counter is what enforces it.**
7803
+ * A declared `Content-Length` over the cap is refused first, before the client
7804
+ * uploads anything — a courtesy, not the enforcement, since a
7805
+ * `Transfer-Encoding: chunked` body declares no length. The counter runs over
7806
+ * the arriving bytes, stops accumulating the moment the running total crosses
7807
+ * the cap, drains the remainder and answers `413`.
7808
+ *
7809
+ * This paragraph used to claim the counter was the enforcement while the
7810
+ * declared check was the courtesy, and observably it was the other way round:
7811
+ * the counter bounded MEMORY correctly and produced no client-visible status
7812
+ * at all. It cut the body by destroying the request, that took the socket with
7813
+ * it, and the 413 it had just built was written into a dead connection —
7814
+ * `curl: (56) Recv failure: Connection reset by peer` where the same bytes
7815
+ * with a `Content-Length` got a clean 413. See `rpcBodyCap.ts`.
7011
7816
  */
7012
7817
  readonly maxRpcBodyBytes?: number;
7818
+ /**
7819
+ * Transport-level security policy for this listener — see
7820
+ * {@link TransportSecurityOptions}. `undefined` → the safe defaults.
7821
+ */
7822
+ readonly security?: TransportSecurityOptions;
7013
7823
  }
7014
7824
 
7015
7825
  /**
@@ -7150,6 +7960,16 @@ export declare const runWithDeadlockRetry: <T>(work: () => Promise<T>, options?:
7150
7960
  */
7151
7961
  export declare const runWithObservedProcedure: <T>(procedure: ObservedProcedure, work: () => T) => T;
7152
7962
 
7963
+ /**
7964
+ * Run `fn` inside a FRESH raw-read scope, restoring whatever was current before.
7965
+ *
7966
+ * The wrapping form, for callers that have a callback: the dispatcher, which
7967
+ * wraps a computed query's recompute so a raw read issued by a LATER handler run
7968
+ * is attributed to that run rather than to whatever opened the request; and
7969
+ * tests, which need a scope that provably does not outlive them.
7970
+ */
7971
+ export declare const runWithRawReadScope: <T>(fn: () => T) => T;
7972
+
7153
7973
  /**
7154
7974
  * Run `fn` with `loader` current, restoring whatever was current before.
7155
7975
  *
@@ -7619,6 +8439,9 @@ declare interface ScopeCtx {
7619
8439
  readonly rowFilter?: RowFilterScope;
7620
8440
  }
7621
8441
 
8442
+ /** The scope strings a UI shows ("ask someone with …"). */
8443
+ export declare const scopeStringsOf: (serialized: ReadonlyArray<SerializedGuard>) => ReadonlyArray<string>;
8444
+
7622
8445
  export declare interface SecretsBackend {
7623
8446
  /** Resolve a secret by key. `undefined` when absent. */
7624
8447
  readonly get: (key: string) => Promise<string | undefined>;
@@ -7633,6 +8456,49 @@ export declare type SecretsBackendConfig = 'env' | {
7633
8456
  readonly backend: SecretsBackend;
7634
8457
  };
7635
8458
 
8459
+ export declare interface SecurityHeadersConfig {
8460
+ readonly mode: SecurityHeadersMode;
8461
+ /** CSP for non-HTML responses. `false` → do not send one. */
8462
+ readonly csp: string | false;
8463
+ /** CSP for `text/html` responses. `false` → do not send one. */
8464
+ readonly cspHtml: string | false;
8465
+ /** `Strict-Transport-Security` value; only emitted over https. */
8466
+ readonly hsts: string | false;
8467
+ readonly frameOptions: string | false;
8468
+ readonly referrerPolicy: string | false;
8469
+ readonly contentTypeOptions: string | false;
8470
+ /** Anything else the app wants on every response (e.g. `Permissions-Policy`,
8471
+ * `Cross-Origin-Resource-Policy`). Deliberately not defaulted: both of those
8472
+ * break legitimate cross-origin dashboards when guessed wrong. */
8473
+ readonly extra: Readonly<Record<string, string>>;
8474
+ }
8475
+
8476
+ /**
8477
+ * The headers to ADD to one response. The caller must not overwrite a header
8478
+ * the route already set.
8479
+ *
8480
+ * `https` gates HSTS only: announcing a year of HTTPS-only from a plain-http
8481
+ * response is either ignored (per RFC 6797) or, on a dev box reached over
8482
+ * `http://localhost`, actively harmful.
8483
+ */
8484
+ export declare const securityHeadersFor: (config: SecurityHeadersConfig, request: {
8485
+ readonly contentType: string | undefined;
8486
+ readonly https: boolean;
8487
+ }) => Readonly<Record<string, string>>;
8488
+
8489
+ export declare type SecurityHeadersMode = 'off' | 'default' | 'strict';
8490
+
8491
+ export declare interface SecurityHeadersOptions {
8492
+ readonly mode?: SecurityHeadersMode | undefined;
8493
+ readonly csp?: string | false | undefined;
8494
+ readonly cspHtml?: string | false | undefined;
8495
+ readonly hsts?: string | false | undefined;
8496
+ readonly frameOptions?: string | false | undefined;
8497
+ readonly referrerPolicy?: string | false | undefined;
8498
+ readonly contentTypeOptions?: string | false | undefined;
8499
+ readonly extra?: Readonly<Record<string, string>> | undefined;
8500
+ }
8501
+
7636
8502
  export declare class SelectBuilder {
7637
8503
  private readonly backend;
7638
8504
  private readonly scope;
@@ -7684,6 +8550,25 @@ export declare class SelectBuilder {
7684
8550
  get descriptor(): QueryDescriptor;
7685
8551
  }
7686
8552
 
8553
+ /** A guard, flattened to something a row can hold. */
8554
+ export declare type SerializedGuard = {
8555
+ readonly kind: 'scope';
8556
+ readonly scope: ReadonlyArray<string>;
8557
+ readonly mode: 'all' | 'any';
8558
+ readonly resource: string | null;
8559
+ } | {
8560
+ readonly kind: 'policy';
8561
+ readonly action: string;
8562
+ readonly resourceType: string;
8563
+ readonly resource: string | null;
8564
+ };
8565
+
8566
+ /**
8567
+ * Flatten the descriptor's approver guards, resolving each `resource` extractor
8568
+ * against the REQUEST's input while it is still available.
8569
+ */
8570
+ export declare const serializeGuards: (guards: ReadonlyArray<AnyGuardSpec>, input: unknown) => ReadonlyArray<SerializedGuard>;
8571
+
7687
8572
  export declare interface ServeRequestContext {
7688
8573
  readonly subject: unknown;
7689
8574
  readonly traceId: string;
@@ -7707,6 +8592,33 @@ export declare interface ServeRequestContext {
7707
8592
  */
7708
8593
  export declare const serverOnlyColumnNames: (tableName: string) => ReadonlyArray<string>;
7709
8594
 
8595
+ /**
8596
+ * A generated CRUD write (`crud.create` / `crud.update`) was handed a
8597
+ * `.serverOnly()` column in its INPUT.
8598
+ *
8599
+ * `.serverOnly()` is the WIRE-exposure axis: the column never crosses the
8600
+ * boundary in EITHER direction. Reads strip it; a write that accepts it is the
8601
+ * same violation mirrored — mass assignment of a column the schema declared the
8602
+ * client may not see, let alone set.
8603
+ *
8604
+ * Refused rather than silently stripped: a stripped field makes an attack
8605
+ * indistinguishable from a no-op and leaves an honest caller wondering why the
8606
+ * value it sent never landed. `columns` names what was rejected so the fix
8607
+ * (drop the field from the descriptor's input schema, or from the caller) is
8608
+ * mechanical.
8609
+ */
8610
+ export declare class ServerOnlyColumnWrite extends ServerOnlyColumnWrite_base {
8611
+ }
8612
+
8613
+ declare const ServerOnlyColumnWrite_base: Schema.TaggedErrorClass<ServerOnlyColumnWrite, "ServerOnlyColumnWrite", {
8614
+ readonly _tag: Schema.tag<"ServerOnlyColumnWrite">;
8615
+ } & {
8616
+ /** The table the write targeted. */
8617
+ table: typeof Schema.String;
8618
+ /** The `.serverOnly()` columns the input tried to set. */
8619
+ columns: Schema.Array$<typeof Schema.String>;
8620
+ }>;
8621
+
7710
8622
  /** One leak: a wire query that declares a serverOnly column in its output. */
7711
8623
  export declare interface ServerOnlyLeak {
7712
8624
  readonly query: string;
@@ -7985,7 +8897,7 @@ export declare interface StoreCredentialInput {
7985
8897
  * a mutation's `error:` schema when you want every kind surfaced
7986
8898
  * typed to the client.
7987
8899
  */
7988
- export declare type StoreError = TenantScopeViolation | StoreOperationFailed | TableValidationFailed;
8900
+ export declare type StoreError = TenantScopeViolation | TenantRowNotFound | ServerOnlyColumnWrite | StoreOperationFailed | TableValidationFailed;
7989
8901
 
7990
8902
  export declare interface StoreMiddlewareContext {
7991
8903
  readonly subject: Subject;
@@ -8343,6 +9255,41 @@ export declare interface TenantCostState {
8343
9255
  readonly lastUpdatedAt: number | null;
8344
9256
  }
8345
9257
 
9258
+ /**
9259
+ * A keyed-by-primary-key write (`ctx.store.update(table, id, patch)`,
9260
+ * `delete(table, id)`, `hardDelete(table, id)`, `patchJson(table, id, …)`) on a
9261
+ * `tenant()`-scoped table did not resolve to a row inside the CALLER's tenant.
9262
+ *
9263
+ * **One error for two situations, on purpose.** It is raised identically when
9264
+ * the row does not exist at all and when it exists but belongs to another
9265
+ * tenant, and it carries no field that separates them. That is the whole point:
9266
+ *
9267
+ * - Reporting "forbidden" for a foreign row and "not found" for a missing one
9268
+ * turns any keyed write into a cross-tenant EXISTENCE ORACLE — an attacker
9269
+ * walks ids and learns which ones are real in someone else's tenant, which
9270
+ * is exactly the isolation the `tenant()` mixin exists to provide.
9271
+ * - Collapsing the other way — silently affecting zero rows — is worse than
9272
+ * either: the handler reads it as "the row is gone", not "you may not touch
9273
+ * it", so a genuine isolation breach shows up in an app as a confusing
9274
+ * absent-row branch and never as a security signal.
9275
+ *
9276
+ * So both cases fail LOUDLY and IDENTICALLY. `id` is the key the caller itself
9277
+ * supplied — never another tenant's data.
9278
+ */
9279
+ export declare class TenantRowNotFound extends TenantRowNotFound_base {
9280
+ }
9281
+
9282
+ declare const TenantRowNotFound_base: Schema.TaggedErrorClass<TenantRowNotFound, "TenantRowNotFound", {
9283
+ readonly _tag: Schema.tag<"TenantRowNotFound">;
9284
+ } & {
9285
+ /** The table the keyed write targeted. */
9286
+ table: typeof Schema.String;
9287
+ /** The primary key the CALLER supplied. Echoing it leaks nothing. */
9288
+ id: typeof Schema.String;
9289
+ /** Human-readable explanation for diagnostics + UI. */
9290
+ reason: typeof Schema.String;
9291
+ }>;
9292
+
8346
9293
  /**
8347
9294
  * A write was attempted against a `tenant()`-scoped table, but the
8348
9295
  * authenticated subject's `tenantId` is null — either anonymous, or
@@ -8566,6 +9513,41 @@ export declare interface TransactionalStore {
8566
9513
  transactional<T>(work: (tx: unknown) => Promise<T>): Promise<T>;
8567
9514
  }
8568
9515
 
9516
+ /**
9517
+ * Transport-level security policy for the api listener.
9518
+ *
9519
+ * Every field is a TUNABLE, so its home is the app's `app.config.ts`
9520
+ * (`security: { … }`, threaded to here by `voltro dev` and `voltro serve`
9521
+ * through one shared resolver); each also carries an env override, for
9522
+ * correcting a deployment without a rebuild. An embedder calling
9523
+ * `startRpcServer` directly passes the same object.
9524
+ *
9525
+ * `undefined` on every field → the safe defaults documented on each resolver:
9526
+ * origin checking ON, `x-forwarded-for` IGNORED, security headers ON.
9527
+ */
9528
+ export declare interface TransportSecurityOptions {
9529
+ /** Cross-site protection for `POST /rpc` + the WS upgrade.
9530
+ * `'same-origin'` (default) accepts a browser request only when its
9531
+ * `Origin` matches the `Host` it was sent to or is in `allowedOrigins`;
9532
+ * a request with NO browser origin signal (the in-process SSR loader,
9533
+ * a mobile SDK, another service) is allowed — see `originGuard.ts`.
9534
+ * Env override: `VOLTRO_ORIGIN_GUARD=off`. */
9535
+ readonly originGuard?: OriginGuardMode;
9536
+ /** Extra origins to accept, for a split web/api deployment where the
9537
+ * browser's `Origin` is a different host than the api's `Host`.
9538
+ * Env override: `VOLTRO_ALLOWED_ORIGINS` (comma-separated). */
9539
+ readonly allowedOrigins?: ReadonlyArray<string>;
9540
+ /** Whose `x-forwarded-for` to believe. Empty (the default) → the header is
9541
+ * IGNORED and `socket.remoteAddress` is the client address. Accepts IPs,
9542
+ * CIDRs, the presets `loopback` / `private`, a hop count (`['2']`), or
9543
+ * `['*']` to trust any peer. Env override: `VOLTRO_TRUSTED_PROXIES`. */
9544
+ readonly trustedProxies?: ReadonlyArray<string>;
9545
+ /** Security response headers. Env override: `VOLTRO_SECURITY_HEADERS`
9546
+ * (`off|default|strict`), plus `VOLTRO_CSP` / `VOLTRO_CSP_HTML` /
9547
+ * `VOLTRO_HSTS`. */
9548
+ readonly headers?: SecurityHeadersOptions;
9549
+ }
9550
+
8569
9551
  /**
8570
9552
  * Declare a workflow trigger.
8571
9553
  *
@@ -8597,6 +9579,16 @@ export declare const triggerWorkflow: <EventPayload = unknown, WorkflowPayload =
8597
9579
  };
8598
9580
  })) => WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>;
8599
9581
 
9582
+ /** How much of the forwarded chain we are willing to believe. */
9583
+ export declare interface TrustedProxyConfig {
9584
+ /** Literal IPs / CIDR blocks / presets (`loopback`, `private`) to trust. */
9585
+ readonly entries: ReadonlyArray<string>;
9586
+ /** `*` — believe the leftmost XFF token from ANY peer. */
9587
+ readonly trustAll: boolean;
9588
+ /** Hop count (express's numeric `trust proxy`). `undefined` → not hop mode. */
9589
+ readonly hops?: number;
9590
+ }
9591
+
8600
9592
  /**
8601
9593
  * Atomically claim a pending wakeup for waking — CAS `pending → fired`.
8602
9594
  * Returns `true` iff THIS caller won the claim (the row was pending).
@@ -8642,10 +9634,10 @@ export declare type TupleSource = (req: {
8642
9634
  }) => Promise<ReadonlyArray<RelationTuple>> | ReadonlyArray<RelationTuple>;
8643
9635
 
8644
9636
  /**
8645
- * Remove the override for a connection. Called by the WS-close
8646
- * finalizer (or explicit logout flows). Idempotent.
9637
+ * Drop everything this connection accumulated. Called by the WS-close finalizer
9638
+ * and by explicit sign-out flows. Idempotent.
8647
9639
  */
8648
- export declare const unbindConnectionSubject: (clientId: number) => void;
9640
+ export declare const unbindConnection: (clientId: number) => void;
8649
9641
 
8650
9642
  /** The store surface an inverse op needs (a subset of the framework DataStore). */
8651
9643
  export declare interface UndoApplyStore {
@@ -8722,6 +9714,29 @@ export declare class UndoStack<E> {
8722
9714
  };
8723
9715
  }
8724
9716
 
9717
+ /**
9718
+ * Is the undo WIRE SURFACE present — the three `__voltro.undo.*` descriptors in
9719
+ * the client's generated rpc group, and the matching routes on the server?
9720
+ *
9721
+ * **Deliberately NOT `undoCaptureEnabled()`, and the difference is the whole
9722
+ * point.** The client half of that surface is a BUILD ARTEFACT:
9723
+ * `rpcGroup.generated.ts` is written by `voltro dev`'s codegen and `voltro
9724
+ * build` does not regenerate it. So the artefact froze whatever
9725
+ * `undoCaptureEnabled()` answered on the developer's machine — on, by its
9726
+ * NODE_ENV default — while the production process it ships to answers off and
9727
+ * binds nothing. The failure appears only after deploy, and only when someone
9728
+ * presses undo.
9729
+ *
9730
+ * The surface therefore reads ONLY the explicit declaration: `VOLTRO_UNDO=off`
9731
+ * removes it everywhere (codegen + both boot paths agree, because nothing in
9732
+ * the answer depends on when or where the code ran), and anything else keeps
9733
+ * it. Whether the mutations are actually RECORDED stays environment-aware —
9734
+ * that is the cost decision, and it is a genuine one. With capture off the
9735
+ * executors answer honestly rather than 404: the log is empty because nothing
9736
+ * was captured, and an apply/redo cannot find its row.
9737
+ */
9738
+ export declare const undoSurfaceEnabled: (env?: Record<string, string | undefined>) => boolean;
9739
+
8725
9740
  /** One `source:` entry that resolves to no declared table. */
8726
9741
  export declare interface UnresolvedSource {
8727
9742
  /** The procedure that declares it. */
@@ -8730,6 +9745,9 @@ export declare interface UnresolvedSource {
8730
9745
  readonly source: string;
8731
9746
  /** A declared table whose name is close — the rename case, usually. */
8732
9747
  readonly didYouMean: string | undefined;
9748
+ /** True when the name is in the `channel:` namespace: it named a reactivity
9749
+ * channel nobody declared, which is a different repair from a stale table. */
9750
+ readonly channel: boolean;
8733
9751
  }
8734
9752
 
8735
9753
  /**
@@ -8743,6 +9761,14 @@ export declare const unresolvedSources: (procedures: ReadonlyArray<{
8743
9761
  readonly source: string | ReadonlyArray<string> | undefined;
8744
9762
  }>, declared: ReadonlySet<string>) => ReadonlyArray<UnresolvedSource>;
8745
9763
 
9764
+ /** Thrown at boot; the message is the whole point, so it names the path and the
9765
+ * three ways out. */
9766
+ export declare class UnverifiedWebhookRoute extends Error {
9767
+ readonly path: string;
9768
+ readonly name = "UnverifiedWebhookRoute";
9769
+ constructor(path: string);
9770
+ }
9771
+
8746
9772
  export declare class UpdateBuilder {
8747
9773
  private readonly backend;
8748
9774
  private readonly scope;
@@ -8800,6 +9826,11 @@ export declare const useExperiment: (def: ExperimentDefinition) => Effect.Effect
8800
9826
 
8801
9827
  declare type VaultStore = Pick<DataStore, 'query' | 'insert' | 'update' | 'updateMany' | 'delete' | 'upsert'>;
8802
9828
 
9829
+ /** A mounted incoming-webhook handler that has declared how it authenticates. */
9830
+ export declare type VerificationDeclared<T> = T & {
9831
+ readonly [WEBHOOK_VERIFICATION_PROPERTY]: WebhookVerification;
9832
+ };
9833
+
8803
9834
  /** The pure core of live revocation: given a subject + action + a row-set,
8804
9835
  * return ONLY the rows the subject may still see. The reactive matcher calls
8805
9836
  * this on a permission-changing write and diffs against the prior visible set;
@@ -8830,6 +9861,16 @@ export declare const VOLTRO_SOFT_DELETE_MIXIN_ID: "voltro/softDelete";
8830
9861
 
8831
9862
  export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";
8832
9863
 
9864
+ /**
9865
+ * One pending (or historical) approval intent.
9866
+ *
9867
+ * Reactive on purpose — the requester's client subscribes to
9868
+ * `__voltro.approvals.pending` and sees its own row flip `pending → approved`
9869
+ * without polling, which is the whole answer to "what does the caller see
9870
+ * meanwhile".
9871
+ */
9872
+ export declare const _voltroApprovalsTable: TableLike;
9873
+
8833
9874
  /**
8834
9875
  * `_voltro_connection_grants` — an IN-FLIGHT oauth2 handshake. Separate from
8835
9876
  * the credential table on purpose: a handshake is a short-lived, single-use
@@ -8966,10 +10007,22 @@ export declare interface WakeupRef {
8966
10007
  * next register cleanly overwrites it). */
8967
10008
  export declare type WakeupStatus = 'pending' | 'fired';
8968
10009
 
10010
+ /** The property `mountIncomingWebhook` stamps onto the handler it returns.
10011
+ * A plain string property rather than a symbol so it survives the structural
10012
+ * hand-off between packages that do not import each other. */
10013
+ export declare const WEBHOOK_VERIFICATION_PROPERTY: "voltroWebhookVerification";
10014
+
8969
10015
  /** Stage 4 webhook-route surface — kept transport-agnostic so the
8970
10016
  * plugin can pipe its IncomingResponse through unchanged. The
8971
10017
  * rpc server reads the request body (UTF-8 / binary) + headers
8972
- * and dispatches by path. */
10018
+ * and dispatches by path.
10019
+ *
10020
+ * `handle` MUST carry a `voltroWebhookVerification` declaration — the
10021
+ * property `@voltro/plugin-webhooks`' `mountIncomingWebhook` stamps onto
10022
+ * the handler it returns. `startRpcServer` refuses to mount a route
10023
+ * without one (see `webhookVerification.ts`): an incoming webhook is a
10024
+ * public POST that runs application code, so "nothing verifies it" has to
10025
+ * be a decision somebody wrote down, not the default. */
8973
10026
  export declare interface WebhookRouteHandler {
8974
10027
  readonly handle: (request: {
8975
10028
  readonly method: string;
@@ -8985,6 +10038,21 @@ export declare interface WebhookRouteHandler {
8985
10038
 
8986
10039
  export declare type WebhooksAppContext = unknown;
8987
10040
 
10041
+ /** How an incoming webhook authenticates its caller. */
10042
+ export declare type WebhookVerification =
10043
+ /** The framework verifies an HMAC signature (+ replay window) before the
10044
+ * handler runs, using the webhook's configured shared secret. */
10045
+ 'signature'
10046
+ /** The handler / provider integration verifies with the provider's own SDK
10047
+ * (Stripe's `constructEvent`, etc.). The framework does not second-guess it. */
10048
+ | 'provider'
10049
+ /** Deliberately unverified — the endpoint is behind a separate trust boundary
10050
+ * (gateway + IP allow-list). A visible decision, never a default. */
10051
+ | 'none';
10052
+
10053
+ /** Read the declaration off a mounted handler. `undefined` → never declared. */
10054
+ export declare const webhookVerificationOf: (handle: unknown) => WebhookVerification | undefined;
10055
+
8988
10056
  /**
8989
10057
  * Comparison operators accepted by the ergonomic `.where(col, op, value)`
8990
10058
  * form. `fts` falls back to a `contains` (LIKE) match here; the index-backed
@@ -9209,6 +10277,31 @@ export declare interface WorkflowLayerExecutionContext {
9209
10277
  export declare interface WorkflowLayerOptions<Context> {
9210
10278
  readonly buildContext: (callerContext: ResolvedWorkflowCallerContext, execution: WorkflowLayerExecutionContext) => Context;
9211
10279
  readonly resolveStartContext?: (workflowName: string, executionId: string) => WorkflowCallerContext | undefined | Promise<WorkflowCallerContext | undefined>;
10280
+ /**
10281
+ * Put AUTHORITY back on a restored caller identity, at execution time
10282
+ * (REL-24).
10283
+ *
10284
+ * `resolveStartContext` returns IDENTITY — the start-context table strips
10285
+ * scopes on write and on read, because a `json()` column read by another
10286
+ * runner days later is authority frozen and made durable, which is REL-5's
10287
+ * cookie one layer down. This hook is what re-establishes the authority, from
10288
+ * the app's live source, on every execution attempt.
10289
+ *
10290
+ * Called ONLY for a run that has a recorded caller. A bootstrap run — no
10291
+ * recorded context — is `SYSTEM_SUBJECT` and is not put through it:
10292
+ * `SYSTEM_SUBJECT` already states its own authority, and running it through
10293
+ * an app resolver keyed on a null id would be asking a question with no
10294
+ * answer.
10295
+ *
10296
+ * Absent ⇒ the identity runs with no scopes. That is the fail-closed
10297
+ * direction and it matches what a cookie-authenticated request already gets
10298
+ * from an app that wires no resolver.
10299
+ *
10300
+ * Rejecting FAILS the attempt. Do not swallow it into "fewer scopes": a run
10301
+ * that skips the branch it was not allowed to take looks exactly like one
10302
+ * whose business logic said no.
10303
+ */
10304
+ readonly resolveAuthority?: (identity: unknown) => Promise<unknown>;
9212
10305
  }
9213
10306
 
9214
10307
  /**