@voltro/protocol 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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. */
@@ -714,8 +1071,77 @@ export declare interface CoordinatedScheduleHandle {
714
1071
  readonly stop: () => void;
715
1072
  /** The task name (for logging / dedup diagnostics). */
716
1073
  readonly name: string;
1074
+ /**
1075
+ * Run a tick NOW because something arrived — the half of the contract that
1076
+ * makes {@link CoordinatedTickOutcome}'s backoff safe to use.
1077
+ *
1078
+ * Wire it to whatever announces work (a `store.onChange` on the plugin's own
1079
+ * queue table, a broadcast message). Coalesced to at most one extra tick per
1080
+ * `intervalMs`, so calling it per row is fine.
1081
+ */
1082
+ readonly wake: () => void;
1083
+ /** The delay the next tick is currently armed for — a task at the idle
1084
+ * ceiling and one at the base interval are indistinguishable otherwise. */
1085
+ readonly currentIntervalMs: () => number;
1086
+ /** `false` once the task has stopped ticking and is only waiting for
1087
+ * `wake()`. A disarmed task and a stopped one look identical from outside
1088
+ * otherwise, and only one of them comes back. */
1089
+ readonly isArmed: () => boolean;
717
1090
  }
718
1091
 
1092
+ /**
1093
+ * What a coordinated tick learned, returned so the runner can stop polling a
1094
+ * queue that has nothing in it.
1095
+ *
1096
+ * Returning nothing means "assume there was work", which is the conservative
1097
+ * reading: a task that does not report is never slowed down on the strength of
1098
+ * an assumption about it.
1099
+ */
1100
+ export declare interface CoordinatedTaskOptions {
1101
+ /**
1102
+ * Stop ticking entirely on an idle tick with no pending deadline, and come
1103
+ * back only on `wake()`. Default `false`.
1104
+ *
1105
+ * Pass `true` only when an arrival is GUARANTEED to call `wake()` — a change
1106
+ * subscription on the table your task drains, on a deployment where remote
1107
+ * writes are visible (Postgres LISTEN/NOTIFY, or a broadcast broker). Without
1108
+ * that, a disarmed task sleeps through a peer's enqueue forever, and the
1109
+ * backoff ceiling is the correct behaviour instead.
1110
+ */
1111
+ readonly disarmWhenIdle?: boolean;
1112
+ }
1113
+
1114
+ export declare interface CoordinatedTickOutcome {
1115
+ /** `true` when the tick found nothing to do. Only an idle tick backs off. */
1116
+ readonly idle: boolean;
1117
+ /** Milliseconds until the earliest deadline this task already knows about.
1118
+ * Caps the backoff, so a task that is idle now but has something due in
1119
+ * 400 ms is armed for 400 ms rather than for the idle ceiling. */
1120
+ readonly nextDueInMs?: number;
1121
+ }
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
+
719
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: {
720
1146
  readonly name: Name;
721
1147
  readonly input: Input;
@@ -725,6 +1151,24 @@ export declare const defineAction: <const Name extends string, Input extends Sch
725
1151
  * scope(s) or the action fails with a typed `ScopeError` before the executor
726
1152
  * runs. `ScopeError` is auto-merged into the wire error union. */
727
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;
728
1172
  /** Table(s) this action READS. Declaring it is what lets `voltro check` know
729
1173
  * the table is alive — without it, a table only an action touches reads as
730
1174
  * an orphan. See `ActionProcedureDescriptor.source`. */
@@ -735,6 +1179,15 @@ export declare const defineAction: <const Name extends string, Input extends Sch
735
1179
  readonly publicApi?: PublicApiSpec;
736
1180
  /** Expose this action as an agent tool (innovation/07). */
737
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>>;
738
1191
  /**
739
1192
  * Keep this procedure OFF the wire entirely.
740
1193
  *
@@ -865,10 +1318,41 @@ export declare const defineMutation: <const Name extends string, Input extends S
865
1318
  * scope(s) or the mutation fails with a typed `ScopeError` BEFORE the
866
1319
  * transaction opens. `ScopeError` is auto-merged into the wire error union. */
867
1320
  readonly guards?: Guards<Schema.Schema.Type<Input>>;
1321
+ /**
1322
+ * Declare that this procedure needs NO authorization check — and say why.
1323
+ *
1324
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1325
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1326
+ * access decision nobody made is the SEC-1 hole, not a default.
1327
+ *
1328
+ * Use it for the endpoints that really are open — a health check, a public
1329
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1330
+ * already holds just to satisfy the gate: that guard reads as protection and
1331
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1332
+ *
1333
+ * The reason is required and is the point — it is what a reviewer reads and
1334
+ * what `voltro doctor` prints beside the tag.
1335
+ *
1336
+ * openAccess: 'public pricing page — reads no caller data'
1337
+ */
1338
+ readonly openAccess?: string;
868
1339
  /** Project this mutation as a public REST endpoint (innovation/11). */
869
1340
  readonly publicApi?: PublicApiSpec;
870
1341
  /** Expose this mutation as an agent tool (innovation/07). */
871
1342
  readonly exposeAsTool?: ExposeAsTool;
1343
+ /**
1344
+ * Require a SECOND human to approve before this mutation takes effect.
1345
+ *
1346
+ * The first call records a durable pending intent and fails with a typed
1347
+ * `ApprovalRequired` carrying its id; the transaction never opens. Once an
1348
+ * authorised, DIFFERENT subject approves, the IDENTICAL call succeeds exactly
1349
+ * once (the approval is consumed).
1350
+ *
1351
+ * Composes with `guards:` rather than replacing them — the requester still has
1352
+ * to be allowed to ASK. Refused together with `openAccess:` (see
1353
+ * `assertApprovalPolicyCoherent`).
1354
+ */
1355
+ readonly requiresApproval?: ApprovalPolicy<Schema.Schema.Type<Input>>;
872
1356
  /**
873
1357
  * Keep this procedure OFF the wire entirely.
874
1358
  *
@@ -954,14 +1438,37 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
954
1438
  * auto-optimistic patch routing from mutations targeting any of those
955
1439
  * tables. For a COMPUTED query (handler returns a shaped value) it is the
956
1440
  * reactive trigger set: the handler re-runs when ANY listed table changes
957
- * — pass an array to depend on several (e.g. a matrix joining two tables). */
958
- readonly source?: string | ReadonlyArray<string>;
1441
+ * — pass an array to depend on several (e.g. a matrix joining two tables).
1442
+ *
1443
+ * A `reactivityChannel(...)` is accepted here too, for state that pushes
1444
+ * without living in a table. Pass the CHANNEL, not its key string: the
1445
+ * import edge is what makes a channel `source:` impossible to leave stale,
1446
+ * which a table name (a bare string) can always be. */
1447
+ readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource>;
959
1448
  /** Opt into server-side snapshot caching with auto-invalidation. */
960
1449
  readonly cache?: QueryCacheConfig;
961
1450
  /** Declarative authorization guard(s) — the caller must hold the named
962
1451
  * scope(s) or the query fails with a typed `ScopeError` before the executor
963
1452
  * runs. `ScopeError` is auto-merged into the wire error union. */
964
1453
  readonly guards?: Guards<Schema.Schema.Type<Input>>;
1454
+ /**
1455
+ * Declare that this procedure needs NO authorization check — and say why.
1456
+ *
1457
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1458
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1459
+ * access decision nobody made is the SEC-1 hole, not a default.
1460
+ *
1461
+ * Use it for the endpoints that really are open — a health check, a public
1462
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1463
+ * already holds just to satisfy the gate: that guard reads as protection and
1464
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1465
+ *
1466
+ * The reason is required and is the point — it is what a reviewer reads and
1467
+ * what `voltro doctor` prints beside the tag.
1468
+ *
1469
+ * openAccess: 'public pricing page — reads no caller data'
1470
+ */
1471
+ readonly openAccess?: string;
965
1472
  /** Project this query as a public REST endpoint (innovation/11). */
966
1473
  readonly publicApi?: PublicApiSpec;
967
1474
  /** Expose this query as an agent tool (innovation/07). */
@@ -1027,6 +1534,24 @@ export declare const defineStream: <const Name extends string, Input extends Sch
1027
1534
  * push. Same shape and same semantics as a query's.
1028
1535
  */
1029
1536
  readonly guards?: Guards;
1537
+ /**
1538
+ * Declare that this procedure needs NO authorization check — and say why.
1539
+ *
1540
+ * The other half of `security.defaultDeny`. With the flag on, a procedure
1541
+ * that declares neither `guards:` nor this is refused at boot, by name: an
1542
+ * access decision nobody made is the SEC-1 hole, not a default.
1543
+ *
1544
+ * Use it for the endpoints that really are open — a health check, a public
1545
+ * price list, a signup precheck. Do NOT reach for a scope every caller
1546
+ * already holds just to satisfy the gate: that guard reads as protection and
1547
+ * enforces nothing, and it is the failure mode this field exists to prevent.
1548
+ *
1549
+ * The reason is required and is the point — it is what a reviewer reads and
1550
+ * what `voltro doctor` prints beside the tag.
1551
+ *
1552
+ * openAccess: 'public pricing page — reads no caller data'
1553
+ */
1554
+ readonly openAccess?: string;
1030
1555
  }) => StreamProcedureDescriptor<Name, Input, Element, Error>;
