@voltro/protocol 0.33.0 → 0.35.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
@@ -31,11 +31,19 @@ export declare interface ActionProcedureDescriptor<Name extends string, Input ex
31
31
  readonly target: TargetSpec | ReadonlyArray<TargetSpec> | undefined;
32
32
  /** Declarative authorization guard(s) — enforced before the executor runs,
33
33
  * failing with a typed `ScopeError`. Absent → no framework-level authz. */
34
- readonly guards: Guards | undefined;
34
+ readonly guards: DeclaredAccess | undefined;
35
+ /** The declared reason this procedure needs NO authorization check —
36
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
37
+ * the only two shapes `security.defaultDeny` accepts. */
38
+ readonly openAccess: string | undefined;
35
39
  /** Opt this action into a public REST endpoint (innovation/11). */
36
40
  readonly publicApi: PublicApiSpec | undefined;
37
41
  /** Opt this action into the auto-synthesized agent toolset (innovation/07). */
38
42
  readonly exposeAsTool: ExposeAsTool | undefined;
43
+ /** Require a SECOND human to approve before this action takes effect. The gate
44
+ * runs in the dispatch spine after `guards:` and BEFORE the executor's
45
+ * external I/O — the only point at which nothing has happened yet. */
46
+ readonly requiresApproval: AnyApprovalPolicy | undefined;
39
47
  /** True when the procedure is kept OFF the wire — no client-group entry and no
40
48
  * route in dev or serve. See `internal` on the definer's options. */
41
49
  /**
@@ -73,8 +81,16 @@ export declare const advisoryResourceGuardWarning: (tags: ReadonlyArray<string>)
73
81
 
74
82
  export declare const anonymousSubject: (tenantId: string | null) => Subject;
75
83
 
76
- /** A guard entry is either a scope check or a relationship check. */
77
- export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec;
84
+ /** The erased policy a descriptor carries (input generic dropped). */
85
+ export declare interface AnyApprovalPolicy {
86
+ readonly approvers: Guards;
87
+ readonly expiresIn?: string;
88
+ readonly reason?: string;
89
+ }
90
+
91
+ /** A guard entry is a scope check, a relationship check, or a declared
92
+ * no-check-needed decision. */
93
+ export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec | OpenAccessSpec;
78
94
 
79
95
  /** Any declared event, with the generics erased — for registries and audits. */
80
96
  export declare type AnyEventDescriptor = EventDescriptor<string, Schema.Schema.Any, Schema.Schema.Any>;
@@ -113,6 +129,13 @@ export declare const APIKEY_ISSUE_OTHER_SCOPE = "apikeys:issue:other";
113
129
  */
114
130
  export declare const APIKEY_ISSUE_SELF_SCOPE = "apikeys:issue:self";
115
131
 
132
+ /** The subject a decision produces, plus what the decision took away. `removed`
133
+ * is never non-empty for a `grant`. */
134
+ export declare interface AppliedScopes {
135
+ readonly subject: Subject;
136
+ readonly removed: ReadonlyArray<string>;
137
+ }
138
+
116
139
  /**
117
140
  * Apply a `RowPatch` to `prev`, producing `next`. Exact inverse of
118
141
  * `diffRows`: `applyRowPatch(prev, diffRows(prev, next))` deep-equals
@@ -128,6 +151,275 @@ export declare const APIKEY_ISSUE_SELF_SCOPE = "apikeys:issue:self";
128
151
  */
129
152
  export declare const applyRowPatch: (prev: ReadonlyArray<PatchRow>, patch: RowPatch) => ReadonlyArray<PatchRow>;
130
153
 
