@voltro/protocol 0.63.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -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';
@@ -88,15 +89,15 @@ export declare interface AnyApprovalPolicy {
88
89
  readonly reason?: string;
89
90
  }
90
91
 
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;
92
+ /** A guard entry is a scope check, a relationship check, an identity check, or
93
+ * a declared no-check-needed decision. */
94
+ export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec | AuthenticatedCheckSpec | OpenAccessSpec;
94
95
 
95
96
  /** Any declared event, with the generics erased — for registries and audits. */
96
97
  export declare type AnyEventDescriptor = EventDescriptor<string, Schema.Schema.Any, Schema.Schema.Any>;
97
98
 
98
- /** A guard is either a scope check or a relationship check. */
99
- export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
99
+ /** A guard is a scope check, a relationship check, or an identity check. */
100
+ export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input> | AuthenticatedGuardSpec;
100
101
 
101
102
  export declare const APIKEY_ISSUE_ORG_SCOPE = "apikeys:issue:org";
102
103
 
@@ -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`. */
@@ -455,6 +460,36 @@ export declare interface AuthCallbackRouter {
455
460
  readonly post: (path: string, handler: AuthRouteHandler) => void;
456
461
  }
457
462
 
463
+ /**
464
+ * The runtime-erased form of `AuthenticatedGuardSpec` — "somebody must be
465
+ * there, and nothing narrower". See that type for why the vocabulary needed it.
466
+ */
467
+ export declare interface AuthenticatedCheckSpec {
468
+ readonly authenticated: true;
469
+ }
470
+
471
+ /**
472
+ * "The caller must have an identity" — and nothing narrower.
473
+ *
474
+ * The third case the vocabulary was missing, and the gap was not theoretical.
475
+ * An app whose authority lives per-team in its executors holds NO raw scopes on
476
+ * the identity, so `{ scope }` refuses every real user; a relationship guard
477
+ * cannot help when the resource does not exist yet (the first message of a new
478
+ * thread); and `openAccess` would be a lie whenever the auth fallback is a
479
+ * deliberate anonymous subject. The one thing left to decide — is anybody
480
+ * there — had no spelling, so apps wrote a resource policy whose entire content
481
+ * was "you are logged in".
482
+ *
483
+ * `checkGuards` has always distinguished this internally: its return type is
484
+ * `ScopeError | Unauthenticated | null`. This makes the distinction sayable.
485
+ *
486
+ * It is a real DECISION, so it satisfies `security.defaultDeny` — unlike
487
+ * `openAccess`, it also enforces something.
488
+ */
489
+ export declare interface AuthenticatedGuardSpec {
490
+ readonly authenticated: true;
491
+ }
492
+
458
493
  /**
459
494
  * The framework's per-request auth middleware. Attach to an RpcGroup with
460
495
  * `group.middleware(AuthMiddleware)` to require every call to flow through
@@ -587,6 +622,55 @@ export declare interface AuthStrategyWithCallback extends AuthStrategy {
587
622
  readonly mountRoutes: (router: AuthCallbackRouter) => void;
588
623
  }
589
624
 
625
+ export declare type AuthSubjectGuard = (subject: Subject, ctx: AuthSubjectGuardContext) => Promise<AuthSubjectGuardVerdict> | AuthSubjectGuardVerdict;
626
+
627
+ export declare interface AuthSubjectGuardContext {
628
+ /** `'request'` for a live call, `'workflow'` for a durable run being executed. */
629
+ readonly origin: 'request' | 'workflow';
630
+ /** The request's headers. EMPTY for `origin: 'workflow'`. */
631
+ readonly headers: Readonly<Record<string, string | undefined>>;
632
+ /** `undefined` for `origin: 'workflow'` and for transports with no connection. */
633
+ readonly clientId: number | undefined;
634
+ /** The app's boot store (see `AuthStrategyInput.store`), or `undefined` while
635
+ * it is still being built — a guard that needs it then fails CLOSED. */
636
+ readonly store: DataStore | undefined;
637
+ }
638
+
639
+ /**
640
+ * A post-authentication VETO on a resolved subject — the framework-level form
641
+ * of `@voltro/plugin-auth`'s `subjectGuards`, and the reason it exists here too.
642
+ *
643
+ * The auth plugin runs its guards once, at LOGIN, before a session is issued —
644
+ * the right place when every session comes from its routes. It is the wrong
645
+ * place for an app whose subjects arrive through its own strategy chain (a JWKS
646
+ * IdP, an API-key table, a session it mints itself): those never pass through
647
+ * a login handler, so a `deactivationGuard()` wired onto the auth plugin vetoed
648
+ * nobody, and `users.deactivate` locked no one out. The guard has to sit where
649
+ * EVERY subject passes — after a strategy matches and authority is resolved,
650
+ * on the request path and on a durable workflow's resume alike.
651
+ *
652
+ * A guard receives the subject WITH its resolved scopes (so it may reason about
653
+ * them) and the same context `resolveScopes` gets: `origin` (a durable run has
654
+ * no request behind it), the headers, and the app's store for the row it needs
655
+ * to read. It returns a verdict; the first rejection wins and the request fails
656
+ * closed with `Unauthenticated` carrying the guard's `code` and `message`. It
657
+ * runs only for a MATCHED subject — an anonymous caller has no identity to veto.
658
+ *
659
+ * It is cached WITH the scope decision, under the same key and window: a veto
660
+ * is a fact about an identity, exactly like its roles, and a guard that reads a
661
+ * row per request would otherwise cost what `scopeCache` exists to avoid. The
662
+ * staleness is therefore the one number the docs already give (30s), and
663
+ * `invalidate(scopeCacheKey(subject))` from the mutation that deactivates makes
664
+ * it zero on that process — the same handle, the same call.
665
+ */
666
+ export declare type AuthSubjectGuardVerdict = {
667
+ readonly ok: true;
668
+ } | {
669
+ readonly ok: false;
670
+ readonly code: string;
671
+ readonly message: string;
672
+ };
673
+
590
674
  /**
591
675
  * Decide what to do for an incoming (scope, key) in ONE atomic store call. On
592
676
  * `fresh` the caller runs the handler then calls `finish`/`fail`; on
@@ -821,6 +905,13 @@ export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrat
821
905
  removed: ReadonlyArray<string>;
822
906
  granted: ReadonlyArray<string>;
823
907
  }) => void;
908
+ /**
909
+ * Post-authentication vetoes on a MATCHED subject — see `AuthSubjectGuard`.
910
+ * Run after `resolveScopes` (so a guard sees the applied scopes), inside the
911
+ * same cache entry, first rejection wins → `Unauthenticated` with the
912
+ * guard's `code` and `message`.
913
+ */
914
+ readonly subjectGuards?: ReadonlyArray<AuthSubjectGuard>;
824
915
  /**
825
916
  * Hands every strategy the app's store on `input.store`.
826
917
  *
@@ -2132,6 +2223,15 @@ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefin
2132
2223
  /** The currently-registered resource-scope resolver, or `undefined`. */
2133
2224
  export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
2134
2225
 
2226
+ /**
2227
+ * Read a definition's decision back. A value that never went through
2228
+ * `workflow()` — a bare `Workflow.make`, a test double — reports UNDECIDED and
2229
+ * not internal, so the boot gate judges it exactly like a definer call that
2230
+ * declared nothing: that is the fail-closed reading, and the only one under
2231
+ * which forgetting the definer cannot open a door.
2232
+ */
2233
+ export declare const getWorkflowAccess: (definition: unknown) => WorkflowAccess;
2234
+
2135
2235
  /** Ask for default-deny semantics on one `checkGuards` call. */
2136
2236
  export declare interface GuardCheckOptions {
2137
2237
  /** Refuse a procedure that declares no access decision at all. */
@@ -2295,6 +2395,9 @@ export declare interface HttpRequestContext {
2295
2395
  */
2296
2396
  export declare type HttpRequestInterceptor = (next: () => Promise<HttpInterceptResponse | null>, ctx: HttpRequestContext) => Promise<HttpInterceptResponse | null>;
2297
2397
 
2398
+ /** Fire-and-forget observer of plugin HTTP route calls — see {@link PluginHttpRouteEvent}. */
2399
+ export declare type HttpRouteObserver = (event: PluginHttpRouteEvent) => void;
2400
+
2298
2401
  export declare type IdempotencyOutcome = {
2299
2402
  readonly kind: 'fresh';
2300
2403
  } | {
@@ -2415,6 +2518,12 @@ export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Rec
2415
2518
  readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
2416
2519
  }
2417
2520
 
2521
+ /** Runtime narrowing for the identity variant. */
2522
+ export declare const isAuthenticatedCheck: (g: AnyCheckSpec) => g is AuthenticatedCheckSpec;
2523
+
2524
+ /** Narrow a guard entry to the identity variant. */
2525
+ export declare const isAuthenticatedGuard: <I>(g: AnyGuardSpec<I>) => g is AuthenticatedGuardSpec;
2526
+
2418
2527
  export declare const isEventDescriptor: (value: unknown) => value is AnyEventDescriptor;
2419
2528
 
2420
2529
  /** True when every row in the set carries an `id`. The dispatcher uses
@@ -2503,6 +2612,24 @@ export declare const memoryIdempotencyStore: () => IdempotencyStore;
2503
2612
  * and the call-time path cannot describe the same defect differently. */
2504
2613
  export declare const missingAccessDecision: (procedure?: string) => ScopeError;
2505
2614
 
2615
+ /** The wait a conflicting replay is told to take. Long enough that a handler
2616
+ * in its last second finishes; short enough that a 10-second replay window
2617
+ * fits several tries. */
2618
+ export declare const MUTATION_IN_FLIGHT_RETRY_AFTER_MS = 1000;
2619
+
2620
+ export declare class MutationInFlight extends MutationInFlight_base {
2621
+ get message(): string;
2622
+ }
2623
+
2624
+ declare const MutationInFlight_base: Schema.TaggedErrorClass<MutationInFlight, "MutationInFlight", {
2625
+ readonly _tag: Schema.tag<"MutationInFlight">;
2626
+ } & {
2627
+ traceId: typeof Schema.String;
2628
+ /** How long to wait before replaying the same key — the first call is still
2629
+ * running; a replay before it finishes conflicts again. */
2630
+ retryAfterMs: typeof Schema.Number;
2631
+ }>;
2632
+
2506
2633
  export declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
2507
2634
  readonly kind: 'mutation';
2508
2635
  readonly name: Name;
@@ -2592,6 +2719,34 @@ export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => C
2592
2719
  */
2593
2720
  export declare const normalizeSource: (source: ReactivitySource | ReadonlyArray<ReactivitySource> | undefined) => string | ReadonlyArray<string> | undefined;
2594
2721
 
2722
+ /**
2723
+ * `.one()` matched a number of rows other than exactly one.
2724
+ *
2725
+ * `found` distinguishes the two failure modes without a second error type:
2726
+ * `0` is "the row you required is missing", `2` is "your filter is not as
2727
+ * unique as you assumed" (the terminal probes with LIMIT 2, so `2` means
2728
+ * "at least two" — it never counts the whole set just to report a number).
2729
+ *
2730
+ * It lives HERE, not in `@voltro/runtime`, and the reason is a promise the
2731
+ * framework was already making. The agent guide says "`NoRowFound` is
2732
+ * declarable in the descriptor's `error:` union" — and a descriptor is loaded
2733
+ * by the BROWSER, while `@voltro/runtime` is on the browser-safety guard's
2734
+ * server-only list. So following that sentence produced a refused boot. The
2735
+ * class is browser-safe by construction (a `Schema.TaggedError` over two
2736
+ * scalars), so the package it sat in was the only thing making the guidance
2737
+ * false. `@voltro/runtime` re-exports it, so nothing that imported it moves.
2738
+ */
2739
+ export declare class NoRowFound extends NoRowFound_base {
2740
+ get message(): string;
2741
+ }
2742
+
2743
+ declare const NoRowFound_base: Schema.TaggedErrorClass<NoRowFound, "NoRowFound", {
2744
+ readonly _tag: Schema.tag<"NoRowFound">;
2745
+ } & {
2746
+ table: typeof Schema.String;
2747
+ found: typeof Schema.Number;
2748
+ }>;
2749
+
2595
2750
  /**
2596
2751
  * What a plugin contributes to the OTel layer via `contributeObservability`.
2597
2752
  * The OTel types are intentionally `unknown` so this browser-safe protocol
@@ -2704,20 +2859,6 @@ export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect
2704
2859
  */
2705
2860
  export declare const pluginAlias: (pluginName: string) => string;
2706
2861
 
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
2862
  export declare interface PluginBindContext {
2722
2863
  /**
2723
2864
  * This process's replica identity — the same value the event bus stamps as
@@ -2754,6 +2895,20 @@ export declare interface PluginBindContext {
2754
2895
  readonly publish: (channel: string, payload: string) => unknown;
2755
2896
  readonly subscribe: (channel: string, handler: (payload: string) => void) => unknown;
2756
2897
  };
2898
+ /**
2899
+ * The framework's cache — the SAME backend `ctx.cache` and query snapshots
2900
+ * use (`CACHE_BACKEND`: in-process memory, or Redis shared across replicas).
2901
+ * A plugin that memoises reference data (a board's configuration, a field
2902
+ * catalogue) reads and writes it here instead of asking the app for a
2903
+ * hand-built store object; the keys a plugin writes are its own to namespace.
2904
+ *
2905
+ * A handle rather than a value: `voltro dev` builds the cache after plugins
2906
+ * are bound and `voltro serve` before, so the two boot paths hand over the
2907
+ * same object whose methods resolve the live cache at call time. Before it
2908
+ * exists a read misses and a write is dropped — best-effort, never a boot
2909
+ * ordering a plugin has to know about.
2910
+ */
2911
+ readonly cache?: PluginCacheHandle;
2757
2912
  /**
2758
2913
  * The framework's already-open `SqlClient` (from `@effect/sql`) — the
2759
2914
  * SAME pool the app's store uses. A plugin runs raw SQL through this
@@ -2805,6 +2960,28 @@ export declare interface PluginBindContext {
2805
2960
  readonly claimChange: (scope: string, key: string) => Promise<boolean>;
2806
2961
  }
2807
2962
 
2963
+ /**
2964
+ * Server-only context delivered to `bindDataStore`, ALONGSIDE the store,
2965
+ * AFTER the framework has opened its store + pool. Gives a plugin the
2966
+ * framework's ALREADY-OPEN handles so it never rebuilds a pg pool from
2967
+ * raw env (the anti-pattern `@voltro/plugin-ratelimit` used to hand-roll)
2968
+ * and never runs an un-coordinated per-replica `setInterval` sweep (what
2969
+ * presence + governance used to do).
2970
+ *
2971
+ * These fields are SERVER-only — they are TYPES only in this browser-safe
2972
+ * protocol package (the `DataStore` / `SqlClient` imports are erased), and
2973
+ * the live values exist only inside the serve pipeline that calls
2974
+ * `bindDataStore`. Nothing in the browser-loaded descriptor/schema graph
2975
+ * touches them.
2976
+ */
2977
+ /** The slice of the framework cache handed to a plugin at bind time. */
2978
+ export declare interface PluginCacheHandle {
2979
+ readonly get: (key: string) => Effect.Effect<Option.Option<unknown>, unknown>;
2980
+ readonly set: (key: string, value: unknown, options?: {
2981
+ readonly ttlMs?: number;
2982
+ }) => Effect.Effect<void, unknown>;
2983
+ }
2984
+
2808
2985
  /**
2809
2986
  * Post-commit change event delivered to a plugin's `onChangeEvent` tap.
2810
2987
  * Minimal, browser-safe shape (protocol must not depend on the database
@@ -3113,6 +3290,25 @@ export declare interface PluginHttpRouteByteStream {
3113
3290
  readonly contentDisposition?: string;
3114
3291
  }
3115
3292
 
3293
+ /**
3294
+ * One plugin HTTP route call, after it answered — what an audit trail records.
3295
+ *
3296
+ * `onHttpRequest` runs BEFORE auth and before routing and sees no status, so a
3297
+ * login, a logout or an avatar fetch served by a plugin's `httpRoutes` left no
3298
+ * trace in a trail that recorded every rpc. This is the other end of the same
3299
+ * pipeline: the method, the path, the status and how long it took, once the
3300
+ * route has answered. No subject — a plugin HTTP route is not part of the rpc
3301
+ * pipeline and nothing upstream authenticated its caller; a route that resolves
3302
+ * one itself is the only party that knows, and can put it in its own log.
3303
+ */
3304
+ export declare interface PluginHttpRouteEvent {
3305
+ readonly method: string;
3306
+ /** Path WITHOUT query string. */
3307
+ readonly path: string;
3308
+ readonly status: number;
3309
+ readonly durationMs: number;
3310
+ }
3311
+
3116
3312
  export declare interface PluginHttpRouteRequest {
3117
3313
  readonly method: string;
3118
3314
  /** Path WITHOUT query string. */
@@ -3220,7 +3416,10 @@ export declare interface PluginHttpRouteResult {
3220
3416
  readonly status: number;
3221
3417
  readonly body?: string | Uint8Array;
3222
3418
  readonly contentType?: string;
3223
- readonly headers?: Record<string, string>;
3419
+ /** Response headers. A header that repeats — two `Set-Cookie` lines, a
3420
+ * session and a CSRF cookie from one login — is an ARRAY; `set-cookie`
3421
+ * arrays become separate lines, every other array joins with `, `. */
3422
+ readonly headers?: Record<string, string | ReadonlyArray<string>>;
3224
3423
  /** Stream the response (SSE) instead of sending `body`. See
3225
3424
  * {@link PluginHttpRouteStream}. */
3226
3425
  readonly stream?: PluginHttpRouteStream;
@@ -3444,7 +3643,7 @@ export declare type PluginMigrationSqlClient = SqlClient.SqlClient;
3444
3643
  * boot log + `/_voltro/inspect/plugins` so operators audit what each
3445
3644
  * plugin asks for before installing.
3446
3645
  */
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}`;
3646
+ 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
3647
 