1031
1556
 
1032
1557
  export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
@@ -1434,6 +1959,14 @@ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefin
1434
1959
  /** The currently-registered resource-scope resolver, or `undefined`. */
1435
1960
  export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
1436
1961
 
1962
+ /** Ask for default-deny semantics on one `checkGuards` call. */
1963
+ export declare interface GuardCheckOptions {
1964
+ /** Refuse a procedure that declares no access decision at all. */
1965
+ readonly defaultDeny?: boolean;
1966
+ /** The procedure's tag, so the refusal can name it. */
1967
+ readonly procedure?: string;
1968
+ }
1969
+
1437
1970
  export declare interface GuardCheckSpec {
1438
1971
  readonly scope: string | ReadonlyArray<string>;
1439
1972
  readonly mode?: 'all' | 'any';
@@ -1470,6 +2003,13 @@ export declare interface GuardSpec<Input = unknown> {
1470
2003
  readonly resource?: (input: Input) => string | undefined;
1471
2004
  }
1472
2005
 
2006
+ /** Does this descriptor declare an access decision — a guard, or a deliberate
2007
+ * `openAccess:`? The boot gate's predicate; `false` is the SEC-1 shape. */
2008
+ export declare const hasAccessDecision: (descriptor: {
2009
+ readonly guards?: ReadonlyArray<unknown> | undefined;
2010
+ readonly openAccess?: string | undefined;
2011
+ }) => boolean;
2012
+
1473
2013
  export declare const hasCallbackRoutes: (s: AuthStrategy) => s is AuthStrategyWithCallback;
1474
2014
 
1475
2015
  /** True if the subject holds `scope` (or the `admin:full` bypass), checking
@@ -1515,8 +2055,22 @@ export declare interface HttpRequestContext {
1515
2055
  readonly path: string;
1516
2056
  /** Lowercased request headers. */
1517
2057
  readonly headers: Readonly<Record<string, string>>;
1518
- /** Best-effort remote address. `undefined` when running behind a
1519
- * proxy without `x-forwarded-for`. */
2058
+ /**
2059
+ * The client address, resolved through the app's `security.trustedProxies`
2060
+ * policy — the SAME value `PluginHttpRouteRequest.remoteAddr`, the rate
2061
+ * limiter, the geo-block and every audit row use (`resolveClientAddress`).
2062
+ * Use this, never `headers['x-forwarded-for']`.
2063
+ *
2064
+ * This is a pre-auth shield's whole key, so getting it from the header is
2065
+ * not a smaller mistake here than elsewhere — it is the one place it is
2066
+ * worst. `x-forwarded-for` is a request header: any client can write it, so
2067
+ * a token bucket keyed on it is bypassed by one extra header per request.
2068
+ * The resolution here ignores the header entirely unless a trusted proxy is
2069
+ * declared, and then believes only the hops that are one.
2070
+ *
2071
+ * `undefined` when the socket address is unavailable (a unix socket, an
2072
+ * in-process test harness that constructs the request by hand).
2073
+ */
1520
2074
  readonly remoteAddr: string | undefined;
1521
2075
  }
1522
2076
 