154
+ /**
155
+ * Apply a decision to a Subject, touching nothing but `scopes`.
156
+ *
157
+ * An `anonymous` subject has no `scopes` field in its schema and is returned
158
+ * untouched under BOTH kinds — writing one onto it would produce a value that
159
+ * no longer decodes as the Subject it claims to be.
160
+ */
161
+ export declare const applyScopeDecision: (subject: Subject, decision: Exclude<ScopeDecision, {
162
+ kind: "unavailable";
163
+ }>) => AppliedScopes;
164
+
165
+ /** The union the `__voltro.approvals.decide` built-in advertises. */
166
+ export declare const ApprovalDecisionErrors: Schema.Union<[typeof ApprovalNotFound, typeof ApprovalSelfApproval, typeof ApprovalExpired, typeof ApprovalNotPending, typeof ApprovalForbidden, typeof ApprovalUnavailable]>;
167
+
168
+ /**
169
+ * The union every approval-requiring descriptor advertises on the wire.
170
+ *
171
+ * Merged in `mutationToRpc` / `actionToRpc` at the SAME lifter the server group
172
+ * and the generated client group both call — the identical argument
173
+ * `withGuardError` makes for `ScopeError`: a denial the framework can produce
174
+ * before the executor MUST be in the error union or it crosses as an untyped
175
+ * defect and the client cannot branch on it.
176
+ */
177
+ export declare const ApprovalErrors: Schema.Union<[typeof ApprovalRequired, typeof ApprovalRejected, typeof ApprovalExpired, typeof ApprovalUnavailable]>;
178
+
179
+ /** Past `expiresAt`. Fails closed: neither approvable nor executable. */
180
+ export declare class ApprovalExpired extends ApprovalExpired_base {
181
+ }
182
+
183
+ declare const ApprovalExpired_base: Schema.TaggedErrorClass<ApprovalExpired, "ApprovalExpired", {
184
+ readonly _tag: Schema.tag<"ApprovalExpired">;
185
+ } & {
186
+ approvalId: typeof Schema.String;
187
+ expiredAt: typeof Schema.String;
188
+ }>;
189
+
190
+ /** The would-be approver does not satisfy the intent's own `approvers` guards. */
191
+ export declare class ApprovalForbidden extends ApprovalForbidden_base {
192
+ }
193
+
194
+ declare const ApprovalForbidden_base: Schema.TaggedErrorClass<ApprovalForbidden, "ApprovalForbidden", {
195
+ readonly _tag: Schema.tag<"ApprovalForbidden">;
196
+ } & {
197
+ approvalId: typeof Schema.String;
198
+ /** The scope whose absence denied them, when the denial was scope-shaped. */
199
+ required: Schema.NullOr<typeof Schema.String>;
200
+ message: typeof Schema.String;
201
+ }>;
202
+
203
+ /** The identity of an approval intent, as a wire string. */
204
+ export declare type ApprovalId = string;
205
+
206
+ /** No approval row with that id (wrong id, or swept past retention). */
207
+ export declare class ApprovalNotFound extends ApprovalNotFound_base {
208
+ }
209
+
210
+ declare const ApprovalNotFound_base: Schema.TaggedErrorClass<ApprovalNotFound, "ApprovalNotFound", {
211
+ readonly _tag: Schema.tag<"ApprovalNotFound">;
212
+ } & {
213
+ approvalId: typeof Schema.String;
214
+ }>;
215
+
216
+ /** Already decided (or already consumed) — a decision is made once. */
217
+ export declare class ApprovalNotPending extends ApprovalNotPending_base {
218
+ }
219
+
220
+ declare const ApprovalNotPending_base: Schema.TaggedErrorClass<ApprovalNotPending, "ApprovalNotPending", {
221
+ readonly _tag: Schema.tag<"ApprovalNotPending">;
222
+ } & {
223
+ approvalId: typeof Schema.String;
224
+ status: typeof Schema.String;
225
+ }>;
226
+
227
+ /**
228
+ * Declare that a mutation/action needs a SECOND human before it takes effect.
229
+ *
230
+ * ```ts
231
+ * export default defineMutation({
232
+ * name: 'invoices.refund',
233
+ * guards: [{ scope: 'invoices:refund' }],
234
+ * requiresApproval: {
235
+ * approvers: [{ scope: 'invoices:approve' }],
236
+ * expiresIn: '4h',
237
+ * reason: 'refunds move money out of the account',
238
+ * },
239
+ * // …
240
+ * })
241
+ * ```
242
+ *
243
+ * The first call records the intent and fails with a typed `ApprovalRequired`
244
+ * carrying the approval id. Once an authorised, DIFFERENT subject approves it,
245
+ * the identical call succeeds — once.
246
+ */
247
+ export declare interface ApprovalPolicy<Input = unknown> {
248
+ /**
249
+ * WHO MAY APPROVE. The same `guards:` vocabulary the procedure itself uses,
250
+ * evaluated against the APPROVER's effective scope set at decision time.
251
+ *
252
+ * Required and non-empty. An approval step whose authority is "anyone" is not
253
+ * a control — it is a second click, and it reads in review like a control.
254
+ * `defineMutation` refuses an empty list at declaration for the same reason
255
+ * `guards: []` is refused.
256
+ */
257
+ readonly approvers: Guards<Input>;
258
+ /**
259
+ * How long the pending intent stays approvable — an interval string (`'30m'`,
260
+ * `'4h'`, `'7d'`).
261
+ *
262
+ * Absent → the app's `approvals.expiresIn` tunable, else 24 h. There is no
263
+ * "never expires": an approval queue with no floor is a list of decisions
264
+ * nobody made, and the framework FAILS CLOSED past the deadline (an expired
265
+ * intent can be neither approved nor executed — the requester re-submits and
266
+ * a fresh decision is asked for).
267
+ */
268
+ readonly expiresIn?: string;
269
+ /** Shown to the approver — why this call needs a second person. */
270
+ readonly reason?: string;
271
+ }
272
+
273
+ /**
274
+ * An approver said NO, and this is the requester learning about it.
275
+ *
276
+ * Reported on the retry rather than pushed, because the requester's call is
277
+ * what has to stop happening. Reported ONCE: the gate frees the intent's content
278
+ * key as it raises this, so a deliberate re-submit afterwards opens a genuinely
279
+ * new decision instead of silently reusing a dead one — a rejection is a verdict
280
+ * on one request, not a permanent ban on the operation.
281
+ */
282
+ export declare class ApprovalRejected extends ApprovalRejected_base {
283
+ }
284
+
285
+ declare const ApprovalRejected_base: Schema.TaggedErrorClass<ApprovalRejected, "ApprovalRejected", {
286
+ readonly _tag: Schema.tag<"ApprovalRejected">;
287
+ } & {
288
+ approvalId: typeof Schema.String;
289
+ decidedBy: Schema.NullOr<typeof Schema.String>;
290
+ note: Schema.NullOr<typeof Schema.String>;
291
+ }>;
292
+
293
+ /**
294
+ * The call was RECORDED as a pending approval and did NOT run.
295
+ *
296
+ * This is the answer the caller gets meanwhile, and it is a typed failure rather
297
+ * than a success with a status field on purpose: a mutation that returns its
298
+ * normal output shape when nothing happened is the single easiest thing for a
299
+ * client to mis-handle, and every existing client already branches on `_tag`.
300
+ */
301
+ export declare class ApprovalRequired extends ApprovalRequired_base {
302
+ }
303
+
304
+ declare const ApprovalRequired_base: Schema.TaggedErrorClass<ApprovalRequired, "ApprovalRequired", {
305
+ readonly _tag: Schema.tag<"ApprovalRequired">;
306
+ } & {
307
+ approvalId: typeof Schema.String;
308
+ procedure: typeof Schema.String;
309
+ /** ISO instant past which this intent can no longer be approved. */
310
+ expiresAt: typeof Schema.String;
311
+ /** The scope(s) an approver must hold — so the UI can say who to ask. */
312
+ requiredScopes: Schema.Array$<typeof Schema.String>;
313
+ /** The declared `reason`, when the descriptor gave one. */
314
+ reason: Schema.NullOr<typeof Schema.String>;
315
+ /** True when THIS call created the intent; false when it found the one an
316
+ * earlier identical call had already recorded. Lets a client tell "I just
317
+ * asked" from "still waiting". */
318
+ created: typeof Schema.Boolean;
319
+ }>;
320
+
321
+ export declare const APPROVALS_DECIDE_TAG: "__voltro.approvals.decide";
322
+
323
+ export declare const APPROVALS_PENDING_TAG: "__voltro.approvals.pending";
324
+
325
+ /**
326
+ * `__voltro.approvals.decide` — approve or reject ONE pending intent.
327
+ *
328
+ * `openAccess` here for a structurally different reason from the query's, and
329
+ * the distinction is worth keeping straight: the authority IS real and IS
330
+ * checked, it just cannot be expressed on the descriptor. Which scopes an
331
+ * approver needs is a property of the PENDING ROW (copied from the target
332
+ * procedure's own `requiresApproval.approvers` when the intent was recorded), so
333
+ * one descriptor-level scope would have to be the union of every
334
+ * approval-requiring procedure in the app — a scope that grants strictly more
335
+ * than any single approval does, which is the rubber stamp in its most damaging
336
+ * form.
337
+ *
338
+ * So the check is per row, in the executor, against the intent's own guards,
339
+ * plus the unconditional self-approval refusal.
340
+ */
341
+ export declare const approvalsDecideDescriptor: MutationProcedureDescriptor<"__voltro.approvals.decide", Schema.Struct<{
342
+ approvalId: typeof Schema.String;
343
+ decision: Schema.Literal<["approve", "reject"]>;
344
+ note: Schema.optional<typeof Schema.String>;
345
+ }>, Schema.Struct<{
346
+ approvalId: typeof Schema.String;
347
+ status: Schema.Literal<["approved", "rejected"]>;
348
+ }>, Schema.Union<[ ApprovalNotFound, ApprovalSelfApproval, ApprovalExpired, ApprovalNotPending, ApprovalForbidden, ApprovalUnavailable]>>;
349
+
350
+ /**
351
+ * The approver IS the requester.
352
+ *
353
+ * Refused unconditionally, with no opt-out flag. The whole content of
354
+ * "a second human" is that it is a second one; a framework that shipped
355
+ * `allowSelfApproval: true` would be shipping a control that every app under
356
+ * deadline pressure turns off, and the audit row would still read "approved".
357
+ */
358
+ export declare class ApprovalSelfApproval extends ApprovalSelfApproval_base {
359
+ }
360
+
361
+ declare const ApprovalSelfApproval_base: Schema.TaggedErrorClass<ApprovalSelfApproval, "ApprovalSelfApproval", {
362
+ readonly _tag: Schema.tag<"ApprovalSelfApproval">;
363
+ } & {
364
+ approvalId: typeof Schema.String;
365
+ subjectId: Schema.NullOr<typeof Schema.String>;
366
+ }>;
367
+
368
+ /**
369
+ * `__voltro.approvals.pending` — the calling subject's approval work.
370
+ *
371
+ * Reactive on `_voltro_approvals`, which is what answers "what does the caller
372
+ * see meanwhile": the requester watches their own row flip `pending →
373
+ * approved` and re-fires the mutation, and the approver's queue appears without
374
+ * a poll.
375
+ *
376
+ * `openAccess`, not a scope guard, deliberately. There is no scope that means
377
+ * "may see my own approval work" — every caller has some — and inventing one
378
+ * would be exactly the rubber-stamp guard the `openAccess` doc warns about. The
379
+ * answer is SUBJECT-SCOPED IN THE EXECUTOR: a row appears only if the caller
380
+ * requested it or satisfies its recorded `approvers` guards, so an anonymous
381
+ * caller sees an empty list.
382
+ */
383
+ export declare const approvalsPendingQueryDescriptor: QueryProcedureDescriptor<"__voltro.approvals.pending", Schema.Struct<{
384
+ limit: Schema.optional<typeof Schema.Number>;
385
+ }>, Schema.Array$<Schema.Struct<{
386
+ id: typeof Schema.String;
387
+ procedure: typeof Schema.String;
388
+ kind: Schema.Literal<["mutation", "action"]>;
389
+ requestedBy: Schema.NullOr<typeof Schema.String>;
390
+ status: Schema.Literal<["pending", "approved", "rejected", "expired", "consumed"]>;
391
+ reason: Schema.NullOr<typeof Schema.String>;
392
+ requiredScopes: Schema.Array$<typeof Schema.String>;
393
+ requestedAt: typeof Schema.String;
394
+ expiresAt: typeof Schema.String;
395
+ decidedBy: Schema.NullOr<typeof Schema.String>;
396
+ decidedAt: Schema.NullOr<typeof Schema.String>;
397
+ note: Schema.NullOr<typeof Schema.String>;
398
+ relation: Schema.Literal<["to-decide", "requested"]>;
399
+ }>>, typeof Schema.Never>;
400
+
401
+ /** The lifecycle states a `_voltro_approvals` row moves through. */
402
+ export declare type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired' | 'consumed';
403
+
404
+ /**
405
+ * The procedure declares `requiresApproval` and the framework could not reach
406
+ * the approvals store — so it cannot record the intent and cannot know whether
407
+ * one was granted.
408
+ *
409
+ * FAIL CLOSED. Running the mutation because the bookkeeping is unavailable is
410
+ * exactly the shape of hole the declaration exists to close; the requester sees
411
+ * a refusal they can report, which is the useful outcome.
412
+ */
413
+ export declare class ApprovalUnavailable extends ApprovalUnavailable_base {
414
+ }
415
+
416
+ declare const ApprovalUnavailable_base: Schema.TaggedErrorClass<ApprovalUnavailable, "ApprovalUnavailable", {
417
+ readonly _tag: Schema.tag<"ApprovalUnavailable">;
418
+ } & {
419
+ procedure: typeof Schema.String;
420
+ message: typeof Schema.String;
421
+ }>;
422
+
131
423
  /**
132
424
  * Throws `Unauthenticated` when the resolved Subject is anonymous (no
133
425
  * real user identity). Handlers that require a signed-in caller put
@@ -297,6 +589,15 @@ declare const BusinessRuleViolation_base: Schema.TaggedErrorClass<BusinessRuleVi
297
589
  severity: Schema.Literal<["error", "warning"]>;
298
590
  }>;
299
591
 
592
+ /** The result of a cached resolution. `fresh` distinguishes a resolution that
593
+ * actually ran from a replayed verdict — the audit hook fires on the former
594
+ * only, so a narrowed subject logs once per window instead of once per
595
+ * request. */
596
+ export declare interface CachedScopeDecision {
597
+ readonly decision: ScopeDecision;
598
+ readonly fresh: boolean;
599
+ }
600
+
300
601
  /**
301
602
  * Validate `plugin.framework` (npm-semver range) against the running
302
603
  * voltro version. Returns either `{ ok: true }` or
@@ -325,12 +626,14 @@ export declare const checkFrameworkCompat: (pluginName: string, range: string |
325
626
  /**
326
627
  * Check a descriptor's declared `guards:` against a subject's EFFECTIVE scopes.
327
628
  * Returns a typed `ScopeError` naming the first unmet scope, or `null` when
328
- * every guard is satisfied (or there are no guards). Pure + synchronous — the
329
- * runtime calls it in the dispatch spine BEFORE the executor (and, for
330
- * mutations, before the transaction opens). The `admin:full` bypass passes
331
- * everything.
629
+ * every guard is satisfied. Pure + synchronous — the runtime calls it in the
630
+ * dispatch spine BEFORE the executor (and, for mutations, before the
631
+ * transaction opens). The `admin:full` bypass passes everything.
632
+ *
633
+ * No guards at all is ALLOWED unless the caller passes `defaultDeny` — see the
634
+ * note above for why that decision is not made process-globally here.
332
635
  */
333
- export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined) => ScopeError | null;
636
+ export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined, options?: GuardCheckOptions) => ScopeError | null;
334
637
 
335
638
  /**
336
639
  * The resource-aware counterpart of `checkGuards`. Identical semantics when no
@@ -347,7 +650,7 @@ export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<AnyCh
347
650
  * every guard passes. The runtime calls this in the dispatch spine BEFORE the
348
651
  * executor (mutations: before the transaction opens).
349
652
  */
350
- export declare const checkGuardsEffect: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined, input: unknown) => Effect.Effect<ScopeError | null>;
653
+ export declare const checkGuardsEffect: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined, input: unknown, options?: GuardCheckOptions) => Effect.Effect<ScopeError | null>;
351
654
 
