@voltro/protocol 0.63.0 → 0.64.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 +90 -0
- package/dist/index.d.ts +355 -23
- package/dist/index.js +503 -451
- package/dist/rest.d.ts +4 -1
- package/dist/rest.js +13 -13
- package/dist/{scopes-B0kGvAnI.js → scopes-D9t3tYc7.js} +98 -69
- package/dist/{serverErrorBus-BeN9pOpY.js → serverErrorBus-QQgyq6fV.js} +15 -7
- package/dist/session.js +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Context } from 'effect';
|
|
|
2
2
|
import { DataStore } from '@voltro/database';
|
|
3
3
|
import { Effect } from 'effect';
|
|
4
4
|
import { Layer } from 'effect';
|
|
5
|
+
import { Option } from 'effect';
|
|
5
6
|
import { Rpc } from '@effect/rpc';
|
|
6
7
|
import { RpcMiddleware } from '@effect/rpc';
|
|
7
8
|
import { Schema } from 'effect';
|
|
@@ -447,6 +448,10 @@ declare const ApprovalUnavailable_base: Schema.TaggedErrorClass<ApprovalUnavaila
|
|
|
447
448
|
*/
|
|
448
449
|
export declare const assertAuthenticated: (subject: Subject, reason?: string) => void;
|
|
449
450
|
|
|
451
|
+
/** Attach a resolved decision to a definition — non-enumerable, like the
|
|
452
|
+
* definer's other metadata, so the engine never sees it as a field. */
|
|
453
|
+
export declare const attachWorkflowAccess: (definition: object, access: WorkflowAccess) => void;
|
|
454
|
+
|
|
450
455
|
/** Optional callback-route mount surface for strategies that handle
|
|
451
456
|
* OAuth callbacks. Apps wire these via the framework's HTTP router
|
|
452
457
|
* when the strategy declares `mountRoutes`. */
|
|
@@ -587,6 +592,55 @@ export declare interface AuthStrategyWithCallback extends AuthStrategy {
|
|
|
587
592
|
readonly mountRoutes: (router: AuthCallbackRouter) => void;
|
|
588
593
|
}
|
|
589
594
|
|
|
595
|
+
export declare type AuthSubjectGuard = (subject: Subject, ctx: AuthSubjectGuardContext) => Promise<AuthSubjectGuardVerdict> | AuthSubjectGuardVerdict;
|
|
596
|
+
|
|
597
|
+
export declare interface AuthSubjectGuardContext {
|
|
598
|
+
/** `'request'` for a live call, `'workflow'` for a durable run being executed. */
|
|
599
|
+
readonly origin: 'request' | 'workflow';
|
|
600
|
+
/** The request's headers. EMPTY for `origin: 'workflow'`. */
|
|
601
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
602
|
+
/** `undefined` for `origin: 'workflow'` and for transports with no connection. */
|
|
603
|
+
readonly clientId: number | undefined;
|
|
604
|
+
/** The app's boot store (see `AuthStrategyInput.store`), or `undefined` while
|
|
605
|
+
* it is still being built — a guard that needs it then fails CLOSED. */
|
|
606
|
+
readonly store: DataStore | undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* A post-authentication VETO on a resolved subject — the framework-level form
|
|
611
|
+
* of `@voltro/plugin-auth`'s `subjectGuards`, and the reason it exists here too.
|
|
612
|
+
*
|
|
613
|
+
* The auth plugin runs its guards once, at LOGIN, before a session is issued —
|
|
614
|
+
* the right place when every session comes from its routes. It is the wrong
|
|
615
|
+
* place for an app whose subjects arrive through its own strategy chain (a JWKS
|
|
616
|
+
* IdP, an API-key table, a session it mints itself): those never pass through
|
|
617
|
+
* a login handler, so a `deactivationGuard()` wired onto the auth plugin vetoed
|
|
618
|
+
* nobody, and `users.deactivate` locked no one out. The guard has to sit where
|
|
619
|
+
* EVERY subject passes — after a strategy matches and authority is resolved,
|
|
620
|
+
* on the request path and on a durable workflow's resume alike.
|
|
621
|
+
*
|
|
622
|
+
* A guard receives the subject WITH its resolved scopes (so it may reason about
|
|
623
|
+
* them) and the same context `resolveScopes` gets: `origin` (a durable run has
|
|
624
|
+
* no request behind it), the headers, and the app's store for the row it needs
|
|
625
|
+
* to read. It returns a verdict; the first rejection wins and the request fails
|
|
626
|
+
* closed with `Unauthenticated` carrying the guard's `code` and `message`. It
|
|
627
|
+
* runs only for a MATCHED subject — an anonymous caller has no identity to veto.
|
|
628
|
+
*
|
|
629
|
+
* It is cached WITH the scope decision, under the same key and window: a veto
|
|
630
|
+
* is a fact about an identity, exactly like its roles, and a guard that reads a
|
|
631
|
+
* row per request would otherwise cost what `scopeCache` exists to avoid. The
|
|
632
|
+
* staleness is therefore the one number the docs already give (30s), and
|
|
633
|
+
* `invalidate(scopeCacheKey(subject))` from the mutation that deactivates makes
|
|
634
|
+
* it zero on that process — the same handle, the same call.
|
|
635
|
+
*/
|
|
636
|
+
export declare type AuthSubjectGuardVerdict = {
|
|
637
|
+
readonly ok: true;
|
|
638
|
+
} | {
|
|
639
|
+
readonly ok: false;
|
|
640
|
+
readonly code: string;
|
|
641
|
+
readonly message: string;
|
|
642
|
+
};
|
|
643
|
+
|
|
590
644
|
/**
|
|
591
645
|
* Decide what to do for an incoming (scope, key) in ONE atomic store call. On
|
|
592
646
|
* `fresh` the caller runs the handler then calls `finish`/`fail`; on
|
|
@@ -821,6 +875,13 @@ export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrat
|
|
|
821
875
|
removed: ReadonlyArray<string>;
|
|
822
876
|
granted: ReadonlyArray<string>;
|
|
823
877
|
}) => void;
|
|
878
|
+
/**
|
|
879
|
+
* Post-authentication vetoes on a MATCHED subject — see `AuthSubjectGuard`.
|
|
880
|
+
* Run after `resolveScopes` (so a guard sees the applied scopes), inside the
|
|
881
|
+
* same cache entry, first rejection wins → `Unauthenticated` with the
|
|
882
|
+
* guard's `code` and `message`.
|
|
883
|
+
*/
|
|
884
|
+
readonly subjectGuards?: ReadonlyArray<AuthSubjectGuard>;
|
|
824
885
|
/**
|
|
825
886
|
* Hands every strategy the app's store on `input.store`.
|
|
826
887
|
*
|
|
@@ -2132,6 +2193,15 @@ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefin
|
|
|
2132
2193
|
/** The currently-registered resource-scope resolver, or `undefined`. */
|
|
2133
2194
|
export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
|
|
2134
2195
|
|
|
2196
|
+
/**
|
|
2197
|
+
* Read a definition's decision back. A value that never went through
|
|
2198
|
+
* `workflow()` — a bare `Workflow.make`, a test double — reports UNDECIDED and
|
|
2199
|
+
* not internal, so the boot gate judges it exactly like a definer call that
|
|
2200
|
+
* declared nothing: that is the fail-closed reading, and the only one under
|
|
2201
|
+
* which forgetting the definer cannot open a door.
|
|
2202
|
+
*/
|
|
2203
|
+
export declare const getWorkflowAccess: (definition: unknown) => WorkflowAccess;
|
|
2204
|
+
|
|
2135
2205
|
/** Ask for default-deny semantics on one `checkGuards` call. */
|
|
2136
2206
|
export declare interface GuardCheckOptions {
|
|
2137
2207
|
/** Refuse a procedure that declares no access decision at all. */
|
|
@@ -2295,6 +2365,9 @@ export declare interface HttpRequestContext {
|
|
|
2295
2365
|
*/
|
|
2296
2366
|
export declare type HttpRequestInterceptor = (next: () => Promise<HttpInterceptResponse | null>, ctx: HttpRequestContext) => Promise<HttpInterceptResponse | null>;
|
|
2297
2367
|
|
|
2368
|
+
/** Fire-and-forget observer of plugin HTTP route calls — see {@link PluginHttpRouteEvent}. */
|
|
2369
|
+
export declare type HttpRouteObserver = (event: PluginHttpRouteEvent) => void;
|
|
2370
|
+
|
|
2298
2371
|
export declare type IdempotencyOutcome = {
|
|
2299
2372
|
readonly kind: 'fresh';
|
|
2300
2373
|
} | {
|
|
@@ -2503,6 +2576,24 @@ export declare const memoryIdempotencyStore: () => IdempotencyStore;
|
|
|
2503
2576
|
* and the call-time path cannot describe the same defect differently. */
|
|
2504
2577
|
export declare const missingAccessDecision: (procedure?: string) => ScopeError;
|
|
2505
2578
|
|
|
2579
|
+
/** The wait a conflicting replay is told to take. Long enough that a handler
|
|
2580
|
+
* in its last second finishes; short enough that a 10-second replay window
|
|
2581
|
+
* fits several tries. */
|
|
2582
|
+
export declare const MUTATION_IN_FLIGHT_RETRY_AFTER_MS = 1000;
|
|
2583
|
+
|
|
2584
|
+
export declare class MutationInFlight extends MutationInFlight_base {
|
|
2585
|
+
get message(): string;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
declare const MutationInFlight_base: Schema.TaggedErrorClass<MutationInFlight, "MutationInFlight", {
|
|
2589
|
+
readonly _tag: Schema.tag<"MutationInFlight">;
|
|
2590
|
+
} & {
|
|
2591
|
+
traceId: typeof Schema.String;
|
|
2592
|
+
/** How long to wait before replaying the same key — the first call is still
|
|
2593
|
+
* running; a replay before it finishes conflicts again. */
|
|
2594
|
+
retryAfterMs: typeof Schema.Number;
|
|
2595
|
+
}>;
|
|
2596
|
+
|
|
2506
2597
|
export declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
|
|
2507
2598
|
readonly kind: 'mutation';
|
|
2508
2599
|
readonly name: Name;
|
|
@@ -2704,20 +2795,6 @@ export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect
|
|
|
2704
2795
|
*/
|
|
2705
2796
|
export declare const pluginAlias: (pluginName: string) => string;
|
|
2706
2797
|
|
|
2707
|
-
/**
|
|
2708
|
-
* Server-only context delivered to `bindDataStore`, ALONGSIDE the store,
|
|
2709
|
-
* AFTER the framework has opened its store + pool. Gives a plugin the
|
|
2710
|
-
* framework's ALREADY-OPEN handles so it never rebuilds a pg pool from
|
|
2711
|
-
* raw env (the anti-pattern `@voltro/plugin-ratelimit` used to hand-roll)
|
|
2712
|
-
* and never runs an un-coordinated per-replica `setInterval` sweep (what
|
|
2713
|
-
* presence + governance used to do).
|
|
2714
|
-
*
|
|
2715
|
-
* These fields are SERVER-only — they are TYPES only in this browser-safe
|
|
2716
|
-
* protocol package (the `DataStore` / `SqlClient` imports are erased), and
|
|
2717
|
-
* the live values exist only inside the serve pipeline that calls
|
|
2718
|
-
* `bindDataStore`. Nothing in the browser-loaded descriptor/schema graph
|
|
2719
|
-
* touches them.
|
|
2720
|
-
*/
|
|
2721
2798
|
export declare interface PluginBindContext {
|
|
2722
2799
|
/**
|
|
2723
2800
|
* This process's replica identity — the same value the event bus stamps as
|
|
@@ -2754,6 +2831,20 @@ export declare interface PluginBindContext {
|
|
|
2754
2831
|
readonly publish: (channel: string, payload: string) => unknown;
|
|
2755
2832
|
readonly subscribe: (channel: string, handler: (payload: string) => void) => unknown;
|
|
2756
2833
|
};
|
|
2834
|
+
/**
|
|
2835
|
+
* The framework's cache — the SAME backend `ctx.cache` and query snapshots
|
|
2836
|
+
* use (`CACHE_BACKEND`: in-process memory, or Redis shared across replicas).
|
|
2837
|
+
* A plugin that memoises reference data (a board's configuration, a field
|
|
2838
|
+
* catalogue) reads and writes it here instead of asking the app for a
|
|
2839
|
+
* hand-built store object; the keys a plugin writes are its own to namespace.
|
|
2840
|
+
*
|
|
2841
|
+
* A handle rather than a value: `voltro dev` builds the cache after plugins
|
|
2842
|
+
* are bound and `voltro serve` before, so the two boot paths hand over the
|
|
2843
|
+
* same object whose methods resolve the live cache at call time. Before it
|
|
2844
|
+
* exists a read misses and a write is dropped — best-effort, never a boot
|
|
2845
|
+
* ordering a plugin has to know about.
|
|
2846
|
+
*/
|
|
2847
|
+
readonly cache?: PluginCacheHandle;
|
|
2757
2848
|
/**
|
|
2758
2849
|
* The framework's already-open `SqlClient` (from `@effect/sql`) — the
|
|
2759
2850
|
* SAME pool the app's store uses. A plugin runs raw SQL through this
|
|
@@ -2805,6 +2896,28 @@ export declare interface PluginBindContext {
|
|
|
2805
2896
|
readonly claimChange: (scope: string, key: string) => Promise<boolean>;
|
|
2806
2897
|
}
|
|
2807
2898
|
|
|
2899
|
+
/**
|
|
2900
|
+
* Server-only context delivered to `bindDataStore`, ALONGSIDE the store,
|
|
2901
|
+
* AFTER the framework has opened its store + pool. Gives a plugin the
|
|
2902
|
+
* framework's ALREADY-OPEN handles so it never rebuilds a pg pool from
|
|
2903
|
+
* raw env (the anti-pattern `@voltro/plugin-ratelimit` used to hand-roll)
|
|
2904
|
+
* and never runs an un-coordinated per-replica `setInterval` sweep (what
|
|
2905
|
+
* presence + governance used to do).
|
|
2906
|
+
*
|
|
2907
|
+
* These fields are SERVER-only — they are TYPES only in this browser-safe
|
|
2908
|
+
* protocol package (the `DataStore` / `SqlClient` imports are erased), and
|
|
2909
|
+
* the live values exist only inside the serve pipeline that calls
|
|
2910
|
+
* `bindDataStore`. Nothing in the browser-loaded descriptor/schema graph
|
|
2911
|
+
* touches them.
|
|
2912
|
+
*/
|
|
2913
|
+
/** The slice of the framework cache handed to a plugin at bind time. */
|
|
2914
|
+
export declare interface PluginCacheHandle {
|
|
2915
|
+
readonly get: (key: string) => Effect.Effect<Option.Option<unknown>, unknown>;
|
|
2916
|
+
readonly set: (key: string, value: unknown, options?: {
|
|
2917
|
+
readonly ttlMs?: number;
|
|
2918
|
+
}) => Effect.Effect<void, unknown>;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2808
2921
|
/**
|
|
2809
2922
|
* Post-commit change event delivered to a plugin's `onChangeEvent` tap.
|
|
2810
2923
|
* Minimal, browser-safe shape (protocol must not depend on the database
|
|
@@ -3113,6 +3226,25 @@ export declare interface PluginHttpRouteByteStream {
|
|
|
3113
3226
|
readonly contentDisposition?: string;
|
|
3114
3227
|
}
|
|
3115
3228
|
|
|
3229
|
+
/**
|
|
3230
|
+
* One plugin HTTP route call, after it answered — what an audit trail records.
|
|
3231
|
+
*
|
|
3232
|
+
* `onHttpRequest` runs BEFORE auth and before routing and sees no status, so a
|
|
3233
|
+
* login, a logout or an avatar fetch served by a plugin's `httpRoutes` left no
|
|
3234
|
+
* trace in a trail that recorded every rpc. This is the other end of the same
|
|
3235
|
+
* pipeline: the method, the path, the status and how long it took, once the
|
|
3236
|
+
* route has answered. No subject — a plugin HTTP route is not part of the rpc
|
|
3237
|
+
* pipeline and nothing upstream authenticated its caller; a route that resolves
|
|
3238
|
+
* one itself is the only party that knows, and can put it in its own log.
|
|
3239
|
+
*/
|
|
3240
|
+
export declare interface PluginHttpRouteEvent {
|
|
3241
|
+
readonly method: string;
|
|
3242
|
+
/** Path WITHOUT query string. */
|
|
3243
|
+
readonly path: string;
|
|
3244
|
+
readonly status: number;
|
|
3245
|
+
readonly durationMs: number;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3116
3248
|
export declare interface PluginHttpRouteRequest {
|
|
3117
3249
|
readonly method: string;
|
|
3118
3250
|
/** Path WITHOUT query string. */
|
|
@@ -3220,7 +3352,10 @@ export declare interface PluginHttpRouteResult {
|
|
|
3220
3352
|
readonly status: number;
|
|
3221
3353
|
readonly body?: string | Uint8Array;
|
|
3222
3354
|
readonly contentType?: string;
|
|
3223
|
-
|
|
3355
|
+
/** Response headers. A header that repeats — two `Set-Cookie` lines, a
|
|
3356
|
+
* session and a CSRF cookie from one login — is an ARRAY; `set-cookie`
|
|
3357
|
+
* arrays become separate lines, every other array joins with `, `. */
|
|
3358
|
+
readonly headers?: Record<string, string | ReadonlyArray<string>>;
|
|
3224
3359
|
/** Stream the response (SSE) instead of sending `body`. See
|
|
3225
3360
|
* {@link PluginHttpRouteStream}. */
|
|
3226
3361
|
readonly stream?: PluginHttpRouteStream;
|
|
@@ -3444,7 +3579,7 @@ export declare type PluginMigrationSqlClient = SqlClient.SqlClient;
|
|
|
3444
3579
|
* boot log + `/_voltro/inspect/plugins` so operators audit what each
|
|
3445
3580
|
* plugin asks for before installing.
|
|
3446
3581
|
*/
|
|
3447
|
-
export declare type PluginPermission = 'rpc:intercept:mutation' | 'rpc:intercept:query' | 'rpc:intercept:action' | 'schedule:fire' | 'schedule:read' | 'workflow:step' | 'http:intercept' | 'inspect:read' | 'inspect:write' | 'dashboard:mount' | 'store:write' | 'migration:run' | 'store:changes:read' | `secrets:read:${string}` | `network:outbound:${string}` | `plugin:hook:${string}`;
|
|
3582
|
+
export declare type PluginPermission = 'rpc:intercept:mutation' | 'rpc:intercept:query' | 'rpc:intercept:action' | 'rpc:intercept:workflow' | 'schedule:fire' | 'schedule:read' | 'workflow:step' | 'http:intercept' | 'inspect:read' | 'inspect:write' | 'dashboard:mount' | 'store:write' | 'migration:run' | 'store:changes:read' | `secrets:read:${string}` | `network:outbound:${string}` | `plugin:hook:${string}`;
|
|
3448
3583
|
|
|
3449
3584
|
/**
|
|
3450
3585
|
* Per-call context handed to a plugin-contributed rpc route's
|
|
@@ -3458,6 +3593,15 @@ export declare interface PluginRouteContext {
|
|
|
3458
3593
|
};
|
|
3459
3594
|
}
|
|
3460
3595
|
|
|
3596
|
+
/**
|
|
3597
|
+
* The kinds a plugin can CONTRIBUTE a route as. A plugin declares procedures,
|
|
3598
|
+
* never workflows (a workflow is a `*.workflow.tsx` pair the app authors and the
|
|
3599
|
+
* engine runs), so this is the interceptor union minus `'workflow'` — kept as
|
|
3600
|
+
* its own name so a route's `kind` cannot select a runtime path that does not
|
|
3601
|
+
* exist for it.
|
|
3602
|
+
*/
|
|
3603
|
+
export declare type PluginRouteKind = 'mutation' | 'query' | 'action';
|
|
3604
|
+
|
|
3461
3605
|
/**
|
|
3462
3606
|
* Client-facing declaration of a plugin RPC route's descriptor, so the codegen
|
|
3463
3607
|
* can emit it into the generated client rpc group (`appGroup` + `appDescriptors`)
|
|
@@ -3474,7 +3618,7 @@ export declare interface PluginRouteContext {
|
|
|
3474
3618
|
*/
|
|
3475
3619
|
export declare interface PluginRpcClientDescriptor {
|
|
3476
3620
|
readonly tag: string;
|
|
3477
|
-
readonly kind:
|
|
3621
|
+
readonly kind: PluginRouteKind;
|
|
3478
3622
|
readonly import: {
|
|
3479
3623
|
readonly module: string;
|
|
3480
3624
|
readonly name: string;
|
|
@@ -3497,7 +3641,7 @@ export declare interface PluginRpcClientDescriptor {
|
|
|
3497
3641
|
*/
|
|
3498
3642
|
export declare interface PluginRpcRoute {
|
|
3499
3643
|
/** 'mutation' | 'query' | 'action' — determines the runtime path. */
|
|
3500
|
-
readonly kind:
|
|
3644
|
+
readonly kind: PluginRouteKind;
|
|
3501
3645
|
/** RPC tag relative to the plugin (e.g. `'list'`, `'admin.events'`). */
|
|
3502
3646
|
readonly name: string;
|
|
3503
3647
|
/** Optional human-readable description; surfaced in the dashboard. */
|
|
@@ -4042,6 +4186,14 @@ export declare const requireScope: (subject: Subject, scope: string) => Effect.E
|
|
|
4042
4186
|
/** Drop every registration. Tests only — production registers once at module load. */
|
|
4043
4187
|
export declare const resetPluginAliases: () => void;
|
|
4044
4188
|
|
|
4189
|
+
/**
|
|
4190
|
+
* Validate an author's access options and normalise them into the array every
|
|
4191
|
+
* enforcement path reads. Throws at DEFINITION time — module load, which is
|
|
4192
|
+
* boot — with the same wording the event and procedure definers use, so the
|
|
4193
|
+
* three refusals read as one rule.
|
|
4194
|
+
*/
|
|
4195
|
+
export declare const resolveWorkflowAccess: (name: string, options: WorkflowAccessOptions<never>) => WorkflowAccess;
|
|
4196
|
+
|
|
4045
4197
|
/** The question the runtime asks a registered resource-scope resolver. */
|
|
4046
4198
|
export declare interface ResourceScopeRequest {
|
|
4047
4199
|
/** The authenticated caller. */
|
|
@@ -4219,7 +4371,7 @@ export declare type RpcInterceptor = (next: Effect.Effect<unknown, unknown>, con
|
|
|
4219
4371
|
export declare interface RpcInterceptorContext {
|
|
4220
4372
|
/** Procedure tag, e.g. `todos.create`. */
|
|
4221
4373
|
readonly tag: string;
|
|
4222
|
-
/** Discriminator — `'mutation' | 'query' | 'action'`. */
|
|
4374
|
+
/** Discriminator — `'mutation' | 'query' | 'action' | 'workflow'`. */
|
|
4223
4375
|
readonly kind: RpcKind;
|
|
4224
4376
|
/** The validated input the executor is about to (or already did) receive. */
|
|
4225
4377
|
readonly input: unknown;
|
|
@@ -4236,10 +4388,11 @@ export declare interface RpcInterceptorContext {
|
|
|
4236
4388
|
/**
|
|
4237
4389
|
* Which kind of rpc the interceptor is wrapping. The runtime passes
|
|
4238
4390
|
* `kind: 'mutation'` to interceptors registered as `interceptMutation`,
|
|
4239
|
-
* `kind: 'query'` to `interceptQuery`,
|
|
4240
|
-
* `interceptAction
|
|
4391
|
+
* `kind: 'query'` to `interceptQuery`, `kind: 'action'` to
|
|
4392
|
+
* `interceptAction`, and `kind: 'workflow'` to `interceptWorkflow` — one
|
|
4393
|
+
* plugin can implement any combination.
|
|
4241
4394
|
*/
|
|
4242
|
-
export declare type RpcKind = 'mutation' | 'query' | 'action';
|
|
4395
|
+
export declare type RpcKind = 'mutation' | 'query' | 'action' | 'workflow';
|
|
4243
4396
|
|
|
4244
4397
|
/**
|
|
4245
4398
|
* Per-firing context passed to schedule-fire interceptors. Shape
|
|
@@ -4351,6 +4504,19 @@ export declare type ScopeDecision =
|
|
|
4351
4504
|
| {
|
|
4352
4505
|
readonly kind: 'unavailable';
|
|
4353
4506
|
readonly reason: string;
|
|
4507
|
+
}
|
|
4508
|
+
/**
|
|
4509
|
+
* A subject guard REFUSED this identity (see `AuthSubjectGuard`). Not a
|
|
4510
|
+
* resolver's answer — a resolver returns one of the three above — but the
|
|
4511
|
+
* composer's, produced after the guards ran, and cached like a grant: a veto
|
|
4512
|
+
* is a durable fact about the identity until whatever changed it invalidates
|
|
4513
|
+
* the key. `applyScopeDecision` THROWS on it, so a caller that forgot the
|
|
4514
|
+
* explicit branch fails closed rather than applying "no scopes" and carrying on.
|
|
4515
|
+
*/
|
|
4516
|
+
| {
|
|
4517
|
+
readonly kind: 'vetoed';
|
|
4518
|
+
readonly code: string;
|
|
4519
|
+
readonly message: string;
|
|
4354
4520
|
};
|
|
4355
4521
|
|
|
4356
4522
|
export declare class ScopeError extends ScopeError_base {
|
|
@@ -5380,6 +5546,22 @@ export declare interface VoltroPlugin {
|
|
|
5380
5546
|
* tenant-scoped IO quotas).
|
|
5381
5547
|
*/
|
|
5382
5548
|
readonly interceptAction?: RpcInterceptor;
|
|
5549
|
+
/**
|
|
5550
|
+
* Wraps every WORKFLOW START call — the unary rpc a `*.workflow.tsx`
|
|
5551
|
+
* becomes on the wire. Same composition semantics as `interceptMutation`.
|
|
5552
|
+
*
|
|
5553
|
+
* A start is a write-class call: it records a run row, it may cost money
|
|
5554
|
+
* (a workflow that calls a model or a paid API), and it runs as the caller's
|
|
5555
|
+
* subject. So the plugins that wrap mutations — audit, rate limiting,
|
|
5556
|
+
* entitlement, rbac's scope publication — want to wrap this too, and before
|
|
5557
|
+
* this hook existed none of them did: the start handler was bound outside
|
|
5558
|
+
* every interceptor chain, so an audit trail that covered every mutation
|
|
5559
|
+
* recorded no workflow start, and a `guards:` on a workflow could not see
|
|
5560
|
+
* the scopes rbac publishes inside the chain. Declaring `interceptAction`
|
|
5561
|
+
* without this hook leaves that gap open for your plugin; the first-party
|
|
5562
|
+
* plugins carry a parity test that fails when one does.
|
|
5563
|
+
*/
|
|
5564
|
+
readonly interceptWorkflow?: RpcInterceptor;
|
|
5383
5565
|
/**
|
|
5384
5566
|
* Effect Layer that contributes services into the per-request
|
|
5385
5567
|
* context. Anything declared here is yieldable from any handler in
|
|
@@ -5515,6 +5697,14 @@ export declare interface VoltroPlugin {
|
|
|
5515
5697
|
* the hook is declared without the matching permission.
|
|
5516
5698
|
*/
|
|
5517
5699
|
readonly onHttpRequest?: HttpRequestInterceptor;
|
|
5700
|
+
/**
|
|
5701
|
+
* Post-response observer of every plugin HTTP route call — method, path,
|
|
5702
|
+
* status, duration. The audit plugin's way of recording a login or an avatar
|
|
5703
|
+
* fetch, which no rpc interceptor ever sees. Fire-and-forget: it cannot
|
|
5704
|
+
* change the response and a throw is swallowed. Same `'http:intercept'`
|
|
5705
|
+
* permission as `onHttpRequest` — it is the same pipeline, the other end.
|
|
5706
|
+
*/
|
|
5707
|
+
readonly onHttpRoute?: HttpRouteObserver;
|
|
5518
5708
|
/**
|
|
5519
5709
|
* Dashboard mount contributions — remote-loaded ESM modules the
|
|
5520
5710
|
* dashboard host renders alongside framework pages. See
|
|
@@ -5735,6 +5925,61 @@ export declare const withRpcTag: <D extends {
|
|
|
5735
5925
|
readonly name: T;
|
|
5736
5926
|
};
|
|
5737
5927
|
|
|
5928
|
+
/** The resolved decision the runtime reads. */
|
|
5929
|
+
export declare interface WorkflowAccess {
|
|
5930
|
+
/** The author's guards, or the erased `openAccess` entry — never both. */
|
|
5931
|
+
readonly guards: DeclaredAccess<unknown> | undefined;
|
|
5932
|
+
readonly openAccess: string | undefined;
|
|
5933
|
+
readonly internal: boolean;
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5936
|
+
/**
|
|
5937
|
+
* The definition as the boot gate, the codegen and the dispatch spine read it:
|
|
5938
|
+
* the structural `{ name, internal, guards, openAccess }` every other kind
|
|
5939
|
+
* presents, so `isWireReachable` and `hasAccessDecision` apply unchanged and a
|
|
5940
|
+
* workflow rides the same walk as the five procedure kinds.
|
|
5941
|
+
*/
|
|
5942
|
+
export declare const workflowAccessDescriptor: (definition: {
|
|
5943
|
+
readonly name: string;
|
|
5944
|
+
}) => {
|
|
5945
|
+
readonly name: string;
|
|
5946
|
+
readonly internal: boolean;
|
|
5947
|
+
readonly guards: DeclaredAccess<unknown> | undefined;
|
|
5948
|
+
readonly openAccess: string | undefined;
|
|
5949
|
+
};
|
|
5950
|
+
|
|
5951
|
+
/** What an author writes on `workflow({ … })`. */
|
|
5952
|
+
export declare interface WorkflowAccessOptions<Payload = unknown> {
|
|
5953
|
+
/**
|
|
5954
|
+
* WHO MAY START this workflow over the wire — the same vocabulary as a
|
|
5955
|
+
* mutation's `guards:`. A resource-scoped guard reads its id from the start
|
|
5956
|
+
* payload: `{ action: 'export', resourceType: 'team', resource: (p) => p.teamId }`.
|
|
5957
|
+
*
|
|
5958
|
+
* Checked ONCE, at the start call, before the run row is written or the
|
|
5959
|
+
* engine is asked for anything. The run itself then executes as the subject
|
|
5960
|
+
* that started it (`ctx.subject` inside the workflow) — the guard decides
|
|
5961
|
+
* whether it may begin, not what it may read; row filters do that.
|
|
5962
|
+
*/
|
|
5963
|
+
readonly guards?: Guards<Payload>;
|
|
5964
|
+
/**
|
|
5965
|
+
* Anyone who can open the socket may start this workflow — and WHY. A
|
|
5966
|
+
* reason, never a boolean; an empty string is refused at definition time.
|
|
5967
|
+
* Read by the boot gate and printed by `voltro doctor` beside the tag.
|
|
5968
|
+
*/
|
|
5969
|
+
readonly openAccess?: string;
|
|
5970
|
+
/**
|
|
5971
|
+
* No start rpc at all. The workflow is still discovered, registered with the
|
|
5972
|
+
* engine and startable from server code — `ctx.workflows.start`, a
|
|
5973
|
+
* `schedule:` block, an event trigger, a parent workflow — but the codegen
|
|
5974
|
+
* emits no client entry and neither boot path binds a handler for it.
|
|
5975
|
+
* Cannot be combined with `openAccess` (there is no wire to decide about).
|
|
5976
|
+
*/
|
|
5977
|
+
readonly internal?: boolean;
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
/** The non-enumerable property a `workflow()` definition carries its decision under. */
|
|
5981
|
+
export declare const WorkflowAccessProperty: unique symbol;
|
|
5982
|
+
|
|
5738
5983
|
export declare const workflowCancelDescriptor: ActionProcedureDescriptor<"__voltro.workflow.cancel", Schema.Struct<{
|
|
5739
5984
|
workflowName: typeof Schema.String;
|
|
5740
5985
|
executionId: typeof Schema.String;
|
|
@@ -5880,7 +6125,14 @@ export declare const WorkflowRunHandleSchema: Schema.Struct<{
|
|
|
5880
6125
|
* `skipped` — a `singleton: { mode: 'skip' }` key was held; `executionId`
|
|
5881
6126
|
* names the run that holds it.
|
|
5882
6127
|
*/
|
|
5883
|
-
|
|
6128
|
+
/**
|
|
6129
|
+
* `replayed` — the idempotency key was already SPENT: a run under it had
|
|
6130
|
+
* finished, so nothing started and the handle addresses that old run. It
|
|
6131
|
+
* used to come back `running` with the old run's id, indistinguishable from
|
|
6132
|
+
* a fresh start; a schedule keyed per day ran once and reported forty-seven
|
|
6133
|
+
* "starts" that were replays. See `workflow()`'s `idempotencyKey` doc.
|
|
6134
|
+
*/
|
|
6135
|
+
status: Schema.Literal<["running", "queued", "dropped", "skipped", "replayed"]>;
|
|
5884
6136
|
deferral: Schema.optional<Schema.Struct<{
|
|
5885
6137
|
/** `debounce` | `batch` | `throttle` | `concurrency` | `paused` |
|
|
5886
6138
|
* `rateLimit` | `singleton`. */
|
|
@@ -6094,6 +6346,71 @@ export declare interface WorkflowStepContext {
|
|
|
6094
6346
|
|
|
6095
6347
|
export declare type WorkflowStepInterceptor = <Success, ErrorE, R>(next: Effect.Effect<Success, ErrorE, R>, ctx: WorkflowStepContext) => Effect.Effect<Success, ErrorE, R>;
|
|
6096
6348
|
|
|
6349
|
+
/**
|
|
6350
|
+
* The fifth lifter — a `workflow()` definition to the unary START rpc it
|
|
6351
|
+
* answers to on the wire. Browser-safe, like the four procedure lifters, and
|
|
6352
|
+
* for the same reason: the generated client group and both boot paths call
|
|
6353
|
+
* THIS, so the three cannot disagree about the payload strictness or the error
|
|
6354
|
+
* union. It used to be spelled three times (a runtime helper for serve, an
|
|
6355
|
+
* inline `Rpc.make` in dev without `strictInput`, another inline one in the
|
|
6356
|
+
* codegen without the guard errors), and the three had already drifted.
|
|
6357
|
+
*
|
|
6358
|
+
* Reads the access decision off the definition: an enforced guard widens the
|
|
6359
|
+
* error union with `ScopeError` / `Unauthenticated`, exactly as it does for a
|
|
6360
|
+
* mutation, and labels the strict-input refusal as a guarded procedure's.
|
|
6361
|
+
*/
|
|
6362
|
+
export declare const workflowToRpc: (workflow: {
|
|
6363
|
+
readonly name: string;
|
|
6364
|
+
readonly payloadSchema: Schema.Schema.Any;
|
|
6365
|
+
readonly errorSchema: Schema.Schema.All;
|
|
6366
|
+
}, extraErrors?: ExtraErrors) => Rpc.Rpc<string, Schema.Schema.Any, Schema.Struct<{
|
|
6367
|
+
/** Stable handle callers pass to `useWorkflowRun(...)`. The durable execution
|
|
6368
|
+
* id for a started run; the pending-intent id for a queued one. */
|
|
6369
|
+
id: typeof Schema.String;
|
|
6370
|
+
/** Workflow tag, e.g. `notes.summarise`. */
|
|
6371
|
+
workflowName: typeof Schema.String;
|
|
6372
|
+
/**
|
|
6373
|
+
* Deterministic durable id derived from workflow name + idempotency key.
|
|
6374
|
+
*
|
|
6375
|
+
* NULL when flow control deferred, dropped, or skipped the start — because
|
|
6376
|
+
* there is no execution yet, and there may never be one. It used to be a
|
|
6377
|
+
* required string, and keeping it that way would have meant inventing a value:
|
|
6378
|
+
* either an empty string or the id the run WOULD have had. Both are polls that
|
|
6379
|
+
* return `status: 'unknown'` forever, which is the failure mode this whole
|
|
6380
|
+
* feature set exists to remove.
|
|
6381
|
+
*
|
|
6382
|
+
* For a `skipped` singleton it is the INCUMBENT's execution id — a real,
|
|
6383
|
+
* pollable run, which is the entire point of `mode: 'skip'`.
|
|
6384
|
+
*/
|
|
6385
|
+
executionId: Schema.NullOr<typeof Schema.String>;
|
|
6386
|
+
/**
|
|
6387
|
+
* `running` — the engine has it; starting is fire-and-return.
|
|
6388
|
+
* `queued` — flow control deferred it; see `deferral.dueAt`.
|
|
6389
|
+
* `dropped` — over a `rateLimit` cap. It will NOT run.
|
|
6390
|
+
* `skipped` — a `singleton: { mode: 'skip' }` key was held; `executionId`
|
|
6391
|
+
* names the run that holds it.
|
|
6392
|
+
*/
|
|
6393
|
+
/**
|
|
6394
|
+
* `replayed` — the idempotency key was already SPENT: a run under it had
|
|
6395
|
+
* finished, so nothing started and the handle addresses that old run. It
|
|
6396
|
+
* used to come back `running` with the old run's id, indistinguishable from
|
|
6397
|
+
* a fresh start; a schedule keyed per day ran once and reported forty-seven
|
|
6398
|
+
* "starts" that were replays. See `workflow()`'s `idempotencyKey` doc.
|
|
6399
|
+
*/
|
|
6400
|
+
status: Schema.Literal<["running", "queued", "dropped", "skipped", "replayed"]>;
|
|
6401
|
+
deferral: Schema.optional<Schema.Struct<{
|
|
6402
|
+
/** `debounce` | `batch` | `throttle` | `concurrency` | `paused` |
|
|
6403
|
+
* `rateLimit` | `singleton`. */
|
|
6404
|
+
mode: typeof Schema.String;
|
|
6405
|
+
/** Epoch ms this may first be admitted. Null for a drop or a skip. */
|
|
6406
|
+
dueAt: Schema.NullOr<typeof Schema.Number>;
|
|
6407
|
+
/** How long until the rate window reopens. Null unless dropped. */
|
|
6408
|
+
retryAfterMs: Schema.NullOr<typeof Schema.Number>;
|
|
6409
|
+
/** The `_voltro_workflow_pending` row holding this start. Null unless queued. */
|
|
6410
|
+
intentId: Schema.NullOr<typeof Schema.String>;
|
|
6411
|
+
}>>;
|
|
6412
|
+
}>, Schema.Schema.All, never>;
|
|
6413
|
+
|
|
6097
6414
|
export declare const workflowUpdateDescriptor: ActionProcedureDescriptor<"__voltro.workflow.update", Schema.Struct<{
|
|
6098
6415
|
id: typeof Schema.String;
|
|
6099
6416
|
updateName: typeof Schema.String;
|
|
@@ -6122,6 +6439,21 @@ export declare const WorkflowUpdateResultSchema: Schema.Struct<{
|
|
|
6122
6439
|
result: typeof Schema.Unknown;
|
|
6123
6440
|
}>;
|
|
6124
6441
|
|
|
6442
|
+
/**
|
|
6443
|
+
* The error union a WORKFLOW START puts on the wire — the workflow's own
|
|
6444
|
+
* `error:` plus what the framework can refuse the start with before the engine
|
|
6445
|
+
* is asked: the guard refusals (when it declares an enforced guard) and any
|
|
6446
|
+
* plugin error the `interceptWorkflow` chain may fail with. Read by the two
|
|
6447
|
+
* boot paths (the rpc group AND `bindMutation`'s declared set) and by
|
|
6448
|
+
* `workflowToRpc` for the generated client — one function, so a `ScopeError`
|
|
6449
|
+
* on a start decodes as the typed failure it is rather than crossing as the
|
|
6450
|
+
* `ExitEncoded` defect tree a mutation used to ship for the same reason.
|
|
6451
|
+
*/
|
|
6452
|
+
export declare const workflowWireErrorUnion: (workflow: {
|
|
6453
|
+
readonly errorSchema: Schema.Schema.All;
|
|
6454
|
+
readonly guards?: DeclaredAccess | undefined;
|
|
6455
|
+
}, extraErrors?: ExtraErrors) => Schema.Schema.All;
|
|
6456
|
+
|
|
6125
6457
|
/**
|
|
6126
6458
|
* Scope for a WS-rpc MUTATION. Unlike the REST scope (tenant + method + path),
|
|
6127
6459
|
* this includes the acting SUBJECT id: a WS app is often single-tenant with many
|