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