352
655
  export declare interface ClientDescriptor {
353
656
  readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'workflow';
@@ -438,16 +741,61 @@ export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrat
438
741
  * bag could overwrite the framework's claim about who a request was. The
439
742
  * strategy owns identity; this owns authority.
440
743
  *
441
- * Returned scopes are UNIONED with whatever the strategy already set, so a
442
- * resolver cannot silently remove a scope either.
744
+ * **It is now the ONLY place a session's authority comes from.** The
745
+ * framework's session cookie carries `SubjectIdentity` no scopes — so for
746
+ * a cookie-authenticated caller the strategy establishes nothing to union
747
+ * with, and whatever this returns IS the caller's authority. Remove a role
748
+ * and the next resolution reflects it; there is nothing frozen left to
749
+ * override. An app that gates on scopes and wires no resolver has callers
750
+ * with no scopes, which is the fail-closed direction.
751
+ *
752
+ * **Return shape.** A bare `ReadonlyArray<string>` means
753
+ * `{ kind: 'grant' }` — unioned, exactly as before. Return
754
+ * `{ kind: 'authoritative', scopes }` to make this resolver the complete
755
+ * answer, which is how you narrow a subject whose scopes came from a TOKEN
756
+ * (a JWT's `scopesFromClaims`, an api key's record) rather than a cookie.
757
+ * Return `{ kind: 'unavailable', reason }` when the lookup itself failed —
758
+ * the request then fails closed with `Unauthenticated` and the reason
759
+ * reaches `onStrategyFailed`, instead of an empty array being mistaken for
760
+ * a policy decision.
443
761
  *
444
762
  * Runs only on a MATCHED subject — never for anonymous, where there is no
445
- * identity to look a role up for. It is on the request path, so cache it
446
- * (a per-connection or short-TTL map keyed by subject id); the framework
447
- * deliberately does not cache for you, because only the app knows how
448
- * quickly a role change must take effect.
763
+ * identity to look a role up for. It is on the request path and the
764
+ * framework caches it for you (`scopeCache`, 30s by default, invalidatable
765
+ * see below); a resolver whose answer depends on anything other than the
766
+ * subject's identity must set `scopeCache: false`.
767
+ */
768
+ readonly resolveScopes?: (subject: Subject, input: AuthStrategyInput) => Promise<ScopeResolverResult> | ScopeResolverResult;
769
+ /**
770
+ * How resolved authority is cached. Omit for the default window
771
+ * (`DEFAULT_SCOPE_CACHE_TTL_MS`, 30s — the same window the session
772
+ * revocation check uses, so the two store reads miss together).
773
+ *
774
+ * - `ScopeCacheOptions` — tune the window in place: `{ ttlMs: 0 }`
775
+ * resolves on every request, staleness zero.
776
+ * - a `ScopeCache` from `makeScopeCache()` — you keep the handle and call
777
+ * `invalidate(scopeCacheKey(subject))` from whatever changes a role.
778
+ * Zero staleness on this process, `ttlMs` on other replicas.
779
+ * - `false` — no caching at all. Required when the resolver reads
780
+ * anything beyond the subject's identity (a header, a request path),
781
+ * because the cache key is the identity and nothing else.
449
782
  */
450
- readonly resolveScopes?: (subject: Subject, input: AuthStrategyInput) => Promise<ReadonlyArray<string>> | ReadonlyArray<string>;
783
+ readonly scopeCache?: ScopeCache | ScopeCacheOptions | false;
784
+ /**
785
+ * Called when an `authoritative` resolution REMOVED scopes the strategy had
786
+ * established — the audit trail for narrowing.
787
+ *
788
+ * Narrowing is a security-relevant event and it must not be inferable only
789
+ * by its absence. It fires on a fresh resolution, not on a cache replay, so
790
+ * a narrowed caller logs once per window rather than once per request.
791
+ */
792
+ readonly onScopesNarrowed?: (event: {
793
+ strategyId: string;
794
+ subjectType: Subject["type"];
795
+ subjectId: string | null;
796
+ removed: ReadonlyArray<string>;
797
+ granted: ReadonlyArray<string>;
798
+ }) => void;
451
799
  /**
452
800
  * Hands every strategy the app's store on `input.store`.
453
801
  *
@@ -478,6 +826,15 @@ export declare const CONNECTION_START_TAG: "__voltro.connections.start";
478
826
 
479
827
  export declare const CONNECTION_SUBMIT_TOKEN_TAG: "__voltro.connections.submitToken";
480
828
 
829
+ export declare interface ConnectionCredential {
830
+ /** Cookies to set or replace INSIDE the connection's `Cookie` header. Other
831
+ * cookies on the connection are left alone. */
832
+ readonly cookies?: Readonly<Record<string, string>>;
833
+ /** Headers to set or replace outright (`authorization`, a custom token
834
+ * header). Matched case-insensitively. */
835
+ readonly headers?: Readonly<Record<string, string>>;
836
+ }
837
+
481
838
  /** `__voltro.connections.disconnect` — forget the calling subject's credential
482
839
  * for this connection. Deletes the row; the provider-side grant (if any) is
483
840
  * the provider's to revoke. */
@@ -763,6 +1120,28 @@ export declare interface CoordinatedTickOutcome {
763
1120
  readonly nextDueInMs?: number;
764
1121
  }
765
1122
 
