@voltro/protocol 0.62.3 → 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/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
- readonly headers?: Record<string, string>;
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: RpcKind;
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: RpcKind;
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`, and `kind: 'action'` to
4240
- * `interceptAction` — one plugin can implement any combination.
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
- status: Schema.Literal<["running", "queued", "dropped", "skipped"]>;
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