@@ -1617,12 +2171,31 @@ export declare const isEventDescriptor: (value: unknown) => value is AnyEventDes
1617
2171
  * diffed by id and falls back to shipping the full data. */
1618
2172
  export declare const isIdKeyed: (rows: ReadonlyArray<Readonly<Record<string, unknown>>>) => rows is ReadonlyArray<PatchRow>;
1619
2173
 
2174
+ /** Runtime narrowing for the declared-open variant. */
2175
+ export declare const isOpenAccess: (g: AnyCheckSpec) => g is OpenAccessSpec;
2176
+
1620
2177
  /** Runtime narrowing for the relationship variant. */
1621
2178
  export declare const isPolicyCheck: (g: AnyCheckSpec) => g is PolicyCheckSpec;
1622
2179
 
1623
2180
  /** Narrow a guard entry to the relationship variant. */
1624
2181
  export declare const isPolicyGuard: <I>(g: AnyGuardSpec<I>) => g is PolicyGuardSpec<I>;
1625
2182
 
2183
+ /** Whether `value` is a channel object (not its key). */
2184
+ export declare const isReactivityChannel: (value: unknown) => value is ReactivityChannel;
2185
+
2186
+ /**
2187
+ * Whether `key` is addressed to the channel namespace.
2188
+ *
2189
+ * A PREFIX test, deliberately not a registry lookup — the two answer different
2190
+ * questions and only one of them belongs on the delivery path. At runtime the
2191
+ * question is "is this a table name or a channel key", and getting it wrong in
2192
+ * the strict direction drops a real push. Whether the channel was DECLARED is a
2193
+ * boot question, and `undeclaredChannelKeys` answers it there — the same
2194
+ * asymmetry tables already have (an unregistered table name still delivers;
2195
+ * the boot audit is what reports it).
2196
+ */
2197
+ export declare const isReactivityChannelKey: (key: string) => boolean;
2198
+
1626
2199
  export declare const isSystemSubject: (subject: Subject) => boolean;
1627
2200
 
1628
2201
  /**
@@ -1643,6 +2216,17 @@ export declare const isWireReachable: (descriptor: {
1643
2216
  readonly internal?: boolean | undefined;
1644
2217
  }) => boolean;
1645
2218
 
2219
+ /**
2220
+ * Build a scope cache. Hand it to `composeAuthStrategies({ scopeCache })` and
2221
+ * keep the handle: `invalidate(scopeCacheKey(subject))` from the mutation that
2222
+ * grants or removes a role is what makes the staleness window zero.
2223
+ *
2224
+ * `composeAuthStrategies` builds one internally when you don't, so the default
2225
+ * posture is cached-with-a-window rather than a store read per request — but a
2226
+ * cache nobody holds cannot be invalidated, which is why this is exported.
2227
+ */
2228
+ export declare const makeScopeCache: (options?: ScopeCacheOptions) => ScopeCache;
2229
+
1646
2230
  /**
1647
2231
  * How large one encoded envelope may be, on EVERY dialect.
1648
2232
  *
@@ -1663,6 +2247,10 @@ export declare const MAX_EVENT_ENVELOPE_BYTES = 7500;
1663
2247
  /** In-memory store — the default for single-process dev + the test double. */
1664
2248
  export declare const memoryIdempotencyStore: () => IdempotencyStore;
1665
2249
 
2250
+ /** The refusal a missing access decision produces. Exported so the boot gate
2251
+ * and the call-time path cannot describe the same defect differently. */
2252
+ export declare const missingAccessDecision: (procedure?: string) => ScopeError;
2253
+
1666
2254
  export declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
1667
2255
  readonly kind: 'mutation';
1668
2256
  readonly name: Name;
@@ -1677,11 +2265,19 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
1677
2265
  /** Declarative authorization guard(s) — enforced before the transaction
1678
2266
  * opens, failing with a typed `ScopeError`. Absent → no framework-level
1679
2267
  * authz (author gates in-handler, or the mutation is unguarded). */
1680
- readonly guards: Guards | undefined;
2268
+ readonly guards: DeclaredAccess | undefined;
2269
+ /** The declared reason this procedure needs NO authorization check —
2270
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
2271
+ * the only two shapes `security.defaultDeny` accepts. */
2272
+ readonly openAccess: string | undefined;
1681
2273
  /** Opt this mutation into a public REST endpoint (innovation/11). */
1682
2274
  readonly publicApi: PublicApiSpec | undefined;
1683
2275
  /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
1684
2276
  readonly exposeAsTool: ExposeAsTool | undefined;
2277
+ /** Require a SECOND human to approve before this mutation takes effect. The
2278
+ * gate runs in the dispatch spine after `guards:` and before the transaction
2279
+ * opens; the pending intent is a durable `_voltro_approvals` row. */
2280
+ readonly requiresApproval: AnyApprovalPolicy | undefined;
1685
2281
  /** True when the procedure is kept OFF the wire — no client-group entry and no
1686
2282
  * route in dev or serve. See `internal` on the definer's options. */
1687
2283
  /**
@@ -1733,6 +2329,17 @@ export declare interface NestedTargetFields<Input = unknown> {
1733
2329
  */
1734
2330
  export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => ClientDescriptor;
1735
2331
 