1123
+ /**
1124
+ * What a descriptor CARRIES: the author's guards, or the erased form of their
1125
+ * `openAccess:` decision — never both.
1126
+ *
1127
+ * The option a user writes is still `Guards` (an `OpenAccessSpec` is not
1128
+ * something to hand-write into `guards:`; there is one spelling for the
1129
+ * decision and it is the `openAccess:` field). The DESCRIPTOR type is wider
1130
+ * because that is where the normalised decision lands, and because every
1131
+ * enforcement path reads the descriptor's array and nothing else.
1132
+ */
1133
+ export declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec>;
1134
+
1135
+ /** Every channel declared in this process, by routing key. */
1136
+ export declare const declaredReactivityChannelKeys: () => ReadonlySet<string>;
1137
+
1138
+ /** Default cap on cached subjects before the cache sweeps + trims. */
1139
+ export declare const DEFAULT_SCOPE_CACHE_MAX_ENTRIES = 10000;
1140
+
1141
+ /** Default scope-cache window. Matches the session-revocation window on
1142
+ * purpose: the two per-request store reads then expire together. */
1143
+ export declare const DEFAULT_SCOPE_CACHE_TTL_MS = 30000;
1144
+
766
1145
  export declare const defineAction: <const Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All = typeof Schema.Never>(options: {
767
1146
  readonly name: Name;
768
1147
  readonly input: Input;
@@ -772,6 +1151,24 @@ export declare const defineAction: <const Name extends string, Input extends Sch
772
1151
  * scope(s) or the action fails with a typed `ScopeError` before the executor
773
1152
  * runs. `ScopeError` is auto-merged into the wire error union. */
774
1153
  readonly guards?: Guards<Schema.Schema.Type<Input>>;
1154
+ /**
1155
+ * Declare that this procedure needs NO authorization check — and say why.
1156
+ *
1157
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1158
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1159
+ * access decision nobody made is the SEC-1 hole, not a default.
1160
+ *
1161
+ * Use it for the endpoints that really are open — a health check, a public
1162
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1163
+ * already holds just to satisfy the gate: that guard reads as protection and
1164
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1165
+ *
1166
+ * The reason is required and is the point — it is what a reviewer reads and
1167
+ * what `voltro doctor` prints beside the tag.
1168
+ *
1169
+ * openAccess: 'public pricing page — reads no caller data'
1170
+ */
1171
+ readonly openAccess?: string;
775
1172
  /** Table(s) this action READS. Declaring it is what lets `voltro check` know
776
1173
  * the table is alive — without it, a table only an action touches reads as
777
1174
  * an orphan. See `ActionProcedureDescriptor.source`. */
@@ -782,6 +1179,15 @@ export declare const defineAction: <const Name extends string, Input extends Sch
782
1179
  readonly publicApi?: PublicApiSpec;
783
1180
  /** Expose this action as an agent tool (innovation/07). */
784
1181
  readonly exposeAsTool?: ExposeAsTool;
1182
+ /**
1183
+ * Require a SECOND human to approve before this action takes effect.
1184
+ *
1185
+ * Same contract as a mutation's, and the gate runs at the same point relative
1186
+ * to the work: after `guards:`, BEFORE the executor. For an action that is the
1187
+ * only point at which nothing has happened yet — there is no transaction to
1188
+ * roll back an outbound HTTP call.
1189
+ */
1190
+ readonly requiresApproval?: ApprovalPolicy<Schema.Schema.Type<Input>>;
785
1191
  /**
786
1192
  * Keep this procedure OFF the wire entirely.
787
1193
  *
@@ -862,6 +1268,27 @@ export declare const defineEvent: <const Name extends string, Key extends Schema
862
1268
  * makes throughput scale with the audience instead of the publish rate.
863
1269
  */
864
1270
  readonly guards?: Guards<Schema.Schema.Type<Key>>;
1271
+ /**
1272
+ * Declare that this event needs NO authorization check — and say why.
1273
+ *
1274
+ * The other half of `security.defaultDeny`, exactly as on a procedure. With
1275
+ * the flag on, an event that declares neither `guards:` nor this is refused
1276
+ * at boot, by name: an access decision nobody made is the SEC-1 hole, not a
1277
+ * default — and an event with no decision is subscribable by ANY
1278
+ * authenticated session that can open the socket.
1279
+ *
1280
+ * Use it for the events that really are open — a public scoreboard tick, a
1281
+ * status broadcast, a service-health pulse. Do NOT reach for a scope every
1282
+ * caller already holds just to satisfy the gate: that guard reads as
1283
+ * protection and enforces nothing, and it is the failure mode this field
1284
+ * exists to prevent.
1285
+ *
1286
+ * The reason is required and is the point — it is what a reviewer reads and
1287
+ * what `voltro doctor` prints beside the tag.
1288
+ *
1289
+ * openAccess: 'public scoreboard — carries no caller data'
1290
+ */
1291
+ readonly openAccess?: string;
865
1292
  /**
866
1293
  * Deliver recently-buffered events on a FIRST attach. Default `false`, and the
867
1294
  * default is the interesting half.
@@ -912,10 +1339,41 @@ export declare const defineMutation: <const Name extends string, Input extends S
912
1339
  * scope(s) or the mutation fails with a typed `ScopeError` BEFORE the
913
1340
  * transaction opens. `ScopeError` is auto-merged into the wire error union. */
914
1341
  readonly guards?: Guards<Schema.Schema.Type<Input>>;
1342
+ /**
1343
+ * Declare that this procedure needs NO authorization check — and say why.
1344
+ *
1345
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1346
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1347
+ * access decision nobody made is the SEC-1 hole, not a default.
1348
+ *
1349
+ * Use it for the endpoints that really are open — a health check, a public
1350
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1351
+ * already holds just to satisfy the gate: that guard reads as protection and
1352
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1353
+ *
1354
+ * The reason is required and is the point — it is what a reviewer reads and
1355
+ * what `voltro doctor` prints beside the tag.
1356
+ *
1357
+ * openAccess: 'public pricing page — reads no caller data'
1358
+ */
1359
+ readonly openAccess?: string;
915
1360
  /** Project this mutation as a public REST endpoint (innovation/11). */
916
1361
  readonly publicApi?: PublicApiSpec;
917
1362
  /** Expose this mutation as an agent tool (innovation/07). */
918
1363
  readonly exposeAsTool?: ExposeAsTool;
1364
+ /**
1365
+ * Require a SECOND human to approve before this mutation takes effect.
1366
+ *
1367
+ * The first call records a durable pending intent and fails with a typed
1368
+ * `ApprovalRequired` carrying its id; the transaction never opens. Once an
1369
+ * authorised, DIFFERENT subject approves, the IDENTICAL call succeeds exactly
1370
+ * once (the approval is consumed).
1371
+ *
1372
+ * Composes with `guards:` rather than replacing them — the requester still has
1373
+ * to be allowed to ASK. Refused together with `openAccess:` (see
1374
+ * `assertApprovalPolicyCoherent`).
1375
+ */
1376
+ readonly requiresApproval?: ApprovalPolicy<Schema.Schema.Type<Input>>;
919
1377
  /**
920
1378
  * Keep this procedure OFF the wire entirely.
921
1379
  *
@@ -1001,14 +1459,37 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
1001
1459
  * auto-optimistic patch routing from mutations targeting any of those
1002
1460
  * tables. For a COMPUTED query (handler returns a shaped value) it is the
1003
1461
  * reactive trigger set: the handler re-runs when ANY listed table changes
1004
- * — pass an array to depend on several (e.g. a matrix joining two tables). */
1005
- readonly source?: string | ReadonlyArray<string>;
1462
+ * — pass an array to depend on several (e.g. a matrix joining two tables).
1463
+ *
1464
+ * A `reactivityChannel(...)` is accepted here too, for state that pushes
1465
+ * without living in a table. Pass the CHANNEL, not its key string: the
1466
+ * import edge is what makes a channel `source:` impossible to leave stale,
1467
+ * which a table name (a bare string) can always be. */
1468
+ readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource>;
1006
1469
  /** Opt into server-side snapshot caching with auto-invalidation. */
1007
1470
  readonly cache?: QueryCacheConfig;
1008
1471
  /** Declarative authorization guard(s) — the caller must hold the named
1009
1472
  * scope(s) or the query fails with a typed `ScopeError` before the executor
1010
1473
  * runs. `ScopeError` is auto-merged into the wire error union. */
1011
1474
  readonly guards?: Guards<Schema.Schema.Type<Input>>;
1475
+ /**
1476
+ * Declare that this procedure needs NO authorization check — and say why.
1477
+ *
1478
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1479
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1480
+ * access decision nobody made is the SEC-1 hole, not a default.
1481
+ *
1482
+ * Use it for the endpoints that really are open — a health check, a public
1483
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1484
+ * already holds just to satisfy the gate: that guard reads as protection and
1485
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1486
+ *
1487
+ * The reason is required and is the point — it is what a reviewer reads and
1488
+ * what `voltro doctor` prints beside the tag.
1489
+ *
1490
+ * openAccess: 'public pricing page — reads no caller data'
1491
+ */
1492
+ readonly openAccess?: string;
1012
1493
  /** Project this query as a public REST endpoint (innovation/11). */
1013
1494
  readonly publicApi?: PublicApiSpec;
1014
1495
  /** Expose this query as an agent tool (innovation/07). */
@@ -1074,6 +1555,24 @@ export declare const defineStream: <const Name extends string, Input extends Sch
1074
1555
  * push. Same shape and same semantics as a query's.
1075
1556
  */
1076
1557
  readonly guards?: Guards;
1558
+ /**
1559
+ * Declare that this procedure needs NO authorization check — and say why.
1560
+ *
1561
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1562
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1563
+ * access decision nobody made is the SEC-1 hole, not a default.
1564
+ *
1565
+ * Use it for the endpoints that really are open — a health check, a public
1566
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1567
+ * already holds just to satisfy the gate: that guard reads as protection and
1568
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1569
+ *
1570
+ * The reason is required and is the point — it is what a reviewer reads and
1571
+ * what `voltro doctor` prints beside the tag.
1572
+ *
1573
+ * openAccess: 'public pricing page — reads no caller data'
1574
+ */
1575
+ readonly openAccess?: string;
1077
1576
  }) => StreamProcedureDescriptor<Name, Input, Element, Error>;
1078
1577
 
1079
1578
  export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
@@ -1200,9 +1699,14 @@ export declare interface EventDescriptor<Name extends string, Key extends Schema
1200
1699
  readonly name: Name;
1201
1700
  readonly key: Key;
1202
1701
  readonly payload: Payload;
1203
- /** Who may LISTEN. Re-checked when the subject changes, not per delivery
1204
- * see `EventDefinition.guards` for why that distinction is deliberate. */
1205
- readonly guards: Guards<Schema.Schema.Type<Key>> | undefined;
1702
+ /** Who may LISTEN the author's guards, or the erased form of their
1703
+ * `openAccess:` decision, never both (see `DeclaredAccess`). Re-checked when
1704
+ * the subject changes, not per delivery — see `EventDefinition.guards` for
1705
+ * why that distinction is deliberate. */
1706
+ readonly guards: DeclaredAccess<Schema.Schema.Type<Key>> | undefined;
1707
+ /** The declared `openAccess:` reason, when there is one — what the boot gate
1708
+ * and `voltro doctor` read. The enforcement paths read `guards`. */
1709
+ readonly openAccess: string | undefined;
1206
1710
  /** Deliver buffered events on a FIRST attach. Default false. */
1207
1711
  readonly rewind: boolean | undefined;
1208
1712
  /** `'each'` (default) or `'latest'` — see the definer. */
@@ -1481,6 +1985,14 @@ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefin
1481
1985
  /** The currently-registered resource-scope resolver, or `undefined`. */
1482
1986
  export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
1483
1987
 
1988
+ /** Ask for default-deny semantics on one `checkGuards` call. */
1989
+ export declare interface GuardCheckOptions {
1990
+ /** Refuse a procedure that declares no access decision at all. */
1991
+ readonly defaultDeny?: boolean;
1992
+ /** The procedure's tag, so the refusal can name it. */
1993
+ readonly procedure?: string;
1994
+ }
1995
+
1484
1996
  export declare interface GuardCheckSpec {
1485
1997
  readonly scope: string | ReadonlyArray<string>;
1486
1998
  readonly mode?: 'all' | 'any';
@@ -1506,17 +2018,43 @@ export declare interface GuardSpec<Input = unknown> {
1506
2018
  * scope); `'any'` = OR (hold at least one). Ignored for a single scope. */
1507
2019
  readonly mode?: 'all' | 'any';
1508
2020
  /**
1509
- * PURE `input → resource id` extractor for a row/resource-scoped guard.
1510
- * Browser-safe (no DB, no server import) — exactly like `target.identify`.
1511
- * The framework passes the extracted id to a resource-aware scope resolver
1512
- * (a future ReBAC / `accessPolicy()` resolver) so the check can be scoped to
1513
- * THAT resource. With only the default (subject-global) resolver installed
1514
- * the id is advisory and the guard checks the subject's global scopes. Omit
1515
- * for a plain subject-scope guard.
2021
+ * PURE `input → resource id` extractor. Browser-safe (no DB, no server
2022
+ * import) — exactly like `target.identify`. Omit for a plain subject-scope
2023
+ * guard.
2024
+ *
2025
+ * **On a `GuardSpec` this id is ADVISORY.** A scope guard answers "what may
2026
+ * this subject do at all", against the subject's global scope set; the id is
2027
+ * carried for logging and for a future subject-scope resolver that narrows by
2028
+ * resource. It does not, on its own, make the check per-resource.
2029
+ *
2030
+ * **If your authority is per-resource, you want {@link PolicyGuardSpec}, not
2031
+ * this field** — `guards: [{ action, resourceType, resource }]`, backed by
2032
+ * `defineResourcePolicy` + a tuple source you register. That is built, wired
2033
+ * on both boot paths, fail-closed without a resolver, and documented under
2034
+ * *Authentication → Authorization*. An app whose relationships already live
2035
+ * in its own tables (a `teamMembers` row, say) registers its own tuple source
2036
+ * rather than copying data across; see `policyGuardResolver.ts`.
2037
+ *
2038
+ * That paragraph is here because its absence cost a consumer their access
2039
+ * gate. This comment used to describe the resolver as "a future ReBAC /
2040
+ * `accessPolicy()` resolver" — written before the ReBAC path shipped and
2041
+ * never updated. They read the type, quoted the sentence, concluded there was
2042
+ * "nothing in between" declaring an untruth and turning the gate off, and set
2043
+ * `security: { defaultDeny: false }` on an app with 565 undecided procedures.
2044
+ * The capability they needed was two fields away. A doc comment that says
2045
+ * "future" about something shipped is not a small inaccuracy: it is the only
2046
+ * thing a careful reader has, and it argued them out of a feature.
1516
2047
  */
1517
2048
  readonly resource?: (input: Input) => string | undefined;
1518
2049
  }
