@voltro/runtime 0.5.0 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ import { CaughtUpVerdict } from '@voltro/database';
4
4
  import { ChangeEvent } from '@voltro/database';
5
5
  import { clearRetentions } from '@voltro/database';
6
6
  import { ConnectionInfo } from '@voltro/protocol';
7
+ import { ConnectionKind } from '@voltro/protocol';
8
+ import { ConnectionState } from '@voltro/protocol';
7
9
  import { Context } from 'effect';
8
10
  import { createServer } from 'node:http';
9
11
  import { Cron } from 'effect';
@@ -40,7 +42,7 @@ import { registerRetention } from '@voltro/database';
40
42
  import { RestRouteDescriptor } from '@voltro/protocol/rest';
41
43
  import { RetentionSpec } from '@voltro/database';
42
44
  import { retentionTtlMsFromEnv } from '@voltro/database';
43
- import { Row as Row_4 } from '@voltro/database';
45
+ import { Row } from '@voltro/database';
44
46
  import { Rpc } from '@effect/rpc';
45
47
  import { RpcGroup } from '@effect/rpc';
46
48
  import { RpcInterceptor } from '@voltro/protocol';
@@ -285,14 +287,14 @@ export declare class AggregateMaintainer {
285
287
  private readonly groups;
286
288
  constructor(shape: AggregateShape);
287
289
  /** Seed from the full base row-set (boot / after a rescan-from-scratch). */
288
- seed(baseRows: ReadonlyArray<Row_3>): void;
290
+ seed(baseRows: ReadonlyArray<Row_4>): void;
289
291
  /**
290
292
  * Apply one delta. Returns the groups (if any) whose min/max extreme was
291
293
  * deleted and so MUST be rebuilt via `resolveRescan` before the next read.
292
294
  */
293
295
  applyChange(delta: CdcDelta): ReadonlyArray<string>;
294
296
  /** Rebuild the flagged groups from the current base rows (min/max recovery). */
295
- resolveRescan(rescanGroups: ReadonlyArray<string>, baseRows: ReadonlyArray<Row_3>): void;
297
+ resolveRescan(rescanGroups: ReadonlyArray<string>, baseRows: ReadonlyArray<Row_4>): void;
296
298
  /** The current materialized result — one entry per non-empty group. */
297
299
  materialize(): ReadonlyArray<MaintainedGroup>;
298
300
  private groupValues;
@@ -654,6 +656,8 @@ export declare interface AnalyticsTopEntry {
654
656
  readonly value: number;
655
657
  }
656
658
 
659
+ declare type AnyRow = Record<string, unknown>;
660
+
657
661
  export declare const API_KEYS_TABLE = "_voltro_api_keys";
658
662
 
659
663
  /** Admin-gated management routes (issue / list / revoke) for the built-in keys.
@@ -678,18 +682,15 @@ export declare interface ApiKeyServiceShape {
678
682
  /** Mint a new key. Returns the raw token ONCE. */
679
683
  readonly issue: (input: IssueInput) => Promise<IssuedApiKey>;
680
684
  /** Hash-lookup used by the auth strategy: returns the key's identity if
681
- * usable (not revoked / expired), and stamps `lastUsedAt`. */
682
- readonly resolveByHash: (hash: string, now?: number) => Promise<{
683
- id: string;
684
- tenantId: string;
685
- scopes: ReadonlyArray<string>;
686
- } | null>;
685
+ * usable (not revoked / expired), and stamps `lastUsedAt`.
686
+ *
687
+ * `createdBy` travels with it because a key always has an accountable
688
+ * human, and downstream needs to know WHICH one: a personal CLI key and a
689
+ * shared CI credential are the same shape on the wire, and telling them
690
+ * apart is what makes per-person accounting possible at all. */
691
+ readonly resolveByHash: (hash: string, now?: number) => Promise<ResolvedApiKey | null>;
687
692
  /** Verify a raw token (hashes then resolves). */
688
- readonly verify: (token: string, now?: number) => Promise<{
689
- id: string;
690
- tenantId: string;
691
- scopes: ReadonlyArray<string>;
692
- } | null>;
693
+ readonly verify: (token: string, now?: number) => Promise<ResolvedApiKey | null>;
693
694
  /** Revoke a key (irreversible). */
694
695
  readonly revoke: (id: string, now?: number) => Promise<boolean>;
695
696
  /** Rotate: revoke the old key + issue a replacement with the same name/scopes/tenant. */
@@ -765,6 +766,14 @@ export declare interface AppContext {
765
766
  * simply isn't there rather than silently accepting writes.
766
767
  */
767
768
  readonly outbox?: OutboxFacade;
769
+ /**
770
+ * Connection vault (`ctx.connections`) — per-subject third-party
771
+ * credentials. Present when the app declares at least one `*.connection.ts`.
772
+ * `ctx.connections.get('jira')` returns the CALLING subject's credential,
773
+ * refreshed if it was near expiry; there is no parameter for "some other
774
+ * subject", by construction.
775
+ */
776
+ readonly connections?: ConnectionsFacade;
768
777
  /**
769
778
  * Request-scoped batching (`ctx.load` / `ctx.loadMany`). Coalesces
770
779
  * same-tick reads of one table into a single `WHERE id IN (...)`.
@@ -811,6 +820,17 @@ export declare const applyInverse: (store: UndoApplyStore, op: InverseOp) => Pro
811
820
  * `synthesizeInverse`), as the caller's transaction wraps them. */
812
821
  export declare const applyInverses: (store: UndoApplyStore, ops: ReadonlyArray<InverseOp>) => Promise<void>;
813
822
 
823
+ /** AND-merge the row filter for `table` onto an existing predicate. */
824
+ export declare const applyRowFilter: (scope: RowFilterScope, table: string, predicate: Predicate | undefined) => Predicate | undefined;
825
+
826
+ /** Apply a resolved scope to a whole descriptor. Used by the dispatcher, which
827
+ * re-derives a subscription's read descriptor from its unfiltered base before
828
+ * every delivery. */
829
+ export declare const applyRowFilterToDescriptor: <D extends {
830
+ readonly table: string;
831
+ readonly predicate?: Predicate | undefined;
832
+ }>(scope: RowFilterScope, descriptor: D) => D;
833
+
814
834
  export declare const applySoftDeleteScope: (descriptor: QueryDescriptor, softDeleteScopedTables: ReadonlySet<string>) => QueryDescriptor;
815
835
 
816
836
  export declare const applyTenantScope: (descriptor: QueryDescriptor, tenantScopedTables: ReadonlySet<string>, tenantId: string | null | undefined) => QueryDescriptor;
@@ -843,6 +863,19 @@ export declare interface AppSupervisor {
843
863
  * deny so it surfaces to the client typed. */
844
864
  export declare const assertCan: (subject: RebacSubject, action: string, resource: RebacResource, deps: CanDeps) => void;
845
865
 
866
+ /**
867
+ * Refuse to boot an app that declares connections without a field cipher.
868
+ *
869
+ * Without this the first `encryptField` call throws at REQUEST time — which
870
+ * means a deployment looks healthy, accepts a user through a whole OAuth
871
+ * consent screen, and fails only at the moment it would have persisted the
872
+ * token. Moving the check to boot makes a missing key an operator-visible
873
+ * startup failure instead of a per-user mystery. It is deliberately NOT a
874
+ * warning: a warning here degrades to plaintext storage the moment someone
875
+ * "fixes" the throw.
876
+ */
877
+ export declare const assertConnectionCipherConfigured: (definitions: ReadonlyArray<ConnectionDefinition>) => void;
878
+
846
879
  /**
847
880
  * Async cache surface exposed as `ctx.cache` to non-Effect handlers — the
848
881
  * facade over the Effect-native `Cache` service from `@voltro/cache`
@@ -936,6 +969,27 @@ export declare const awaitServerListening: (server: ReturnType<typeof createServ
936
969
  /** Exponential backoff with a ceiling — 1s, 2s, 4s … capped at 5 minutes. */
937
970
  export declare const backoffMs: (attempt: number) => number;
938
971
 
972
+ /** Mint a single-use handshake grant and build the provider's consent URL. */
973
+ export declare const beginOAuthConnect: (deps: BeginOAuthDeps) => Promise<BeginOAuthResult>;
974
+
975
+ export declare interface BeginOAuthDeps {
976
+ readonly store: VaultStore;
977
+ readonly definition: OAuth2ConnectionDefinition;
978
+ readonly subjectId: string;
979
+ readonly tenantId: string | null;
980
+ readonly redirectTo?: string;
981
+ /** Absolute base URL the provider will redirect back to, e.g.
982
+ * `https://app.example.com`. Used only when the declaration omits an
983
+ * explicit `redirectUri`. */
984
+ readonly publicUrl: string;
985
+ readonly now?: () => Date;
986
+ }
987
+
988
+ export declare interface BeginOAuthResult {
989
+ readonly authorizeUrl: string;
990
+ readonly state: string;
991
+ }
992
+
939
993
  /**
940
994
  * Override the subject for an active connection. Called from the
941
995
  * `auth.signin` (or any "I just authenticated this caller") handler
@@ -974,7 +1028,7 @@ export declare const bindStream: <Element, Err = unknown>(buildStream: (context:
974
1028
  * the single explicit type-erasure boundary between the framework's
975
1029
  * dynamic data layer and the rpc layer's static schemas.
976
1030
  */
977
- export declare const bindSubscription: <T = ReadonlyArray<Row_4>>(buildDescriptor: (context: RuntimeContext) => QueryDescriptor | ComputedQuery | Effect.Effect<QueryDescriptor | ComputedQuery, unknown, never>, dispatcher: Dispatcher, spanName?: string,
1031
+ export declare const bindSubscription: <T = ReadonlyArray<Row>>(buildDescriptor: (context: RuntimeContext) => QueryDescriptor | ComputedQuery | Effect.Effect<QueryDescriptor | ComputedQuery, unknown, never>, dispatcher: Dispatcher, spanName?: string,
978
1032
  /**
979
1033
  * Optional resolver: given the materialised descriptor (after any
980
1034
  * `.using()` hint), return the `MatcherIndexHint` the matcher should
@@ -1006,16 +1060,24 @@ cache?: {
1006
1060
  * revoked grant CLOSES the stream rather than continuing to serve it.
1007
1061
  * Omitted for a query that declares no guards.
1008
1062
  */
1009
- reauthorize?: (subject: Subject) => () => Promise<unknown>) => Stream.Stream<SubscriptionEvent<T>, never, SubjectService | ConnectionInfo>;
1063
+ reauthorize?: (subject: Subject) => () => Promise<unknown>,
1064
+ /**
1065
+ * Per-delivery row-visibility resolver. Rarely passed: when omitted, one is
1066
+ * DERIVED from the registered row filter (see `defaultRefilter`), so an
1067
+ * entrypoint cannot end up with unfiltered subscriptions by forgetting to
1068
+ * thread it — which is precisely how a row filter ends up applying to
1069
+ * queries and mutations but not to the live stream.
1070
+ */
1071
+ refilter?: (subject: Subject) => () => Promise<RowFilterScope>) => Stream.Stream<SubscriptionEvent<T>, never, SubjectService | ConnectionInfo>;
1010
1072
 
1011
- export declare const bindSubscriptionUntyped: (descriptor: QueryDescriptor, dispatcher: Dispatcher, context: RuntimeContext, indexHint: MatcherIndexHint | undefined, label: string, cacheBinding: SnapshotCacheBinding | undefined, reauthorize: (() => Promise<unknown>) | undefined) => Stream.Stream<SubscriptionEvent<ReadonlyArray<Row_4>>, never, never>;
1073
+ export declare const bindSubscriptionUntyped: (descriptor: QueryDescriptor, dispatcher: Dispatcher, context: RuntimeContext, indexHint: MatcherIndexHint | undefined, label: string, cacheBinding: SnapshotCacheBinding | undefined, reauthorize: (() => Promise<unknown>) | undefined, refilter?: (() => Promise<RowFilterScope>) | undefined) => Stream.Stream<SubscriptionEvent<ReadonlyArray<Row>>, never, never>;
1012
1074
 
1013
1075
  export declare interface BrandedScheduleDefinition extends ScheduleDefinition {
1014
1076
  readonly [SCHEDULE_BRAND]: true;
1015
1077
  }
1016
1078
 
1017
1079
  /** Build the full state from a base row-set (seed + per-group rescan). */
1018
- export declare const buildIvmState: (shape: AggregateShape, rows: ReadonlyArray<Row_2>) => IvmState;
1080
+ export declare const buildIvmState: (shape: AggregateShape, rows: ReadonlyArray<Row_3>) => IvmState;
1019
1081
 
1020
1082
  /** Build the preview diff from a mutation's captured change-set. Pure. */
1021
1083
  export declare const buildPreview: (changes: ReadonlyArray<CdcChange>) => PreviewDiff;
@@ -1126,8 +1188,8 @@ export declare interface CdcChange {
1126
1188
  /** A CDC change as the matcher delivers it. */
1127
1189
  export declare interface CdcDelta {
1128
1190
  readonly op: 'insert' | 'update' | 'delete';
1129
- readonly old?: Row_2 | null;
1130
- readonly new?: Row_2 | null;
1191
+ readonly old?: Row_3 | null;
1192
+ readonly new?: Row_3 | null;
1131
1193
  }
1132
1194
 
1133
1195
  export { ChangeEvent }
@@ -1199,6 +1261,10 @@ export declare type CircuitState = 'closed' | 'open' | 'half-open';
1199
1261
  */
1200
1262
  export declare const classifyShape: (c: CandidateShape) => ShapeClassification;
1201
1263
 
1264
+ /** Test seam — the in-flight map is module state; a test that asserts
1265
+ * single-flight must be able to start from empty. */
1266
+ export declare const clearConnectionRefreshFlights: () => void;
1267
+
1202
1268
  /** Test/boot reset. */
1203
1269
  export declare const clearResourcePolicies: () => void;
1204
1270
 
@@ -1212,6 +1278,30 @@ export declare const clearSystemStoreHandle: () => void;
1212
1278
  * branded definition can trust this won't throw. */
1213
1279
  export declare const compileCron: (def: ScheduleDefinition) => Cron.Cron;
1214
1280
 
1281
+ /**
1282
+ * Consume a handshake grant and store the resulting credential.
1283
+ *
1284
+ * The grant row is deleted BEFORE the exchange, not after: a `state` is
1285
+ * single-use, and deleting it up front means a replayed callback (the user
1286
+ * double-clicks, a crawler follows the URL) cannot start a second exchange
1287
+ * even if the first is still in flight.
1288
+ */
1289
+ export declare const completeOAuthConnect: (deps: CompleteOAuthDeps) => Promise<CompleteOAuthResult>;
1290
+
1291
+ export declare interface CompleteOAuthDeps {
1292
+ readonly store: VaultStore;
1293
+ readonly registry: ConnectionRegistry;
1294
+ readonly state: string;
1295
+ readonly code: string;
1296
+ readonly publicUrl: string;
1297
+ readonly now?: () => Date;
1298
+ }
1299
+
1300
+ export declare interface CompleteOAuthResult {
1301
+ readonly connectionId: string;
1302
+ readonly redirectTo: string;
1303
+ }
1304
+
1215
1305
  /**
1216
1306
  * Combine multiple analytics specs into one composite spec at
1217
1307
  * app.config.ts time. The framework's boot path materialises each
@@ -1269,6 +1359,105 @@ declare interface ComputedQuery {
1269
1359
  readonly recompute: () => Promise<unknown>;
1270
1360
  }
1271
1361
 
1362
+ export declare const CONNECTION_GRANTS_TABLE = "_voltro_connection_grants";
1363
+
1364
+ /** What a declaration can learn about the account it just connected. Optional
1365
+ * — a connection with no `identify` still works, it just has no display name. */
1366
+ export declare interface ConnectionAccount {
1367
+ readonly accountId?: string;
1368
+ readonly accountLabel?: string;
1369
+ readonly metadata?: Record<string, unknown>;
1370
+ }
1371
+
1372
+ export declare interface ConnectionBuiltinDeps {
1373
+ readonly registry: ConnectionRegistry;
1374
+ /** Absolute origin the OAuth provider redirects back to. */
1375
+ readonly publicUrl: string;
1376
+ }
1377
+
1378
+ /** Default callback path for a connection. The `<id>` segment is why
1379
+ * `defineConnection` constrains the id shape. */
1380
+ export declare const connectionCallbackPath: (connectionId: string) => string;
1381
+
1382
+ export declare type ConnectionDefinition = OAuth2ConnectionDefinition | PatConnectionDefinition;
1383
+
1384
+ declare interface ConnectionDefinitionCommon {
1385
+ /** Stable id — the value handlers pass to `ctx.connections.get(...)` and the
1386
+ * path segment of the callback route. */
1387
+ readonly id: string;
1388
+ /** Human label for the connect UI. Defaults to `id`. */
1389
+ readonly label?: string;
1390
+ }
1391
+
1392
+ /** The slice of AppContext the connection executors need. */
1393
+ export declare interface ConnectionExecutorCtx {
1394
+ readonly store: DataStore;
1395
+ readonly request?: {
1396
+ readonly subject?: {
1397
+ readonly id?: string | null;
1398
+ readonly tenantId?: string | null;
1399
+ } | null;
1400
+ };
1401
+ }
1402
+
1403
+ /** The subset of `fetch` the vault uses. Structurally satisfied by the global
1404
+ * `fetch` and by `@voltro/integration-http`'s `FetchLike`; declared locally so
1405
+ * the runtime takes no dependency on the http package for a two-field type. */
1406
+ export declare type ConnectionFetch = (url: string, init: {
1407
+ readonly method: string;
1408
+ readonly headers: Record<string, string>;
1409
+ readonly body: string;
1410
+ }) => Promise<{
1411
+ readonly status: number;
1412
+ readonly text: () => Promise<string>;
1413
+ }>;
1414
+
1415
+ /** `label` with the id as fallback. */
1416
+ export declare const connectionLabel: (definition: ConnectionDefinition) => string;
1417
+
1418
+ /** The subject has no credential on file for this connection (or it was
1419
+ * revoked and cleared). The caller's move is to prompt a connect. */
1420
+ export declare class ConnectionNotConnected extends Error {
1421
+ readonly connectionId: string;
1422
+ readonly subjectId: string;
1423
+ readonly _tag = "ConnectionNotConnected";
1424
+ constructor(connectionId: string, subjectId: string);
1425
+ }
1426
+
1427
+ export declare interface ConnectionRegistry {
1428
+ readonly get: (connectionId: string) => ConnectionDefinition | undefined;
1429
+ readonly list: () => ReadonlyArray<ConnectionDefinition>;
1430
+ }
1431
+
1432
+ /** Resolve `connectionId` for `subject`, refreshed. */
1433
+ export declare type ConnectionResolver = (subject: Pick<Subject, 'id'>, connectionId: string) => Promise<ResolvedConnection>;
1434
+
1435
+ export declare const CONNECTIONS_TABLE = "_voltro_connections";
1436
+
1437
+ export declare interface ConnectionsFacade {
1438
+ /**
1439
+ * The calling subject's credential for `connectionId`, refreshed if it was
1440
+ * near expiry. Throws `ConnectionNotConnected` when there is nothing on file
1441
+ * (prompt a connect) or `ConnectionHandshakeFailed` when a refresh was owed
1442
+ * and failed.
1443
+ */
1444
+ get(connectionId: string): Promise<ResolvedConnection>;
1445
+ /** Same, but `null` instead of throwing when not connected — for the common
1446
+ * "use it if we have it" branch. A failing REFRESH still throws: silently
1447
+ * degrading a connected-but-broken account to "not connected" would hide a
1448
+ * revoked grant behind a feature quietly doing nothing. */
1449
+ tryGet(connectionId: string): Promise<ResolvedConnection | null>;
1450
+ /** Every declared connection with the caller's status — the same projection
1451
+ * `__voltro.connections.list` serves, for server-side rendering. */
1452
+ list(): Promise<ReadonlyArray<ConnectionState>>;
1453
+ }
1454
+
1455
+ export declare interface ConnectionsFacadeDeps {
1456
+ readonly store: DataStore;
1457
+ readonly registry: ConnectionRegistry;
1458
+ readonly subject: Subject;
1459
+ }
1460
+
1272
1461
  /** Test-only — number of streams currently registered for a client. */
1273
1462
  export declare const _connectionStreamCount: (clientId: number) => number;
1274
1463
 
@@ -1279,6 +1468,14 @@ export declare const connectionSubjectsSnapshot: () => ReadonlyArray<{
1279
1468
  tenantId: string | null;
1280
1469
  }>;
1281
1470
 
1471
+ /** A resolved token set. `expiresAt` is an absolute epoch-ms deadline. */
1472
+ export declare interface ConnectionTokens {
1473
+ readonly accessToken: string;
1474
+ readonly refreshToken?: string;
1475
+ readonly expiresAt?: number;
1476
+ readonly scopes: ReadonlyArray<string>;
1477
+ }
1478
+
1282
1479
  export declare interface CoordinatedScheduleDeps {
1283
1480
  /** The exactly-once gate. `singleCoordinator` for one-process
1284
1481
  * deployments; `makeAdvisoryLockCoordinator(store, replicaId)` for
@@ -1337,10 +1534,10 @@ export declare interface DataLoader {
1337
1534
  * to assemble, and a missing edge in a graph walk is usually data, not an
1338
1535
  * error. Use `.one()` when absence IS an error.
1339
1536
  */
1340
- load(table: string, id: string): Promise<Row_4 | null>;
1537
+ load(table: string, id: string): Promise<Row | null>;
1341
1538
  /** Load many by key, in the order asked. Missing rows come back as `null`,
1342
1539
  * so the result lines up positionally with the input. */
1343
- loadMany(table: string, ids: ReadonlyArray<string>): Promise<ReadonlyArray<Row_4 | null>>;
1540
+ loadMany(table: string, ids: ReadonlyArray<string>): Promise<ReadonlyArray<Row | null>>;
1344
1541
  }
1345
1542
 
1346
1543
  export { DataStore }
@@ -1364,6 +1561,8 @@ export declare const decryptField: (value: string) => string;
1364
1561
  * small enough to stop a pathological body being buffered into memory. */
1365
1562
  export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
1366
1563
 
1564
+ export declare const DEFAULT_REFRESH_SKEW_MS = 60000;
1565
+
1367
1566
  /**
1368
1567
  * Construct an aggregate definition. The returned object brands itself
1369
1568
  * so the cli's file-discovery pass picks it up from default exports.
@@ -1392,6 +1591,16 @@ export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
1392
1591
  */
1393
1592
  export declare const defineAggregate: <Row>(input: AggregateDefinitionInput<Row>) => AggregateDefinition<Row>;
1394
1593
 
1594
+ /**
1595
+ * Declare a connection. One per `*.connection.ts` file, default-exported.
1596
+ *
1597
+ * This is a pure declaration — no IO, no registration side effect. The CLI's
1598
+ * boot discovery imports the file, reads the default export, and registers it;
1599
+ * that keeps the declaration testable in isolation and keeps a stray import of
1600
+ * a connection file from mutating global state.
1601
+ */
1602
+ export declare const defineConnection: <D extends ConnectionDefinition>(definition: D) => D;
1603
+
1395
1604
  export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayload = EventPayload>(spec: Omit<WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>, "_tag">) => WorkflowEventTriggerDefinition<EventPayload, WorkflowPayload>;
1396
1605
 
1397
1606
  /**
@@ -1493,7 +1702,7 @@ export declare class Dispatcher {
1493
1702
  * Returns an `unsubscribe` function the caller MUST invoke when the
1494
1703
  * subscriber goes away (client disconnect, stream finalization, etc.).
1495
1704
  */
1496
- subscribe(descriptor: QueryDescriptor, emit: (event: SubscriptionEvent<ReadonlyArray<Row_4>>) => void, context: RuntimeContext,
1705
+ subscribe(descriptor: QueryDescriptor, emit: (event: SubscriptionEvent<ReadonlyArray<Row>>) => void, context: RuntimeContext,
1497
1706
  /**
1498
1707
  * Optional matcher-side index hint. When set, the registry hashes
1499
1708
  * it into the fingerprint AND the matcher uses it for tuple
@@ -1512,7 +1721,9 @@ export declare class Dispatcher {
1512
1721
  cacheBinding?: SnapshotCacheBinding,
1513
1722
  /** Re-run the query's `guards:` before every delivery — see
1514
1723
  * `ActiveSubscription.reauthorize`. Omitted for unguarded queries. */
1515
- reauthorize?: () => Promise<unknown>): Promise<() => void>;
1724
+ reauthorize?: () => Promise<unknown>,
1725
+ /** Re-resolve row visibility per delivery — see `ActiveSubscription.refilter`. */
1726
+ refilter?: () => Promise<RowFilterScope>): Promise<() => void>;
1516
1727
  /**
1517
1728
  * Tear a subscription down because authorization was WITHDRAWN mid-stream,
1518
1729
  * and tell the client why.
@@ -1574,11 +1785,13 @@ export declare interface DispatcherDependencies {
1574
1785
  }
1575
1786
 
1576
1787
  export declare interface DrainDeps {
1577
- readonly store: Pick<DataStore, 'query' | 'update'>;
1788
+ readonly store: Pick<DataStore, 'query' | 'update' | 'insert' | 'delete'>;
1578
1789
  readonly handlers: ReadonlyMap<string, OutboxHandlerDefinition>;
1579
1790
  readonly now?: () => Date;
1580
1791
  /** Max rows per drain pass. */
1581
1792
  readonly batchSize?: number;
1793
+ /** Per-entry cap on retained attempt rows. See {@link OUTBOX_ATTEMPT_LOG_CAP}. */
1794
+ readonly attemptLogCap?: number;
1582
1795
  }
1583
1796
 
1584
1797
  /**
@@ -1620,9 +1833,9 @@ export declare interface EffectStoreOps {
1620
1833
  * descriptor, so an Effect-form handler reads real fields instead of
1621
1834
  * casting off `Record<string, unknown>`. Falls back to `Row` for a
1622
1835
  * hand-built descriptor. */
1623
- readonly query: <R = Row_4>(descriptor: QueryDescriptor<R>) => Effect.Effect<ReadonlyArray<R>, StoreError>;
1624
- readonly insert: (table: string, row: Row_4) => Effect.Effect<Row_4, StoreError>;
1625
- readonly update: (table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>) => Effect.Effect<Row_4 | null, StoreError>;
1836
+ readonly query: <R = Row>(descriptor: QueryDescriptor<R>) => Effect.Effect<ReadonlyArray<R>, StoreError>;
1837
+ readonly insert: (table: string, row: Row) => Effect.Effect<Row, StoreError>;
1838
+ readonly update: (table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>) => Effect.Effect<Row | null, StoreError>;
1626
1839
  readonly delete: (table: string, primaryKey: string) => Effect.Effect<boolean, StoreError>;
1627
1840
  /** Bypass soft-delete. See `MutationStore.hardDelete`. */
1628
1841
  readonly hardDelete: (table: string, primaryKey: string) => Effect.Effect<boolean, StoreError>;
@@ -1668,6 +1881,13 @@ export declare interface EventsFacadeOptions {
1668
1881
  readonly makeId?: (prefix: 'wfe' | 'wfed') => string;
1669
1882
  }
1670
1883
 
1884
+ /** Exchange an authorization code (from the callback) for a token set. */
1885
+ export declare const exchangeAuthorizationCode: (definition: OAuth2ConnectionDefinition, input: {
1886
+ readonly code: string;
1887
+ readonly redirectUri: string;
1888
+ readonly codeVerifier?: string | null;
1889
+ }, now?: () => number) => Promise<ConnectionTokens>;
1890
+
1671
1891
  /** Build an Effect `ExternalSpan` parent from a `traceparent` header so
1672
1892
  * inbound HTTP work continues the caller's trace. Returns undefined
1673
1893
  * when the header is missing/invalid (no parent → a fresh root span). */
@@ -1709,6 +1929,32 @@ export declare interface FluentStore extends Omit<MutationStore, 'update' | 'del
1709
1929
  * rows off the wire. The type is re-applied here, at the handler boundary.
1710
1930
  */
1711
1931
  query<R = Row_5>(descriptor: QueryDescriptor<R>): Promise<ReadonlyArray<R>>;
1932
+ /**
1933
+ * Terminal: EXACTLY one row, TYPED. Fails with `NoRowFound` on zero matches
1934
+ * and equally on two or more.
1935
+ *
1936
+ * This exists because the two things we recommend did not compose. The typed
1937
+ * read path lives on the typed builder (`database.users.where(...)`), while
1938
+ * `.one()` lived only on the string-keyed `select('users')` builder, which
1939
+ * yields untyped `Row`. So adopting `.one()` meant re-introducing the very
1940
+ * casts the typed path removes — you could have typed rows OR the terminal,
1941
+ * not both. That is not a "last mile" gap for sophisticated apps; it is two
1942
+ * features from the same release failing to meet.
1943
+ *
1944
+ * Takes the builder itself or its `.descriptor` — both read fine:
1945
+ *
1946
+ * const user = await ctx.store.one(database.users.where(eq('id', id)))
1947
+ *
1948
+ * Scoping is identical to `query()` (tenant + soft-delete apply), because it
1949
+ * IS `query()` underneath — the probe just asks for one row more than it
1950
+ * needs so "the first of several" cannot masquerade as "the one you meant".
1951
+ */
1952
+ one<R = Row_5>(query: QueryLike<R>): Promise<R>;
1953
+ /** Terminal: the first matching row or `null`, TYPED. Use when "any match" is
1954
+ * genuinely what you mean. */
1955
+ first<R = Row_5>(query: QueryLike<R>): Promise<R | null>;
1956
+ /** Alias of `first` — the first matching row or `null`, TYPED. */
1957
+ maybeOne<R = Row_5>(query: QueryLike<R>): Promise<R | null>;
1712
1958
  /** Fluent, scope-applying read builder: `select('notes').where(...).all()`. */
1713
1959
  select(table: string): SelectBuilder;
1714
1960
  /** Fluent predicate update: `update('notes').where('id', id).set({...})`. */
@@ -1739,6 +1985,9 @@ export declare interface FluentStoreBackend {
1739
1985
  readonly delete: DataStore['delete'];
1740
1986
  }
1741
1987
 
1988
+ /** Forget the calling subject's credential. Returns whether a row was removed. */
1989
+ export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
1990
+
1742
1991
  /** Serialise a span's context as a `traceparent` header value. */
1743
1992
  export declare const formatTraceparent: (ctx: TraceContext) => string;
1744
1993
 
@@ -1776,6 +2025,9 @@ export declare const gauge: (name: string, description?: string) => Metric.Metri
1776
2025
  /** Generate a fresh token `<prefix><40-char-base64url>`. */
1777
2026
  export declare const generateApiKeyToken: (prefix?: string) => string;
1778
2027
 
2028
+ /** The registered resolver, if the app declared any connections. */
2029
+ export declare const getConnectionResolver: () => ConnectionResolver | undefined;
2030
+
1779
2031
  /** Look up the override for a connection. Returns `undefined` if no override is set. */
1780
2032
  export declare const getConnectionSubject: (clientId: number) => Subject | undefined;
1781
2033
 
@@ -1784,14 +2036,22 @@ export declare const getFieldCipher: () => FieldCipher | undefined;
1784
2036
 
1785
2037
  export declare const getResourcePolicy: (resourceType: string) => ResourcePolicy | undefined;
1786
2038
 
2039
+ /** The registered row filter, or `undefined`. */
2040
+ export declare const getRowFilter: () => RowFilter<never> | undefined;
2041
+
1787
2042
  /** The per-process recorder (created lazily from `VOLTRO_TIMELINE`). The change
1788
2043
  * tap feeds it; the inspect endpoint reads it. */
1789
2044
  export declare const getTimelineRecorder: () => TimelineRecorder;
1790
2045
 
1791
2046
  export declare const getTupleSource: () => TupleSource | undefined;
1792
2047
 
2048
+ /** How long a started handshake stays claimable. Long enough for a slow
2049
+ * consent screen (and a password-manager detour), short enough that a stolen
2050
+ * state value is worthless by the time it is found. */
2051
+ export declare const GRANT_TTL_MS: number;
2052
+
1793
2053
  /** Stable group key from the groupBy column values (JSON to disambiguate types). */
1794
- export declare const groupKeyOf: (row: Row_2, groupBy: ReadonlyArray<string> | undefined) => string;
2054
+ export declare const groupKeyOf: (row: Row_3, groupBy: ReadonlyArray<string> | undefined) => string;
1795
2055
 
1796
2056
  /** Per-group accumulator. `count` is always kept (to know when a group empties
1797
2057
  * and to compute avg); `sum` for sum/avg; `extreme` for min/max. */
@@ -1891,7 +2151,7 @@ export declare class IndexedMatcher implements Matcher {
1891
2151
 
1892
2152
  export declare class InMemoryDataStore implements DataStore {
1893
2153
  #private;
1894
- constructor(initial?: Record<string, ReadonlyArray<Row_4>>, namespace?: string | null);
2154
+ constructor(initial?: Record<string, ReadonlyArray<Row>>, namespace?: string | null);
1895
2155
  /**
1896
2156
  * Return a namespace-bound view. The view shares THIS store's table
1897
2157
  * map, emitter, and id counters (one process, one in-memory database)
@@ -1900,11 +2160,11 @@ export declare class InMemoryDataStore implements DataStore {
1900
2160
  * table that never carried the `tenant()` mixin.
1901
2161
  */
1902
2162
  withNamespace(namespace: string | null): DataStore;
1903
- query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row_4>>;
1904
- insert(table: string, row: Row_4): Promise<Row_4>;
1905
- insertMany(table: string, rows: ReadonlyArray<Row_4>): Promise<ReadonlyArray<Row_4>>;
1906
- patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row_4 | null>;
1907
- update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_4 | null>;
2163
+ query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
2164
+ insert(table: string, row: Row): Promise<Row>;
2165
+ insertMany(table: string, rows: ReadonlyArray<Row>): Promise<ReadonlyArray<Row>>;
2166
+ patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row | null>;
2167
+ update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row | null>;
1908
2168
  delete(table: string, primaryKey: string): Promise<boolean>;
1909
2169
  updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
1910
2170
  where: Predicate;
@@ -1912,13 +2172,13 @@ export declare class InMemoryDataStore implements DataStore {
1912
2172
  deleteMany(table: string, options: {
1913
2173
  where: Predicate;
1914
2174
  }): Promise<number>;
1915
- upsert(table: string, row: Row_4, options: {
2175
+ upsert(table: string, row: Row, options: {
1916
2176
  conflictColumns: ReadonlyArray<string>;
1917
- update?: ReadonlyArray<string> | ((existing: Row_4) => Readonly<Record<string, unknown>>);
1918
- }): Promise<Row_4>;
1919
- insertIgnore(table: string, row: Row_4, options: {
2177
+ update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
2178
+ }): Promise<Row>;
2179
+ insertIgnore(table: string, row: Row, options: {
1920
2180
  conflictColumns: ReadonlyArray<string>;
1921
- }): Promise<Row_4>;
2181
+ }): Promise<Row>;
1922
2182
  transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
1923
2183
  onChange(listener: (event: ChangeEvent) => void): () => void;
1924
2184
  /** Cross-instance reactivity seam — emit an externally-sourced event to
@@ -2143,6 +2403,9 @@ export declare class LinearScanMatcher implements Matcher {
2143
2403
  stats(): MatcherStats;
2144
2404
  }
2145
2405
 
2406
+ /** Every connection the app declares, projected for ONE subject. */
2407
+ export declare const listConnectionStates: (store: VaultStore, registry: ConnectionRegistry, subjectId: string) => Promise<ReadonlyArray<ConnectionState>>;
2408
+
2146
2409
  export declare const listResourcePolicies: () => ReadonlyArray<ResourcePolicy>;
2147
2410
 
2148
2411
  export { listRetentions }
@@ -2275,6 +2538,20 @@ export declare const makeAsyncKv: ({ store, env }: KvFacadeOptions) => Promise<K
2275
2538
  */
2276
2539
  export declare const makeBufferingSpanProcessor: (onSpanEnd: (record: TraceSpanRecordLike) => void) => SpanProcessor;
2277
2540
 
2541
+ /** Build a registry from the discovered declarations. Duplicate ids throw —
2542
+ * two files claiming one id means whichever loaded last silently wins and the
2543
+ * other file looks live but is dead. */
2544
+ export declare const makeConnectionRegistry: (definitions: ReadonlyArray<ConnectionDefinition>) => ConnectionRegistry;
2545
+
2546
+ /** Build the process resolver over a store + registry. The CLI hands the
2547
+ * result to `setConnectionResolver`. */
2548
+ export declare const makeConnectionResolver: (deps: {
2549
+ readonly store: DataStore;
2550
+ readonly registry: ConnectionRegistry;
2551
+ }) => ConnectionResolver;
2552
+
2553
+ export declare const makeConnectionsFacade: (deps: ConnectionsFacadeDeps) => ConnectionsFacade;
2554
+
2278
2555
  /**
2279
2556
  * Factory the CLI binds into the plugin bind-ctx: closes over the
2280
2557
  * process-wide coordinator + replicaId so a plugin calls the ergonomic
@@ -2290,6 +2567,13 @@ export declare const makeCoordinatedScheduler: (deps: CoordinatedScheduleDeps) =
2290
2567
  */
2291
2568
  export declare const makeDataLoader: (deps: LoaderDeps) => DataLoader;
2292
2569
 
2570
+ /** `__voltro.connections.disconnect` — forget the caller's credential. */
2571
+ export declare const makeDisconnectConnectionExecutor: (deps: ConnectionBuiltinDeps) => (input: {
2572
+ readonly connectionId: string;
2573
+ }, ctx: ConnectionExecutorCtx) => Promise<{
2574
+ readonly ok: boolean;
2575
+ }>;
2576
+
2293
2577
  /**
2294
2578
  * Build the Layer that provides `EffectStore` from a `MutationStore`.
2295
2579
  * dev.ts (CLI) and any standalone test setup uses this to wire up the
@@ -2320,6 +2604,9 @@ export declare const makeIdleGate: (graceMs: number, now?: () => number) => Idle
2320
2604
 
2321
2605
  export declare const makeLazyWorkflowFacade: (resolve: () => WorkflowsAppContext | undefined) => WorkflowsAppContext;
2322
2606
 
2607
+ /** `__voltro.connections.list` — every declared connection, for the caller. */
2608
+ export declare const makeListConnectionsExecutor: (deps: ConnectionBuiltinDeps) => (_input: Record<string, never>, ctx: ConnectionExecutorCtx) => Promise<ReadonlyArray<ConnectionState>>;
2609
+
2323
2610
  /**
2324
2611
  * Build the shared mutation runner. Used by BOTH the rpc WS handler and
2325
2612
  * the `/_voltro/inspect/invoke` endpoint (and the prod entrypoint) so they
@@ -2386,6 +2673,23 @@ export declare const makeRouterActivity: () => RouterActivity;
2386
2673
  */
2387
2674
  export declare const makeSchemaRegistry: (tables: ReadonlyArray<RegistryTableLike>) => SchemaRegistry;
2388
2675
 
2676
+ /** `__voltro.connections.start` — mint the consent URL for an oauth2 flow. */
2677
+ export declare const makeStartConnectionExecutor: (deps: ConnectionBuiltinDeps) => (input: {
2678
+ readonly connectionId: string;
2679
+ readonly redirectTo?: string;
2680
+ }, ctx: ConnectionExecutorCtx) => Promise<{
2681
+ readonly authorizeUrl: string;
2682
+ readonly state: string;
2683
+ }>;
2684
+
2685
+ /** `__voltro.connections.submitToken` — store a pasted personal access token. */
2686
+ export declare const makeSubmitTokenExecutor: (deps: ConnectionBuiltinDeps) => (input: {
2687
+ readonly connectionId: string;
2688
+ readonly token: string;
2689
+ }, ctx: ConnectionExecutorCtx) => Promise<{
2690
+ readonly ok: boolean;
2691
+ }>;
2692
+
2389
2693
  /**
2390
2694
  * The factory the serve entrypoints inject into `makeMutationRunner`'s
2391
2695
  * `undoCapture` dep. Collects changes from the wrapped tx; `persist` writes ONE
@@ -2617,6 +2921,10 @@ export declare const nextFiring: (def: ScheduleDefinition, after?: Date) => Date
2617
2921
  */
2618
2922
  export declare const nextWakeup: (store: DataStore) => Promise<Wakeup | null>;
2619
2923
 
2924
+ /** A scope that constrains nothing — the shape used when no filter is
2925
+ * registered, or for a system subject. */
2926
+ export declare const NO_ROW_FILTER: RowFilterScope;
2927
+
2620
2928
  /**
2621
2929
  * The real single-node supervisor: `spawn`s the app command and considers
2622
2930
  * it healthy once `healthUrl` returns 2xx. Stopping sends `stopSignal`
@@ -2711,6 +3019,32 @@ export declare interface Notification {
2711
3019
  readonly newMatched: boolean;
2712
3020
  }
2713
3021
 
3022
+ export declare interface OAuth2ConnectionDefinition extends ConnectionDefinitionCommon {
3023
+ readonly kind: 'oauth2';
3024
+ readonly authorizeUrl: string;
3025
+ readonly tokenUrl: string;
3026
+ readonly clientId: string;
3027
+ /** Runtime secret. Never logged, never sent to the browser, never bundled —
3028
+ * `*.connection.ts` is server-only and is NOT part of the rpcGroup codegen. */
3029
+ readonly clientSecret: string;
3030
+ readonly scopes?: ReadonlyArray<string>;
3031
+ /** Exact redirect URI registered with the provider. Defaults to
3032
+ * `<publicUrl>/_voltro/connections/<id>/callback`. */
3033
+ readonly redirectUri?: string;
3034
+ /** Extra fixed query params on the authorize URL (`audience`, `prompt`, …). */
3035
+ readonly authorizeParams?: Record<string, string>;
3036
+ /** PKCE (S256). On by default — it costs one hash and closes the
3037
+ * authorization-code interception hole for public clients. */
3038
+ readonly pkce?: boolean;
3039
+ /** Renew when the token is within this many ms of expiry. Default 60_000 —
3040
+ * enough headroom that an in-flight request can't race the deadline. */
3041
+ readonly refreshSkewMs?: number;
3042
+ /** Injectable transport (tests, proxies). Defaults to the global `fetch`. */
3043
+ readonly fetchImpl?: ConnectionFetch;
3044
+ /** Resolve who the freshly-minted token belongs to, for display. */
3045
+ readonly identify?: (tokens: ConnectionTokens) => Promise<ConnectionAccount>;
3046
+ }
3047
+
2714
3048
  /**
2715
3049
  * Listen for re-bind events. Returns an unsubscribe function. Used by
2716
3050
  * the dispatcher to re-scope active subscriptions when a connection's
@@ -2742,8 +3076,68 @@ export declare interface OrchestratorTickDeps {
2742
3076
  readonly log?: WakeOrchestratorLogger;
2743
3077
  }
2744
3078
 
3079
+ /**
3080
+ * Retention, part 1 of 2 — the PER-ENTRY cap.
3081
+ *
3082
+ * The automatic path is already bounded by `maxAttempts` (default 8), so the
3083
+ * only way one entry's history grows without limit is repeated manual resends
3084
+ * of the same row — a support workflow that can genuinely run for years. Once an
3085
+ * entry passes this many attempts, recording a new one trims the oldest, so the
3086
+ * log is a bounded ring per entry: the recent attempts are what a history UI
3087
+ * shows, and the 200th-oldest failure of a chronically broken effect is not
3088
+ * worth a row.
3089
+ *
3090
+ * The check is keyed off the attempt NUMBER, which the drain already knows, so
3091
+ * the normal (never-resent) path pays no extra query at all — it can never
3092
+ * reach the cap.
3093
+ *
3094
+ * Part 2 is the time-based bound: the CLI's boot sweep registers a retention
3095
+ * spec over `startedAt` (default 30 days, `VOLTRO_OUTBOX_ATTEMPTS_TTL_HOURS`).
3096
+ * Both exist because they bound different failure modes — the cap stops ONE hot
3097
+ * entry, the TTL stops the accumulation of MANY cold ones — and only the cap
3098
+ * works on every dialect (the sweep is postgres-gated).
3099
+ */
3100
+ export declare const OUTBOX_ATTEMPT_LOG_CAP = 50;
3101
+
3102
+ export declare const OUTBOX_ATTEMPTS_TABLE = "_voltro_outbox_attempts";
3103
+
2745
3104
  export declare const OUTBOX_TABLE = "_voltro_outbox";
2746
3105
 
3106
+ /** Terminal outcome of ONE attempt. `dead` is a failure that also exhausted the
3107
+ * budget — kept separate from `failed` so a listing can render "gave up here"
3108
+ * without cross-referencing the parent row's status. */
3109
+ export declare type OutboxAttemptOutcome = 'delivered' | 'failed' | 'dead';
3110
+
3111
+ /**
3112
+ * One row per delivery ATTEMPT — the queryable history behind `_voltro_outbox`.
3113
+ *
3114
+ * The outbox row alone answers "did this eventually land"; it cannot answer
3115
+ * "what did the remote say on attempt 3", "how long did it take", "who resent
3116
+ * it", which is what a delivery-history UI renders. Columns are chosen from
3117
+ * that render, not from what is convenient to write:
3118
+ *
3119
+ * - a listing row needs: when, which effect, attempt #, outcome, duration,
3120
+ * and (for a failure) the error — all here, no JOIN;
3121
+ * - `effect` / `subjectId` / `tenantId` are DENORMALISED so the history stays
3122
+ * readable (and tenant-scopable) after the parent outbox row is purged by
3123
+ * retention, and so a per-tenant listing needs no join;
3124
+ * - `response` carries the handler's own return value, which is where an
3125
+ * HTTP-shaped handler puts `{ status, body }` — the framework does not
3126
+ * model HTTP, so it snapshots whatever the handler chose to return rather
3127
+ * than inventing status/response columns only some handlers can fill.
3128
+ *
3129
+ * Append-only. Bounded two ways — see `recordAttempt` + the retention
3130
+ * registration in the CLI's boot sweep.
3131
+ */
3132
+ export declare const outboxAttemptsTable: TableLike;
3133
+
3134
+ /** How an attempt came to be made. `automatic` = the drain picked the row up on
3135
+ * its own schedule; `manual` = a human/API asked for it via `resend()`. The
3136
+ * distinction is the point of the field: without it a support-triggered
3137
+ * redelivery is indistinguishable from a backoff retry, and "did we send this
3138
+ * twice because of us or because of them" becomes unanswerable. */
3139
+ export declare type OutboxAttemptTrigger = 'automatic' | 'manual';
3140
+
2747
3141
  export declare interface OutboxFacade {
2748
3142
  /**
2749
3143
  * Persist the intent to run `effect` after this transaction commits.
@@ -2752,12 +3146,36 @@ export declare interface OutboxFacade {
2752
3146
  * watch to render external-side-effect progress ("saving… syncing… synced").
2753
3147
  */
2754
3148
  enqueue(effect: string, payload: Record<string, unknown>, options?: EnqueueOptions): Promise<string>;
3149
+ /**
3150
+ * Re-arm one entry for delivery NOW, on purpose, attributed to the caller.
3151
+ *
3152
+ * Legitimate because the delivery contract is already at-least-once and
3153
+ * handlers must therefore be idempotent — a manual resend is the SAME hazard
3154
+ * the contract already requires handlers to absorb, not a new one. What it is
3155
+ * not allowed to be is invisible: the next attempt is recorded with
3156
+ * `trigger: 'manual'` plus the requesting subject and reason, so an operator
3157
+ * redelivery can never be mistaken for a backoff retry when someone later
3158
+ * asks why the remote saw the effect twice.
3159
+ *
3160
+ * Three deliberate choices:
3161
+ * - It RE-ARMS the existing row rather than enqueuing a new one. A copy
3162
+ * would carry the same `idempotencyKey` (breaking the dedupe invariant),
3163
+ * duplicate the payload, and split one entry's history across two ids.
3164
+ * - It grants a SMALL fresh budget (`attempts`, default 1) instead of
3165
+ * resetting the counter. A dead row has `attempts >= maxAttempts`, so
3166
+ * without this it would re-die untried; and "try again now" is what the
3167
+ * button means — not "restart the whole eight-attempt backoff schedule".
3168
+ * - It refuses a row that is mid-flight (`delivering`), where re-arming
3169
+ * races the in-progress attempt into a same-process double delivery this
3170
+ * layer CAN prevent (unlike the cross-replica one).
3171
+ */
3172
+ resend(outboxId: string, options?: ResendOptions): Promise<void>;
2755
3173
  }
2756
3174
 
2757
3175
  export declare interface OutboxFacadeDeps {
2758
3176
  /** The REQUEST's store. Inside a mutation this is the transactional view —
2759
3177
  * which is what makes the enqueue atomic with the domain write. */
2760
- readonly store: Pick<DataStore, 'insert' | 'query'>;
3178
+ readonly store: Pick<DataStore, 'insert' | 'query' | 'update'>;
2761
3179
  readonly subject: Subject;
2762
3180
  readonly traceId: string | null;
2763
3181
  /** Nudge the drain worker once the transaction commits. Optional: without
@@ -2800,6 +3218,18 @@ declare interface P2COptions {
2800
3218
  * treated as "no parent"). */
2801
3219
  export declare const parseTraceparent: (header: string | undefined | null) => TraceContext | null;
2802
3220
 
3221
+ export declare interface PatConnectionDefinition extends ConnectionDefinitionCommon {
3222
+ readonly kind: 'pat';
3223
+ /** Where to send the user to mint a token — rendered as a help link. */
3224
+ readonly instructionsUrl?: string;
3225
+ /**
3226
+ * Verify a pasted token before storing it. Rejecting here is what turns
3227
+ * "the user typed it wrong" into an immediate error instead of a mysterious
3228
+ * 401 from a background job three hours later. Throwing fails the submit.
3229
+ */
3230
+ readonly validate?: (token: string) => Promise<ConnectionAccount>;
3231
+ }
3232
+
2803
3233
  /** The 3-arg signature a plugin sees on its bind-ctx. */
2804
3234
  export declare type PluginScheduleCoordinated = (name: string, intervalMs: number, effect: () => void | Promise<void>) => CoordinatedScheduleHandle;
2805
3235
 
@@ -2827,6 +3257,11 @@ export declare interface PreviewStore {
2827
3257
  transactional: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>;
2828
3258
  }
2829
3259
 
3260
+ /** Project a stored row into the wire shape. Deliberately total over the
3261
+ * columns the descriptor declares — and deliberately silent about the token
3262
+ * columns, which have no wire representation at all. */
3263
+ export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow | undefined) => ConnectionState;
3264
+
2830
3265
  /** Exact-match property filter. Values are AND'd. */
2831
3266
  export declare type PropertyFilter = Readonly<Record<string, string | number | boolean | null>>;
2832
3267
 
@@ -2903,6 +3338,12 @@ export declare interface PublicApiKey {
2903
3338
  readonly createdAt: number;
2904
3339
  }
2905
3340
 
3341
+ /** A typed builder or its descriptor — the single-row terminals accept either,
3342
+ * so a call site never has to reach for `.descriptor` just to use them. */
3343
+ export declare type QueryLike<R = Row_5> = QueryDescriptor<R> | {
3344
+ readonly descriptor: QueryDescriptor<R>;
3345
+ };
3346
+
2906
3347
  export declare interface QueryProducerDeps<D> {
2907
3348
  readonly buildContext: (request: ServeRequestContext) => {
2908
3349
  readonly store: unknown;
@@ -3071,7 +3512,7 @@ export declare const recordTimelineEvent: (change: CdcChange & {
3071
3512
  }) => void;
3072
3513
 
3073
3514
  /** Blank sensitive-looking columns. Returns a new object; null passes through. */
3074
- export declare const redactRow: (row: Row | null | undefined) => Row | null;
3515
+ export declare const redactRow: (row: Row_2 | null | undefined) => Row_2 | null;
3075
3516
 
3076
3517
  /**
3077
3518
  * Options for `redisRywPositionStore`. The store needs TWO Redis
@@ -3152,6 +3593,15 @@ export declare const redoUndoInvocation: (input: {
3152
3593
  readonly ok: boolean;
3153
3594
  }>;
3154
3595
 
3596
+ /** How long a refresh claim is held before another replica may take it. Long
3597
+ * enough for a slow token endpoint, short enough that a crashed replica does
3598
+ * not block renewal for long. */
3599
+ export declare const REFRESH_LEASE_MS = 30000;
3600
+
3601
+ /** Swap a refresh token for a fresh set. Providers that ROTATE return a new
3602
+ * refresh token; the caller persists whatever comes back. */
3603
+ export declare const refreshAccessToken: (definition: OAuth2ConnectionDefinition, refreshToken: string, now?: () => number) => Promise<ConnectionTokens>;
3604
+
3155
3605
  /**
3156
3606
  * Register an interrupt for a stream bound to `clientId`. Returns a
3157
3607
  * deregister fn the stream's scope finalizer must call on normal end (so
@@ -3315,13 +3765,13 @@ export declare class ReplicatedDataStore implements DataStore {
3315
3765
  private readonly recordRouting?;
3316
3766
  private readonly now;
3317
3767
  constructor(options: ReplicatedDataStoreOptions);
3318
- query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row_4>>;
3768
+ query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
3319
3769
  private routeToReplica;
3320
3770
  private routeWaitMode;
3321
- insert(table: string, row: Row_4): Promise<Row_4>;
3322
- insertMany(table: string, rows: ReadonlyArray<Row_4>): Promise<ReadonlyArray<Row_4>>;
3323
- patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row_4 | null>;
3324
- update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_4 | null>;
3771
+ insert(table: string, row: Row): Promise<Row>;
3772
+ insertMany(table: string, rows: ReadonlyArray<Row>): Promise<ReadonlyArray<Row>>;
3773
+ patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row | null>;
3774
+ update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row | null>;
3325
3775
  delete(table: string, primaryKey: string): Promise<boolean>;
3326
3776
  updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
3327
3777
  where: Predicate;
@@ -3329,13 +3779,13 @@ export declare class ReplicatedDataStore implements DataStore {
3329
3779
  deleteMany(table: string, options: {
3330
3780
  where: Predicate;
3331
3781
  }): Promise<number>;
3332
- upsert(table: string, row: Row_4, options: {
3782
+ upsert(table: string, row: Row, options: {
3333
3783
  conflictColumns: ReadonlyArray<string>;
3334
- update?: ReadonlyArray<string> | ((existing: Row_4) => Readonly<Record<string, unknown>>);
3335
- }): Promise<Row_4>;
3336
- insertIgnore(table: string, row: Row_4, options: {
3784
+ update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
3785
+ }): Promise<Row>;
3786
+ insertIgnore(table: string, row: Row, options: {
3337
3787
  conflictColumns: ReadonlyArray<string>;
3338
- }): Promise<Row_4>;
3788
+ }): Promise<Row>;
3339
3789
  transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
3340
3790
  onChange(listener: (event: ChangeEvent) => void): () => void;
3341
3791
  /** Cross-instance reactivity seam — delegates to the primary, where the
@@ -3399,12 +3849,49 @@ export declare interface ReplicatedDataStoreOptions {
3399
3849
  readonly now?: () => number;
3400
3850
  }
3401
3851
 
3852
+ export declare interface ResendOptions {
3853
+ /** Operator's note, recorded on the resulting attempt row. */
3854
+ readonly reason?: string;
3855
+ /** How many attempts the resend is worth. Default 1 — see `resend`. */
3856
+ readonly attempts?: number;
3857
+ }
3858
+
3402
3859
  /** Test/dev-only — clear ALL overrides. Don't call from app code. */
3403
3860
  export declare const _resetConnectionSubjectsForTest: () => void;
3404
3861
 
3405
3862
  /** Reset to the env default (tests). */
3406
3863
  export declare const resetSecretsBackend: () => void;
3407
3864
 
3865
+ /**
3866
+ * Resolve the calling subject's credential for one connection, refreshed.
3867
+ *
3868
+ * This is what a plugin or handler asks for. It never returns an expired
3869
+ * token: either it hands back one that is valid past the skew window, or it
3870
+ * throws. There is no "here's a token, good luck" third case, because the
3871
+ * whole reason apps hand-roll a retry-on-401 is that such a case exists.
3872
+ */
3873
+ export declare const resolveConnection: (deps: ResolveConnectionDeps) => Promise<ResolvedConnection>;
3874
+
3875
+ export declare interface ResolveConnectionDeps {
3876
+ readonly store: VaultStore;
3877
+ readonly definition: ConnectionDefinition;
3878
+ readonly subjectId: string;
3879
+ readonly now?: () => number;
3880
+ /** Injectable sleep so the wait-for-another-replica path is testable without
3881
+ * real time. */
3882
+ readonly sleep?: (ms: number) => Promise<void>;
3883
+ }
3884
+
3885
+ /**
3886
+ * Resolve one subject's credential from anywhere on the server — a plugin's
3887
+ * `credentialsResolver`, a cron, an outbox handler.
3888
+ *
3889
+ * Throws (rather than returning null) when no vault is registered: an app that
3890
+ * asks for a connection it never declared has a configuration bug, and
3891
+ * returning "not connected" would present it as a user problem.
3892
+ */
3893
+ export declare const resolveConnectionForSubject: (subject: Pick<Subject, "id">, connectionId: string) => Promise<ResolvedConnection>;
3894
+
3408
3895
  /**
3409
3896
  * Resolve the current process's region from the environment. Checks,
3410
3897
  * in priority order:
@@ -3419,6 +3906,27 @@ export declare const resetSecretsBackend: () => void;
3419
3906
  */
3420
3907
  export declare const resolveCurrentRegion: (env?: NodeJS.ProcessEnv) => string | undefined;
3421
3908
 
3909
+ /** What the auth strategy learns from a presented key. */
3910
+ export declare interface ResolvedApiKey {
3911
+ readonly id: string;
3912
+ readonly tenantId: string;
3913
+ /** The user who created the key, when one is recorded. Null for a key minted
3914
+ * outside a user session (bootstrap / admin tooling). */
3915
+ readonly createdBy: string | null;
3916
+ readonly scopes: ReadonlyArray<string>;
3917
+ }
3918
+
3919
+ /** A usable credential, handed to a plugin / handler. */
3920
+ export declare interface ResolvedConnection {
3921
+ readonly connectionId: string;
3922
+ readonly kind: ConnectionKind;
3923
+ readonly accessToken: string;
3924
+ readonly expiresAt: Date | null;
3925
+ readonly scopes: ReadonlyArray<string>;
3926
+ readonly accountId: string | null;
3927
+ readonly accountLabel: string | null;
3928
+ }
3929
+
3422
3930
  /**
3423
3931
  * Walk a query descriptor's eager-load tree and return the FULL set
3424
3932
  * of tables whose changes could affect the resolved snapshot.
@@ -3437,10 +3945,26 @@ export declare const resolveCurrentRegion: (env?: NodeJS.ProcessEnv) => string |
3437
3945
  */
3438
3946
  export declare const resolveDependentTables: (descriptor: QueryDescriptor) => ReadonlySet<string>;
3439
3947
 
3948
+ export declare const resolveRedirectUri: (definition: OAuth2ConnectionDefinition, publicUrl: string) => string;
3949
+
3440
3950
  /** Rebuild the flagged groups from the (already-committed) base rows + merge —
3441
3951
  * the caller's resolution of a min/max rescan signal. `baseRows` is the full
3442
3952
  * current table (or at least every row in the rescan groups). */
3443
- export declare const resolveRescan: (shape: AggregateShape, state: IvmState, rescanGroups: ReadonlyArray<string>, baseRows: ReadonlyArray<Row_2>) => IvmState;
3953
+ export declare const resolveRescan: (shape: AggregateShape, state: IvmState, rescanGroups: ReadonlyArray<string>, baseRows: ReadonlyArray<Row_3>) => IvmState;
3954
+
3955
+ /**
3956
+ * Resolve the request-scoped filter for `subject`.
3957
+ *
3958
+ * Fails CLOSED in the one way that matters: if `load` throws, every constrained
3959
+ * read is refused rather than silently unfiltered. A row filter that degrades to
3960
+ * "no filter" under load failure is worse than none, because the system keeps
3961
+ * serving and nothing looks wrong.
3962
+ *
3963
+ * Refusal is expressed as a predicate that matches nothing, not as an error, so
3964
+ * a failure surfaces as an empty result rather than a 500 on every page — and
3965
+ * the failure itself is reported through `onError` so it cannot pass unnoticed.
3966
+ */
3967
+ export declare const resolveRowFilterScope: (subject: Subject, onError?: (error: unknown) => void) => Effect.Effect<RowFilterScope>;
3444
3968
 
3445
3969
  /** Resolve a secret through the active backend. Falls back to `process.env`
3446
3970
  * when the backend returns nothing, so a partially-populated remote backend
@@ -3483,7 +4007,7 @@ export { retentionTtlMsFromEnv }
3483
4007
  * - delete → the row was removed, so add `old` back.
3484
4008
  * Pure. Applied newest-first to walk from "now" to a past point.
3485
4009
  */
3486
- export declare const reverseEventOverRows: (rows: ReadonlyArray<Row>, e: TimelineEvent) => ReadonlyArray<Row>;
4010
+ export declare const reverseEventOverRows: (rows: ReadonlyArray<Row_2>, e: TimelineEvent) => ReadonlyArray<Row_2>;
3487
4011
 
3488
4012
  /** The ids that were visible before but are not now — what a live-revocation
3489
4013
  * delta removes from an open subscription. */
@@ -3521,12 +4045,14 @@ export declare interface RoutingContext {
3521
4045
  readonly forcePrimary?: boolean;
3522
4046
  }
3523
4047
 
3524
- declare type Row = Record<string, unknown>;
4048
+ export { Row }
3525
4049
 
3526
- declare type Row_2 = Readonly<Record<string, unknown>>;
4050
+ declare type Row_2 = Record<string, unknown>;
3527
4051
 
3528
4052
  declare type Row_3 = Readonly<Record<string, unknown>>;
3529
4053
 
4054
+ declare type Row_4 = Readonly<Record<string, unknown>>;
4055
+
3530
4056
  declare type Row_5 = Readonly<Record<string, unknown>>;
3531
4057
 
3532
4058
  export declare interface RowDiff {
@@ -3537,6 +4063,28 @@ export declare interface RowDiff {
3537
4063
  readonly fields: Readonly<Record<string, FieldChange>>;
3538
4064
  }
3539
4065
 
4066
+ /**
4067
+ * The per-request filter, resolved from a subject.
4068
+ *
4069
+ * `Ctx` is whatever your `load` returns — typically the ids the predicate needs
4070
+ * (team ids, project ids, a role map). The framework never inspects it.
4071
+ */
4072
+ export declare interface RowFilter<Ctx = unknown> {
4073
+ /** Resolve everything the predicates need, ONCE per request. May read the
4074
+ * store. A failure DENIES (see `resolveRowFilterContext`). */
4075
+ readonly load: (subject: Subject) => Effect.Effect<Ctx, unknown>;
4076
+ /**
4077
+ * The filter for one table, derived purely from the loaded context. Return
4078
+ * `undefined` for a table this filter does not constrain — most tables.
4079
+ *
4080
+ * Must be PURE and SYNC: it runs on every read.
4081
+ */
4082
+ readonly predicate: (ctx: Ctx, table: string) => Predicate | undefined;
4083
+ }
4084
+
4085
+ /** What a scoped store needs: a sync `table → Predicate?` lookup. */
4086
+ export declare type RowFilterScope = (table: string) => Predicate | undefined;
4087
+
3540
4088
  /** One `_voltro_row_history` row, structurally (from @voltro/plugin-versioning). */
3541
4089
  export declare interface RowHistoryEntry {
3542
4090
  readonly value?: unknown;
@@ -3731,6 +4279,23 @@ export declare interface RuntimeContext {
3731
4279
  */
3732
4280
  export declare const runWakeLoop: (deps: WakeOrchestratorDeps, intervalMs?: number) => WakeLoopHandle;
3733
4281
 
4282
+ /**
4283
+ * Run a full-transaction thunk, replaying it on a transient DB error
4284
+ * (deadlock / lock-wait). Non-transient failures throw on the first try.
4285
+ * Backoff escalates with random jitter so concurrent victims don't
4286
+ * re-collide in lockstep.
4287
+ *
4288
+ * `delay` overrides that backoff. The only caller that does is the unit-test
4289
+ * harness (`@voltro/testing`'s `invoke`), which reproduces the replay against
4290
+ * an in-memory store: there is no lock manager to de-correlate against there,
4291
+ * so a real sleep would buy nothing and would stall a suite using fake timers.
4292
+ * Exported for exactly that — one retry implementation, not a second one that
4293
+ * drifts from this classification.
4294
+ */
4295
+ export declare const runWithDeadlockRetry: <T>(work: () => Promise<T>, options?: {
4296
+ readonly delay?: (attempt: number) => Promise<void>;
4297
+ }) => Promise<T>;
4298
+
3734
4299
  /**
3735
4300
  * Run `work` inside a routing scope. Every `query()` call against a
3736
4301
  * `ReplicatedDataStore` from inside `work` (or its async children)
@@ -3781,6 +4346,14 @@ export declare interface RywPositionStore {
3781
4346
  size(): number;
3782
4347
  }
3783
4348
 
4349
+ /**
4350
+ * Reject anything but a same-origin path. The callback ends in a 302 to this
4351
+ * value, so accepting `https://evil.example` would turn every app that
4352
+ * declares a connection into an open redirector — a phishing primitive handed
4353
+ * out for free with a feature about credentials.
4354
+ */
4355
+ export declare const sanitizeRedirectTo: (value: string | undefined) => string;
4356
+
3784
4357
  /** Brand so the CLI's discovery loop can duck-type a `*.cron.tsx`
3785
4358
  * default export without importing the concrete class. */
3786
4359
  declare const SCHEDULE_BRAND: unique symbol;
@@ -4085,6 +4658,8 @@ export declare interface SchemaRegistry {
4085
4658
  declare interface ScopeCtx {
4086
4659
  readonly subject: Subject;
4087
4660
  readonly schemaRegistry: SchemaRegistry;
4661
+ /** Row-level security scope for this request — see `./rowFilter`. */
4662
+ readonly rowFilter?: RowFilterScope;
4088
4663
  }
4089
4664
 
4090
4665
  export declare interface SecretsBackend {
@@ -4108,7 +4683,7 @@ export declare class SelectBuilder {
4108
4683
  private query;
4109
4684
  private skipTenant;
4110
4685
  private skipSoftDelete;
4111
- constructor(backend: FluentStoreBackend, scope: ScopeCtx, table: string, query: Query<Row_4>, skipTenant?: boolean, skipSoftDelete?: boolean);
4686
+ constructor(backend: FluentStoreBackend, scope: ScopeCtx, table: string, query: Query<Row>, skipTenant?: boolean, skipSoftDelete?: boolean);
4112
4687
  private clone;
4113
4688
  /** `.where(eq(...))` (predicate AST) OR `.where(col, value)` /
4114
4689
  * `.where(col, op, value)` (ergonomic). Multiple calls AND-merge. */
@@ -4118,7 +4693,7 @@ export declare class SelectBuilder {
4118
4693
  offset(n: number): SelectBuilder;
4119
4694
  select(...columns: ReadonlyArray<string>): SelectBuilder;
4120
4695
  using(indexName: string): SelectBuilder;
4121
- with(spec: Parameters<Query<Row_4>['with']>[0]): SelectBuilder;
4696
+ with(spec: Parameters<Query<Row>['with']>[0]): SelectBuilder;
4122
4697
  groupBy(columns: ReadonlyArray<string>): SelectBuilder;
4123
4698
  matching(indexName: string, query: string): SelectBuilder;
4124
4699
  /** Drop the automatic tenant filter (cross-tenant staff reads). */
@@ -4127,11 +4702,11 @@ export declare class SelectBuilder {
4127
4702
  withDeleted(): SelectBuilder;
4128
4703
  private scopedQuery;
4129
4704
  /** Terminal: all matching rows. */
4130
- all(): Promise<ReadonlyArray<Row_4>>;
4705
+ all(): Promise<ReadonlyArray<Row>>;
4131
4706
  /** Terminal: the first matching row or `null`. */
4132
- maybeOne(): Promise<Row_4 | null>;
4707
+ maybeOne(): Promise<Row | null>;
4133
4708
  /** Alias of `maybeOne` — the first row or `null`. */
4134
- first(): Promise<Row_4 | null>;
4709
+ first(): Promise<Row | null>;
4135
4710
  /**
4136
4711
  * Terminal: EXACTLY one row. Fails with `NoRowFound` on zero matches —
4137
4712
  * and equally on two or more.
@@ -4143,7 +4718,7 @@ export declare class SelectBuilder {
4143
4718
  * wire buys a loud failure at the point the assumption breaks. Use
4144
4719
  * `.first()` / `.maybeOne()` when you genuinely want "any match".
4145
4720
  */
4146
- one(): Promise<Row_4>;
4721
+ one(): Promise<Row>;
4147
4722
  /** Terminal: COUNT(*) of matching rows (real aggregate, not a fetch). */
4148
4723
  count(): Promise<number>;
4149
4724
  /** Terminal: does any row match? Uses a `LIMIT 1` probe. */
@@ -4156,6 +4731,16 @@ export declare interface ServeRequestContext {
4156
4731
  readonly subject: unknown;
4157
4732
  readonly traceId: string;
4158
4733
  readonly spanId?: string;
4734
+ /**
4735
+ * Row-level visibility for this request, resolved by `withRowFilter` and read
4736
+ * by the context builder when it wraps the store.
4737
+ *
4738
+ * Carried on the REQUEST because resolving it may read the database while the
4739
+ * context builder is synchronous. Making the builder async would ripple
4740
+ * through every entrypoint; resolving here, already inside an async
4741
+ * boundary, does not.
4742
+ */
4743
+ readonly rowFilter?: RowFilterScope;
4159
4744
  }
4160
4745
 
4161
4746
  /** A plugin interceptor — wraps the base run Effect (Effect-native chain). */
@@ -4168,9 +4753,19 @@ export declare type ServeRpcInterceptor = (base: Effect.Effect<unknown, unknown,
4168
4753
  readonly spanId?: string;
4169
4754
  }) => Effect.Effect<unknown, unknown, never>;
4170
4755
 
4756
+ /** Register the process-wide connection resolver (or clear with `undefined`).
4757
+ * Called by the CLI at boot; tests call it directly. */
4758
+ export declare const setConnectionResolver: (resolver: ConnectionResolver | undefined) => void;
4759
+
4171
4760
  /** Register the field cipher (or clear with `undefined`). */
4172
4761
  export declare const setFieldCipher: (cipher: FieldCipher | undefined) => void;
4173
4762
 
4763
+ /**
4764
+ * Register (or clear, with `undefined`) the process-global row filter. Call
4765
+ * once at boot. Last write wins.
4766
+ */
4767
+ export declare const setRowFilter: <Ctx>(filter: RowFilter<Ctx> | undefined) => void;
4768
+
4174
4769
  /** Install the process-wide secrets backend (called once at boot). */
4175
4770
  export declare const setSecretsBackend: (backend: SecretsBackend) => void;
4176
4771
 
@@ -4246,10 +4841,10 @@ export declare interface SingleNodeOrchestratorOptions {
4246
4841
  * tables invalidates the entry via the invalidation bus.
4247
4842
  */
4248
4843
  export declare interface SnapshotCache {
4249
- readonly wrap: (binding: SnapshotCacheBinding, tags: ReadonlyArray<string>, compute: () => Promise<ReadonlyArray<Row_4>>) => Promise<ReadonlyArray<Row_4>>;
4844
+ readonly wrap: (binding: SnapshotCacheBinding, tags: ReadonlyArray<string>, compute: () => Promise<ReadonlyArray<Row>>) => Promise<ReadonlyArray<Row>>;
4250
4845
  /** Write-through from the live recompute, so the cache is never staler
4251
4846
  * than the freshest live subscriber. Fire-and-forget. */
4252
- readonly put: (binding: SnapshotCacheBinding, tags: ReadonlyArray<string>, rows: ReadonlyArray<Row_4>) => void;
4847
+ readonly put: (binding: SnapshotCacheBinding, tags: ReadonlyArray<string>, rows: ReadonlyArray<Row>) => void;
4253
4848
  }
4254
4849
 
4255
4850
  /**
@@ -4305,6 +4900,26 @@ export declare interface StartupContext {
4305
4900
  /** The default export shape every `*.startup.ts` must provide. */
4306
4901
  export declare type StartupFn = (ctx: StartupContext) => void | Promise<void>;
4307
4902
 
4903
+ /**
4904
+ * Write (or replace) the calling subject's credential, encrypted.
4905
+ *
4906
+ * `upsert` on the `(connectionId, subjectId)` unique keeps re-connecting a
4907
+ * replace rather than an accumulation of stale rows — which matters because a
4908
+ * second row would make "which token is current" a question the resolver would
4909
+ * have to answer with a heuristic.
4910
+ */
4911
+ export declare const storeCredential: (input: StoreCredentialInput) => Promise<Row>;
4912
+
4913
+ export declare interface StoreCredentialInput {
4914
+ readonly store: VaultStore;
4915
+ readonly definition: ConnectionDefinition;
4916
+ readonly subjectId: string;
4917
+ readonly tenantId: string | null;
4918
+ readonly tokens: ConnectionTokens;
4919
+ readonly account?: ConnectionAccount;
4920
+ readonly now?: () => Date;
4921
+ }
4922
+
4308
4923
  /**
4309
4924
  * Discriminated union of all framework-owned store errors. Use it on
4310
4925
  * a mutation's `error:` schema when you want every kind surfaced
@@ -4322,6 +4937,14 @@ export declare interface StoreMiddlewareContext {
4322
4937
  * natively, or there are no array columns to translate).
4323
4938
  */
4324
4939
  readonly dialect?: DialectId;
4940
+ /**
4941
+ * Row-level security scope for THIS request's subject, resolved once by the
4942
+ * context builder (`resolveRowFilterScope`). Omitted → no row filtering,
4943
+ * which is right when the app registered none. A filter that failed to load
4944
+ * arrives here as a deny-all scope, never as `undefined` — the difference
4945
+ * between "nothing to filter" and "we could not tell" must not collapse.
4946
+ */
4947
+ readonly rowFilter?: RowFilterScope;
4325
4948
  }
4326
4949
 
4327
4950
  /**
@@ -4458,6 +5081,11 @@ export declare interface SubscriptionSnapshot {
4458
5081
  readonly subscriberCount: number;
4459
5082
  }
4460
5083
 
5084
+ /** Drop handshake grants past their TTL. Wired into the boot retention sweep;
5085
+ * a grant is worthless the moment it expires and keeping them turns a hot
5086
+ * table into an audit liability. */
5087
+ export declare const sweepExpiredGrants: (store: VaultStore, now?: Date) => Promise<number>;
5088
+
4461
5089
  export { sweepRetention }
4462
5090
 
4463
5091
  /**
@@ -4557,8 +5185,8 @@ export declare interface TimelineEvent {
4557
5185
  readonly table: string;
4558
5186
  readonly op: 'insert' | 'update' | 'delete';
4559
5187
  /** Redacted row images. `old` present on update/delete; `new` on insert/update. */
4560
- readonly old: Row | null;
4561
- readonly new: Row | null;
5188
+ readonly old: Row_2 | null;
5189
+ readonly new: Row_2 | null;
4562
5190
  readonly tenantId: string | null;
4563
5191
  }
4564
5192
 
@@ -4602,7 +5230,7 @@ export declare class TimelineRecorder {
4602
5230
  * Read-only. Correct as long as the ring still holds the events after
4603
5231
  * `asOfSeq` (bounded by `capacity`).
4604
5232
  */
4605
- replay(table: string, asOfSeq: number, currentRows: ReadonlyArray<Row>): ReadonlyArray<Row>;
5233
+ replay(table: string, asOfSeq: number, currentRows: ReadonlyArray<Row_2>): ReadonlyArray<Row_2>;
4606
5234
  }
4607
5235
 
4608
5236
  /** Time window. `to` defaults to now. */
@@ -4898,6 +5526,8 @@ export declare const useAggregate: <Row>(def: AggregateDefinition<Row>) => Effec
4898
5526
  */
4899
5527
  export declare const useAnalytics: () => Effect.Effect<AnalyticsSinkImpl, never, AnalyticsSink>;
4900
5528
 
5529
+ declare type VaultStore = Pick<DataStore, 'query' | 'insert' | 'update' | 'updateMany' | 'delete' | 'upsert'>;
5530
+
4901
5531
  /** The pure core of live revocation: given a subject + action + a row-set,
4902
5532
  * return ONLY the rows the subject may still see. The reactive matcher calls
4903
5533
  * this on a permission-changing write and diffs against the prior visible set;
@@ -4919,6 +5549,39 @@ export declare const VOLTRO_SOFT_DELETE_MIXIN_ID: "voltro/softDelete";
4919
5549
 
4920
5550
  export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";
4921
5551
 
5552
+ /**
5553
+ * `_voltro_connection_grants` — an IN-FLIGHT oauth2 handshake. Separate from
5554
+ * the credential table on purpose: a handshake is a short-lived, single-use
5555
+ * anti-CSRF token with a different lifecycle (seconds, deleted on use) from a
5556
+ * credential (months, updated in place). Folding it into a `status:'pending'`
5557
+ * row on the credential table would mean a failed handshake leaves a row that
5558
+ * the resolver + the list projection both have to learn to ignore.
5559
+ *
5560
+ * `state` is what carries identity across the redirect: the browser comes back
5561
+ * from the provider with no session guarantee, so the row — minted for a known
5562
+ * subject before the redirect — is the only trustworthy statement of who the
5563
+ * arriving code belongs to.
5564
+ */
5565
+ export declare const _voltroConnectionGrantsTable: TableLike;
5566
+
5567
+ /**
5568
+ * `_voltro_connections` — ONE row per (connection, subject). The stored
5569
+ * credential.
5570
+ *
5571
+ * `accessToken` / `refreshToken` hold CIPHERTEXT (`enc:v1:…`), never the raw
5572
+ * token. They are plain `text()` rather than `.encrypted()` columns on purpose:
5573
+ * `.encrypted()` decrypts inside the `ctx.store` mixin middleware, and the
5574
+ * vault must be readable from the resolver path, the OAuth callback route, and
5575
+ * the refresh worker — none of which hold a request-scoped `ctx.store`. Using
5576
+ * `encryptField` / `decryptField` explicitly keeps ONE cipher and one key while
5577
+ * letting every one of those paths read the column. (Same reason the api-key
5578
+ * and session paths use the standalone helpers.)
5579
+ *
5580
+ * Reactive so `__voltro.connections.list` updates an open settings page the
5581
+ * moment a callback lands, with no polling.
5582
+ */
5583
+ export declare const _voltroConnectionsTable: TableLike;
5584
+
4922
5585
  /** `_voltro_rebac_tuples` — the relationship tuples `can()` reads. One row =
4923
5586
  * "`subjectId` has `relation` on `<resourceType>:<resourceId>`". Grant by
4924
5587
  * inserting; revoke by deleting (a delete is the live-revocation trigger). */
@@ -5047,6 +5710,17 @@ export declare type WebhooksAppContext = unknown;
5047
5710
  * builder. */
5048
5711
  export declare type WhereOp = '=' | '!=' | '<>' | '>' | '>=' | '<' | '<=' | 'in' | 'like' | 'contains' | 'fts';
5049
5712
 
5713
+ /**
5714
+ * Resolve this request's row-filter scope. A no-op (same object back) when the
5715
+ * app registered no filter, so an app that uses none pays nothing.
5716
+ *
5717
+ * Every runner calls this before building a context. A read path that skipped
5718
+ * it would have no row-level security and would look exactly like one that
5719
+ * does — which is why it lives in the shared spine rather than in each
5720
+ * entrypoint.
5721
+ */
5722
+ export declare const withRowFilter: <R extends ServeRequestContext>(request: R) => Promise<R>;
5723
+
5050
5724
  export declare interface WorkflowCallerContext {
5051
5725
  readonly subject?: unknown;
5052
5726
  readonly traceId?: string | null;