2332
+ /**
2333
+ * A `source:` as the DESCRIPTOR stores it — channels resolved to their keys,
2334
+ * with the caller's shape preserved.
2335
+ *
2336
+ * Shape-preserving on purpose: a single `source: 'notes'` must stay the string
2337
+ * `'notes'` and not become `['notes']`. It is serialised into the capability
2338
+ * manifest and into every api golden, so widening the shape would rewrite those
2339
+ * artefacts for every query in every app to express nothing.
2340
+ */
2341
+ export declare const normalizeSource: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => string | ReadonlyArray<string> | undefined;
2342
+
1736
2343
  /**
1737
2344
  * What a plugin contributes to the OTel layer via `contributeObservability`.
1738
2345
  * The OTel types are intentionally `unknown` so this browser-safe protocol
@@ -1752,6 +2359,31 @@ export declare interface ObservabilityContribution {
1752
2359
  readonly sampler?: unknown;
1753
2360
  }
1754
2361
 
2362
+ /**
2363
+ * The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision
2364
+ * that this procedure needs no authorization check, and the reason.
2365
+ *
2366
+ * It is a guard entry rather than a bare descriptor field on purpose. Every
2367
+ * enforcement path in the framework — `servePipeline`'s `enforceGuards`,
2368
+ * `bindStream`, `bindEvent`, `@voltro/testing`'s `invoke` — is handed the
2369
+ * `guards` ARRAY and nothing else. A decision that does not live in that array
2370
+ * is invisible to all of them, so "guarded" and "deliberately open" would be
2371
+ * distinguishable in the source and identical at the point that enforces.
2372
+ *
2373
+ * It always passes. The value is the WHY, and the why is the point: it is what
2374
+ * a reviewer reads, what `voltro doctor` prints, and what makes an open
2375
+ * procedure a decision somebody made rather than a field somebody forgot.
2376
+ */
2377
+ export declare interface OpenAccessSpec {
2378
+ /** Why this procedure is callable without an authorization check. Non-empty
2379
+ * by construction — `defineQuery` & co. refuse an empty reason. */
2380
+ readonly open: string;
2381
+ }
2382
+
2383
+ /** Build the erased `openAccess:` entry. The definers call this; an app writes
2384
+ * `openAccess: '<why>'` on the descriptor and never sees the spec. */
2385
+ export declare const openAccessSpec: (reason: string) => OpenAccessSpec;
2386
+
1755
2387
  /** Split a route back into its three parts. */
1756
2388
  export declare const parseEventRoute: (route: string) => {
1757
2389
  readonly tenantId: string | null;
@@ -1770,6 +2402,35 @@ export declare type PatchRow = Readonly<Record<string, unknown>> & {
1770
2402
  * needs to re-parse a number out of a path. */
1771
2403
  export declare const pathToId: (path: string) => string;
1772
2404
 
2405
+ export declare const PendingApproval: Schema.Struct<{
2406
+ id: typeof Schema.String;
2407
+ /** The rpc tag whose execution is pending. */
2408
+ procedure: typeof Schema.String;
2409
+ kind: Schema.Literal<["mutation", "action"]>;
2410
+ /** Subject id of whoever asked. Never the approver. */
2411
+ requestedBy: Schema.NullOr<typeof Schema.String>;
2412
+ status: Schema.Literal<["pending", "approved", "rejected", "expired", "consumed"]>;
2413
+ /** The declared reason from the descriptor, if any. */
2414
+ reason: Schema.NullOr<typeof Schema.String>;
2415
+ /** Scope(s) an approver must hold. */
2416
+ requiredScopes: Schema.Array$<typeof Schema.String>;
2417
+ requestedAt: typeof Schema.String;
2418
+ expiresAt: typeof Schema.String;
2419
+ decidedBy: Schema.NullOr<typeof Schema.String>;
2420
+ decidedAt: Schema.NullOr<typeof Schema.String>;
2421
+ /** The approver's note, when they left one. */
2422
+ note: Schema.NullOr<typeof Schema.String>;
2423
+ /**
2424
+ * How this row relates to the CALLING subject — the whole point of the feed.
2425
+ * `'to-decide'` = they may act on it; `'requested'` = they asked for it.
2426
+ * A row is never both: self-approval is refused, so a requester never
2427
+ * qualifies as its approver.
2428
+ */
2429
+ relation: Schema.Literal<["to-decide", "requested"]>;
2430
+ }>;
2431
+
2432
+ export declare type PendingApproval = Schema.Schema.Type<typeof PendingApproval>;
2433
+
1773
2434
  /**
1774
2435
  * Called once at app boot, after `voltro dev` resolves the plugin
1775
2436
  * list and before the rpc server starts accepting connections. Use
@@ -1850,7 +2511,7 @@ export declare interface PluginBindContext {
1850
2511
  * it becomes the claim key. Returns a handle whose `stop()` cancels the
1851
2512
  * task early. The framework also stops every armed task at shutdown.
1852
2513
  */
1853
- readonly scheduleCoordinated: (name: string, intervalMs: number, effect: () => void | Promise<void>) => CoordinatedScheduleHandle;
2514
+ readonly scheduleCoordinated: (name: string, intervalMs: number, effect: () => void | CoordinatedTickOutcome | Promise<void | CoordinatedTickOutcome>, options?: CoordinatedTaskOptions) => CoordinatedScheduleHandle;
1854
2515
  }
1855
2516
 
1856
2517
  /**
@@ -1866,6 +2527,23 @@ export declare interface PluginChangeEvent {
1866
2527
  readonly new: Record<string, unknown> | null;
1867
2528
  /** Row before the change — present on update + delete, null on insert. */
1868
2529
  readonly old: Record<string, unknown> | null;
2530
+ /**
2531
+ * Set when the transport could not carry this change's images and they were
2532
+ * RECONSTRUCTED — a row over postgres' 8000-byte `pg_notify` cap. Absent on
2533
+ * every ordinary event.
2534
+ *
2535
+ * A tap must read it before trusting an image as a snapshot:
2536
+ *
2537
+ * - `'rehydrated'` — `new` is the row RE-READ from the database. Correct to
2538
+ * index, mirror or forward; NOT necessarily the image the write that
2539
+ * fired this event produced (a later write may already have landed).
2540
+ * - `'tombstone'` — a delete whose `old` is the PRIMARY KEY and nothing
2541
+ * else. Enough to remove the row; never a record of what it contained.
2542
+ * A history/versioning tap must not store it as a snapshot.
2543
+ * - `'unrecovered'` — both images are null and the content is gone. The
2544
+ * change happened; re-read or resync if you need it.
2545
+ */
2546
+ readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
1869
2547
  /** How the event reached this process: absent/'inline' = this process's
1870
2548
  * own write; 'injected' = delivered over a cross-instance transport
1871
2549
  * (broadcast bus / CDC consumer). Combine with `changeScope` to act
@@ -1898,6 +2576,14 @@ export declare interface PluginChangeEvent {
1898
2576
  * join table is a member removal, a cascade or an expiry.
1899
2577
  */
1900
2578
  readonly procedure?: string;
2579
+ /**
2580
+ * The write was made BY AN AGENT acting as `subjectId` — the fourth member of
2581
+ * the shape the comment above tracks (`WriteAttribution` → `ChangeEvent` →
2582
+ * here). A tap that logs "who changed this" reads WRONG without it: the
2583
+ * subject is the person the agent acted as, by construction, so an agent
2584
+ * write and a human write are otherwise identical rows.
2585
+ */
2586
+ readonly via?: 'agent';
1901
2587
  /** The store's change visibility (see `DataStore.changeScope`):
1902
2588
  * 'local' — own writes inline (skip origin 'injected' for
1903
2589
  * exactly-once-on-the-writer); 'fleet' — every replica sees the full
@@ -2066,6 +2752,42 @@ export declare interface PluginHttpRoute {
2066
2752
  * any sub-path (`/_voltro/storage/abc123`). */
2067
2753
  readonly path: string;
2068
2754
  readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
2755
+ /**
2756
+ * Opt this route's path OUT of the listener's cross-site origin check.
2757
+ *
2758
+ * Every state-changing request (anything but GET/HEAD/OPTIONS) is
2759
+ * origin-checked by default, because the default assumption has to be that a
2760
+ * route can be reached with the browser's ambient session cookie — and a
2761
+ * route that can is CSRF-reachable. Declaring `'exempt'` is a claim that this
2762
+ * route CANNOT be: its caller must present something a browser will not
2763
+ * attach cross-site.
2764
+ *
2765
+ * The test to apply, and it is the only one:
2766
+ *
2767
+ * > If an attacker's page makes a browser send this request with the
2768
+ * > victim's cookies attached, does anything happen?
2769
+ *
2770
+ * If the answer is "no, the request still needs a signature / a bearer token
2771
+ * / a signed ticket the attacker does not have", the route is exempt.
2772
+ * Otherwise it is not, and no amount of "but it is behind the dashboard"
2773
+ * makes it so.
2774
+ *
2775
+ * The first-party exemptions and why each qualifies:
2776
+ * - `@voltro/plugin-sso-saml` `/saml` — the IdP delivers the assertion as a
2777
+ * genuine cross-site browser form POST; authority is the signed
2778
+ * SAMLResponse, not the cookie.
2779
+ * - `@voltro/plugin-storage` `/…/upload` + `/…/upload/resumable` — a signed
2780
+ * upload ticket in the query string, and the route ships its own CORS
2781
+ * allowlist because a cross-origin upload is the point.
2782
+ * - `@voltro/plugin-billing` `/billing/webhook` — an HMAC-verified provider
2783
+ * callback.
2784
+ * - `@voltro/plugin-scim` `/scim/v2` — bearer-only, refuses to mount without
2785
+ * a token.
2786
+ *
2787
+ * Granularity is the PATH PREFIX the route mounts, not the sub-path its
2788
+ * handler branches on: exempting `/saml` exempts `POST /saml/anything`.
2789
+ */
2790
+ readonly originGuard?: 'exempt';
2069
2791
  }