1519
2050
 
2051
+ /** Does this descriptor declare an access decision — a guard, or a deliberate
2052
+ * `openAccess:`? The boot gate's predicate; `false` is the SEC-1 shape. */
2053
+ export declare const hasAccessDecision: (descriptor: {
2054
+ readonly guards?: ReadonlyArray<unknown> | undefined;
2055
+ readonly openAccess?: string | undefined;
2056
+ }) => boolean;
2057
+
1520
2058
  export declare const hasCallbackRoutes: (s: AuthStrategy) => s is AuthStrategyWithCallback;
1521
2059
 
1522
2060
  /** True if the subject holds `scope` (or the `admin:full` bypass), checking
@@ -1562,8 +2100,22 @@ export declare interface HttpRequestContext {
1562
2100
  readonly path: string;
1563
2101
  /** Lowercased request headers. */
1564
2102
  readonly headers: Readonly<Record<string, string>>;
1565
- /** Best-effort remote address. `undefined` when running behind a
1566
- * proxy without `x-forwarded-for`. */
2103
+ /**
2104
+ * The client address, resolved through the app's `security.trustedProxies`
2105
+ * policy — the SAME value `PluginHttpRouteRequest.remoteAddr`, the rate
2106
+ * limiter, the geo-block and every audit row use (`resolveClientAddress`).
2107
+ * Use this, never `headers['x-forwarded-for']`.
2108
+ *
2109
+ * This is a pre-auth shield's whole key, so getting it from the header is
2110
+ * not a smaller mistake here than elsewhere — it is the one place it is
2111
+ * worst. `x-forwarded-for` is a request header: any client can write it, so
2112
+ * a token bucket keyed on it is bypassed by one extra header per request.
2113
+ * The resolution here ignores the header entirely unless a trusted proxy is
2114
+ * declared, and then believes only the hops that are one.
2115
+ *
2116
+ * `undefined` when the socket address is unavailable (a unix socket, an
2117
+ * in-process test harness that constructs the request by hand).
2118
+ */
1567
2119
  readonly remoteAddr: string | undefined;
1568
2120
  }
1569
2121
 
@@ -1664,12 +2216,31 @@ export declare const isEventDescriptor: (value: unknown) => value is AnyEventDes
1664
2216
  * diffed by id and falls back to shipping the full data. */
1665
2217
  export declare const isIdKeyed: (rows: ReadonlyArray<Readonly<Record<string, unknown>>>) => rows is ReadonlyArray<PatchRow>;
1666
2218
 
2219
+ /** Runtime narrowing for the declared-open variant. */
2220
+ export declare const isOpenAccess: (g: AnyCheckSpec) => g is OpenAccessSpec;
2221
+
1667
2222
  /** Runtime narrowing for the relationship variant. */
1668
2223
  export declare const isPolicyCheck: (g: AnyCheckSpec) => g is PolicyCheckSpec;
1669
2224
 
1670
2225
  /** Narrow a guard entry to the relationship variant. */
1671
2226
  export declare const isPolicyGuard: <I>(g: AnyGuardSpec<I>) => g is PolicyGuardSpec<I>;
1672
2227
 
2228
+ /** Whether `value` is a channel object (not its key). */
2229
+ export declare const isReactivityChannel: (value: unknown) => value is ReactivityChannel;
2230
+
2231
+ /**
2232
+ * Whether `key` is addressed to the channel namespace.
2233
+ *
2234
+ * A PREFIX test, deliberately not a registry lookup — the two answer different
2235
+ * questions and only one of them belongs on the delivery path. At runtime the
2236
+ * question is "is this a table name or a channel key", and getting it wrong in
2237
+ * the strict direction drops a real push. Whether the channel was DECLARED is a
2238
+ * boot question, and `undeclaredChannelKeys` answers it there — the same
2239
+ * asymmetry tables already have (an unregistered table name still delivers;
2240
+ * the boot audit is what reports it).
2241
+ */
2242
+ export declare const isReactivityChannelKey: (key: string) => boolean;
2243
+
1673
2244
  export declare const isSystemSubject: (subject: Subject) => boolean;
1674
2245
 
1675
2246
  /**
@@ -1690,6 +2261,17 @@ export declare const isWireReachable: (descriptor: {
1690
2261
  readonly internal?: boolean | undefined;
1691
2262
  }) => boolean;
1692
2263
 
2264
+ /**
2265
+ * Build a scope cache. Hand it to `composeAuthStrategies({ scopeCache })` and
2266
+ * keep the handle: `invalidate(scopeCacheKey(subject))` from the mutation that
2267
+ * grants or removes a role is what makes the staleness window zero.
2268
+ *
2269
+ * `composeAuthStrategies` builds one internally when you don't, so the default
2270
+ * posture is cached-with-a-window rather than a store read per request — but a
2271
+ * cache nobody holds cannot be invalidated, which is why this is exported.
2272
+ */
2273
+ export declare const makeScopeCache: (options?: ScopeCacheOptions) => ScopeCache;
2274
+
1693
2275
  /**
1694
2276
  * How large one encoded envelope may be, on EVERY dialect.
1695
2277
  *
@@ -1710,6 +2292,10 @@ export declare const MAX_EVENT_ENVELOPE_BYTES = 7500;
1710
2292
  /** In-memory store — the default for single-process dev + the test double. */
1711
2293
  export declare const memoryIdempotencyStore: () => IdempotencyStore;
1712
2294
 
2295
+ /** The refusal a missing access decision produces. Exported so the boot gate
2296
+ * and the call-time path cannot describe the same defect differently. */
2297
+ export declare const missingAccessDecision: (procedure?: string) => ScopeError;
2298
+
1713
2299
  export declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
1714
2300
  readonly kind: 'mutation';
1715
2301
  readonly name: Name;
@@ -1724,11 +2310,19 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
1724
2310
  /** Declarative authorization guard(s) — enforced before the transaction
1725
2311
  * opens, failing with a typed `ScopeError`. Absent → no framework-level
1726
2312
  * authz (author gates in-handler, or the mutation is unguarded). */
1727
- readonly guards: Guards | undefined;
2313
+ readonly guards: DeclaredAccess | undefined;
2314
+ /** The declared reason this procedure needs NO authorization check —
2315
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
2316
+ * the only two shapes `security.defaultDeny` accepts. */
2317
+ readonly openAccess: string | undefined;
1728
2318
  /** Opt this mutation into a public REST endpoint (innovation/11). */
1729
2319
  readonly publicApi: PublicApiSpec | undefined;
1730
2320
  /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
1731
2321
  readonly exposeAsTool: ExposeAsTool | undefined;
2322
+ /** Require a SECOND human to approve before this mutation takes effect. The
2323
+ * gate runs in the dispatch spine after `guards:` and before the transaction
2324
+ * opens; the pending intent is a durable `_voltro_approvals` row. */
2325
+ readonly requiresApproval: AnyApprovalPolicy | undefined;
1732
2326
  /** True when the procedure is kept OFF the wire — no client-group entry and no
1733
2327
  * route in dev or serve. See `internal` on the definer's options. */
1734
2328
  /**
@@ -1780,6 +2374,17 @@ export declare interface NestedTargetFields<Input = unknown> {
1780
2374
  */
1781
2375
  export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => ClientDescriptor;
1782
2376
 
2377
+ /**
2378
+ * A `source:` as the DESCRIPTOR stores it — channels resolved to their keys,
2379
+ * with the caller's shape preserved.
2380
+ *
2381
+ * Shape-preserving on purpose: a single `source: 'notes'` must stay the string
2382
+ * `'notes'` and not become `['notes']`. It is serialised into the capability
2383
+ * manifest and into every api golden, so widening the shape would rewrite those
2384
+ * artefacts for every query in every app to express nothing.
2385
+ */
2386
+ export declare const normalizeSource: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => string | ReadonlyArray<string> | undefined;
2387
+
1783
2388
  /**
1784
2389
  * What a plugin contributes to the OTel layer via `contributeObservability`.
1785
2390
  * The OTel types are intentionally `unknown` so this browser-safe protocol
@@ -1799,6 +2404,31 @@ export declare interface ObservabilityContribution {
1799
2404
  readonly sampler?: unknown;
1800
2405
  }
1801
2406
 
2407
+ /**
2408
+ * The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision
2409
+ * that this procedure needs no authorization check, and the reason.
2410
+ *
2411
+ * It is a guard entry rather than a bare descriptor field on purpose. Every
2412
+ * enforcement path in the framework — `servePipeline`'s `enforceGuards`,
2413
+ * `bindStream`, `bindEvent`, `@voltro/testing`'s `invoke` — is handed the
2414
+ * `guards` ARRAY and nothing else. A decision that does not live in that array
2415
+ * is invisible to all of them, so "guarded" and "deliberately open" would be
2416
+ * distinguishable in the source and identical at the point that enforces.
2417
+ *
2418
+ * It always passes. The value is the WHY, and the why is the point: it is what
2419
+ * a reviewer reads, what `voltro doctor` prints, and what makes an open
2420
+ * procedure a decision somebody made rather than a field somebody forgot.
2421
+ */
2422
+ export declare interface OpenAccessSpec {
2423
+ /** Why this procedure is callable without an authorization check. Non-empty
2424
+ * by construction — `defineQuery` & co. refuse an empty reason. */
2425
+ readonly open: string;
2426
+ }
2427
+
2428
+ /** Build the erased `openAccess:` entry. The definers call this; an app writes
2429
+ * `openAccess: '<why>'` on the descriptor and never sees the spec. */
2430
+ export declare const openAccessSpec: (reason: string) => OpenAccessSpec;
2431
+
1802
2432
  /** Split a route back into its three parts. */