3449
3648
  /**
3450
3649
  * Per-call context handed to a plugin-contributed rpc route's
@@ -3458,6 +3657,15 @@ export declare interface PluginRouteContext {
3458
3657
  };
3459
3658
  }
3460
3659
 
3660
+ /**
3661
+ * The kinds a plugin can CONTRIBUTE a route as. A plugin declares procedures,
3662
+ * never workflows (a workflow is a `*.workflow.tsx` pair the app authors and the
3663
+ * engine runs), so this is the interceptor union minus `'workflow'` — kept as
3664
+ * its own name so a route's `kind` cannot select a runtime path that does not
3665
+ * exist for it.
3666
+ */
3667
+ export declare type PluginRouteKind = 'mutation' | 'query' | 'action';
3668
+
3461
3669
  /**
3462
3670
  * Client-facing declaration of a plugin RPC route's descriptor, so the codegen
3463
3671
  * can emit it into the generated client rpc group (`appGroup` + `appDescriptors`)
@@ -3474,7 +3682,7 @@ export declare interface PluginRouteContext {
3474
3682
  */
3475
3683
  export declare interface PluginRpcClientDescriptor {
3476
3684
  readonly tag: string;
3477
- readonly kind: RpcKind;
3685
+ readonly kind: PluginRouteKind;
3478
3686
  readonly import: {
3479
3687
  readonly module: string;
3480
3688
  readonly name: string;
@@ -3497,7 +3705,7 @@ export declare interface PluginRpcClientDescriptor {
3497
3705
  */
3498
3706
  export declare interface PluginRpcRoute {
3499
3707
  /** 'mutation' | 'query' | 'action' — determines the runtime path. */
3500
- readonly kind: RpcKind;
3708
+ readonly kind: PluginRouteKind;
3501
3709
  /** RPC tag relative to the plugin (e.g. `'list'`, `'admin.events'`). */
3502
3710
  readonly name: string;
3503
3711
  /** Optional human-readable description; surfaced in the dashboard. */
@@ -4042,6 +4250,14 @@ export declare const requireScope: (subject: Subject, scope: string) => Effect.E
4042
4250
  /** Drop every registration. Tests only — production registers once at module load. */
4043
4251
  export declare const resetPluginAliases: () => void;
4044
4252
 
4253
+ /**
4254
+ * Validate an author's access options and normalise them into the array every
4255
+ * enforcement path reads. Throws at DEFINITION time — module load, which is
4256
+ * boot — with the same wording the event and procedure definers use, so the
4257
+ * three refusals read as one rule.
4258
+ */
4259
+ export declare const resolveWorkflowAccess: (name: string, options: WorkflowAccessOptions<never>) => WorkflowAccess;
4260
+
4045
4261
  /** The question the runtime asks a registered resource-scope resolver. */
4046
4262
  export declare interface ResourceScopeRequest {
4047
4263
  /** The authenticated caller. */
@@ -4219,7 +4435,7 @@ export declare type RpcInterceptor = (next: Effect.Effect<unknown, unknown>, con
4219
4435
  export declare interface RpcInterceptorContext {
4220
4436
  /** Procedure tag, e.g. `todos.create`. */
4221
4437
  readonly tag: string;
4222
- /** Discriminator — `'mutation' | 'query' | 'action'`. */
4438
+ /** Discriminator — `'mutation' | 'query' | 'action' | 'workflow'`. */
4223
4439
  readonly kind: RpcKind;
4224
4440
  /** The validated input the executor is about to (or already did) receive. */
4225
4441
  readonly input: unknown;
@@ -4236,10 +4452,11 @@ export declare interface RpcInterceptorContext {
4236
4452
  /**
4237
4453
  * Which kind of rpc the interceptor is wrapping. The runtime passes
4238
4454
  * `kind: 'mutation'` to interceptors registered as `interceptMutation`,
4239
- * `kind: 'query'` to `interceptQuery`, and `kind: 'action'` to
4240
- * `interceptAction` — one plugin can implement any combination.
4455
+ * `kind: 'query'` to `interceptQuery`, `kind: 'action'` to
4456
+ * `interceptAction`, and `kind: 'workflow'` to `interceptWorkflow` — one
4457
+ * plugin can implement any combination.
4241
4458
  */
4242
- export declare type RpcKind = 'mutation' | 'query' | 'action';
4459
+ export declare type RpcKind = 'mutation' | 'query' | 'action' | 'workflow';
4243
4460
 
4244
4461
  /**
4245
4462
  * Per-firing context passed to schedule-fire interceptors. Shape
@@ -4351,6 +4568,19 @@ export declare type ScopeDecision =
4351
4568
  | {
4352
4569
  readonly kind: 'unavailable';
4353
4570
  readonly reason: string;
4571
+ }
4572
+ /**
4573
+ * A subject guard REFUSED this identity (see `AuthSubjectGuard`). Not a
4574
+ * resolver's answer — a resolver returns one of the three above — but the
4575
+ * composer's, produced after the guards ran, and cached like a grant: a veto
4576
+ * is a durable fact about the identity until whatever changed it invalidates
4577
+ * the key. `applyScopeDecision` THROWS on it, so a caller that forgot the
4578
+ * explicit branch fails closed rather than applying "no scopes" and carrying on.
4579
+ */
4580
+ | {
4581
+ readonly kind: 'vetoed';
4582
+ readonly code: string;
4583
+ readonly message: string;
4354
4584
  };
4355
4585
 
4356
4586
  export declare class ScopeError extends ScopeError_base {
@@ -4466,7 +4696,12 @@ declare const StoreOperationFailed_base: Schema.TaggedErrorClass<StoreOperationF
4466
4696
  operation: typeof Schema.String;
4467
4697
  /** The table the op targeted. */
4468
4698
  table: typeof Schema.String;
4469
- /** `String(originalError)` — round-trip-safe diagnostic form. */
4699
+ /**
4700
+ * A BOUNDED description: the constraint kind and its constraint/column
4701
+ * NAME, or the driver's code. Never the driver's sentence and never a
4702
+ * stack — `wireSafeCause` builds it, and the reasoning is there. The full
4703
+ * detail is logged server-side instead.
4704
+ */
4470
4705
  cause: typeof Schema.String;
4471
4706
  }>;
4472
4707
 
@@ -5380,6 +5615,22 @@ export declare interface VoltroPlugin {
5380
5615
  * tenant-scoped IO quotas).
5381
5616
  */
5382
5617
  readonly interceptAction?: RpcInterceptor;
5618
+ /**
5619
+ * Wraps every WORKFLOW START call — the unary rpc a `*.workflow.tsx`
5620
+ * becomes on the wire. Same composition semantics as `interceptMutation`.
5621
+ *
5622
+ * A start is a write-class call: it records a run row, it may cost money
5623
+ * (a workflow that calls a model or a paid API), and it runs as the caller's
5624
+ * subject. So the plugins that wrap mutations — audit, rate limiting,
5625
+ * entitlement, rbac's scope publication — want to wrap this too, and before
5626
+ * this hook existed none of them did: the start handler was bound outside
5627
+ * every interceptor chain, so an audit trail that covered every mutation
5628
+ * recorded no workflow start, and a `guards:` on a workflow could not see
5629
+ * the scopes rbac publishes inside the chain. Declaring `interceptAction`
5630
+ * without this hook leaves that gap open for your plugin; the first-party
5631
+ * plugins carry a parity test that fails when one does.
5632
+ */
5633
+ readonly interceptWorkflow?: RpcInterceptor;
5383
5634
  /**
5384
5635
  * Effect Layer that contributes services into the per-request
5385
5636
  * context. Anything declared here is yieldable from any handler in
@@ -5515,6 +5766,14 @@ export declare interface VoltroPlugin {
5515
5766
  * the hook is declared without the matching permission.
5516
5767
  */
5517
5768
  readonly onHttpRequest?: HttpRequestInterceptor;
5769
+ /**
5770
+ * Post-response observer of every plugin HTTP route call — method, path,
5771
+ * status, duration. The audit plugin's way of recording a login or an avatar
5772
+ * fetch, which no rpc interceptor ever sees. Fire-and-forget: it cannot
5773
+ * change the response and a throw is swallowed. Same `'http:intercept'`
5774
+ * permission as `onHttpRequest` — it is the same pipeline, the other end.
5775
+ */
5776
+ readonly onHttpRoute?: HttpRouteObserver;
5518
5777
  /**
5519
5778
  * Dashboard mount contributions — remote-loaded ESM modules the
5520
5779
  * dashboard host renders alongside framework pages. See
@@ -5735,6 +5994,61 @@ export declare const withRpcTag: <D extends {
5735
5994
  readonly name: T;
5736
5995
  };
5737
5996
 
5997
+ /** The resolved decision the runtime reads. */
5998
+ export declare interface WorkflowAccess {
5999
+ /** The author's guards, or the erased `openAccess` entry — never both. */
6000
+ readonly guards: DeclaredAccess<unknown> | undefined;
6001
+ readonly openAccess: string | undefined;
6002
+ readonly internal: boolean;
6003
+ }
6004
+
6005
+ /**
6006
+ * The definition as the boot gate, the codegen and the dispatch spine read it:
6007
+ * the structural `{ name, internal, guards, openAccess }` every other kind
6008
+ * presents, so `isWireReachable` and `hasAccessDecision` apply unchanged and a
6009
+ * workflow rides the same walk as the five procedure kinds.
6010
+ */
6011
+ export declare const workflowAccessDescriptor: (definition: {
6012
+ readonly name: string;
6013
+ }) => {
6014
+ readonly name: string;
6015
+ readonly internal: boolean;
6016
+ readonly guards: DeclaredAccess<unknown> | undefined;
6017
+ readonly openAccess: string | undefined;
6018
+ };
6019
+
6020
+ /** What an author writes on `workflow({ … })`. */
6021
+ export declare interface WorkflowAccessOptions<Payload = unknown> {
6022
+ /**
6023
+ * WHO MAY START this workflow over the wire — the same vocabulary as a
6024
+ * mutation's `guards:`. A resource-scoped guard reads its id from the start
6025
+ * payload: `{ action: 'export', resourceType: 'team', resource: (p) => p.teamId }`.
6026
+ *
6027
+ * Checked ONCE, at the start call, before the run row is written or the
6028
+ * engine is asked for anything. The run itself then executes as the subject
6029
+ * that started it (`ctx.subject` inside the workflow) — the guard decides
6030
+ * whether it may begin, not what it may read; row filters do that.
6031
+ */
6032
+ readonly guards?: Guards<Payload>;
6033
+ /**
6034
+ * Anyone who can open the socket may start this workflow — and WHY. A
6035
+ * reason, never a boolean; an empty string is refused at definition time.
6036
+ * Read by the boot gate and printed by `voltro doctor` beside the tag.
6037
+ */
6038
+ readonly openAccess?: string;
6039
+ /**
6040
+ * No start rpc at all. The workflow is still discovered, registered with the
6041
+ * engine and startable from server code — `ctx.workflows.start`, a
6042
+ * `schedule:` block, an event trigger, a parent workflow — but the codegen
6043
+ * emits no client entry and neither boot path binds a handler for it.
6044
+ * Cannot be combined with `openAccess` (there is no wire to decide about).
6045
+ */
6046
+ readonly internal?: boolean;
6047
+ }
6048
+
6049
+ /** The non-enumerable property a `workflow()` definition carries its decision under. */
6050
+ export declare const WorkflowAccessProperty: unique symbol;
6051
+
5738
6052
  export declare const workflowCancelDescriptor: ActionProcedureDescriptor<"__voltro.workflow.cancel", Schema.Struct<{
5739
6053
  workflowName: typeof Schema.String;
5740
6054
  executionId: typeof Schema.String;
@@ -5880,7 +6194,14 @@ export declare const WorkflowRunHandleSchema: Schema.Struct<{
5880
6194
  * `skipped` — a `singleton: { mode: 'skip' }` key was held; `executionId`
5881
6195
  * names the run that holds it.
5882
6196
  */
5883
- status: Schema.Literal<["running", "queued", "dropped", "skipped"]>;
6197
+ /**
6198
+ * `replayed` — the idempotency key was already SPENT: a run under it had
6199
+ * finished, so nothing started and the handle addresses that old run. It
6200
+ * used to come back `running` with the old run's id, indistinguishable from
6201
+ * a fresh start; a schedule keyed per day ran once and reported forty-seven
6202
+ * "starts" that were replays. See `workflow()`'s `idempotencyKey` doc.
6203
+ */
6204
+ status: Schema.Literal<["running", "queued", "dropped", "skipped", "replayed"]>;
5884
6205
  deferral: Schema.optional<Schema.Struct<{
5885
6206
  /** `debounce` | `batch` | `throttle` | `concurrency` | `paused` |
5886
6207
  * `rateLimit` | `singleton`. */
@@ -6094,6 +6415,71 @@ export declare interface WorkflowStepContext {
6094
6415
 
6095
6416
  export declare type WorkflowStepInterceptor = <Success, ErrorE, R>(next: Effect.Effect<Success, ErrorE, R>, ctx: WorkflowStepContext) => Effect.Effect<Success, ErrorE, R>;
6096
6417
 
6418
+ /**
6419
+ * The fifth lifter — a `workflow()` definition to the unary START rpc it
6420
+ * answers to on the wire. Browser-safe, like the four procedure lifters, and
6421
+ * for the same reason: the generated client group and both boot paths call
6422
+ * THIS, so the three cannot disagree about the payload strictness or the error
6423
+ * union. It used to be spelled three times (a runtime helper for serve, an
6424
+ * inline `Rpc.make` in dev without `strictInput`, another inline one in the
6425
+ * codegen without the guard errors), and the three had already drifted.
6426
+ *
6427
+ * Reads the access decision off the definition: an enforced guard widens the
6428
+ * error union with `ScopeError` / `Unauthenticated`, exactly as it does for a
6429
+ * mutation, and labels the strict-input refusal as a guarded procedure's.
6430
+ */
6431
+ export declare const workflowToRpc: (workflow: {
6432
+ readonly name: string;
6433
+ readonly payloadSchema: Schema.Schema.Any;
6434
+ readonly errorSchema: Schema.Schema.All;
6435
+ }, extraErrors?: ExtraErrors) => Rpc.Rpc<string, Schema.Schema.Any, Schema.Struct<{
6436
+ /** Stable handle callers pass to `useWorkflowRun(...)`. The durable execution
6437
+ * id for a started run; the pending-intent id for a queued one. */
6438
+ id: typeof Schema.String;
6439
+ /** Workflow tag, e.g. `notes.summarise`. */
6440
+ workflowName: typeof Schema.String;
6441
+ /**
6442
+ * Deterministic durable id derived from workflow name + idempotency key.
6443
+ *
6444
+ * NULL when flow control deferred, dropped, or skipped the start — because
6445
+ * there is no execution yet, and there may never be one. It used to be a
6446
+ * required string, and keeping it that way would have meant inventing a value:
6447
+ * either an empty string or the id the run WOULD have had. Both are polls that
6448
+ * return `status: 'unknown'` forever, which is the failure mode this whole
6449
+ * feature set exists to remove.
6450
+ *
6451
+ * For a `skipped` singleton it is the INCUMBENT's execution id — a real,
6452
+ * pollable run, which is the entire point of `mode: 'skip'`.
6453
+ */
6454
+ executionId: Schema.NullOr<typeof Schema.String>;
6455
+ /**
6456
+ * `running` — the engine has it; starting is fire-and-return.
6457
+ * `queued` — flow control deferred it; see `deferral.dueAt`.
6458
+ * `dropped` — over a `rateLimit` cap. It will NOT run.
6459
+ * `skipped` — a `singleton: { mode: 'skip' }` key was held; `executionId`
6460
+ * names the run that holds it.
6461
+ */
6462
+ /**
6463
+ * `replayed` — the idempotency key was already SPENT: a run under it had
6464
+ * finished, so nothing started and the handle addresses that old run. It
6465
+ * used to come back `running` with the old run's id, indistinguishable from
6466
+ * a fresh start; a schedule keyed per day ran once and reported forty-seven
6467
+ * "starts" that were replays. See `workflow()`'s `idempotencyKey` doc.
6468
+ */
6469
+ status: Schema.Literal<["running", "queued", "dropped", "skipped", "replayed"]>;
6470
+ deferral: Schema.optional<Schema.Struct<{
6471
+ /** `debounce` | `batch` | `throttle` | `concurrency` | `paused` |
6472
+ * `rateLimit` | `singleton`. */
6473
+ mode: typeof Schema.String;
6474
+ /** Epoch ms this may first be admitted. Null for a drop or a skip. */
6475
+ dueAt: Schema.NullOr<typeof Schema.Number>;
6476
+ /** How long until the rate window reopens. Null unless dropped. */
6477
+ retryAfterMs: Schema.NullOr<typeof Schema.Number>;
6478
+ /** The `_voltro_workflow_pending` row holding this start. Null unless queued. */
6479
+ intentId: Schema.NullOr<typeof Schema.String>;
6480
+ }>>;
6481
+ }>, Schema.Schema.All, never>;
6482
+
6097
6483
  export declare const workflowUpdateDescriptor: ActionProcedureDescriptor<"__voltro.workflow.update", Schema.Struct<{
6098
6484
  id: typeof Schema.String;
6099
6485
  updateName: typeof Schema.String;
@@ -6122,6 +6508,21 @@ export declare const WorkflowUpdateResultSchema: Schema.Struct<{
6122
6508
  result: typeof Schema.Unknown;
6123
6509
  }>;
6124
6510
 
6511
+ /**
6512
+ * The error union a WORKFLOW START puts on the wire — the workflow's own
6513
+ * `error:` plus what the framework can refuse the start with before the engine
6514
+ * is asked: the guard refusals (when it declares an enforced guard) and any
6515
+ * plugin error the `interceptWorkflow` chain may fail with. Read by the two
6516
+ * boot paths (the rpc group AND `bindMutation`'s declared set) and by
6517
+ * `workflowToRpc` for the generated client — one function, so a `ScopeError`
6518
+ * on a start decodes as the typed failure it is rather than crossing as the
6519
+ * `ExitEncoded` defect tree a mutation used to ship for the same reason.
6520
+ */
6521
+ export declare const workflowWireErrorUnion: (workflow: {
6522
+ readonly errorSchema: Schema.Schema.All;
6523
+ readonly guards?: DeclaredAccess | undefined;
6524
+ }, extraErrors?: ExtraErrors) => Schema.Schema.All;
6525
+
6125
6526
  /**
6126
6527
  * Scope for a WS-rpc MUTATION. Unlike the REST scope (tenant + method + path),
6127
6528
  * this includes the acting SUBJECT id: a WS app is often single-tenant with many