2070
2792
 
2071
2793
  export declare interface PluginHttpRouteRequest {
@@ -2128,6 +2850,22 @@ export declare interface PluginHttpRouteRequest {
2128
2850
  * this store reads them.
2129
2851
  */
2130
2852
  readonly store?: DataStore;
2853
+ /**
2854
+ * The client address, resolved through the app's `security.trustedProxies`
2855
+ * policy — the SAME value the rate limiter, the geo-block and every audit row
2856
+ * use (`resolveClientAddress`). Use this, never `headers['x-forwarded-for']`.
2857
+ *
2858
+ * `x-forwarded-for` is a request header: any client can write it. Reading it
2859
+ * raw means a caller picks the IP that lands in your `sessions.ipAddress`
2860
+ * column, which is the one field a breach investigation leans on. Three
2861
+ * first-party routes did exactly that until SEC-8 was extended down to this
2862
+ * surface. The resolution here ignores the header entirely unless a trusted
2863
+ * proxy is declared, and then believes only the hops that are one.
2864
+ *
2865
+ * `undefined` when the socket address is unavailable (a unix socket, an
2866
+ * in-process test harness that constructs the request by hand).
2867
+ */
2868
+ readonly remoteAddr?: string | undefined;
2131
2869
  }
2132
2870
 
2133
2871
  /**
@@ -2244,6 +2982,55 @@ export declare type PluginInspectResponse = {
2244
2982
  */
2245
2983
  export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
2246
2984
 
2985
+ /**
2986
+ * The two things a plugin's `name` is asked to encode — and they are NOT the
2987
+ * same question, which is why they are two fields.
2988
+ *
2989
+ * A plugin's name decides its rpc-tag prefix (`pluginAlias`) and its inspect
2990
+ * URL slug (`pluginSlug`). Two different app-side problems land on it:
2991
+ *
2992
+ * - **`alias` — "your namespace collides with mine."** An app that already
2993
+ * publishes `notifications.*` routes cannot install a plugin that also wants
2994
+ * `notifications.*`; the collision is fatal at codegen. `alias` REPLACES the
2995
+ * namespace, so the app keeps its own name and the plugin moves.
2996
+ * - **`instance` — "I want two of these."** A second cdc-out pipeline, a
2997
+ * second mail transport. The base name stays (so it is still recognisably
2998
+ * that plugin) and gains a `#suffix` discriminator.
2999
+ *
3000
+ * Both were already in the tree, one of them eleven times. The `#suffix`
3001
+ * ternary was copy-pasted verbatim into eleven plugins, and `alias` existed on
3002
+ * exactly one (`ai-flows`) with its own hand-rolled shape — so the two
3003
+ * mechanisms had no defined interaction at all. This is the one implementation.
3004
+ *
3005
+ * **What an alias costs, stated because it is not obvious and nothing else
3006
+ * says it:** the local and cloud dashboards fetch a plugin's inspect panel at
3007
+ * `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in. Alias
3008
+ * a plugin that ships `inspectEndpoints` and the endpoints keep working, the
3009
+ * rpc tags move as intended, and the dashboard panel 404s — because the panel
3010
+ * is in a different repository and cannot follow. Alias to dodge a tag
3011
+ * collision; do not alias a plugin whose dashboard panel you use.
3012
+ *
3013
+ * @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'`
3014
+ * @param alias replaces the whole namespace — an app-chosen name
3015
+ * @param instance discriminates one installation from another (`#suffix`)
3016
+ *
3017
+ * ```ts
3018
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out' })
3019
+ * //=> '@voltro/plugin-cdc-out' tag `cdcOut.*` slug `cdc-out`
3020
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out', instance: 'analytics' })
3021
+ * //=> '@voltro/plugin-cdc-out#analytics' tag `cdcOut.*` slug `cdc-out--analytics`
3022
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror' })
3023
+ * //=> 'mirror' tag `mirror.*` slug `mirror`
3024
+ * pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror', instance: 'analytics' })
3025
+ * //=> 'mirror#analytics' tag `mirror.*` slug `mirror--analytics'
3026
+ * ```
3027
+ */
3028
+ export declare const pluginInstanceName: (args: {
3029
+ readonly base: string;
3030
+ readonly alias?: string | undefined;
3031
+ readonly instance?: string | undefined;
3032
+ }) => string;
3033
+
2247
3034
  /**
2248
3035
  * Per-app context handed to lifecycle hooks. The shape is intentionally
2249
3036
  * thin — the framework's deeper services (DataStore, Logger, etc.) are
@@ -2397,14 +3184,19 @@ export declare interface PluginRpcRoute {
2397
3184
  * The executor returns the query's VALUE (an array / object) — the same
2398
3185
  * shape it returns for a poll; the framework recomputes it on change. Omit
2399
3186
  * for a plain (poll-only) plugin query, a mutation, or an action. For a
2400
- * query that mirrors a plugin table, set this to that table's name (e.g.
2401
- * `@voltro/plugin-presence` sets `_voltro_presence`, which is reactive).
3187
+ * query that mirrors a plugin table, set this to that table's name.
3188
+ *
3189
+ * For plugin state that is NOT in a table, declare a
3190
+ * `reactivityChannel(...)` and pass the CHANNEL here — do not declare a table
3191
+ * you never write in order to own the name. `@voltro/plugin-presence` did
3192
+ * exactly that for a release, and the empty `_voltro_presence` table it left
3193
+ * in every user's database is what the channel primitive replaced.
2402
3194
  *
2403
3195
  * `| undefined` is explicit so a plugin can spread a browser-safe query
2404
3196
  * DESCRIPTOR (which always carries `source: … | undefined`) into a route
2405
3197
  * literal under `exactOptionalPropertyTypes` without a cast.
2406
3198
  */
2407
- readonly source?: string | ReadonlyArray<string> | undefined;
3199
+ readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined;
2408
3200
  /**
2409
3201
  * Effect-only executor. The base layer is provided by the framework
2410
3202
  * (DataStore, HttpClient, the plugin's own service Tags) — the
@@ -2570,6 +3362,21 @@ export declare interface PublicApiSpec {
2570
3362
  readonly stream?: 'snapshot' | 'sse';
2571
3363
  }
2572
3364
 
3365
+ /**
3366
+ * Push every subscriber of `channel`.
3367
+ *
3368
+ * Returns whether the store could deliver it. A store that cannot inject (a
3369
+ * limited fake, a transactional view) is a no-op rather than a throw: a missing
3370
+ * seam must not be able to take down the write that called this, and the read
3371
+ * that follows is still correct.
3372
+ *
3373
+ * This exists so no caller hand-builds the event. The one that did wrote
3374
+ * `injectExternalChange({ … } as never)` — and `as never` on a wiring object is
3375
+ * how the webhook trigger context came to differ between the two boot paths
3376
+ * while both compiled.
3377
+ */
3378
+ export declare const publishReactivity: (store: ReactivityPublisher | undefined, channel: ReactivityChannel) => boolean;
3379
+
2573
3380
  /** Publish a server-primitive error. Cheap + sync; a broken reporter can
2574
3381
  * never break the caller (each listener is isolated). Safe no-op when
2575
3382
  * nothing is subscribed — primitives call it unconditionally. */
@@ -2635,7 +3442,11 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
2635
3442
  readonly cache: QueryCacheConfig | undefined;
2636
3443
  /** Declarative authorization guard(s) — enforced before the executor runs,
2637
3444
  * failing with a typed `ScopeError`. Absent → no framework-level authz. */
2638
- readonly guards: Guards | undefined;
3445
+ readonly guards: DeclaredAccess | undefined;
3446
+ /** The declared reason this procedure needs NO authorization check —
3447
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
3448
+ * the only two shapes `security.defaultDeny` accepts. */
3449
+ readonly openAccess: string | undefined;
2639
3450
  /** Opt this query into a public REST endpoint (innovation/11). */
2640
3451
  readonly publicApi: PublicApiSpec | undefined;
2641
3452
  /** Opt this query into the auto-synthesized agent toolset (innovation/07). */
@@ -2713,6 +3524,55 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Sche
2713
3524
  revision: Schema.optional<typeof Schema.Number>;
2714
3525
  }>]>, Schema.Schema.All>, typeof Schema.Never, never>;