1803
2433
  export declare const parseEventRoute: (route: string) => {
1804
2434
  readonly tenantId: string | null;
@@ -1817,6 +2447,35 @@ export declare type PatchRow = Readonly<Record<string, unknown>> & {
1817
2447
  * needs to re-parse a number out of a path. */
1818
2448
  export declare const pathToId: (path: string) => string;
1819
2449
 
2450
+ export declare const PendingApproval: Schema.Struct<{
2451
+ id: typeof Schema.String;
2452
+ /** The rpc tag whose execution is pending. */
2453
+ procedure: typeof Schema.String;
2454
+ kind: Schema.Literal<["mutation", "action"]>;
2455
+ /** Subject id of whoever asked. Never the approver. */
2456
+ requestedBy: Schema.NullOr<typeof Schema.String>;
2457
+ status: Schema.Literal<["pending", "approved", "rejected", "expired", "consumed"]>;
2458
+ /** The declared reason from the descriptor, if any. */
2459
+ reason: Schema.NullOr<typeof Schema.String>;
2460
+ /** Scope(s) an approver must hold. */
2461
+ requiredScopes: Schema.Array$<typeof Schema.String>;
2462
+ requestedAt: typeof Schema.String;
2463
+ expiresAt: typeof Schema.String;
2464
+ decidedBy: Schema.NullOr<typeof Schema.String>;
2465
+ decidedAt: Schema.NullOr<typeof Schema.String>;
2466
+ /** The approver's note, when they left one. */
2467
+ note: Schema.NullOr<typeof Schema.String>;
2468
+ /**
2469
+ * How this row relates to the CALLING subject — the whole point of the feed.
2470
+ * `'to-decide'` = they may act on it; `'requested'` = they asked for it.
2471
+ * A row is never both: self-approval is refused, so a requester never
2472
+ * qualifies as its approver.
2473
+ */
2474
+ relation: Schema.Literal<["to-decide", "requested"]>;
2475
+ }>;
2476
+
2477
+ export declare type PendingApproval = Schema.Schema.Type<typeof PendingApproval>;
2478
+
1820
2479
  /**
1821
2480
  * Called once at app boot, after `voltro dev` resolves the plugin
1822
2481
  * list and before the rpc server starts accepting connections. Use
@@ -1913,6 +2572,23 @@ export declare interface PluginChangeEvent {
1913
2572
  readonly new: Record<string, unknown> | null;
1914
2573
  /** Row before the change — present on update + delete, null on insert. */
1915
2574
  readonly old: Record<string, unknown> | null;
2575
+ /**
2576
+ * Set when the transport could not carry this change's images and they were
2577
+ * RECONSTRUCTED — a row over postgres' 8000-byte `pg_notify` cap. Absent on
2578
+ * every ordinary event.
2579
+ *
2580
+ * A tap must read it before trusting an image as a snapshot:
2581
+ *
2582
+ * - `'rehydrated'` — `new` is the row RE-READ from the database. Correct to
2583
+ * index, mirror or forward; NOT necessarily the image the write that
2584
+ * fired this event produced (a later write may already have landed).
2585
+ * - `'tombstone'` — a delete whose `old` is the PRIMARY KEY and nothing
2586
+ * else. Enough to remove the row; never a record of what it contained.
2587
+ * A history/versioning tap must not store it as a snapshot.
2588
+ * - `'unrecovered'` — both images are null and the content is gone. The
2589
+ * change happened; re-read or resync if you need it.
2590
+ */
2591
+ readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
1916
2592
  /** How the event reached this process: absent/'inline' = this process's
1917
2593
  * own write; 'injected' = delivered over a cross-instance transport
1918
2594
  * (broadcast bus / CDC consumer). Combine with `changeScope` to act
@@ -1945,6 +2621,14 @@ export declare interface PluginChangeEvent {
1945
2621
  * join table is a member removal, a cascade or an expiry.
1946
2622
  */
1947
2623
  readonly procedure?: string;
2624
+ /**
2625
+ * The write was made BY AN AGENT acting as `subjectId` — the fourth member of
2626
+ * the shape the comment above tracks (`WriteAttribution` → `ChangeEvent` →
2627
+ * here). A tap that logs "who changed this" reads WRONG without it: the
2628
+ * subject is the person the agent acted as, by construction, so an agent
2629
+ * write and a human write are otherwise identical rows.
2630
+ */
2631
+ readonly via?: 'agent';
1948
2632
  /** The store's change visibility (see `DataStore.changeScope`):
1949
2633
  * 'local' — own writes inline (skip origin 'injected' for
1950
2634
  * exactly-once-on-the-writer); 'fleet' — every replica sees the full
@@ -2113,6 +2797,42 @@ export declare interface PluginHttpRoute {
2113
2797
  * any sub-path (`/_voltro/storage/abc123`). */
2114
2798
  readonly path: string;
2115
2799
  readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
2800
+ /**
2801
+ * Opt this route's path OUT of the listener's cross-site origin check.
2802
+ *
2803
+ * Every state-changing request (anything but GET/HEAD/OPTIONS) is
2804
+ * origin-checked by default, because the default assumption has to be that a
2805
+ * route can be reached with the browser's ambient session cookie — and a
2806
+ * route that can is CSRF-reachable. Declaring `'exempt'` is a claim that this
2807
+ * route CANNOT be: its caller must present something a browser will not
2808
+ * attach cross-site.
2809
+ *
2810
+ * The test to apply, and it is the only one:
2811
+ *
2812
+ * > If an attacker's page makes a browser send this request with the
2813
+ * > victim's cookies attached, does anything happen?
2814
+ *
2815
+ * If the answer is "no, the request still needs a signature / a bearer token
2816
+ * / a signed ticket the attacker does not have", the route is exempt.
2817
+ * Otherwise it is not, and no amount of "but it is behind the dashboard"
2818
+ * makes it so.
2819
+ *
2820
+ * The first-party exemptions and why each qualifies:
2821
+ * - `@voltro/plugin-sso-saml` `/saml` — the IdP delivers the assertion as a
2822
+ * genuine cross-site browser form POST; authority is the signed
2823
+ * SAMLResponse, not the cookie.
2824
+ * - `@voltro/plugin-storage` `/…/upload` + `/…/upload/resumable` — a signed
2825
+ * upload ticket in the query string, and the route ships its own CORS
2826
+ * allowlist because a cross-origin upload is the point.
2827
+ * - `@voltro/plugin-billing` `/billing/webhook` — an HMAC-verified provider
2828
+ * callback.
2829
+ * - `@voltro/plugin-scim` `/scim/v2` — bearer-only, refuses to mount without
2830
+ * a token.
2831
+ *
2832
+ * Granularity is the PATH PREFIX the route mounts, not the sub-path its
2833
+ * handler branches on: exempting `/saml` exempts `POST /saml/anything`.
2834
+ */
2835
+ readonly originGuard?: 'exempt';
2116
2836
  }
2117
2837
 
2118
2838
  export declare interface PluginHttpRouteRequest {
@@ -2175,6 +2895,22 @@ export declare interface PluginHttpRouteRequest {
2175
2895
  * this store reads them.
2176
2896
  */
2177
2897
  readonly store?: DataStore;
2898
+ /**
2899
+ * The client address, resolved through the app's `security.trustedProxies`
2900
+ * policy — the SAME value the rate limiter, the geo-block and every audit row
2901
+ * use (`resolveClientAddress`). Use this, never `headers['x-forwarded-for']`.
2902
+ *
2903
+ * `x-forwarded-for` is a request header: any client can write it. Reading it
2904
+ * raw means a caller picks the IP that lands in your `sessions.ipAddress`
2905
+ * column, which is the one field a breach investigation leans on. Three
2906
+ * first-party routes did exactly that until SEC-8 was extended down to this
2907
+ * surface. The resolution here ignores the header entirely unless a trusted
2908
+ * proxy is declared, and then believes only the hops that are one.
2909
+ *
2910
+ * `undefined` when the socket address is unavailable (a unix socket, an
2911
+ * in-process test harness that constructs the request by hand).
2912
+ */
2913
+ readonly remoteAddr?: string | undefined;
2178
2914
  }
2179
2915
 
2180
2916
  /**
@@ -2291,6 +3027,55 @@ export declare type PluginInspectResponse = {
2291
3027
  */
2292
3028
  export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
2293
3029
 
3030
+ /**
3031
+ * The two things a plugin's `name` is asked to encode — and they are NOT the
3032
+ * same question, which is why they are two fields.
3033
+ *
3034
+ * A plugin's name decides its rpc-tag prefix (`pluginAlias`) and its inspect
3035
+ * URL slug (`pluginSlug`). Two different app-side problems land on it:
3036
+ *
3037
+ * - **`alias` — "your namespace collides with mine."** An app that already
3038
+ * publishes `notifications.*` routes cannot install a plugin that also wants
3039
+ * `notifications.*`; the collision is fatal at codegen. `alias` REPLACES the
3040
+ * namespace, so the app keeps its own name and the plugin moves.
3041
+ * - **`instance` — "I want two of these."** A second cdc-out pipeline, a
3042
+ * second mail transport. The base name stays (so it is still recognisably
3043
+ * that plugin) and gains a `#suffix` discriminator.
3044
+ *
3045
+ * Both were already in the tree, one of them eleven times. The `#suffix`
3046
+ * ternary was copy-pasted verbatim into eleven plugins, and `alias` existed on
3047
+ * exactly one (`ai-flows`) with its own hand-rolled shape — so the two
3048
+ * mechanisms had no defined interaction at all. This is the one implementation.
3049
+ *
3050
+ * **What an alias costs, stated because it is not obvious and nothing else
3051
+ * says it:** the local and cloud dashboards fetch a plugin's inspect panel at
3052
+ * `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in. Alias
3053
+ * a plugin that ships `inspectEndpoints` and the endpoints keep working, the
3054
+ * rpc tags move as intended, and the dashboard panel 404s — because the panel
3055
+ * is in a different repository and cannot follow. Alias to dodge a tag
3056
+ * collision; do not alias a plugin whose dashboard panel you use.
3057
+ *
3058
+ * @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'`
3059
+ * @param alias replaces the whole namespace — an app-chosen name
3060
+ * @param instance discriminates one installation from another (`#suffix`)
3061
+ *
3062
+ * ```ts
3063
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out' })
3064
+ * //=> '@voltro/plugin-cdc-out' tag `cdcOut.*` slug `cdc-out`
3065
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out', instance: 'analytics' })
3066
+ * //=> '@voltro/plugin-cdc-out#analytics' tag `cdcOut.*` slug `cdc-out--analytics`
3067
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror' })
3068
+ * //=> 'mirror' tag `mirror.*` slug `mirror`
3069
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror', instance: 'analytics' })
3070
+ * //=> 'mirror#analytics' tag `mirror.*` slug `mirror--analytics'
3071
+ * ```
3072
+ */
3073
+ export declare const pluginInstanceName: (args: {
3074
+ readonly base: string;
3075
+ readonly alias?: string | undefined;
3076
+ readonly instance?: string | undefined;
3077
+ }) => string;
3078
+
2294
3079
  /**
2295
3080
  * Per-app context handed to lifecycle hooks. The shape is intentionally
2296
3081
  * thin — the framework's deeper services (DataStore, Logger, etc.) are
@@ -2444,14 +3229,41 @@ export declare interface PluginRpcRoute {
2444
3229
  * The executor returns the query's VALUE (an array / object) — the same
2445
3230
  * shape it returns for a poll; the framework recomputes it on change. Omit
2446
3231
  * for a plain (poll-only) plugin query, a mutation, or an action. For a
2447
- * query that mirrors a plugin table, set this to that table's name (e.g.
2448
- * `@voltro/plugin-presence` sets `_voltro_presence`, which is reactive).
3232
+ * query that mirrors a plugin table, set this to that table's name.
3233
+ *
3234
+ * For plugin state that is NOT in a table, declare a
3235
+ * `reactivityChannel(...)` and pass the CHANNEL here — do not declare a table
3236
+ * you never write in order to own the name. `@voltro/plugin-presence` did
3237
+ * exactly that for a release, and the empty `_voltro_presence` table it left
3238
+ * in every user's database is what the channel primitive replaced.
2449
3239
  *
2450
3240
  * `| undefined` is explicit so a plugin can spread a browser-safe query
2451
3241
  * DESCRIPTOR (which always carries `source: … | undefined`) into a route
2452
3242
  * literal under `exactOptionalPropertyTypes` without a cast.
2453
3243
  */
2454
- readonly source?: string | ReadonlyArray<string> | undefined;
3244
+ readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined;
3245
+ /**
3246
+ * The route's ACCESS DECISION — the same two-field vocabulary user
3247
+ * procedures carry, because a plugin route is dispatched through the exact
3248
+ * same spine. Spreading a browser-safe descriptor
3249
+ * (`{ ...listDescriptor, execute }`) carries the decision automatically; the
3250
+ * lift (`pluginRoutes.ts`) forwards it onto the descriptor the runtime
3251
+ * enforces.
3252
+ *
3253
+ * `| undefined` on both, like `source`: a spread descriptor always carries
3254
+ * the properties, and `exactOptionalPropertyTypes` would otherwise refuse
3255
+ * the spread without a cast.
3256
+ *
3257
+ * A route that declares NEITHER is the SEC-1 shape — callable by any
3258
+ * authenticated session, and REFUSED per-request by the dispatch spine when
3259
+ * the app runs with `security.defaultDeny` (the default). First-party
3260
+ * plugins declare a decision on every route; third-party plugins must too.
3261
+ */
3262
+ readonly guards?: DeclaredAccess | undefined;
3263
+ /** Deliberate no-check marker with the REASON — see `openAccess:` on
3264
+ * `defineQuery`/`defineMutation`/`defineAction`. Mutually exclusive with a
3265
+ * non-empty `guards`. */
3266
+ readonly openAccess?: string | undefined;
2455
3267
  /**
2456
3268
  * Effect-only executor. The base layer is provided by the framework
2457
3269
  * (DataStore, HttpClient, the plugin's own service Tags) — the
@@ -2617,6 +3429,21 @@ export declare interface PublicApiSpec {
2617
3429
  readonly stream?: 'snapshot' | 'sse';
2618
3430
  }
2619
3431
 
3432
+ /**
3433
+ * Push every subscriber of `channel`.
3434
+ *
3435
+ * Returns whether the store could deliver it. A store that cannot inject (a
3436
+ * limited fake, a transactional view) is a no-op rather than a throw: a missing
3437
+ * seam must not be able to take down the write that called this, and the read
3438
+ * that follows is still correct.
3439
+ *
3440
+ * This exists so no caller hand-builds the event. The one that did wrote
3441
+ * `injectExternalChange({ … } as never)` — and `as never` on a wiring object is
3442
+ * how the webhook trigger context came to differ between the two boot paths
3443
+ * while both compiled.
3444
+ */
3445
+ export declare const publishReactivity: (store: ReactivityPublisher | undefined, channel: ReactivityChannel) => boolean;
3446
+
2620
3447
  /** Publish a server-primitive error. Cheap + sync; a broken reporter can
2621
3448
  * never break the caller (each listener is isolated). Safe no-op when
2622
3449
  * nothing is subscribed — primitives call it unconditionally. */
@@ -2682,7 +3509,11 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
2682
3509
  readonly cache: QueryCacheConfig | undefined;
2683
3510
  /** Declarative authorization guard(s) — enforced before the executor runs,
2684
3511
  * failing with a typed `ScopeError`. Absent → no framework-level authz. */
2685
- readonly guards: Guards | undefined;
3512
+ readonly guards: DeclaredAccess | undefined;
3513
+ /** The declared reason this procedure needs NO authorization check —
3514
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
3515
+ * the only two shapes `security.defaultDeny` accepts. */
3516
+ readonly openAccess: string | undefined;
2686
3517
  /** Opt this query into a public REST endpoint (innovation/11). */
2687
3518
  readonly publicApi: PublicApiSpec | undefined;
2688
3519
  /** Opt this query into the auto-synthesized agent toolset (innovation/07). */
@@ -2760,6 +3591,56 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Sche
2760
3591
  revision: Schema.optional<typeof Schema.Number>;
2761
3592
  }>]>, Schema.Schema.All>, typeof Schema.Never, never>;
