@voltro/protocol 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1968 -0
- package/dist/auth-B6YZuSBr.js +303 -0
- package/dist/index.d.ts +1082 -36
- package/dist/index.js +468 -333
- package/dist/rest.d.ts +143 -11
- 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`.
|
|
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
|
|
783
|
+
readonly scopeCache?: ScopeCache | ScopeCacheOptions | false;
|
|
784
|
+
/**
|
|
785
|
+
* Called when an `authoritative` resolution REMOVED scopes the strategy had
|
|
786
|
+
* established — the audit trail for narrowing.
|
|
787
|
+
*
|
|
788
|
+
* Narrowing is a security-relevant event and it must not be inferable only
|
|
789
|
+
* by its absence. It fires on a fresh resolution, not on a cache replay, so
|
|
790
|
+
* a narrowed caller logs once per window rather than once per request.
|
|
791
|
+
*/
|
|
792
|
+
readonly onScopesNarrowed?: (event: {
|
|
793
|
+
strategyId: string;
|
|
794
|
+
subjectType: Subject["type"];
|
|
795
|
+
subjectId: string | null;
|
|
796
|
+
removed: ReadonlyArray<string>;
|
|
797
|
+
granted: ReadonlyArray<string>;
|
|
798
|
+
}) => void;
|
|
451
799
|
/**
|
|
452
800
|
* Hands every strategy the app's store on `input.store`.
|
|
453
801
|
*
|
|
@@ -478,6 +826,15 @@ export declare const CONNECTION_START_TAG: "__voltro.connections.start";
|
|
|
478
826
|
|
|
479
827
|
export declare const CONNECTION_SUBMIT_TOKEN_TAG: "__voltro.connections.submitToken";
|
|
480
828
|
|
|
829
|
+
export declare interface ConnectionCredential {
|
|
830
|
+
/** Cookies to set or replace INSIDE the connection's `Cookie` header. Other
|
|
831
|
+
* cookies on the connection are left alone. */
|
|
832
|
+
readonly cookies?: Readonly<Record<string, string>>;
|
|
833
|
+
/** Headers to set or replace outright (`authorization`, a custom token
|
|
834
|
+
* header). Matched case-insensitively. */
|
|
835
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
836
|
+
}
|
|
837
|
+
|
|
481
838
|
/** `__voltro.connections.disconnect` — forget the calling subject's credential
|
|
482
839
|
* for this connection. Deletes the row; the provider-side grant (if any) is
|
|
483
840
|
* the provider's to revoke. */
|
|
@@ -763,6 +1120,28 @@ export declare interface CoordinatedTickOutcome {
|
|
|
763
1120
|
readonly nextDueInMs?: number;
|
|
764
1121
|
}
|
|
765
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* What a descriptor CARRIES: the author's guards, or the erased form of their
|
|
1125
|
+
* `openAccess:` decision — never both.
|
|
1126
|
+
*
|
|
1127
|
+
* The option a user writes is still `Guards` (an `OpenAccessSpec` is not
|
|
1128
|
+
* something to hand-write into `guards:`; there is one spelling for the
|
|
1129
|
+
* decision and it is the `openAccess:` field). The DESCRIPTOR type is wider
|
|
1130
|
+
* because that is where the normalised decision lands, and because every
|
|
1131
|
+
* enforcement path reads the descriptor's array and nothing else.
|
|
1132
|
+
*/
|
|
1133
|
+
export declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec>;
|
|
1134
|
+
|
|
1135
|
+
/** Every channel declared in this process, by routing key. */
|
|
1136
|
+
export declare const declaredReactivityChannelKeys: () => ReadonlySet<string>;
|
|
1137
|
+
|
|
1138
|
+
/** Default cap on cached subjects before the cache sweeps + trims. */
|
|
1139
|
+
export declare const DEFAULT_SCOPE_CACHE_MAX_ENTRIES = 10000;
|
|
1140
|
+
|
|
1141
|
+
/** Default scope-cache window. Matches the session-revocation window on
|
|
1142
|
+
* purpose: the two per-request store reads then expire together. */
|
|
1143
|
+
export declare const DEFAULT_SCOPE_CACHE_TTL_MS = 30000;
|
|
1144
|
+
|
|
766
1145
|
export declare const defineAction: <const Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All = typeof Schema.Never>(options: {
|
|
767
1146
|
readonly name: Name;
|
|
768
1147
|
readonly input: Input;
|
|
@@ -772,6 +1151,24 @@ export declare const defineAction: <const Name extends string, Input extends Sch
|
|
|
772
1151
|
* scope(s) or the action fails with a typed `ScopeError` before the executor
|
|
773
1152
|
* runs. `ScopeError` is auto-merged into the wire error union. */
|
|
774
1153
|
readonly guards?: Guards<Schema.Schema.Type<Input>>;
|
|
1154
|
+
/**
|
|
1155
|
+
* Declare that this procedure needs NO authorization check — and say why.
|
|
1156
|
+
*
|
|
1157
|
+
* The other half of `security.defaultDeny`. With the flag on, a procedure
|
|
1158
|
+
* that declares neither `guards:` nor this is refused at boot, by name: an
|
|
1159
|
+
* access decision nobody made is the SEC-1 hole, not a default.
|
|
1160
|
+
*
|
|
1161
|
+
* Use it for the endpoints that really are open — a health check, a public
|
|
1162
|
+
* price list, a signup precheck. Do NOT reach for a scope every caller
|
|
1163
|
+
* already holds just to satisfy the gate: that guard reads as protection and
|
|
1164
|
+
* enforces nothing, and it is the failure mode this field exists to prevent.
|
|
1165
|
+
*
|
|
1166
|
+
* The reason is required and is the point — it is what a reviewer reads and
|
|
1167
|
+
* what `voltro doctor` prints beside the tag.
|
|
1168
|
+
*
|
|
1169
|
+
* openAccess: 'public pricing page — reads no caller data'
|
|
1170
|
+
*/
|
|
1171
|
+
readonly openAccess?: string;
|
|
775
1172
|
/** Table(s) this action READS. Declaring it is what lets `voltro check` know
|
|
776
1173
|
* the table is alive — without it, a table only an action touches reads as
|
|
777
1174
|
* an orphan. See `ActionProcedureDescriptor.source`. */
|
|
@@ -782,6 +1179,15 @@ export declare const defineAction: <const Name extends string, Input extends Sch
|
|
|
782
1179
|
readonly publicApi?: PublicApiSpec;
|
|
783
1180
|
/** Expose this action as an agent tool (innovation/07). */
|
|
784
1181
|
readonly exposeAsTool?: ExposeAsTool;
|
|
1182
|
+
/**
|
|
1183
|
+
* Require a SECOND human to approve before this action takes effect.
|
|
1184
|
+
*
|
|
1185
|
+
* Same contract as a mutation's, and the gate runs at the same point relative
|
|
1186
|
+
* to the work: after `guards:`, BEFORE the executor. For an action that is the
|
|
1187
|
+
* only point at which nothing has happened yet — there is no transaction to
|
|
1188
|
+
* roll back an outbound HTTP call.
|
|
1189
|
+
*/
|
|
1190
|
+
readonly requiresApproval?: ApprovalPolicy<Schema.Schema.Type<Input>>;
|
|
785
1191
|
/**
|
|
786
1192
|
* Keep this procedure OFF the wire entirely.
|
|
787
1193
|
*
|
|
@@ -862,6 +1268,27 @@ export declare const defineEvent: <const Name extends string, Key extends Schema
|
|
|
862
1268
|
* makes throughput scale with the audience instead of the publish rate.
|
|
863
1269
|
*/
|
|
864
1270
|
readonly guards?: Guards<Schema.Schema.Type<Key>>;
|
|
1271
|
+
/**
|
|
1272
|
+
* Declare that this event needs NO authorization check — and say why.
|
|
1273
|
+
*
|
|
1274
|
+
* The other half of `security.defaultDeny`, exactly as on a procedure. With
|
|
1275
|
+
* the flag on, an event that declares neither `guards:` nor this is refused
|
|
1276
|
+
* at boot, by name: an access decision nobody made is the SEC-1 hole, not a
|
|
1277
|
+
* default — and an event with no decision is subscribable by ANY
|
|
1278
|
+
* authenticated session that can open the socket.
|
|
1279
|
+
*
|
|
1280
|
+
* Use it for the events that really are open — a public scoreboard tick, a
|
|
1281
|
+
* status broadcast, a service-health pulse. Do NOT reach for a scope every
|
|
1282
|
+
* caller already holds just to satisfy the gate: that guard reads as
|
|
1283
|
+
* protection and enforces nothing, and it is the failure mode this field
|
|
1284
|
+
* exists to prevent.
|
|
1285
|
+
*
|
|
1286
|
+
* The reason is required and is the point — it is what a reviewer reads and
|
|
1287
|
+
* what `voltro doctor` prints beside the tag.
|
|
1288
|
+
*
|
|
1289
|
+
* openAccess: 'public scoreboard — carries no caller data'
|
|
1290
|
+
*/
|
|
1291
|
+
readonly openAccess?: string;
|
|
865
1292
|
/**
|
|
866
1293
|
* Deliver recently-buffered events on a FIRST attach. Default `false`, and the
|
|
867
1294
|
* default is the interesting half.
|
|
@@ -912,10 +1339,41 @@ export declare const defineMutation: <const Name extends string, Input extends S
|
|
|
912
1339
|
* scope(s) or the mutation fails with a typed `ScopeError` BEFORE the
|
|
913
1340
|
* transaction opens. `ScopeError` is auto-merged into the wire error union. */
|
|
914
1341
|
readonly guards?: Guards<Schema.Schema.Type<Input>>;
|
|
1342
|
+
/**
|
|
1343
|
+
* Declare that this procedure needs NO authorization check — and say why.
|
|
1344
|
+
*
|
|
1345
|
+
* The other half of `security.defaultDeny`. With the flag on, a procedure
|
|
1346
|
+
* that declares neither `guards:` nor this is refused at boot, by name: an
|
|
1347
|
+
* access decision nobody made is the SEC-1 hole, not a default.
|
|
1348
|
+
*
|
|
1349
|
+
* Use it for the endpoints that really are open — a health check, a public
|
|
1350
|
+
* price list, a signup precheck. Do NOT reach for a scope every caller
|
|
1351
|
+
* already holds just to satisfy the gate: that guard reads as protection and
|
|
1352
|
+
* enforces nothing, and it is the failure mode this field exists to prevent.
|
|
1353
|
+
*
|
|
1354
|
+
* The reason is required and is the point — it is what a reviewer reads and
|
|
1355
|
+
* what `voltro doctor` prints beside the tag.
|
|
1356
|
+
*
|
|
1357
|
+
* openAccess: 'public pricing page — reads no caller data'
|
|
1358
|
+
*/
|
|
1359
|
+
readonly openAccess?: string;
|
|
915
1360
|
/** Project this mutation as a public REST endpoint (innovation/11). */
|
|
916
1361
|
readonly publicApi?: PublicApiSpec;
|
|
917
1362
|
/** Expose this mutation as an agent tool (innovation/07). */
|
|
918
1363
|
readonly exposeAsTool?: ExposeAsTool;
|
|
1364
|
+
/**
|
|
1365
|
+
* Require a SECOND human to approve before this mutation takes effect.
|
|
1366
|
+
*
|
|
1367
|
+
* The first call records a durable pending intent and fails with a typed
|
|
1368
|
+
* `ApprovalRequired` carrying its id; the transaction never opens. Once an
|
|
1369
|
+
* authorised, DIFFERENT subject approves, the IDENTICAL call succeeds exactly
|
|
1370
|
+
* once (the approval is consumed).
|
|
1371
|
+
*
|
|
1372
|
+
* Composes with `guards:` rather than replacing them — the requester still has
|
|
1373
|
+
* to be allowed to ASK. Refused together with `openAccess:` (see
|
|
1374
|
+
* `assertApprovalPolicyCoherent`).
|
|
1375
|
+
*/
|
|
1376
|
+
readonly requiresApproval?: ApprovalPolicy<Schema.Schema.Type<Input>>;
|
|
919
1377
|
/**
|
|
920
1378
|
* Keep this procedure OFF the wire entirely.
|
|
921
1379
|
*
|
|
@@ -1001,14 +1459,37 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
|
|
|
1001
1459
|
* auto-optimistic patch routing from mutations targeting any of those
|
|
1002
1460
|
* tables. For a COMPUTED query (handler returns a shaped value) it is the
|
|
1003
1461
|
* reactive trigger set: the handler re-runs when ANY listed table changes
|
|
1004
|
-
* — pass an array to depend on several (e.g. a matrix joining two tables).
|
|
1005
|
-
|
|
1462
|
+
* — pass an array to depend on several (e.g. a matrix joining two tables).
|
|
1463
|
+
*
|
|
1464
|
+
* A `reactivityChannel(...)` is accepted here too, for state that pushes
|
|
1465
|
+
* without living in a table. Pass the CHANNEL, not its key string: the
|
|
1466
|
+
* import edge is what makes a channel `source:` impossible to leave stale,
|
|
1467
|
+
* which a table name (a bare string) can always be. */
|
|
1468
|
+
readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource>;
|
|
1006
1469
|
/** Opt into server-side snapshot caching with auto-invalidation. */
|
|
1007
1470
|
readonly cache?: QueryCacheConfig;
|
|
1008
1471
|
/** Declarative authorization guard(s) — the caller must hold the named
|
|
1009
1472
|
* scope(s) or the query fails with a typed `ScopeError` before the executor
|
|
1010
1473
|
* runs. `ScopeError` is auto-merged into the wire error union. */
|
|
1011
1474
|
readonly guards?: Guards<Schema.Schema.Type<Input>>;
|
|
1475
|
+
/**
|
|
1476
|
+
* Declare that this procedure needs NO authorization check — and say why.
|
|
1477
|
+
*
|
|
1478
|
+
* The other half of `security.defaultDeny`. With the flag on, a procedure
|
|
1479
|
+
* that declares neither `guards:` nor this is refused at boot, by name: an
|
|
1480
|
+
* access decision nobody made is the SEC-1 hole, not a default.
|
|
1481
|
+
*
|
|
1482
|
+
* Use it for the endpoints that really are open — a health check, a public
|
|
1483
|
+
* price list, a signup precheck. Do NOT reach for a scope every caller
|
|
1484
|
+
* already holds just to satisfy the gate: that guard reads as protection and
|
|
1485
|
+
* enforces nothing, and it is the failure mode this field exists to prevent.
|
|
1486
|
+
*
|
|
1487
|
+
* The reason is required and is the point — it is what a reviewer reads and
|
|
1488
|
+
* what `voltro doctor` prints beside the tag.
|
|
1489
|
+
*
|
|
1490
|
+
* openAccess: 'public pricing page — reads no caller data'
|
|
1491
|
+
*/
|
|
1492
|
+
readonly openAccess?: string;
|
|
1012
1493
|
/** Project this query as a public REST endpoint (innovation/11). */
|
|
1013
1494
|
readonly publicApi?: PublicApiSpec;
|
|
1014
1495
|
/** Expose this query as an agent tool (innovation/07). */
|
|
@@ -1074,6 +1555,24 @@ export declare const defineStream: <const Name extends string, Input extends Sch
|
|
|
1074
1555
|
* push. Same shape and same semantics as a query's.
|
|
1075
1556
|
*/
|
|
1076
1557
|
readonly guards?: Guards;
|
|
1558
|
+
/**
|
|
1559
|
+
* Declare that this procedure needs NO authorization check — and say why.
|
|
1560
|
+
*
|
|
1561
|
+
* The other half of `security.defaultDeny`. With the flag on, a procedure
|
|
1562
|
+
* that declares neither `guards:` nor this is refused at boot, by name: an
|
|
1563
|
+
* access decision nobody made is the SEC-1 hole, not a default.
|
|
1564
|
+
*
|
|
1565
|
+
* Use it for the endpoints that really are open — a health check, a public
|
|
1566
|
+
* price list, a signup precheck. Do NOT reach for a scope every caller
|
|
1567
|
+
* already holds just to satisfy the gate: that guard reads as protection and
|
|
1568
|
+
* enforces nothing, and it is the failure mode this field exists to prevent.
|
|
1569
|
+
*
|
|
1570
|
+
* The reason is required and is the point — it is what a reviewer reads and
|
|
1571
|
+
* what `voltro doctor` prints beside the tag.
|
|
1572
|
+
*
|
|
1573
|
+
* openAccess: 'public pricing page — reads no caller data'
|
|
1574
|
+
*/
|
|
1575
|
+
readonly openAccess?: string;
|
|
1077
1576
|
}) => StreamProcedureDescriptor<Name, Input, Element, Error>;
|
|
1078
1577
|
|
|
1079
1578
|
export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
|
|
@@ -1200,9 +1699,14 @@ export declare interface EventDescriptor<Name extends string, Key extends Schema
|
|
|
1200
1699
|
readonly name: Name;
|
|
1201
1700
|
readonly key: Key;
|
|
1202
1701
|
readonly payload: Payload;
|
|
1203
|
-
/** Who may LISTEN
|
|
1204
|
-
*
|
|
1205
|
-
|
|
1702
|
+
/** Who may LISTEN — the author's guards, or the erased form of their
|
|
1703
|
+
* `openAccess:` decision, never both (see `DeclaredAccess`). Re-checked when
|
|
1704
|
+
* the subject changes, not per delivery — see `EventDefinition.guards` for
|
|
1705
|
+
* why that distinction is deliberate. */
|
|
1706
|
+
readonly guards: DeclaredAccess<Schema.Schema.Type<Key>> | undefined;
|
|
1707
|
+
/** The declared `openAccess:` reason, when there is one — what the boot gate
|
|
1708
|
+
* and `voltro doctor` read. The enforcement paths read `guards`. */
|
|
1709
|
+
readonly openAccess: string | undefined;
|
|
1206
1710
|
/** Deliver buffered events on a FIRST attach. Default false. */
|
|
1207
1711
|
readonly rewind: boolean | undefined;
|
|
1208
1712
|
/** `'each'` (default) or `'latest'` — see the definer. */
|
|
@@ -1481,6 +1985,14 @@ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefin
|
|
|
1481
1985
|
/** The currently-registered resource-scope resolver, or `undefined`. */
|
|
1482
1986
|
export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
|
|
1483
1987
|
|
|
1988
|
+
/** Ask for default-deny semantics on one `checkGuards` call. */
|
|
1989
|
+
export declare interface GuardCheckOptions {
|
|
1990
|
+
/** Refuse a procedure that declares no access decision at all. */
|
|
1991
|
+
readonly defaultDeny?: boolean;
|
|
1992
|
+
/** The procedure's tag, so the refusal can name it. */
|
|
1993
|
+
readonly procedure?: string;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1484
1996
|
export declare interface GuardCheckSpec {
|
|
1485
1997
|
readonly scope: string | ReadonlyArray<string>;
|
|
1486
1998
|
readonly mode?: 'all' | 'any';
|
|
@@ -1506,17 +2018,43 @@ export declare interface GuardSpec<Input = unknown> {
|
|
|
1506
2018
|
* scope); `'any'` = OR (hold at least one). Ignored for a single scope. */
|
|
1507
2019
|
readonly mode?: 'all' | 'any';
|
|
1508
2020
|
/**
|
|
1509
|
-
* PURE `input → resource id` extractor
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
* for a
|
|
2021
|
+
* PURE `input → resource id` extractor. Browser-safe (no DB, no server
|
|
2022
|
+
* import) — exactly like `target.identify`. Omit for a plain subject-scope
|
|
2023
|
+
* guard.
|
|
2024
|
+
*
|
|
2025
|
+
* **On a `GuardSpec` this id is ADVISORY.** A scope guard answers "what may
|
|
2026
|
+
* this subject do at all", against the subject's global scope set; the id is
|
|
2027
|
+
* carried for logging and for a future subject-scope resolver that narrows by
|
|
2028
|
+
* resource. It does not, on its own, make the check per-resource.
|
|
2029
|
+
*
|
|
2030
|
+
* **If your authority is per-resource, you want {@link PolicyGuardSpec}, not
|
|
2031
|
+
* this field** — `guards: [{ action, resourceType, resource }]`, backed by
|
|
2032
|
+
* `defineResourcePolicy` + a tuple source you register. That is built, wired
|
|
2033
|
+
* on both boot paths, fail-closed without a resolver, and documented under
|
|
2034
|
+
* *Authentication → Authorization*. An app whose relationships already live
|
|
2035
|
+
* in its own tables (a `teamMembers` row, say) registers its own tuple source
|
|
2036
|
+
* rather than copying data across; see `policyGuardResolver.ts`.
|
|
2037
|
+
*
|
|
2038
|
+
* That paragraph is here because its absence cost a consumer their access
|
|
2039
|
+
* gate. This comment used to describe the resolver as "a future ReBAC /
|
|
2040
|
+
* `accessPolicy()` resolver" — written before the ReBAC path shipped and
|
|
2041
|
+
* never updated. They read the type, quoted the sentence, concluded there was
|
|
2042
|
+
* "nothing in between" declaring an untruth and turning the gate off, and set
|
|
2043
|
+
* `security: { defaultDeny: false }` on an app with 565 undecided procedures.
|
|
2044
|
+
* The capability they needed was two fields away. A doc comment that says
|
|
2045
|
+
* "future" about something shipped is not a small inaccuracy: it is the only
|
|
2046
|
+
* thing a careful reader has, and it argued them out of a feature.
|
|
1516
2047
|
*/
|
|
1517
2048
|
readonly resource?: (input: Input) => string | undefined;
|
|
1518
2049
|
}
|
|
1519
2050
|
|
|
2051
|
+
/** Does this descriptor declare an access decision — a guard, or a deliberate
|
|
2052
|
+
* `openAccess:`? The boot gate's predicate; `false` is the SEC-1 shape. */
|
|
2053
|
+
export declare const hasAccessDecision: (descriptor: {
|
|
2054
|
+
readonly guards?: ReadonlyArray<unknown> | undefined;
|
|
2055
|
+
readonly openAccess?: string | undefined;
|
|
2056
|
+
}) => boolean;
|
|
2057
|
+
|
|
1520
2058
|
export declare const hasCallbackRoutes: (s: AuthStrategy) => s is AuthStrategyWithCallback;
|
|
1521
2059
|
|
|
1522
2060
|
/** True if the subject holds `scope` (or the `admin:full` bypass), checking
|
|
@@ -1562,8 +2100,22 @@ export declare interface HttpRequestContext {
|
|
|
1562
2100
|
readonly path: string;
|
|
1563
2101
|
/** Lowercased request headers. */
|
|
1564
2102
|
readonly headers: Readonly<Record<string, string>>;
|
|
1565
|
-
/**
|
|
1566
|
-
*
|
|
2103
|
+
/**
|
|
2104
|
+
* The client address, resolved through the app's `security.trustedProxies`
|
|
2105
|
+
* policy — the SAME value `PluginHttpRouteRequest.remoteAddr`, the rate
|
|
2106
|
+
* limiter, the geo-block and every audit row use (`resolveClientAddress`).
|
|
2107
|
+
* Use this, never `headers['x-forwarded-for']`.
|
|
2108
|
+
*
|
|
2109
|
+
* This is a pre-auth shield's whole key, so getting it from the header is
|
|
2110
|
+
* not a smaller mistake here than elsewhere — it is the one place it is
|
|
2111
|
+
* worst. `x-forwarded-for` is a request header: any client can write it, so
|
|
2112
|
+
* a token bucket keyed on it is bypassed by one extra header per request.
|
|
2113
|
+
* The resolution here ignores the header entirely unless a trusted proxy is
|
|
2114
|
+
* declared, and then believes only the hops that are one.
|
|
2115
|
+
*
|
|
2116
|
+
* `undefined` when the socket address is unavailable (a unix socket, an
|
|
2117
|
+
* in-process test harness that constructs the request by hand).
|
|
2118
|
+
*/
|
|
1567
2119
|
readonly remoteAddr: string | undefined;
|
|
1568
2120
|
}
|
|
1569
2121
|
|
|
@@ -1664,12 +2216,31 @@ export declare const isEventDescriptor: (value: unknown) => value is AnyEventDes
|
|
|
1664
2216
|
* diffed by id and falls back to shipping the full data. */
|
|
1665
2217
|
export declare const isIdKeyed: (rows: ReadonlyArray<Readonly<Record<string, unknown>>>) => rows is ReadonlyArray<PatchRow>;
|
|
1666
2218
|
|
|
2219
|
+
/** Runtime narrowing for the declared-open variant. */
|
|
2220
|
+
export declare const isOpenAccess: (g: AnyCheckSpec) => g is OpenAccessSpec;
|
|
2221
|
+
|
|
1667
2222
|
/** Runtime narrowing for the relationship variant. */
|
|
1668
2223
|
export declare const isPolicyCheck: (g: AnyCheckSpec) => g is PolicyCheckSpec;
|
|
1669
2224
|
|
|
1670
2225
|
/** Narrow a guard entry to the relationship variant. */
|
|
1671
2226
|
export declare const isPolicyGuard: <I>(g: AnyGuardSpec<I>) => g is PolicyGuardSpec<I>;
|
|
1672
2227
|
|
|
2228
|
+
/** Whether `value` is a channel object (not its key). */
|
|
2229
|
+
export declare const isReactivityChannel: (value: unknown) => value is ReactivityChannel;
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Whether `key` is addressed to the channel namespace.
|
|
2233
|
+
*
|
|
2234
|
+
* A PREFIX test, deliberately not a registry lookup — the two answer different
|
|
2235
|
+
* questions and only one of them belongs on the delivery path. At runtime the
|
|
2236
|
+
* question is "is this a table name or a channel key", and getting it wrong in
|
|
2237
|
+
* the strict direction drops a real push. Whether the channel was DECLARED is a
|
|
2238
|
+
* boot question, and `undeclaredChannelKeys` answers it there — the same
|
|
2239
|
+
* asymmetry tables already have (an unregistered table name still delivers;
|
|
2240
|
+
* the boot audit is what reports it).
|
|
2241
|
+
*/
|
|
2242
|
+
export declare const isReactivityChannelKey: (key: string) => boolean;
|
|
2243
|
+
|
|
1673
2244
|
export declare const isSystemSubject: (subject: Subject) => boolean;
|
|
1674
2245
|
|
|
1675
2246
|
/**
|
|
@@ -1690,6 +2261,17 @@ export declare const isWireReachable: (descriptor: {
|
|
|
1690
2261
|
readonly internal?: boolean | undefined;
|
|
1691
2262
|
}) => boolean;
|
|
1692
2263
|
|
|
2264
|
+
/**
|
|
2265
|
+
* Build a scope cache. Hand it to `composeAuthStrategies({ scopeCache })` and
|
|
2266
|
+
* keep the handle: `invalidate(scopeCacheKey(subject))` from the mutation that
|
|
2267
|
+
* grants or removes a role is what makes the staleness window zero.
|
|
2268
|
+
*
|
|
2269
|
+
* `composeAuthStrategies` builds one internally when you don't, so the default
|
|
2270
|
+
* posture is cached-with-a-window rather than a store read per request — but a
|
|
2271
|
+
* cache nobody holds cannot be invalidated, which is why this is exported.
|
|
2272
|
+
*/
|
|
2273
|
+
export declare const makeScopeCache: (options?: ScopeCacheOptions) => ScopeCache;
|
|
2274
|
+
|
|
1693
2275
|
/**
|
|
1694
2276
|
* How large one encoded envelope may be, on EVERY dialect.
|
|
1695
2277
|
*
|
|
@@ -1710,6 +2292,10 @@ export declare const MAX_EVENT_ENVELOPE_BYTES = 7500;
|
|
|
1710
2292
|
/** In-memory store — the default for single-process dev + the test double. */
|
|
1711
2293
|
export declare const memoryIdempotencyStore: () => IdempotencyStore;
|
|
1712
2294
|
|
|
2295
|
+
/** The refusal a missing access decision produces. Exported so the boot gate
|
|
2296
|
+
* and the call-time path cannot describe the same defect differently. */
|
|
2297
|
+
export declare const missingAccessDecision: (procedure?: string) => ScopeError;
|
|
2298
|
+
|
|
1713
2299
|
export declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
|
|
1714
2300
|
readonly kind: 'mutation';
|
|
1715
2301
|
readonly name: Name;
|
|
@@ -1724,11 +2310,19 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
|
|
|
1724
2310
|
/** Declarative authorization guard(s) — enforced before the transaction
|
|
1725
2311
|
* opens, failing with a typed `ScopeError`. Absent → no framework-level
|
|
1726
2312
|
* authz (author gates in-handler, or the mutation is unguarded). */
|
|
1727
|
-
readonly guards:
|
|
2313
|
+
readonly guards: DeclaredAccess | undefined;
|
|
2314
|
+
/** The declared reason this procedure needs NO authorization check —
|
|
2315
|
+
* `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
|
|
2316
|
+
* the only two shapes `security.defaultDeny` accepts. */
|
|
2317
|
+
readonly openAccess: string | undefined;
|
|
1728
2318
|
/** Opt this mutation into a public REST endpoint (innovation/11). */
|
|
1729
2319
|
readonly publicApi: PublicApiSpec | undefined;
|
|
1730
2320
|
/** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
|
|
1731
2321
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
2322
|
+
/** Require a SECOND human to approve before this mutation takes effect. The
|
|
2323
|
+
* gate runs in the dispatch spine after `guards:` and before the transaction
|
|
2324
|
+
* opens; the pending intent is a durable `_voltro_approvals` row. */
|
|
2325
|
+
readonly requiresApproval: AnyApprovalPolicy | undefined;
|
|
1732
2326
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
1733
2327
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
1734
2328
|
/**
|
|
@@ -1780,6 +2374,17 @@ export declare interface NestedTargetFields<Input = unknown> {
|
|
|
1780
2374
|
*/
|
|
1781
2375
|
export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => ClientDescriptor;
|
|
1782
2376
|
|
|
2377
|
+
/**
|
|
2378
|
+
* A `source:` as the DESCRIPTOR stores it — channels resolved to their keys,
|
|
2379
|
+
* with the caller's shape preserved.
|
|
2380
|
+
*
|
|
2381
|
+
* Shape-preserving on purpose: a single `source: 'notes'` must stay the string
|
|
2382
|
+
* `'notes'` and not become `['notes']`. It is serialised into the capability
|
|
2383
|
+
* manifest and into every api golden, so widening the shape would rewrite those
|
|
2384
|
+
* artefacts for every query in every app to express nothing.
|
|
2385
|
+
*/
|
|
2386
|
+
export declare const normalizeSource: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => string | ReadonlyArray<string> | undefined;
|
|
2387
|
+
|
|
1783
2388
|
/**
|
|
1784
2389
|
* What a plugin contributes to the OTel layer via `contributeObservability`.
|
|
1785
2390
|
* The OTel types are intentionally `unknown` so this browser-safe protocol
|
|
@@ -1799,6 +2404,31 @@ export declare interface ObservabilityContribution {
|
|
|
1799
2404
|
readonly sampler?: unknown;
|
|
1800
2405
|
}
|
|
1801
2406
|
|
|
2407
|
+
/**
|
|
2408
|
+
* The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision
|
|
2409
|
+
* that this procedure needs no authorization check, and the reason.
|
|
2410
|
+
*
|
|
2411
|
+
* It is a guard entry rather than a bare descriptor field on purpose. Every
|
|
2412
|
+
* enforcement path in the framework — `servePipeline`'s `enforceGuards`,
|
|
2413
|
+
* `bindStream`, `bindEvent`, `@voltro/testing`'s `invoke` — is handed the
|
|
2414
|
+
* `guards` ARRAY and nothing else. A decision that does not live in that array
|
|
2415
|
+
* is invisible to all of them, so "guarded" and "deliberately open" would be
|
|
2416
|
+
* distinguishable in the source and identical at the point that enforces.
|
|
2417
|
+
*
|
|
2418
|
+
* It always passes. The value is the WHY, and the why is the point: it is what
|
|
2419
|
+
* a reviewer reads, what `voltro doctor` prints, and what makes an open
|
|
2420
|
+
* procedure a decision somebody made rather than a field somebody forgot.
|
|
2421
|
+
*/
|
|
2422
|
+
export declare interface OpenAccessSpec {
|
|
2423
|
+
/** Why this procedure is callable without an authorization check. Non-empty
|
|
2424
|
+
* by construction — `defineQuery` & co. refuse an empty reason. */
|
|
2425
|
+
readonly open: string;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/** Build the erased `openAccess:` entry. The definers call this; an app writes
|
|
2429
|
+
* `openAccess: '<why>'` on the descriptor and never sees the spec. */
|
|
2430
|
+
export declare const openAccessSpec: (reason: string) => OpenAccessSpec;
|
|
2431
|
+
|
|
1802
2432
|
/** Split a route back into its three parts. */
|
|
1803
2433
|
export declare const parseEventRoute: (route: string) => {
|
|
1804
2434
|
readonly tenantId: string | null;
|
|
@@ -1817,6 +2447,35 @@ export declare type PatchRow = Readonly<Record<string, unknown>> & {
|
|
|
1817
2447
|
* needs to re-parse a number out of a path. */
|
|
1818
2448
|
export declare const pathToId: (path: string) => string;
|
|
1819
2449
|
|
|
2450
|
+
export declare const PendingApproval: Schema.Struct<{
|
|
2451
|
+
id: typeof Schema.String;
|
|
2452
|
+
/** The rpc tag whose execution is pending. */
|
|
2453
|
+
procedure: typeof Schema.String;
|
|
2454
|
+
kind: Schema.Literal<["mutation", "action"]>;
|
|
2455
|
+
/** Subject id of whoever asked. Never the approver. */
|
|
2456
|
+
requestedBy: Schema.NullOr<typeof Schema.String>;
|
|
2457
|
+
status: Schema.Literal<["pending", "approved", "rejected", "expired", "consumed"]>;
|
|
2458
|
+
/** The declared reason from the descriptor, if any. */
|
|
2459
|
+
reason: Schema.NullOr<typeof Schema.String>;
|
|
2460
|
+
/** Scope(s) an approver must hold. */
|
|
2461
|
+
requiredScopes: Schema.Array$<typeof Schema.String>;
|
|
2462
|
+
requestedAt: typeof Schema.String;
|
|
2463
|
+
expiresAt: typeof Schema.String;
|
|
2464
|
+
decidedBy: Schema.NullOr<typeof Schema.String>;
|
|
2465
|
+
decidedAt: Schema.NullOr<typeof Schema.String>;
|
|
2466
|
+
/** The approver's note, when they left one. */
|
|
2467
|
+
note: Schema.NullOr<typeof Schema.String>;
|
|
2468
|
+
/**
|
|
2469
|
+
* How this row relates to the CALLING subject — the whole point of the feed.
|
|
2470
|
+
* `'to-decide'` = they may act on it; `'requested'` = they asked for it.
|
|
2471
|
+
* A row is never both: self-approval is refused, so a requester never
|
|
2472
|
+
* qualifies as its approver.
|
|
2473
|
+
*/
|
|
2474
|
+
relation: Schema.Literal<["to-decide", "requested"]>;
|
|
2475
|
+
}>;
|
|
2476
|
+
|
|
2477
|
+
export declare type PendingApproval = Schema.Schema.Type<typeof PendingApproval>;
|
|
2478
|
+
|
|
1820
2479
|
/**
|
|
1821
2480
|
* Called once at app boot, after `voltro dev` resolves the plugin
|
|
1822
2481
|
* list and before the rpc server starts accepting connections. Use
|
|
@@ -1913,6 +2572,23 @@ export declare interface PluginChangeEvent {
|
|
|
1913
2572
|
readonly new: Record<string, unknown> | null;
|
|
1914
2573
|
/** Row before the change — present on update + delete, null on insert. */
|
|
1915
2574
|
readonly old: Record<string, unknown> | null;
|
|
2575
|
+
/**
|
|
2576
|
+
* Set when the transport could not carry this change's images and they were
|
|
2577
|
+
* RECONSTRUCTED — a row over postgres' 8000-byte `pg_notify` cap. Absent on
|
|
2578
|
+
* every ordinary event.
|
|
2579
|
+
*
|
|
2580
|
+
* A tap must read it before trusting an image as a snapshot:
|
|
2581
|
+
*
|
|
2582
|
+
* - `'rehydrated'` — `new` is the row RE-READ from the database. Correct to
|
|
2583
|
+
* index, mirror or forward; NOT necessarily the image the write that
|
|
2584
|
+
* fired this event produced (a later write may already have landed).
|
|
2585
|
+
* - `'tombstone'` — a delete whose `old` is the PRIMARY KEY and nothing
|
|
2586
|
+
* else. Enough to remove the row; never a record of what it contained.
|
|
2587
|
+
* A history/versioning tap must not store it as a snapshot.
|
|
2588
|
+
* - `'unrecovered'` — both images are null and the content is gone. The
|
|
2589
|
+
* change happened; re-read or resync if you need it.
|
|
2590
|
+
*/
|
|
2591
|
+
readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
|
|
1916
2592
|
/** How the event reached this process: absent/'inline' = this process's
|
|
1917
2593
|
* own write; 'injected' = delivered over a cross-instance transport
|
|
1918
2594
|
* (broadcast bus / CDC consumer). Combine with `changeScope` to act
|
|
@@ -1945,6 +2621,14 @@ export declare interface PluginChangeEvent {
|
|
|
1945
2621
|
* join table is a member removal, a cascade or an expiry.
|
|
1946
2622
|
*/
|
|
1947
2623
|
readonly procedure?: string;
|
|
2624
|
+
/**
|
|
2625
|
+
* The write was made BY AN AGENT acting as `subjectId` — the fourth member of
|
|
2626
|
+
* the shape the comment above tracks (`WriteAttribution` → `ChangeEvent` →
|
|
2627
|
+
* here). A tap that logs "who changed this" reads WRONG without it: the
|
|
2628
|
+
* subject is the person the agent acted as, by construction, so an agent
|
|
2629
|
+
* write and a human write are otherwise identical rows.
|
|
2630
|
+
*/
|
|
2631
|
+
readonly via?: 'agent';
|
|
1948
2632
|
/** The store's change visibility (see `DataStore.changeScope`):
|
|
1949
2633
|
* 'local' — own writes inline (skip origin 'injected' for
|
|
1950
2634
|
* exactly-once-on-the-writer); 'fleet' — every replica sees the full
|
|
@@ -2113,6 +2797,42 @@ export declare interface PluginHttpRoute {
|
|
|
2113
2797
|
* any sub-path (`/_voltro/storage/abc123`). */
|
|
2114
2798
|
readonly path: string;
|
|
2115
2799
|
readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
|
|
2800
|
+
/**
|
|
2801
|
+
* Opt this route's path OUT of the listener's cross-site origin check.
|
|
2802
|
+
*
|
|
2803
|
+
* Every state-changing request (anything but GET/HEAD/OPTIONS) is
|
|
2804
|
+
* origin-checked by default, because the default assumption has to be that a
|
|
2805
|
+
* route can be reached with the browser's ambient session cookie — and a
|
|
2806
|
+
* route that can is CSRF-reachable. Declaring `'exempt'` is a claim that this
|
|
2807
|
+
* route CANNOT be: its caller must present something a browser will not
|
|
2808
|
+
* attach cross-site.
|
|
2809
|
+
*
|
|
2810
|
+
* The test to apply, and it is the only one:
|
|
2811
|
+
*
|
|
2812
|
+
* > If an attacker's page makes a browser send this request with the
|
|
2813
|
+
* > victim's cookies attached, does anything happen?
|
|
2814
|
+
*
|
|
2815
|
+
* If the answer is "no, the request still needs a signature / a bearer token
|
|
2816
|
+
* / a signed ticket the attacker does not have", the route is exempt.
|
|
2817
|
+
* Otherwise it is not, and no amount of "but it is behind the dashboard"
|
|
2818
|
+
* makes it so.
|
|
2819
|
+
*
|
|
2820
|
+
* The first-party exemptions and why each qualifies:
|
|
2821
|
+
* - `@voltro/plugin-sso-saml` `/saml` — the IdP delivers the assertion as a
|
|
2822
|
+
* genuine cross-site browser form POST; authority is the signed
|
|
2823
|
+
* SAMLResponse, not the cookie.
|
|
2824
|
+
* - `@voltro/plugin-storage` `/…/upload` + `/…/upload/resumable` — a signed
|
|
2825
|
+
* upload ticket in the query string, and the route ships its own CORS
|
|
2826
|
+
* allowlist because a cross-origin upload is the point.
|
|
2827
|
+
* - `@voltro/plugin-billing` `/billing/webhook` — an HMAC-verified provider
|
|
2828
|
+
* callback.
|
|
2829
|
+
* - `@voltro/plugin-scim` `/scim/v2` — bearer-only, refuses to mount without
|
|
2830
|
+
* a token.
|
|
2831
|
+
*
|
|
2832
|
+
* Granularity is the PATH PREFIX the route mounts, not the sub-path its
|
|
2833
|
+
* handler branches on: exempting `/saml` exempts `POST /saml/anything`.
|
|
2834
|
+
*/
|
|
2835
|
+
readonly originGuard?: 'exempt';
|
|
2116
2836
|
}
|
|
2117
2837
|
|
|
2118
2838
|
export declare interface PluginHttpRouteRequest {
|
|
@@ -2175,6 +2895,22 @@ export declare interface PluginHttpRouteRequest {
|
|
|
2175
2895
|
* this store reads them.
|
|
2176
2896
|
*/
|
|
2177
2897
|
readonly store?: DataStore;
|
|
2898
|
+
/**
|
|
2899
|
+
* The client address, resolved through the app's `security.trustedProxies`
|
|
2900
|
+
* policy — the SAME value the rate limiter, the geo-block and every audit row
|
|
2901
|
+
* use (`resolveClientAddress`). Use this, never `headers['x-forwarded-for']`.
|
|
2902
|
+
*
|
|
2903
|
+
* `x-forwarded-for` is a request header: any client can write it. Reading it
|
|
2904
|
+
* raw means a caller picks the IP that lands in your `sessions.ipAddress`
|
|
2905
|
+
* column, which is the one field a breach investigation leans on. Three
|
|
2906
|
+
* first-party routes did exactly that until SEC-8 was extended down to this
|
|
2907
|
+
* surface. The resolution here ignores the header entirely unless a trusted
|
|
2908
|
+
* proxy is declared, and then believes only the hops that are one.
|
|
2909
|
+
*
|
|
2910
|
+
* `undefined` when the socket address is unavailable (a unix socket, an
|
|
2911
|
+
* in-process test harness that constructs the request by hand).
|
|
2912
|
+
*/
|
|
2913
|
+
readonly remoteAddr?: string | undefined;
|
|
2178
2914
|
}
|
|
2179
2915
|
|
|
2180
2916
|
/**
|
|
@@ -2291,6 +3027,55 @@ export declare type PluginInspectResponse = {
|
|
|
2291
3027
|
*/
|
|
2292
3028
|
export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
|
|
2293
3029
|
|
|
3030
|
+
/**
|
|
3031
|
+
* The two things a plugin's `name` is asked to encode — and they are NOT the
|
|
3032
|
+
* same question, which is why they are two fields.
|
|
3033
|
+
*
|
|
3034
|
+
* A plugin's name decides its rpc-tag prefix (`pluginAlias`) and its inspect
|
|
3035
|
+
* URL slug (`pluginSlug`). Two different app-side problems land on it:
|
|
3036
|
+
*
|
|
3037
|
+
* - **`alias` — "your namespace collides with mine."** An app that already
|
|
3038
|
+
* publishes `notifications.*` routes cannot install a plugin that also wants
|
|
3039
|
+
* `notifications.*`; the collision is fatal at codegen. `alias` REPLACES the
|
|
3040
|
+
* namespace, so the app keeps its own name and the plugin moves.
|
|
3041
|
+
* - **`instance` — "I want two of these."** A second cdc-out pipeline, a
|
|
3042
|
+
* second mail transport. The base name stays (so it is still recognisably
|
|
3043
|
+
* that plugin) and gains a `#suffix` discriminator.
|
|
3044
|
+
*
|
|
3045
|
+
* Both were already in the tree, one of them eleven times. The `#suffix`
|
|
3046
|
+
* ternary was copy-pasted verbatim into eleven plugins, and `alias` existed on
|
|
3047
|
+
* exactly one (`ai-flows`) with its own hand-rolled shape — so the two
|
|
3048
|
+
* mechanisms had no defined interaction at all. This is the one implementation.
|
|
3049
|
+
*
|
|
3050
|
+
* **What an alias costs, stated because it is not obvious and nothing else
|
|
3051
|
+
* says it:** the local and cloud dashboards fetch a plugin's inspect panel at
|
|
3052
|
+
* `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in. Alias
|
|
3053
|
+
* a plugin that ships `inspectEndpoints` and the endpoints keep working, the
|
|
3054
|
+
* rpc tags move as intended, and the dashboard panel 404s — because the panel
|
|
3055
|
+
* is in a different repository and cannot follow. Alias to dodge a tag
|
|
3056
|
+
* collision; do not alias a plugin whose dashboard panel you use.
|
|
3057
|
+
*
|
|
3058
|
+
* @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'`
|
|
3059
|
+
* @param alias replaces the whole namespace — an app-chosen name
|
|
3060
|
+
* @param instance discriminates one installation from another (`#suffix`)
|
|
3061
|
+
*
|
|
3062
|
+
* ```ts
|
|
3063
|
+
* pluginInstanceName({ base: '@voltro/plugin-cdc-out' })
|
|
3064
|
+
* //=> '@voltro/plugin-cdc-out' tag `cdcOut.*` slug `cdc-out`
|
|
3065
|
+
* pluginInstanceName({ base: '@voltro/plugin-cdc-out', instance: 'analytics' })
|
|
3066
|
+
* //=> '@voltro/plugin-cdc-out#analytics' tag `cdcOut.*` slug `cdc-out--analytics`
|
|
3067
|
+
* pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror' })
|
|
3068
|
+
* //=> 'mirror' tag `mirror.*` slug `mirror`
|
|
3069
|
+
* pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror', instance: 'analytics' })
|
|
3070
|
+
* //=> 'mirror#analytics' tag `mirror.*` slug `mirror--analytics'
|
|
3071
|
+
* ```
|
|
3072
|
+
*/
|
|
3073
|
+
export declare const pluginInstanceName: (args: {
|
|
3074
|
+
readonly base: string;
|
|
3075
|
+
readonly alias?: string | undefined;
|
|
3076
|
+
readonly instance?: string | undefined;
|
|
3077
|
+
}) => string;
|
|
3078
|
+
|
|
2294
3079
|
/**
|
|
2295
3080
|
* Per-app context handed to lifecycle hooks. The shape is intentionally
|
|
2296
3081
|
* thin — the framework's deeper services (DataStore, Logger, etc.) are
|
|
@@ -2444,14 +3229,41 @@ export declare interface PluginRpcRoute {
|
|
|
2444
3229
|
* The executor returns the query's VALUE (an array / object) — the same
|
|
2445
3230
|
* shape it returns for a poll; the framework recomputes it on change. Omit
|
|
2446
3231
|
* for a plain (poll-only) plugin query, a mutation, or an action. For a
|
|
2447
|
-
* query that mirrors a plugin table, set this to that table's name
|
|
2448
|
-
*
|
|
3232
|
+
* query that mirrors a plugin table, set this to that table's name.
|
|
3233
|
+
*
|
|
3234
|
+
* For plugin state that is NOT in a table, declare a
|
|
3235
|
+
* `reactivityChannel(...)` and pass the CHANNEL here — do not declare a table
|
|
3236
|
+
* you never write in order to own the name. `@voltro/plugin-presence` did
|
|
3237
|
+
* exactly that for a release, and the empty `_voltro_presence` table it left
|
|
3238
|
+
* in every user's database is what the channel primitive replaced.
|
|
2449
3239
|
*
|
|
2450
3240
|
* `| undefined` is explicit so a plugin can spread a browser-safe query
|
|
2451
3241
|
* DESCRIPTOR (which always carries `source: … | undefined`) into a route
|
|
2452
3242
|
* literal under `exactOptionalPropertyTypes` without a cast.
|
|
2453
3243
|
*/
|
|
2454
|
-
readonly source?:
|
|
3244
|
+
readonly source?: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined;
|
|
3245
|
+
/**
|
|
3246
|
+
* The route's ACCESS DECISION — the same two-field vocabulary user
|
|
3247
|
+
* procedures carry, because a plugin route is dispatched through the exact
|
|
3248
|
+
* same spine. Spreading a browser-safe descriptor
|
|
3249
|
+
* (`{ ...listDescriptor, execute }`) carries the decision automatically; the
|
|
3250
|
+
* lift (`pluginRoutes.ts`) forwards it onto the descriptor the runtime
|
|
3251
|
+
* enforces.
|
|
3252
|
+
*
|
|
3253
|
+
* `| undefined` on both, like `source`: a spread descriptor always carries
|
|
3254
|
+
* the properties, and `exactOptionalPropertyTypes` would otherwise refuse
|
|
3255
|
+
* the spread without a cast.
|
|
3256
|
+
*
|
|
3257
|
+
* A route that declares NEITHER is the SEC-1 shape — callable by any
|
|
3258
|
+
* authenticated session, and REFUSED per-request by the dispatch spine when
|
|
3259
|
+
* the app runs with `security.defaultDeny` (the default). First-party
|
|
3260
|
+
* plugins declare a decision on every route; third-party plugins must too.
|
|
3261
|
+
*/
|
|
3262
|
+
readonly guards?: DeclaredAccess | undefined;
|
|
3263
|
+
/** Deliberate no-check marker with the REASON — see `openAccess:` on
|
|
3264
|
+
* `defineQuery`/`defineMutation`/`defineAction`. Mutually exclusive with a
|
|
3265
|
+
* non-empty `guards`. */
|
|
3266
|
+
readonly openAccess?: string | undefined;
|
|
2455
3267
|
/**
|
|
2456
3268
|
* Effect-only executor. The base layer is provided by the framework
|
|
2457
3269
|
* (DataStore, HttpClient, the plugin's own service Tags) — the
|
|
@@ -2617,6 +3429,21 @@ export declare interface PublicApiSpec {
|
|
|
2617
3429
|
readonly stream?: 'snapshot' | 'sse';
|
|
2618
3430
|
}
|
|
2619
3431
|
|
|
3432
|
+
/**
|
|
3433
|
+
* Push every subscriber of `channel`.
|
|
3434
|
+
*
|
|
3435
|
+
* Returns whether the store could deliver it. A store that cannot inject (a
|
|
3436
|
+
* limited fake, a transactional view) is a no-op rather than a throw: a missing
|
|
3437
|
+
* seam must not be able to take down the write that called this, and the read
|
|
3438
|
+
* that follows is still correct.
|
|
3439
|
+
*
|
|
3440
|
+
* This exists so no caller hand-builds the event. The one that did wrote
|
|
3441
|
+
* `injectExternalChange({ … } as never)` — and `as never` on a wiring object is
|
|
3442
|
+
* how the webhook trigger context came to differ between the two boot paths
|
|
3443
|
+
* while both compiled.
|
|
3444
|
+
*/
|
|
3445
|
+
export declare const publishReactivity: (store: ReactivityPublisher | undefined, channel: ReactivityChannel) => boolean;
|
|
3446
|
+
|
|
2620
3447
|
/** Publish a server-primitive error. Cheap + sync; a broken reporter can
|
|
2621
3448
|
* never break the caller (each listener is isolated). Safe no-op when
|
|
2622
3449
|
* nothing is subscribed — primitives call it unconditionally. */
|
|
@@ -2682,7 +3509,11 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
|
|
|
2682
3509
|
readonly cache: QueryCacheConfig | undefined;
|
|
2683
3510
|
/** Declarative authorization guard(s) — enforced before the executor runs,
|
|
2684
3511
|
* failing with a typed `ScopeError`. Absent → no framework-level authz. */
|
|
2685
|
-
readonly guards:
|
|
3512
|
+
readonly guards: DeclaredAccess | undefined;
|
|
3513
|
+
/** The declared reason this procedure needs NO authorization check —
|
|
3514
|
+
* `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
|
|
3515
|
+
* the only two shapes `security.defaultDeny` accepts. */
|
|
3516
|
+
readonly openAccess: string | undefined;
|
|
2686
3517
|
/** Opt this query into a public REST endpoint (innovation/11). */
|
|
2687
3518
|
readonly publicApi: PublicApiSpec | undefined;
|
|
2688
3519
|
/** Opt this query into the auto-synthesized agent toolset (innovation/07). */
|
|
@@ -2760,6 +3591,56 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Sche
|
|
|
2760
3591
|
revision: Schema.optional<typeof Schema.Number>;
|
|
2761
3592
|
}>]>, Schema.Schema.All>, typeof Schema.Never, never>;
|
|
2762
3593
|
|
|
3594
|
+
/** The `channel:` namespace. Every channel key starts with it. */
|
|
3595
|
+
export declare const REACTIVITY_CHANNEL_PREFIX = "channel:";
|
|
3596
|
+
|
|
3597
|
+
/**
|
|
3598
|
+
* A declared reactivity channel — a push target with no table behind it.
|
|
3599
|
+
*
|
|
3600
|
+
* Create one with `reactivityChannel(name)`; pass it as a query's `source:`.
|
|
3601
|
+
*/
|
|
3602
|
+
export declare interface ReactivityChannel {
|
|
3603
|
+
readonly kind: 'reactivity-channel';
|
|
3604
|
+
/** The name as declared, without the namespace. */
|
|
3605
|
+
readonly name: string;
|
|
3606
|
+
/** The routing key — what the dispatcher indexes and `source:` resolves to. */
|
|
3607
|
+
readonly key: string;
|
|
3608
|
+
/** The key, so a channel interpolates into a message as its routing key. */
|
|
3609
|
+
toString(): string;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
/**
|
|
3613
|
+
* Declare a reactivity channel.
|
|
3614
|
+
*
|
|
3615
|
+
* Idempotent by name: calling it twice returns the SAME object. A module
|
|
3616
|
+
* evaluated twice (hot reload, a dual-instance resolve) must not produce two
|
|
3617
|
+
* channels that compare unequal while routing to one key.
|
|
3618
|
+
*
|
|
3619
|
+
* export const presenceRoster = reactivityChannel('presence')
|
|
3620
|
+
* // …
|
|
3621
|
+
* defineQuery({ name: 'presence.list', source: presenceRoster, … })
|
|
3622
|
+
* // …
|
|
3623
|
+
* publishReactivity(store, presenceRoster)
|
|
3624
|
+
*/
|
|
3625
|
+
export declare const reactivityChannel: (name: string) => ReactivityChannel;
|
|
3626
|
+
|
|
3627
|
+
/**
|
|
3628
|
+
* The one method of a store this needs — structural, so publishing does not
|
|
3629
|
+
* pull `@voltro/database`'s `DataStore` into a caller that had no reason for it.
|
|
3630
|
+
*/
|
|
3631
|
+
export declare interface ReactivityPublisher {
|
|
3632
|
+
readonly injectExternalChange?: (event: {
|
|
3633
|
+
readonly table: string;
|
|
3634
|
+
readonly op: 'insert' | 'update' | 'delete';
|
|
3635
|
+
readonly new: Record<string, unknown>;
|
|
3636
|
+
readonly old: Record<string, unknown>;
|
|
3637
|
+
readonly origin?: 'inline' | 'injected';
|
|
3638
|
+
}) => void;
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
/** What a query may declare as its reactive source. */
|
|
3642
|
+
export declare type ReactivitySource = string | ReactivityChannel;
|
|
3643
|
+
|
|
2763
3644
|
/** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
|
|
2764
3645
|
* Checks the EFFECTIVE set so role-derived scopes count. */
|
|
2765
3646
|
export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
|
|
@@ -2988,6 +3869,72 @@ export declare interface ScheduleFireContext {
|
|
|
2988
3869
|
*/
|
|
2989
3870
|
export declare type ScheduleFireInterceptor = (next: () => Promise<void>, ctx: ScheduleFireContext) => Promise<void>;
|
|
2990
3871
|
|
|
3872
|
+
export declare interface ScopeCache {
|
|
3873
|
+
/** The effective window in milliseconds. */
|
|
3874
|
+
readonly ttlMs: number;
|
|
3875
|
+
/** Resolve through the cache. `compute` runs only on a miss. */
|
|
3876
|
+
readonly resolve: (key: string, compute: () => Promise<ScopeDecision>) => Promise<CachedScopeDecision>;
|
|
3877
|
+
/** Drop one subject's cached authority — call this from whatever changes a
|
|
3878
|
+
* role, and the change is live on this process immediately. */
|
|
3879
|
+
readonly invalidate: (key: string) => void;
|
|
3880
|
+
/** Drop every cached verdict — for a change that alters a ROLE's definition
|
|
3881
|
+
* rather than one user's membership, where the affected subjects aren't
|
|
3882
|
+
* enumerable. */
|
|
3883
|
+
readonly invalidateAll: () => void;
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
/**
|
|
3887
|
+
* The cache key for a subject's authority: type, tenant, id.
|
|
3888
|
+
*
|
|
3889
|
+
* `tenantId` is in the key and it is load-bearing. A user who switches tenants
|
|
3890
|
+
* keeps their `id`, and their authority is per-tenant — key on the id alone and
|
|
3891
|
+
* a switch serves the previous tenant's scopes for a whole window. Use this
|
|
3892
|
+
* when calling `invalidate` so the key you drop is the key that was written.
|
|
3893
|
+
*/
|
|
3894
|
+
export declare const scopeCacheKey: (subject: Subject | SubjectIdentity) => string;
|
|
3895
|
+
|
|
3896
|
+
export declare interface ScopeCacheOptions {
|
|
3897
|
+
/** Cache window in milliseconds. Default 30 000. `0` disables caching (every
|
|
3898
|
+
* request resolves). Overridden by `VOLTRO_AUTH_SCOPE_CACHE_TTL_MS` only
|
|
3899
|
+
* when this is left unset — an explicit number in code wins over the env. */
|
|
3900
|
+
readonly ttlMs?: number;
|
|
3901
|
+
/** Upper bound on cached subjects. Default 10 000. */
|
|
3902
|
+
readonly maxEntries?: number;
|
|
3903
|
+
/** Clock override (epoch ms). Testing seam. */
|
|
3904
|
+
readonly now?: () => number;
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
/** Compose multiple strategies into a single resolver function. The
|
|
3908
|
+
* composer evaluates them in declaration order; first `matched`
|
|
3909
|
+
* wins; first `failed` short-circuits to anonymous (does NOT fall
|
|
3910
|
+
* through — see StrategyResolution doc).
|
|
3911
|
+
*
|
|
3912
|
+
* NOTHING may answer ahead of this chain. The per-connection soft-reauth
|
|
3913
|
+
* override used to (`getConnectionSubject`, `@voltro/runtime`), and that is
|
|
3914
|
+
* precisely what made a rebound connection immune to session revocation, to
|
|
3915
|
+
* `resolveScopes` and to the scope cache. It patches the connection's HEADERS
|
|
3916
|
+
* now and this chain runs unchanged — see `ConnectionCredential` above.
|
|
3917
|
+
*
|
|
3918
|
+
* Returns an async resolver `(input) => Promise<Subject>`.
|
|
3919
|
+
*/
|
|
3920
|
+
/** A resolver's verdict on a subject's authority. */
|
|
3921
|
+
export declare type ScopeDecision =
|
|
3922
|
+
/** Union these onto what the strategy established. */
|
|
3923
|
+
{
|
|
3924
|
+
readonly kind: 'grant';
|
|
3925
|
+
readonly scopes: ReadonlyArray<string>;
|
|
3926
|
+
}
|
|
3927
|
+
/** These are the caller's COMPLETE authority — anything else is removed. */
|
|
3928
|
+
| {
|
|
3929
|
+
readonly kind: 'authoritative';
|
|
3930
|
+
readonly scopes: ReadonlyArray<string>;
|
|
3931
|
+
}
|
|
3932
|
+
/** The authority source could not be reached. Fails the request closed. */
|
|
3933
|
+
| {
|
|
3934
|
+
readonly kind: 'unavailable';
|
|
3935
|
+
readonly reason: string;
|
|
3936
|
+
};
|
|
3937
|
+
|
|
2991
3938
|
export declare class ScopeError extends ScopeError_base {
|
|
2992
3939
|
}
|
|
2993
3940
|
|
|
@@ -2998,6 +3945,11 @@ declare const ScopeError_base: Schema.TaggedErrorClass<ScopeError, "ScopeError",
|
|
|
2998
3945
|
message: typeof Schema.String;
|
|
2999
3946
|
}>;
|
|
3000
3947
|
|
|
3948
|
+
/** What `resolveScopes` may return. A bare array is the shorthand for
|
|
3949
|
+
* `{ kind: 'grant', scopes }` — the common case, and the reading an existing
|
|
3950
|
+
* resolver already has. */
|
|
3951
|
+
export declare type ScopeResolverResult = ReadonlyArray<string> | ScopeDecision;
|
|
3952
|
+
|
|
3001
3953
|
export declare interface ServerErrorEvent {
|
|
3002
3954
|
/** The thrown value (Error | tagged error | anything). */
|
|
3003
3955
|
readonly error: unknown;
|
|
@@ -3036,6 +3988,16 @@ export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | un
|
|
|
3036
3988
|
*/
|
|
3037
3989
|
export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
|
|
3038
3990
|
|
|
3991
|
+
/**
|
|
3992
|
+
* Every routing key a `source:` names, flattened.
|
|
3993
|
+
*
|
|
3994
|
+
* The one normaliser. `source` has been read with an inline
|
|
3995
|
+
* `typeof s === 'string' ? [s] : s` at nine sites, which was already a decision
|
|
3996
|
+
* written nine times; adding a second accepted shape to each of them is how the
|
|
3997
|
+
* ninth ends up handling channels differently from the first.
|
|
3998
|
+
*/
|
|
3999
|
+
export declare const sourceKeys: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => ReadonlyArray<string>;
|
|
4000
|
+
|
|
3039
4001
|
export declare type StrategyResolution = {
|
|
3040
4002
|
readonly kind: 'matched';
|
|
3041
4003
|
readonly subject: Subject;
|
|
@@ -3084,7 +4046,11 @@ export declare interface StreamProcedureDescriptor<Name extends string, Input ex
|
|
|
3084
4046
|
/** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element,
|
|
3085
4047
|
* the same as a query's — a stream is a long-lived grant and the scopes that
|
|
3086
4048
|
* justified it can be withdrawn while it is still open. */
|
|
3087
|
-
readonly guards:
|
|
4049
|
+
readonly guards: DeclaredAccess | undefined;
|
|
4050
|
+
/** The declared reason this procedure needs NO authorization check —
|
|
4051
|
+
* `openAccess: '<why>'`. Mutually exclusive with `guards`; together they are
|
|
4052
|
+
* the only two shapes `security.defaultDeny` accepts. */
|
|
4053
|
+
readonly openAccess: string | undefined;
|
|
3088
4054
|
}
|
|
3089
4055
|
|
|
3090
4056
|
export declare const streamToRpc: <Name extends string, Input extends Schema.Schema.Any, Element extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: StreamProcedureDescriptor<Name, Input, Element, Err>, extraErrors?: ExtraErrors) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Element, Schema.Schema.All>, typeof Schema.Never, never>;
|
|
@@ -3121,6 +4087,44 @@ export declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
3121
4087
|
|
|
3122
4088
|
export declare type Subject = typeof Subject.Type;
|
|
3123
4089
|
|
|
4090
|
+
export declare const SubjectIdentity: Schema.Union<[Schema.Struct<{
|
|
4091
|
+
id: typeof Schema.String;
|
|
4092
|
+
type: Schema.Literal<["user"]>;
|
|
4093
|
+
tenantId: typeof Schema.String;
|
|
4094
|
+
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
4095
|
+
}>, Schema.Struct<{
|
|
4096
|
+
id: typeof Schema.String;
|
|
4097
|
+
type: Schema.Literal<["apiKey"]>;
|
|
4098
|
+
tenantId: typeof Schema.String;
|
|
4099
|
+
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
4100
|
+
}>, Schema.Struct<{
|
|
4101
|
+
id: typeof Schema.String;
|
|
4102
|
+
type: Schema.Literal<["serviceAccount"]>;
|
|
4103
|
+
tenantId: typeof Schema.String;
|
|
4104
|
+
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
4105
|
+
}>, Schema.Struct<{
|
|
4106
|
+
type: Schema.Literal<["anonymous"]>;
|
|
4107
|
+
id: typeof Schema.Null;
|
|
4108
|
+
tenantId: Schema.NullOr<typeof Schema.String>;
|
|
4109
|
+
}>, Schema.Struct<{
|
|
4110
|
+
id: typeof Schema.String;
|
|
4111
|
+
type: Schema.Literal<["system"]>;
|
|
4112
|
+
tenantId: typeof Schema.Null;
|
|
4113
|
+
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
4114
|
+
}>]>;
|
|
4115
|
+
|
|
4116
|
+
export declare type SubjectIdentity = typeof SubjectIdentity.Type;
|
|
4117
|
+
|
|
4118
|
+
/**
|
|
4119
|
+
* Drop a Subject's authority, keeping everything that identifies it.
|
|
4120
|
+
*
|
|
4121
|
+
* Total and lossy on purpose — there is no variant it can fail on and no
|
|
4122
|
+
* option to keep the scopes. A caller that wants to mint a credential from a
|
|
4123
|
+
* Subject goes through here, so "did this cookie carry authority?" has one
|
|
4124
|
+
* answer at every mint site instead of one per site.
|
|
4125
|
+
*/
|
|
4126
|
+
export declare const subjectIdentity: (subject: Subject) => SubjectIdentity;
|
|
4127
|
+
|
|
3124
4128
|
/** A strategy's verdict on a request.
|
|
3125
4129
|
* - `matched`: this strategy claims the request; here's the Subject.
|
|
3126
4130
|
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
@@ -3342,6 +4346,23 @@ declare const Unauthenticated_base: Schema.TaggedErrorClass<Unauthenticated, "Un
|
|
|
3342
4346
|
reason: Schema.optional<typeof Schema.String>;
|
|
3343
4347
|
}>;
|
|
3344
4348
|
|
|
4349
|
+
/**
|
|
4350
|
+
* Channel keys named by a `source:` that no `reactivityChannel()` declared.
|
|
4351
|
+
*
|
|
4352
|
+
* The channel half of the stale-`source:` audit. It should be structurally
|
|
4353
|
+
* unreachable when the channel is authored as an object — you cannot import a
|
|
4354
|
+
* declaration that does not exist — so it exists for the two ways round that:
|
|
4355
|
+
* a hand-written `source: 'channel:presence'` string, and a channel whose
|
|
4356
|
+
* declaring module the boot did not load.
|
|
4357
|
+
*/
|
|
4358
|
+
export declare const undeclaredChannelKeys: (procedures: ReadonlyArray<{
|
|
4359
|
+
readonly name: string;
|
|
4360
|
+
readonly source: string | ReadonlyArray<string> | undefined;
|
|
4361
|
+
}>) => ReadonlyArray<{
|
|
4362
|
+
readonly procedure: string;
|
|
4363
|
+
readonly key: string;
|
|
4364
|
+
}>;
|
|
4365
|
+
|
|
3345
4366
|
export declare const UNDO_APPLY_TAG: "__voltro.undo.apply";
|
|
3346
4367
|
|
|
3347
4368
|
export declare const UNDO_LOG_TAG: "__voltro.undo.log";
|
|
@@ -3461,6 +4482,31 @@ export declare interface VoltroPlugin {
|
|
|
3461
4482
|
* (`@voltro/audit#analytics`).
|
|
3462
4483
|
*/
|
|
3463
4484
|
readonly name: string;
|
|
4485
|
+
/**
|
|
4486
|
+
* The plugin's CANONICAL name, before the app renamed it — set this whenever
|
|
4487
|
+
* `name` can come from an app-supplied `alias`.
|
|
4488
|
+
*
|
|
4489
|
+
* It exists because without it an alias silently half-works, and that was the
|
|
4490
|
+
* shipped state. `effectiveRouteTag` prefixes a route with the plugin's
|
|
4491
|
+
* alias UNLESS the route name already contains a dot — an escape hatch for a
|
|
4492
|
+
* plugin wanting a deeper namespace. But every first-party plugin declares
|
|
4493
|
+
* its routes fully qualified (`name: 'notifications.inbox'`, 96 such
|
|
4494
|
+
* declarations across seven plugins), so the escape hatch fires on all of
|
|
4495
|
+
* them and the alias moves NOTHING on the rpc surface. A user aliasing
|
|
4496
|
+
* `notifications` to escape a collision with their own `notifications.*`
|
|
4497
|
+
* routes would still collide — and would additionally lose the dashboard
|
|
4498
|
+
* panel, which fetches the default slug. Worse than not shipping the field.
|
|
4499
|
+
*
|
|
4500
|
+
* With `baseName` set, both tag derivations strip a leading
|
|
4501
|
+
* `<default-alias>.` before applying the EFFECTIVE alias, so
|
|
4502
|
+
* `notifications.inbox` under `alias: 'inbox'` becomes `inbox.inbox` rather
|
|
4503
|
+
* than staying put. A dotted name that does NOT start with the default alias
|
|
4504
|
+
* is left alone — that is the genuine escape hatch, and it survives.
|
|
4505
|
+
*
|
|
4506
|
+
* Derived, not declared per route, so the 96 declarations stay as they are
|
|
4507
|
+
* and cannot drift out of step with the strip.
|
|
4508
|
+
*/
|
|
4509
|
+
readonly baseName?: string;
|
|
3464
4510
|
/**
|
|
3465
4511
|
* Plugin version — the package's own semver. Distinct from
|
|
3466
4512
|
* `framework` (the FRAMEWORK range the plugin works with). Used by
|