2715
3526
 
3527
+ /** The `channel:` namespace. Every channel key starts with it. */
3528
+ export declare const REACTIVITY_CHANNEL_PREFIX = "channel:";
3529
+
3530
+ /**
3531
+ * A declared reactivity channel — a push target with no table behind it.
3532
+ *
3533
+ * Create one with `reactivityChannel(name)`; pass it as a query's `source:`.
3534
+ */
3535
+ export declare interface ReactivityChannel {
3536
+ readonly kind: 'reactivity-channel';
3537
+ /** The name as declared, without the namespace. */
3538
+ readonly name: string;
3539
+ /** The routing key — what the dispatcher indexes and `source:` resolves to. */
3540
+ readonly key: string;
3541
+ /** The key, so a channel interpolates into a message as its routing key. */
3542
+ toString(): string;
3543
+ }
3544
+
3545
+ /**
3546
+ * Declare a reactivity channel.
3547
+ *
3548
+ * Idempotent by name: calling it twice returns the SAME object. A module
3549
+ * evaluated twice (hot reload, a dual-instance resolve) must not produce two
3550
+ * channels that compare unequal while routing to one key.
3551
+ *
3552
+ * export const presenceRoster = reactivityChannel('presence')
3553
+ * // …
3554
+ * defineQuery({ name: 'presence.list', source: presenceRoster, … })
3555
+ * // …
3556
+ * publishReactivity(store, presenceRoster)
3557
+ */
3558
+ export declare const reactivityChannel: (name: string) => ReactivityChannel;
3559
+
3560
+ /**
3561
+ * The one method of a store this needs — structural, so publishing does not
3562
+ * pull `@voltro/database`'s `DataStore` into a caller that had no reason for it.
3563
+ */
3564
+ export declare interface ReactivityPublisher {
3565
+ readonly injectExternalChange?: (event: {
3566
+ readonly table: string;
3567
+ readonly op: 'insert' | 'update' | 'delete';
3568
+ readonly new: Record<string, unknown>;
3569
+ readonly old: Record<string, unknown>;
3570
+ }) => void;
3571
+ }
3572
+
3573
+ /** What a query may declare as its reactive source. */
3574
+ export declare type ReactivitySource = string | ReactivityChannel;
3575
+
2716
3576
  /** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
2717
3577
  * Checks the EFFECTIVE set so role-derived scopes count. */