2762
3593
 
3594
+ /** The `channel:` namespace. Every channel key starts with it. */
3595
+ export declare const REACTIVITY_CHANNEL_PREFIX = "channel:";
3596
+
3597
+ /**
3598
+ * A declared reactivity channel — a push target with no table behind it.
3599
+ *
3600
+ * Create one with `reactivityChannel(name)`; pass it as a query's `source:`.
3601
+ */
3602
+ export declare interface ReactivityChannel {
3603
+ readonly kind: 'reactivity-channel';
3604
+ /** The name as declared, without the namespace. */
3605
+ readonly name: string;
3606
+ /** The routing key — what the dispatcher indexes and `source:` resolves to. */
3607
+ readonly key: string;
3608
+ /** The key, so a channel interpolates into a message as its routing key. */
3609
+ toString(): string;
3610
+ }
3611
+
3612
+ /**
3613
+ * Declare a reactivity channel.
3614
+ *
3615
+ * Idempotent by name: calling it twice returns the SAME object. A module
3616
+ * evaluated twice (hot reload, a dual-instance resolve) must not produce two
3617
+ * channels that compare unequal while routing to one key.
3618
+ *
3619
+ * export const presenceRoster = reactivityChannel('presence')
3620
+ * // …
3621
+ * defineQuery({ name: 'presence.list', source: presenceRoster, … })
3622
+ * // …
3623
+ * publishReactivity(store, presenceRoster)
3624
+ */
3625
+ export declare const reactivityChannel: (name: string) => ReactivityChannel;
3626
+
3627
+ /**
3628
+ * The one method of a store this needs — structural, so publishing does not
3629
+ * pull `@voltro/database`'s `DataStore` into a caller that had no reason for it.
3630
+ */
3631
+ export declare interface ReactivityPublisher {
3632
+ readonly injectExternalChange?: (event: {
3633
+ readonly table: string;
3634
+ readonly op: 'insert' | 'update' | 'delete';
3635
+ readonly new: Record<string, unknown>;
3636
+ readonly old: Record<string, unknown>;
3637
+ readonly origin?: 'inline' | 'injected';
3638
+ }) => void;
3639
+ }
3640
+
3641
+ /** What a query may declare as its reactive source. */
3642
+ export declare type ReactivitySource = string | ReactivityChannel;
3643
+
2763
3644
  /** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
2764
3645
  * Checks the EFFECTIVE set so role-derived scopes count. */
2765
3646
  export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
