@voltro/protocol 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1801 -0
- package/dist/auth-B6YZuSBr.js +303 -0
- package/dist/index.d.ts +1004 -26
- package/dist/index.js +267 -148
- package/dist/rest.d.ts +115 -3
- package/dist/rest.js +12 -12
- package/dist/serverErrorBus-BeN9pOpY.js +43 -0
- package/dist/session.d.ts +77 -16
- package/dist/session.js +62 -49
- package/package.json +3 -2
- package/dist/auth-CXMrvPyX.js +0 -90
- package/dist/serverErrorBus-C3JTqgIc.js +0 -157
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:
|
|
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
|
-
/**
|
|
77
|
-
export declare
|
|
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
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
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
|
-
*
|
|
442
|
-
*
|
|
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
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
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`.
|
|
449
767
|
*/
|
|
450
|
-
readonly resolveScopes?: (subject: Subject, input: AuthStrategyInput) => Promise<
|
|
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.
|
|
782
|
+
*/
|
|
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
|
*
|
|
@@ -912,10 +1318,41 @@ export declare const defineMutation: <const Name extends string, Input extends S
|
|
|
912
1318
|
* scope(s) or the mutation fails with a typed `ScopeError` BEFORE the
|
|
913
1319
|
* transaction opens. `ScopeError` is auto-merged into the wire error union. */
|
|
914
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;
|
|
915
1339
|
/** Project this mutation as a public REST endpoint (innovation/11). */
|
|
916
1340
|
readonly publicApi?: PublicApiSpec;
|
|
917
1341
|
/** Expose this mutation as an agent tool (innovation/07). */
|
|
918
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>>;
|
|
919
1356
|
/**
|
|
920
1357
|
* Keep this procedure OFF the wire entirely.
|
|
921
1358
|
*
|
|
@@ -1001,14 +1438,37 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
|
|
|
1001
1438
|
* auto-optimistic patch routing from mutations targeting any of those
|
|
1002
1439
|
* tables. For a COMPUTED query (handler returns a shaped value) it is the
|
|
1003
1440
|
* 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
|
-
|
|
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>;
|
|
1006
1448
|
/** Opt into server-side snapshot caching with auto-invalidation. */
|
|
1007
1449
|
readonly cache?: QueryCacheConfig;
|
|
1008
1450
|
/** Declarative authorization guard(s) — the caller must hold the named
|
|
1009
1451
|
* scope(s) or the query fails with a typed `ScopeError` before the executor
|
|
1010
1452
|
* runs. `ScopeError` is auto-merged into the wire error union. */
|
|
1011
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;
|
|
1012
1472
|
/** Project this query as a public REST endpoint (innovation/11). */
|
|
1013
1473
|
readonly publicApi?: PublicApiSpec;
|
|
1014
1474
|
/** Expose this query as an agent tool (innovation/07). */
|
|
@@ -1074,6 +1534,24 @@ export declare const defineStream: <const Name extends string, Input extends Sch
|
|
|
1074
1534
|
* push. Same shape and same semantics as a query's.
|
|
1075
1535
|
*/
|
|
1076
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;
|
|
1077
1555
|
}) => StreamProcedureDescriptor<Name, Input, Element, Error>;
|
|
1078
1556
|
|
|
1079
1557
|
export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
|
|
@@ -1481,6 +1959,14 @@ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefin
|
|
|
1481
1959
|
/** The currently-registered resource-scope resolver, or `undefined`. */
|
|
1482
1960
|
export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
|
|
1483
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
|
+
|
|
1484
1970
|
export declare interface GuardCheckSpec {
|
|
1485
1971
|
readonly scope: string | ReadonlyArray<string>;
|
|
1486
1972
|
readonly mode?: 'all' | 'any';
|
|
@@ -1517,6 +2003,13 @@ export declare interface GuardSpec<Input = unknown> {
|
|
|
1517
2003
|
readonly resource?: (input: Input) => string | undefined;
|
|
1518
2004
|
}
|
|
1519
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
|
+
|
|
1520
2013
|
export declare const hasCallbackRoutes: (s: AuthStrategy) => s is AuthStrategyWithCallback;
|
|
1521
2014
|
|
|
1522
2015
|
/** True if the subject holds `scope` (or the `admin:full` bypass), checking
|
|
@@ -1562,8 +2055,22 @@ export declare interface HttpRequestContext {
|
|
|
1562
2055
|
readonly path: string;
|
|
1563
2056
|
/** Lowercased request headers. */
|
|
1564
2057
|
readonly headers: Readonly<Record<string, string>>;
|
|
1565
|
-
/**
|
|
1566
|
-
*
|
|
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
|
+
*/
|
|
1567
2074
|
readonly remoteAddr: string | undefined;
|
|
1568
2075
|
}
|
|
1569
2076
|
|
|
@@ -1664,12 +2171,31 @@ export declare const isEventDescriptor: (value: unknown) => value is AnyEventDes
|
|
|
1664
2171
|
* diffed by id and falls back to shipping the full data. */
|
|
1665
2172
|
export declare const isIdKeyed: (rows: ReadonlyArray<Readonly<Record<string, unknown>>>) => rows is ReadonlyArray<PatchRow>;
|
|
1666
2173
|
|
|
2174
|
+
/** Runtime narrowing for the declared-open variant. */
|
|
2175
|
+
export declare const isOpenAccess: (g: AnyCheckSpec) => g is OpenAccessSpec;
|
|
2176
|
+
|
|
1667
2177
|
/** Runtime narrowing for the relationship variant. */
|
|
1668
2178
|
export declare const isPolicyCheck: (g: AnyCheckSpec) => g is PolicyCheckSpec;
|
|
1669
2179
|
|
|
1670
2180
|
/** Narrow a guard entry to the relationship variant. */
|
|
1671
2181
|
export declare const isPolicyGuard: <I>(g: AnyGuardSpec<I>) => g is PolicyGuardSpec<I>;
|
|
1672
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
|
+
|
|
1673
2199
|
export declare const isSystemSubject: (subject: Subject) => boolean;
|
|
1674
2200
|
|
|
1675
2201
|
/**
|
|
@@ -1690,6 +2216,17 @@ export declare const isWireReachable: (descriptor: {
|
|
|
1690
2216
|
readonly internal?: boolean | undefined;
|
|
1691
2217
|
}) => boolean;
|
|
1692
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
|
+
|
|
1693
2230
|
/**
|
|
1694
2231
|
* How large one encoded envelope may be, on EVERY dialect.
|
|
1695
2232
|
*
|
|
@@ -1710,6 +2247,10 @@ export declare const MAX_EVENT_ENVELOPE_BYTES = 7500;
|
|
|
1710
2247
|
/** In-memory store — the default for single-process dev + the test double. */
|
|
1711
2248
|
export declare const memoryIdempotencyStore: () => IdempotencyStore;
|
|
1712
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
|
+
|
|
1713
2254
|
export declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
|
|
1714
2255
|
readonly kind: 'mutation';
|
|
1715
2256
|
readonly name: Name;
|
|
@@ -1724,11 +2265,19 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
|
|
|
1724
2265
|
/** Declarative authorization guard(s) — enforced before the transaction
|
|
1725
2266
|
* opens, failing with a typed `ScopeError`. Absent → no framework-level
|
|
1726
2267
|
* authz (author gates in-handler, or the mutation is unguarded). */
|
|
1727
|
-
readonly guards:
|
|
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;
|
|
1728
2273
|
/** Opt this mutation into a public REST endpoint (innovation/11). */
|
|
1729
2274
|
readonly publicApi: PublicApiSpec | undefined;
|
|
1730
2275
|
/** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
|
|
1731
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;
|
|
1732
2281
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
1733
2282
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
1734
2283
|
/**
|
|
@@ -1780,6 +2329,17 @@ export declare interface NestedTargetFields<Input = unknown> {
|
|
|
1780
2329
|
*/
|
|
1781
2330
|
export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => ClientDescriptor;
|
|
1782
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
|
+
|
|
1783
2343
|
/**
|
|
1784
2344
|
* What a plugin contributes to the OTel layer via `contributeObservability`.
|
|
1785
2345
|
* The OTel types are intentionally `unknown` so this browser-safe protocol
|
|
@@ -1799,6 +2359,31 @@ export declare interface ObservabilityContribution {
|
|
|
1799
2359
|
readonly sampler?: unknown;
|
|
1800
2360
|
}
|
|
1801
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
|
+
|
|
1802
2387
|
/** Split a route back into its three parts. */
|
|
1803
2388
|
export declare const parseEventRoute: (route: string) => {
|
|
1804
2389
|
readonly tenantId: string | null;
|
|
@@ -1817,6 +2402,35 @@ export declare type PatchRow = Readonly<Record<string, unknown>> & {
|
|
|
1817
2402
|
* needs to re-parse a number out of a path. */
|
|
1818
2403
|
export declare const pathToId: (path: string) => string;
|
|
1819
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
|
+
|
|
1820
2434
|
/**
|
|
1821
2435
|
* Called once at app boot, after `voltro dev` resolves the plugin
|
|
1822
2436
|
* list and before the rpc server starts accepting connections. Use
|
|
@@ -1913,6 +2527,23 @@ export declare interface PluginChangeEvent {
|
|
|
1913
2527
|
readonly new: Record<string, unknown> | null;
|
|
1914
2528
|
/** Row before the change — present on update + delete, null on insert. */
|
|
1915
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';
|
|
1916
2547
|
/** How the event reached this process: absent/'inline' = this process's
|
|
1917
2548
|
* own write; 'injected' = delivered over a cross-instance transport
|
|
1918
2549
|
* (broadcast bus / CDC consumer). Combine with `changeScope` to act
|
|
@@ -1945,6 +2576,14 @@ export declare interface PluginChangeEvent {
|
|
|
1945
2576
|
* join table is a member removal, a cascade or an expiry.
|
|
1946
2577
|
*/
|
|
1947
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';
|
|
1948
2587
|
/** The store's change visibility (see `DataStore.changeScope`):
|
|
1949
2588
|
* 'local' — own writes inline (skip origin 'injected' for
|
|
1950
2589
|
* exactly-once-on-the-writer); 'fleet' — every replica sees the full
|
|
@@ -2113,6 +2752,42 @@ export declare interface PluginHttpRoute {
|
|
|
2113
2752
|
* any sub-path (`/_voltro/storage/abc123`). */
|
|
2114
2753
|
readonly path: string;
|
|
2115
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';
|
|
2116
2791
|
}
|
|
2117
2792
|
|
|
2118
2793
|
export declare interface PluginHttpRouteRequest {
|
|
@@ -2175,6 +2850,22 @@ export declare interface PluginHttpRouteRequest {
|
|
|
2175
2850
|
* this store reads them.
|
|
2176
2851
|
*/
|
|
2177
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;
|
|
2178
2869
|
}
|
|
2179
2870
|
|
|
2180
2871
|
/**
|
|
@@ -2291,6 +2982,55 @@ export declare type PluginInspectResponse = {
|
|
|
2291
2982
|
*/
|
|
2292
2983
|
export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
|
|
2293
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
|
+
|
|
2294
3034
|
/**
|
|
2295
3035
|
* Per-app context handed to lifecycle hooks. The shape is intentionally
|
|
2296
3036
|
* thin — the framework's deeper services (DataStore, Logger, etc.) are
|
|
@@ -2444,14 +3184,19 @@ export declare interface PluginRpcRoute {
|
|
|
2444
3184
|
* The executor returns the query's VALUE (an array / object) — the same
|
|
2445
3185
|
* shape it returns for a poll; the framework recomputes it on change. Omit
|
|
2446
3186
|
* 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
|
|
2448
|
-
*
|
|
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.
|
|
2449
3194
|
*
|
|
2450
3195
|
* `| undefined` is explicit so a plugin can spread a browser-safe query
|
|
2451
3196
|
* DESCRIPTOR (which always carries `source: … | undefined`) into a route
|
|
2452
3197
|
* literal under `exactOptionalPropertyTypes` without a cast.
|
|
2453
3198
|
*/
|
|
2454
|
-
readonly source?:
|
|
3199
|
+
readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined;
|
|
2455
3200
|
/**
|
|
2456
3201
|
* Effect-only executor. The base layer is provided by the framework
|
|
2457
3202
|
* (DataStore, HttpClient, the plugin's own service Tags) — the
|
|
@@ -2617,6 +3362,21 @@ export declare interface PublicApiSpec {
|
|
|
2617
3362
|
readonly stream?: 'snapshot' | 'sse';
|
|
2618
3363
|
}
|
|
2619
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
|
+
|
|
2620
3380
|
/** Publish a server-primitive error. Cheap + sync; a broken reporter can
|
|
2621
3381
|
* never break the caller (each listener is isolated). Safe no-op when
|
|
2622
3382
|
* nothing is subscribed — primitives call it unconditionally. */
|
|
@@ -2682,7 +3442,11 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
|
|
|
2682
3442
|
readonly cache: QueryCacheConfig | undefined;
|
|
2683
3443
|
/** Declarative authorization guard(s) — enforced before the executor runs,
|
|
2684
3444
|
* failing with a typed `ScopeError`. Absent → no framework-level authz. */
|
|
2685
|
-
readonly guards:
|
|
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;
|
|
2686
3450
|
/** Opt this query into a public REST endpoint (innovation/11). */
|
|
2687
3451
|
readonly publicApi: PublicApiSpec | undefined;
|
|
2688
3452
|
/** Opt this query into the auto-synthesized agent toolset (innovation/07). */
|
|
@@ -2760,6 +3524,55 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Sche
|
|
|
2760
3524
|
revision: Schema.optional<typeof Schema.Number>;
|
|
2761
3525
|
}>]>, Schema.Schema.All>, typeof Schema.Never, never>;
|
|
2762
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
|
+
|
|
2763
3576
|
/** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
|
|
2764
3577
|
* Checks the EFFECTIVE set so role-derived scopes count. */
|
|
2765
3578
|
export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
|
|
@@ -2988,6 +3801,72 @@ export declare interface ScheduleFireContext {
|
|
|
2988
3801
|
*/
|
|
2989
3802
|
export declare type ScheduleFireInterceptor = (next: () => Promise<void>, ctx: ScheduleFireContext) => Promise<void>;
|
|
2990
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
|
+
|
|
2991
3870
|
export declare class ScopeError extends ScopeError_base {
|
|
2992
3871
|
}
|
|
2993
3872
|
|
|
@@ -2998,6 +3877,11 @@ declare const ScopeError_base: Schema.TaggedErrorClass<ScopeError, "ScopeError",
|
|
|
2998
3877
|
message: typeof Schema.String;
|
|
2999
3878
|
}>;
|
|
3000
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
|
+
|
|
3001
3885
|
export declare interface ServerErrorEvent {
|
|
3002
3886
|
/** The thrown value (Error | tagged error | anything). */
|
|
3003
3887
|
readonly error: unknown;
|
|
@@ -3036,6 +3920,16 @@ export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | un
|
|
|
3036
3920
|
*/
|
|
3037
3921
|
export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
|
|
3038
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
|
+
|
|
3039
3933
|
export declare type StrategyResolution = {
|
|
3040
3934
|
readonly kind: 'matched';
|
|
3041
3935
|
readonly subject: Subject;
|
|
@@ -3084,7 +3978,11 @@ export declare interface StreamProcedureDescriptor<Name extends string, Input ex
|
|
|
3084
3978
|
/** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element,
|
|
3085
3979
|
* the same as a query's — a stream is a long-lived grant and the scopes that
|
|
3086
3980
|
* justified it can be withdrawn while it is still open. */
|
|
3087
|
-
readonly guards:
|
|
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;
|
|
3088
3986
|
}
|
|
3089
3987
|
|
|
3090
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>;
|
|
@@ -3121,6 +4019,44 @@ export declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
3121
4019
|
|
|
3122
4020
|
export declare type Subject = typeof Subject.Type;
|
|
3123
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
|
+
|
|
3124
4060
|
/** A strategy's verdict on a request.
|
|
3125
4061
|
* - `matched`: this strategy claims the request; here's the Subject.
|
|
3126
4062
|
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
@@ -3342,6 +4278,23 @@ declare const Unauthenticated_base: Schema.TaggedErrorClass<Unauthenticated, "Un
|
|
|
3342
4278
|
reason: Schema.optional<typeof Schema.String>;
|
|
3343
4279
|
}>;
|
|
3344
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
|
+
|
|
3345
4298
|
export declare const UNDO_APPLY_TAG: "__voltro.undo.apply";
|
|
3346
4299
|
|
|
3347
4300
|
export declare const UNDO_LOG_TAG: "__voltro.undo.log";
|
|
@@ -3461,6 +4414,31 @@ export declare interface VoltroPlugin {
|
|
|
3461
4414
|
* (`@voltro/audit#analytics`).
|
|
3462
4415
|
*/
|
|
3463
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;
|
|
3464
4442
|
/**
|
|
3465
4443
|
* Plugin version — the package's own semver. Distinct from
|
|
3466
4444
|
* `framework` (the FRAMEWORK range the plugin works with). Used by
|