2718
3578
  export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
@@ -2941,6 +3801,72 @@ export declare interface ScheduleFireContext {
2941
3801
  */
2942
3802
  export declare type ScheduleFireInterceptor = (next: () => Promise<void>, ctx: ScheduleFireContext) => Promise<void>;
2943
3803
 
3804
+ export declare interface ScopeCache {
3805
+ /** The effective window in milliseconds. */
3806
+ readonly ttlMs: number;
3807
+ /** Resolve through the cache. `compute` runs only on a miss. */
3808
+ readonly resolve: (key: string, compute: () => Promise<ScopeDecision>) => Promise<CachedScopeDecision>;
3809
+ /** Drop one subject's cached authority — call this from whatever changes a
3810
+ * role, and the change is live on this process immediately. */
3811
+ readonly invalidate: (key: string) => void;
3812
+ /** Drop every cached verdict — for a change that alters a ROLE's definition
3813
+ * rather than one user's membership, where the affected subjects aren't
3814
+ * enumerable. */
3815
+ readonly invalidateAll: () => void;
3816
+ }
3817
+
3818
+ /**
3819
+ * The cache key for a subject's authority: type, tenant, id.
3820
+ *
3821
+ * `tenantId` is in the key and it is load-bearing. A user who switches tenants
3822
+ * keeps their `id`, and their authority is per-tenant — key on the id alone and
3823
+ * a switch serves the previous tenant's scopes for a whole window. Use this
3824
+ * when calling `invalidate` so the key you drop is the key that was written.
3825
+ */
3826
+ export declare const scopeCacheKey: (subject: Subject | SubjectIdentity) => string;
3827
+
3828
+ export declare interface ScopeCacheOptions {
3829
+ /** Cache window in milliseconds. Default 30 000. `0` disables caching (every
3830
+ * request resolves). Overridden by `VOLTRO_AUTH_SCOPE_CACHE_TTL_MS` only
3831
+ * when this is left unset — an explicit number in code wins over the env. */
3832
+ readonly ttlMs?: number;
3833
+ /** Upper bound on cached subjects. Default 10 000. */
3834
+ readonly maxEntries?: number;
3835
+ /** Clock override (epoch ms). Testing seam. */
3836
+ readonly now?: () => number;
3837
+ }
3838
+
3839
+ /** Compose multiple strategies into a single resolver function. The
3840
+ * composer evaluates them in declaration order; first `matched`
3841
+ * wins; first `failed` short-circuits to anonymous (does NOT fall
3842
+ * through — see StrategyResolution doc).
3843
+ *
3844
+ * NOTHING may answer ahead of this chain. The per-connection soft-reauth
3845
+ * override used to (`getConnectionSubject`, `@voltro/runtime`), and that is
3846
+ * precisely what made a rebound connection immune to session revocation, to
3847
+ * `resolveScopes` and to the scope cache. It patches the connection's HEADERS
3848
+ * now and this chain runs unchanged — see `ConnectionCredential` above.
3849
+ *
3850
+ * Returns an async resolver `(input) => Promise<Subject>`.
3851
+ */
3852
+ /** A resolver's verdict on a subject's authority. */
3853
+ export declare type ScopeDecision =
3854
+ /** Union these onto what the strategy established. */
3855
+ {
3856
+ readonly kind: 'grant';
3857
+ readonly scopes: ReadonlyArray<string>;
3858
+ }
3859
+ /** These are the caller's COMPLETE authority — anything else is removed. */
3860
+ | {
3861
+ readonly kind: 'authoritative';
3862
+ readonly scopes: ReadonlyArray<string>;
3863
+ }
3864
+ /** The authority source could not be reached. Fails the request closed. */
3865
+ | {
3866
+ readonly kind: 'unavailable';
3867
+ readonly reason: string;
3868
+ };
3869
+
2944
3870
  export declare class ScopeError extends ScopeError_base {
2945
3871
  }
2946
3872
 
@@ -2951,6 +3877,11 @@ declare const ScopeError_base: Schema.TaggedErrorClass<ScopeError, "ScopeError",
2951
3877
  message: typeof Schema.String;
2952
3878
  }>;
2953
3879
 