@@ -2988,6 +3869,72 @@ export declare interface ScheduleFireContext {
2988
3869
  */
2989
3870
  export declare type ScheduleFireInterceptor = (next: () => Promise<void>, ctx: ScheduleFireContext) => Promise<void>;
2990
3871
 
3872
+ export declare interface ScopeCache {
3873
+ /** The effective window in milliseconds. */
3874
+ readonly ttlMs: number;
3875
+ /** Resolve through the cache. `compute` runs only on a miss. */
3876
+ readonly resolve: (key: string, compute: () => Promise<ScopeDecision>) => Promise<CachedScopeDecision>;
3877
+ /** Drop one subject's cached authority — call this from whatever changes a
3878
+ * role, and the change is live on this process immediately. */
3879
+ readonly invalidate: (key: string) => void;
3880
+ /** Drop every cached verdict — for a change that alters a ROLE's definition
3881
+ * rather than one user's membership, where the affected subjects aren't
3882
+ * enumerable. */
3883
+ readonly invalidateAll: () => void;
3884
+ }
3885
+
3886
+ /**
3887
+ * The cache key for a subject's authority: type, tenant, id.
3888
+ *
3889
+ * `tenantId` is in the key and it is load-bearing. A user who switches tenants
3890
+ * keeps their `id`, and their authority is per-tenant — key on the id alone and
3891
+ * a switch serves the previous tenant's scopes for a whole window. Use this
3892
+ * when calling `invalidate` so the key you drop is the key that was written.
3893
+ */
3894
+ export declare const scopeCacheKey: (subject: Subject | SubjectIdentity) => string;
3895
+
3896
+ export declare interface ScopeCacheOptions {
3897
+ /** Cache window in milliseconds. Default 30 000. `0` disables caching (every
3898
+ * request resolves). Overridden by `VOLTRO_AUTH_SCOPE_CACHE_TTL_MS` only
3899
+ * when this is left unset — an explicit number in code wins over the env. */
3900
+ readonly ttlMs?: number;
3901
+ /** Upper bound on cached subjects. Default 10 000. */
3902
+ readonly maxEntries?: number;
3903
+ /** Clock override (epoch ms). Testing seam. */
3904
+ readonly now?: () => number;
3905
+ }
3906
+
3907
+ /** Compose multiple strategies into a single resolver function. The
3908
+ * composer evaluates them in declaration order; first `matched`
3909
+ * wins; first `failed` short-circuits to anonymous (does NOT fall
3910
+ * through — see StrategyResolution doc).
3911
+ *
3912
+ * NOTHING may answer ahead of this chain. The per-connection soft-reauth
3913
+ * override used to (`getConnectionSubject`, `@voltro/runtime`), and that is
3914
+ * precisely what made a rebound connection immune to session revocation, to
3915
+ * `resolveScopes` and to the scope cache. It patches the connection's HEADERS
3916
+ * now and this chain runs unchanged — see `ConnectionCredential` above.
3917
+ *
3918
+ * Returns an async resolver `(input) => Promise<Subject>`.
3919
+ */
3920
+ /** A resolver's verdict on a subject's authority. */
3921
+ export declare type ScopeDecision =
3922
+ /** Union these onto what the strategy established. */
3923
+ {
3924
+ readonly kind: 'grant';
3925
+ readonly scopes: ReadonlyArray<string>;
3926
+ }
3927
+ /** These are the caller's COMPLETE authority — anything else is removed. */
3928
+ | {
3929
+ readonly kind: 'authoritative';
3930
+ readonly scopes: ReadonlyArray<string>;
3931
+ }
3932
+ /** The authority source could not be reached. Fails the request closed. */
3933
+ | {
3934
+ readonly kind: 'unavailable';
3935
+ readonly reason: string;
3936
+ };
3937
+
2991
3938
  export declare class ScopeError extends ScopeError_base {
2992
3939
  }
2993
3940
 
@@ -2998,6 +3945,11 @@ declare const ScopeError_base: Schema.TaggedErrorClass<ScopeError, "ScopeError",
2998
3945
  message: typeof Schema.String;
2999
3946
  }>;
3000
3947
 
3948
+ /** What `resolveScopes` may return. A bare array is the shorthand for
3949
+ * `{ kind: 'grant', scopes }` — the common case, and the reading an existing
3950
+ * resolver already has. */
3951
+ export declare type ScopeResolverResult = ReadonlyArray<string> | ScopeDecision;
3952
+
3001
3953
  export declare interface ServerErrorEvent {
3002
3954
  /** The thrown value (Error | tagged error | anything). */
3003
3955
  readonly error: unknown;
@@ -3036,6 +3988,16 @@ export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | un
3036
3988
  */
3037
3989
  export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
3038
3990
 
3991
+ /**
3992
+ * Every routing key a `source:` names, flattened.
3993
+ *
3994
+ * The one normaliser. `source` has been read with an inline
3995
+ * `typeof s === 'string' ? [s] : s` at nine sites, which was already a decision
3996
+ * written nine times; adding a second accepted shape to each of them is how the
3997
+ * ninth ends up handling channels differently from the first.
3998
+ */
3999
+ export declare const sourceKeys: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => ReadonlyArray<string>;
4000
+
3039
4001
  export declare type StrategyResolution = {
3040
4002
  readonly kind: 'matched';
3041
4003
  readonly subject: Subject;
@@ -3084,7 +4046,11 @@ export declare interface StreamProcedureDescriptor<Name extends string, Input ex
3084
4046
  /** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element,
3085
4047
  * the same as a query's — a stream is a long-lived grant and the scopes that
3086
4048
  * justified it can be withdrawn while it is still open. */
3087
- readonly guards: Guards | undefined;
4049
+ readonly guards: DeclaredAccess | undefined;
4050
+ /** The declared reason this procedure needs NO authorization check —
4051
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
4052
+ * the only two shapes `security.defaultDeny` accepts. */
4053
+ readonly openAccess: string | undefined;
3088
4054
  }
3089
4055
 
3090
4056
  export declare const streamToRpc: <Name extends string, Input extends Schema.Schema.Any, Element extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: StreamProcedureDescriptor<Name, Input, Element, Err>, extraErrors?: ExtraErrors) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Element, Schema.Schema.All>, typeof Schema.Never, never>;
@@ -3121,6 +4087,44 @@ export declare const Subject: Schema.Union<[Schema.Struct<{
3121
4087
 
3122
4088
  export declare type Subject = typeof Subject.Type;
3123
4089
 
4090
+ export declare const SubjectIdentity: Schema.Union<[Schema.Struct<{
4091
+ id: typeof Schema.String;
4092
+ type: Schema.Literal<["user"]>;
4093
+ tenantId: typeof Schema.String;
4094
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4095
+ }>, Schema.Struct<{
4096
+ id: typeof Schema.String;
4097
+ type: Schema.Literal<["apiKey"]>;
4098
+ tenantId: typeof Schema.String;
4099
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4100
+ }>, Schema.Struct<{
4101
+ id: typeof Schema.String;
4102
+ type: Schema.Literal<["serviceAccount"]>;
4103
+ tenantId: typeof Schema.String;
4104
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4105
+ }>, Schema.Struct<{
4106
+ type: Schema.Literal<["anonymous"]>;
4107
+ id: typeof Schema.Null;
4108
+ tenantId: Schema.NullOr<typeof Schema.String>;
4109
+ }>, Schema.Struct<{
4110
+ id: typeof Schema.String;
4111
+ type: Schema.Literal<["system"]>;
4112
+ tenantId: typeof Schema.Null;
4113
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4114
+ }>]>;
4115
+
4116
+ export declare type SubjectIdentity = typeof SubjectIdentity.Type;
4117
+
4118
+ /**
4119
+ * Drop a Subject's authority, keeping everything that identifies it.
4120
+ *
4121
+ * Total and lossy on purpose — there is no variant it can fail on and no
4122
+ * option to keep the scopes. A caller that wants to mint a credential from a
4123
+ * Subject goes through here, so "did this cookie carry authority?" has one
4124
+ * answer at every mint site instead of one per site.
4125
+ */
4126
+ export declare const subjectIdentity: (subject: Subject) => SubjectIdentity;
4127
+
3124
4128
  /** A strategy's verdict on a request.
3125
4129
  * - `matched`: this strategy claims the request; here's the Subject.
3126
4130
  * - `skip`: not my request (e.g. cookie absent); try next strategy.
@@ -3342,6 +4346,23 @@ declare const Unauthenticated_base: Schema.TaggedErrorClass<Unauthenticated, "Un
3342
4346
  reason: Schema.optional<typeof Schema.String>;
3343
4347
  }>;
3344
4348
 
4349
+ /**
4350
+ * Channel keys named by a `source:` that no `reactivityChannel()` declared.
4351
+ *
4352
+ * The channel half of the stale-`source:` audit. It should be structurally
4353
+ * unreachable when the channel is authored as an object — you cannot import a
4354
+ * declaration that does not exist — so it exists for the two ways round that:
4355
+ * a hand-written `source: 'channel:presence'` string, and a channel whose
4356
+ * declaring module the boot did not load.
4357
+ */
4358
+ export declare const undeclaredChannelKeys: (procedures: ReadonlyArray<{
4359
+ readonly name: string;
4360
+ readonly source: string | ReadonlyArray<string> | undefined;
4361
+ }>) => ReadonlyArray<{
4362
+ readonly procedure: string;
4363
+ readonly key: string;
4364
+ }>;
4365
+
3345
4366
  export declare const UNDO_APPLY_TAG: "__voltro.undo.apply";
3346
4367
 
3347
4368
  export declare const UNDO_LOG_TAG: "__voltro.undo.log";
@@ -3461,6 +4482,31 @@ export declare interface VoltroPlugin {
3461
4482
  * (`@voltro/audit#analytics`).
3462
4483
  */
3463
4484
  readonly name: string;
4485
+ /**
4486
+ * The plugin's CANONICAL name, before the app renamed it — set this whenever
4487
+ * `name` can come from an app-supplied `alias`.
4488
+ *
4489
+ * It exists because without it an alias silently half-works, and that was the
4490
+ * shipped state. `effectiveRouteTag` prefixes a route with the plugin's
4491
+ * alias UNLESS the route name already contains a dot — an escape hatch for a
4492
+ * plugin wanting a deeper namespace. But every first-party plugin declares
4493
+ * its routes fully qualified (`name: 'notifications.inbox'`, 96 such
4494
+ * declarations across seven plugins), so the escape hatch fires on all of
4495
+ * them and the alias moves NOTHING on the rpc surface. A user aliasing
4496
+ * `notifications` to escape a collision with their own `notifications.*`
4497
+ * routes would still collide — and would additionally lose the dashboard
4498
+ * panel, which fetches the default slug. Worse than not shipping the field.
4499
+ *
4500
+ * With `baseName` set, both tag derivations strip a leading
4501
+ * `<default-alias>.` before applying the EFFECTIVE alias, so
4502
+ * `notifications.inbox` under `alias: 'inbox'` becomes `inbox.inbox` rather
4503
+ * than staying put. A dotted name that does NOT start with the default alias
4504
+ * is left alone — that is the genuine escape hatch, and it survives.
4505
+ *
4506
+ * Derived, not declared per route, so the 96 declarations stay as they are
4507
+ * and cannot drift out of step with the strip.
4508
+ */
4509
+ readonly baseName?: string;
3464
4510
  /**
3465
4511
  * Plugin version — the package's own semver. Distinct from
3466
4512
  * `framework` (the FRAMEWORK range the plugin works with). Used by