3880
+ /** What `resolveScopes` may return. A bare array is the shorthand for
3881
+ * `{ kind: 'grant', scopes }` — the common case, and the reading an existing
3882
+ * resolver already has. */
3883
+ export declare type ScopeResolverResult = ReadonlyArray<string> | ScopeDecision;
3884
+
2954
3885
  export declare interface ServerErrorEvent {
2955
3886
  /** The thrown value (Error | tagged error | anything). */
2956
3887
  readonly error: unknown;
@@ -2989,6 +3920,16 @@ export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | un
2989
3920
  */
2990
3921
  export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
2991
3922
 
3923
+ /**
3924
+ * Every routing key a `source:` names, flattened.
3925
+ *
3926
+ * The one normaliser. `source` has been read with an inline
3927
+ * `typeof s === 'string' ? [s] : s` at nine sites, which was already a decision
3928
+ * written nine times; adding a second accepted shape to each of them is how the
3929
+ * ninth ends up handling channels differently from the first.
3930
+ */
3931
+ export declare const sourceKeys: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => ReadonlyArray<string>;
3932
+
2992
3933
  export declare type StrategyResolution = {
2993
3934
  readonly kind: 'matched';
2994
3935
  readonly subject: Subject;
@@ -3037,7 +3978,11 @@ export declare interface StreamProcedureDescriptor<Name extends string, Input ex
3037
3978
  /** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element,
3038
3979
  * the same as a query's — a stream is a long-lived grant and the scopes that
3039
3980
  * justified it can be withdrawn while it is still open. */
3040
- readonly guards: Guards | undefined;
3981
+ readonly guards: DeclaredAccess | undefined;
3982
+ /** The declared reason this procedure needs NO authorization check —
3983
+ * `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
3984
+ * the only two shapes `security.defaultDeny` accepts. */
3985
+ readonly openAccess: string | undefined;
3041
3986
  }
3042
3987
 
3043
3988
  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>;
@@ -3074,6 +4019,44 @@ export declare const Subject: Schema.Union<[Schema.Struct<{
3074
4019
 
3075
4020
  export declare type Subject = typeof Subject.Type;
3076
4021
 
4022
+ export declare const SubjectIdentity: Schema.Union<[Schema.Struct<{
4023
+ id: typeof Schema.String;
4024
+ type: Schema.Literal<["user"]>;
4025
+ tenantId: typeof Schema.String;
4026
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4027
+ }>, Schema.Struct<{
4028
+ id: typeof Schema.String;
4029
+ type: Schema.Literal<["apiKey"]>;
4030
+ tenantId: typeof Schema.String;
4031
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4032
+ }>, Schema.Struct<{
4033
+ id: typeof Schema.String;
4034
+ type: Schema.Literal<["serviceAccount"]>;
4035
+ tenantId: typeof Schema.String;
4036
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4037
+ }>, Schema.Struct<{
4038
+ type: Schema.Literal<["anonymous"]>;
4039
+ id: typeof Schema.Null;
4040
+ tenantId: Schema.NullOr<typeof Schema.String>;
4041
+ }>, Schema.Struct<{
4042
+ id: typeof Schema.String;
4043
+ type: Schema.Literal<["system"]>;
4044
+ tenantId: typeof Schema.Null;
4045
+ metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
4046
+ }>]>;
4047
+
4048
+ export declare type SubjectIdentity = typeof SubjectIdentity.Type;
4049
+
4050
+ /**
4051
+ * Drop a Subject's authority, keeping everything that identifies it.
4052
+ *
4053
+ * Total and lossy on purpose — there is no variant it can fail on and no
4054
+ * option to keep the scopes. A caller that wants to mint a credential from a
4055
+ * Subject goes through here, so "did this cookie carry authority?" has one
4056
+ * answer at every mint site instead of one per site.
4057
+ */
4058
+ export declare const subjectIdentity: (subject: Subject) => SubjectIdentity;
4059
+
3077
4060
  /** A strategy's verdict on a request.
3078
4061
  * - `matched`: this strategy claims the request; here's the Subject.
3079
4062
  * - `skip`: not my request (e.g. cookie absent); try next strategy.
@@ -3295,6 +4278,23 @@ declare const Unauthenticated_base: Schema.TaggedErrorClass<Unauthenticated, "Un
3295
4278
  reason: Schema.optional<typeof Schema.String>;
3296
4279
  }>;
3297
4280
 
4281
+ /**
4282
+ * Channel keys named by a `source:` that no `reactivityChannel()` declared.
4283
+ *
4284
+ * The channel half of the stale-`source:` audit. It should be structurally
4285
+ * unreachable when the channel is authored as an object — you cannot import a
4286
+ * declaration that does not exist — so it exists for the two ways round that:
4287
+ * a hand-written `source: 'channel:presence'` string, and a channel whose
4288
+ * declaring module the boot did not load.
4289
+ */
4290
+ export declare const undeclaredChannelKeys: (procedures: ReadonlyArray<{
4291
+ readonly name: string;
4292
+ readonly source: string | ReadonlyArray<string> | undefined;
4293
+ }>) => ReadonlyArray<{
4294
+ readonly procedure: string;
4295
+ readonly key: string;
4296
+ }>;
4297
+
3298
4298
  export declare const UNDO_APPLY_TAG: "__voltro.undo.apply";
3299
4299
 
3300
4300
  export declare const UNDO_LOG_TAG: "__voltro.undo.log";
@@ -3414,6 +4414,31 @@ export declare interface VoltroPlugin {
3414
4414
  * (`@voltro/audit#analytics`).
3415
4415
  */
3416
4416
  readonly name: string;
4417
+ /**
4418
+ * The plugin's CANONICAL name, before the app renamed it — set this whenever
4419
+ * `name` can come from an app-supplied `alias`.
4420
+ *
4421
+ * It exists because without it an alias silently half-works, and that was the
4422
+ * shipped state. `effectiveRouteTag` prefixes a route with the plugin's
4423
+ * alias UNLESS the route name already contains a dot — an escape hatch for a
4424
+ * plugin wanting a deeper namespace. But every first-party plugin declares
4425
+ * its routes fully qualified (`name: 'notifications.inbox'`, 96 such
4426
+ * declarations across seven plugins), so the escape hatch fires on all of
4427
+ * them and the alias moves NOTHING on the rpc surface. A user aliasing
4428
+ * `notifications` to escape a collision with their own `notifications.*`
4429
+ * routes would still collide — and would additionally lose the dashboard
4430
+ * panel, which fetches the default slug. Worse than not shipping the field.
4431
+ *
4432
+ * With `baseName` set, both tag derivations strip a leading
4433
+ * `<default-alias>.` before applying the EFFECTIVE alias, so
4434
+ * `notifications.inbox` under `alias: 'inbox'` becomes `inbox.inbox` rather
4435
+ * than staying put. A dotted name that does NOT start with the default alias
4436
+ * is left alone — that is the genuine escape hatch, and it survives.
4437
+ *
4438
+ * Derived, not declared per route, so the 96 declarations stay as they are
4439
+ * and cannot drift out of step with the strip.
4440
+ */
4441
+ readonly baseName?: string;
3417
4442
  /**
3418
4443
  * Plugin version — the package's own semver. Distinct from
3419
4444
  * `framework` (the FRAMEWORK range the plugin works with). Used by