import { Context } from 'effect'; import { DataStore } from '@voltro/database'; import { Effect } from 'effect'; import { Layer } from 'effect'; import { Rpc } from '@effect/rpc'; import { RpcMiddleware } from '@effect/rpc'; import { Schema } from 'effect'; import { SqlClient } from '@effect/sql'; import { Stream } from '@effect/rpc/RpcSchema'; export declare interface ActionProcedureDescriptor { readonly kind: 'action'; readonly name: Name; readonly input: Input; readonly output: Output; readonly error: Error; /** * Store table(s) this action READS / WRITES. * * An action is non-transactional external I/O, and it very often touches a * table on the way — a cache it fills, a job row it stamps. Until these * existed there was NO WAY to declare it, which made every such table * invisible to static analysis: `voltro check` reported one that five action * paths read and wrote as an orphan, and advised removing it. * * A primitive that cannot declare what it touches turns every analysis over * it into a guess. These are the slots that let the answer be checked instead. * Same shape as a query's `source` and a mutation's `target`. */ readonly source: string | ReadonlyArray | undefined; readonly target: TargetSpec | ReadonlyArray | undefined; /** Declarative authorization guard(s) — enforced before the executor runs, * failing with a typed `ScopeError`. Absent → no framework-level authz. */ readonly guards: DeclaredAccess | undefined; /** The declared reason this procedure needs NO authorization check — * `openAccess: ''`. Mutually exclusive with `guards`; together they are * the only two shapes `security.defaultDeny` accepts. */ readonly openAccess: string | undefined; /** Opt this action into a public REST endpoint (innovation/11). */ readonly publicApi: PublicApiSpec | undefined; /** Opt this action into the auto-synthesized agent toolset (innovation/07). */ readonly exposeAsTool: ExposeAsTool | undefined; /** Require a SECOND human to approve before this action takes effect. The gate * runs in the dispatch spine after `guards:` and BEFORE the executor's * external I/O — the only point at which nothing has happened yet. */ readonly requiresApproval: AnyApprovalPolicy | undefined; /** True when the procedure is kept OFF the wire — no client-group entry and no * route in dev or serve. See `internal` on the definer's options. */ /** * Replace a PLUGIN route that answers to this same tag. * * Without it, a user route and a plugin route sharing a tag is a hard error, * and correctly so — two handlers behind one name is not a thing a caller can * reason about. But refusing is the wrong answer when the app deliberately * wants its own version: the two escapes available otherwise are to rename * your procedure (so the split runs along "who built it" rather than along a * domain boundary) or to `alias` the whole plugin away (same, one level up). * For a frontend developer that is the worst possible partition. * * A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose * surface is richer than theirs, add `archive`/`unarchive` beside it — which * already composes, since the collision check compares FULL tags and not * prefixes — and replace `markRead`, because theirs maintains archive state. * * Explicit, never inferred. Silently letting the app win would mean a plugin * upgrade that adds a route could shadow an app procedure with no diff to * read; declaring it makes the intent reviewable and puts the override in the * file that performs it. */ readonly overridesPlugin: boolean | undefined; readonly internal: boolean | undefined; } export declare const actionToRpc: (descriptor: ActionProcedureDescriptor, extraErrors?: ExtraErrors) => Rpc.Rpc : Input, Output, Schema.Schema.All, never>; export declare const ADMIN_SCOPE = "admin:full"; /** The message a boot prints for {@link findAdvisoryResourceGuards}. Kept here * so the wording lives beside the semantics it describes. */ export declare const advisoryResourceGuardWarning: (tags: ReadonlyArray) => string; export declare const anonymousSubject: (tenantId: string | null, credentialRejected?: string) => Subject; /** The erased policy a descriptor carries (input generic dropped). */ export declare interface AnyApprovalPolicy { readonly approvers: Guards; readonly expiresIn?: string; readonly reason?: string; } /** A guard entry is a scope check, a relationship check, or a declared * no-check-needed decision. */ export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec | OpenAccessSpec; /** Any declared event, with the generics erased — for registries and audits. */ export declare type AnyEventDescriptor = EventDescriptor; /** A guard is either a scope check or a relationship check. */ export declare type AnyGuardSpec = GuardSpec | PolicyGuardSpec; export declare const APIKEY_ISSUE_ORG_SCOPE = "apikeys:issue:org"; export declare const APIKEY_ISSUE_OTHER_SCOPE = "apikeys:issue:other"; /** * API-key issuance rights. * * One `admin:full` gate for all key minting was too coarse to express what * products actually need: it means only a full admin can ever mint a key, so a * normal user cannot create their own even narrowly-scoped credential, and an * admin minting one FOR someone is indistinguishable from minting one for * themselves. * * Three separable capabilities instead: * * `apikeys:issue:self` - mint a key that acts as ME. The common self-service * case; the key can never exceed the holder's own * scopes. * `apikeys:issue:org` - mint an ORG key: acts as no person, belongs to the * organization. A CI credential. Separate because * "may create a personal token" and "may create a * credential that outlives my account" are genuinely * different levels of trust. * `apikeys:issue:other` - mint a key ON BEHALF OF another user. Admin * territory: it is the ability to act as someone * else, so it is never implied by the other two. * * `admin:full` satisfies all three, as it does every scope. */ export declare const APIKEY_ISSUE_SELF_SCOPE = "apikeys:issue:self"; /** The subject a decision produces, plus what the decision took away. `removed` * is never non-empty for a `grant`. */ export declare interface AppliedScopes { readonly subject: Subject; readonly removed: ReadonlyArray; } /** * Apply a `RowPatch` to `prev`, producing `next`. Exact inverse of * `diffRows`: `applyRowPatch(prev, diffRows(prev, next))` deep-equals * `next` for adds, removes, replaces, and reorderings. * * Builds an id→row map from prev, applies `add` / `replace` (which set the * id → next row), then materialises the result strictly in the patch's * `order`. A `remove`d id simply isn't in `order`, so it drops out of the * output naturally — no key-type juggling to delete it from the map. Any * id in `order` that the map doesn't resolve (a desync — the client missed * an `add`) is skipped rather than throwing, so one dropped frame degrades * to a slightly stale set instead of a crash. */ export declare const applyRowPatch: (prev: ReadonlyArray, patch: RowPatch) => ReadonlyArray; /** * Apply a decision to a Subject, touching nothing but `scopes`. * * An `anonymous` subject has no `scopes` field in its schema and is returned * untouched under BOTH kinds — writing one onto it would produce a value that * no longer decodes as the Subject it claims to be. */ export declare const applyScopeDecision: (subject: Subject, decision: Exclude) => AppliedScopes; /** The union the `__voltro.approvals.decide` built-in advertises. */ export declare const ApprovalDecisionErrors: Schema.Union<[typeof ApprovalNotFound, typeof ApprovalSelfApproval, typeof ApprovalExpired, typeof ApprovalNotPending, typeof ApprovalForbidden, typeof ApprovalUnavailable]>; /** * The union every approval-requiring descriptor advertises on the wire. * * Merged in `mutationToRpc` / `actionToRpc` at the SAME lifter the server group * and the generated client group both call — the identical argument * `withGuardError` makes for `ScopeError`: a denial the framework can produce * before the executor MUST be in the error union or it crosses as an untyped * defect and the client cannot branch on it. */ export declare const ApprovalErrors: Schema.Union<[typeof ApprovalRequired, typeof ApprovalRejected, typeof ApprovalExpired, typeof ApprovalUnavailable]>; /** Past `expiresAt`. Fails closed: neither approvable nor executable. */ export declare class ApprovalExpired extends ApprovalExpired_base { } declare const ApprovalExpired_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; expiredAt: typeof Schema.String; }>; /** The would-be approver does not satisfy the intent's own `approvers` guards. */ export declare class ApprovalForbidden extends ApprovalForbidden_base { } declare const ApprovalForbidden_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; /** The scope whose absence denied them, when the denial was scope-shaped. */ required: Schema.NullOr; message: typeof Schema.String; }>; /** The identity of an approval intent, as a wire string. */ export declare type ApprovalId = string; /** No approval row with that id (wrong id, or swept past retention). */ export declare class ApprovalNotFound extends ApprovalNotFound_base { } declare const ApprovalNotFound_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; }>; /** Already decided (or already consumed) — a decision is made once. */ export declare class ApprovalNotPending extends ApprovalNotPending_base { } declare const ApprovalNotPending_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; status: typeof Schema.String; }>; /** * Declare that a mutation/action needs a SECOND human before it takes effect. * * ```ts * export default defineMutation({ * name: 'invoices.refund', * guards: [{ scope: 'invoices:refund' }], * requiresApproval: { * approvers: [{ scope: 'invoices:approve' }], * expiresIn: '4h', * reason: 'refunds move money out of the account', * }, * // … * }) * ``` * * The first call records the intent and fails with a typed `ApprovalRequired` * carrying the approval id. Once an authorised, DIFFERENT subject approves it, * the identical call succeeds — once. */ export declare interface ApprovalPolicy { /** * WHO MAY APPROVE. The same `guards:` vocabulary the procedure itself uses, * evaluated against the APPROVER's effective scope set at decision time. * * Required and non-empty. An approval step whose authority is "anyone" is not * a control — it is a second click, and it reads in review like a control. * `defineMutation` refuses an empty list at declaration for the same reason * `guards: []` is refused. */ readonly approvers: Guards; /** * How long the pending intent stays approvable — an interval string (`'30m'`, * `'4h'`, `'7d'`). * * Absent → the app's `approvals.expiresIn` tunable, else 24 h. There is no * "never expires": an approval queue with no floor is a list of decisions * nobody made, and the framework FAILS CLOSED past the deadline (an expired * intent can be neither approved nor executed — the requester re-submits and * a fresh decision is asked for). */ readonly expiresIn?: string; /** Shown to the approver — why this call needs a second person. */ readonly reason?: string; } /** * An approver said NO, and this is the requester learning about it. * * Reported on the retry rather than pushed, because the requester's call is * what has to stop happening. Reported ONCE: the gate frees the intent's content * key as it raises this, so a deliberate re-submit afterwards opens a genuinely * new decision instead of silently reusing a dead one — a rejection is a verdict * on one request, not a permanent ban on the operation. */ export declare class ApprovalRejected extends ApprovalRejected_base { } declare const ApprovalRejected_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; decidedBy: Schema.NullOr; note: Schema.NullOr; }>; /** * The call was RECORDED as a pending approval and did NOT run. * * This is the answer the caller gets meanwhile, and it is a typed failure rather * than a success with a status field on purpose: a mutation that returns its * normal output shape when nothing happened is the single easiest thing for a * client to mis-handle, and every existing client already branches on `_tag`. */ export declare class ApprovalRequired extends ApprovalRequired_base { } declare const ApprovalRequired_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; procedure: typeof Schema.String; /** ISO instant past which this intent can no longer be approved. */ expiresAt: typeof Schema.String; /** The scope(s) an approver must hold — so the UI can say who to ask. */ requiredScopes: Schema.Array$; /** The declared `reason`, when the descriptor gave one. */ reason: Schema.NullOr; /** True when THIS call created the intent; false when it found the one an * earlier identical call had already recorded. Lets a client tell "I just * asked" from "still waiting". */ created: typeof Schema.Boolean; }>; export declare const APPROVALS_DECIDE_TAG: "__voltro.approvals.decide"; export declare const APPROVALS_PENDING_TAG: "__voltro.approvals.pending"; /** * `__voltro.approvals.decide` — approve or reject ONE pending intent. * * `openAccess` here for a structurally different reason from the query's, and * the distinction is worth keeping straight: the authority IS real and IS * checked, it just cannot be expressed on the descriptor. Which scopes an * approver needs is a property of the PENDING ROW (copied from the target * procedure's own `requiresApproval.approvers` when the intent was recorded), so * one descriptor-level scope would have to be the union of every * approval-requiring procedure in the app — a scope that grants strictly more * than any single approval does, which is the rubber stamp in its most damaging * form. * * So the check is per row, in the executor, against the intent's own guards, * plus the unconditional self-approval refusal. */ export declare const approvalsDecideDescriptor: MutationProcedureDescriptor<"__voltro.approvals.decide", Schema.Struct<{ approvalId: typeof Schema.String; decision: Schema.Literal<["approve", "reject"]>; note: Schema.optional; }>, Schema.Struct<{ approvalId: typeof Schema.String; status: Schema.Literal<["approved", "rejected"]>; }>, Schema.Union<[ ApprovalNotFound, ApprovalSelfApproval, ApprovalExpired, ApprovalNotPending, ApprovalForbidden, ApprovalUnavailable]>>; /** * The approver IS the requester. * * Refused unconditionally, with no opt-out flag. The whole content of * "a second human" is that it is a second one; a framework that shipped * `allowSelfApproval: true` would be shipping a control that every app under * deadline pressure turns off, and the audit row would still read "approved". */ export declare class ApprovalSelfApproval extends ApprovalSelfApproval_base { } declare const ApprovalSelfApproval_base: Schema.TaggedErrorClass; } & { approvalId: typeof Schema.String; subjectId: Schema.NullOr; }>; /** * `__voltro.approvals.pending` — the calling subject's approval work. * * Reactive on `_voltro_approvals`, which is what answers "what does the caller * see meanwhile": the requester watches their own row flip `pending → * approved` and re-fires the mutation, and the approver's queue appears without * a poll. * * `openAccess`, not a scope guard, deliberately. There is no scope that means * "may see my own approval work" — every caller has some — and inventing one * would be exactly the rubber-stamp guard the `openAccess` doc warns about. The * answer is SUBJECT-SCOPED IN THE EXECUTOR: a row appears only if the caller * requested it or satisfies its recorded `approvers` guards, so an anonymous * caller sees an empty list. */ export declare const approvalsPendingQueryDescriptor: QueryProcedureDescriptor<"__voltro.approvals.pending", Schema.Struct<{ limit: Schema.optional; }>, Schema.Array$; requestedBy: Schema.NullOr; status: Schema.Literal<["pending", "approved", "rejected", "expired", "consumed"]>; reason: Schema.NullOr; requiredScopes: Schema.Array$; requestedAt: typeof Schema.String; expiresAt: typeof Schema.String; decidedBy: Schema.NullOr; decidedAt: Schema.NullOr; note: Schema.NullOr; relation: Schema.Literal<["to-decide", "requested"]>; }>>, typeof Schema.Never>; /** The lifecycle states a `_voltro_approvals` row moves through. */ export declare type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired' | 'consumed'; /** * The procedure declares `requiresApproval` and the framework could not reach * the approvals store — so it cannot record the intent and cannot know whether * one was granted. * * FAIL CLOSED. Running the mutation because the bookkeeping is unavailable is * exactly the shape of hole the declaration exists to close; the requester sees * a refusal they can report, which is the useful outcome. */ export declare class ApprovalUnavailable extends ApprovalUnavailable_base { } declare const ApprovalUnavailable_base: Schema.TaggedErrorClass; } & { procedure: typeof Schema.String; message: typeof Schema.String; }>; /** * Throws `Unauthenticated` when the resolved Subject is anonymous (no * real user identity). Handlers that require a signed-in caller put * this as the first line of their executor: * * ```ts * const execute = async (input, ctx) => { * assertAuthenticated(ctx.request.subject) * … * } * ``` */ export declare const assertAuthenticated: (subject: Subject, reason?: string) => void; /** Optional callback-route mount surface for strategies that handle * OAuth callbacks. Apps wire these via the framework's HTTP router * when the strategy declares `mountRoutes`. */ export declare interface AuthCallbackRouter { readonly get: (path: string, handler: AuthRouteHandler) => void; readonly post: (path: string, handler: AuthRouteHandler) => void; } /** * The framework's per-request auth middleware. Attach to an RpcGroup with * `group.middleware(AuthMiddleware)` to require every call to flow through * an auth resolver before reaching its handler. The application provides * the resolver via `Layer.succeed(AuthMiddleware, AuthMiddleware.of(fn))`, * where `fn` receives `{headers, payload, rpc, clientId}` and returns an * `Effect` — typically by inspecting headers / Bearer token and * looking the caller up. * * `provides: SubjectService` means the resolver's return value is placed * into the handler's context tagged as SubjectService; handlers read it * with `yield* SubjectService`. Connections with no headers (or whatever * the resolver requires) can fail the middleware — that becomes a * mutationResult error / error frame for the caller. * * Apps that don't need per-request resolution can skip attaching this * middleware and provide SubjectService directly with a static * `Layer.succeed(SubjectService, anonymousSubject(tenantId))` instead. */ export declare class AuthMiddleware extends AuthMiddleware_base { } declare const AuthMiddleware_base: RpcMiddleware.TagClass; export declare type AuthRouteHandler = (req: AuthRouteRequest) => Promise; export declare interface AuthRouteRequest { readonly url: URL; readonly headers: Readonly>; readonly body: () => Promise; } export declare interface AuthRouteResponse { readonly status: number; readonly headers?: Readonly>; readonly body: string; } /** Pluggable auth strategy. Strategies are SYNC-fast on no-match * (cookie-name lookup) and cache any JWKS / DB roundtrips on match * so steady-state verification stays CPU-local. */ export declare interface AuthStrategy { /** Stable id (`'voltro-password'`, `'workos'`, `'kinde'`, …). Used * in logs + `Subject.metadata.provider`. */ readonly id: string; readonly resolve: (input: AuthStrategyInput) => Promise | StrategyResolution; /** * The bearer-token PREFIX this strategy claims, when it gates on one * (`'sk_'`, `'awb_'`). Declared so a collision is DETECTABLE. * * Two strategies claiming the same prefix is not a harmless duplicate: the * chain is first-match-wins, so whichever runs first decides the Subject — * and if they resolve the same token to different authority, which one * answered decides whether authorization works. A downstream app hit exactly * this and had to pin a test asserting it never sets `apiKeys: true`, because * doing so would append the framework strategy alongside its own on the same * `sk_` prefix, with the framework one resolving without the app's team * binding. * * Optional: a cookie or JWKS strategy claims no prefix and omits it. Only * what is declared can be checked — a strategy that gates on a prefix without * saying so is invisible to the boot check, exactly as before. */ readonly claimsBearerPrefix?: string; } /** Per-call input the framework hands every strategy. */ export declare interface AuthStrategyInput { readonly headers: Readonly>; readonly clientId: number; /** * The app's DataStore, for a strategy that must READ to identify the caller. * * Without it, a DB-backed strategy — a session row, an API-key record, a PAT * table — had to open a SECOND connection path beside the framework's, to the * same database the request store opens a moment later. One adopter's * `auth/db.ts` is 105 lines of exactly that: a second `ManagedRuntime` plus a * `MysqlClient`, load-bearing for session lookup and their ApiKeyStore. Every * DB-backed OIDC / SAML / PAT integration rebuilds it. * * It is the SAME value `auth.resolveScopes` receives — one store, handed to * both, rather than a second narrower type for the same object. A read-only * surface would be the better guarantee and it is not available cheaply here: * `DataStore` is the driver SPI, and a strategy that writes during subject * resolution is a design mistake the type system is not going to catch for * you. Read users / sessions / keys; do not run domain writes. * * It is the BOOT store, not a request-scoped one — strategies resolve before * a request store exists. `undefined` only while the store is still being * built (`voltro dev` builds it after the auth chain; `voltro serve` before), * and on an app with no store at all. * * **What it does and does not carry.** It applies the STORAGE codec — * `.encrypted()` columns decrypt on read and encrypt on write, and array * columns round-trip on dialects with no native array type. It applies NONE * of the Subject-dependent behaviour: no tenant scope, no soft-delete filter, * no audit-column stamping, no row-level security. That split is not an * omission on either side. Those need a Subject, and a strategy runs BEFORE * one exists — so a read of tenant-owned rows here must derive and apply that * scope itself. Encryption needs no Subject, and handing back `enc:v1:…` * would be a silent wrong answer: the ciphertext is a string, so it compares * and renders and simply never matches. */ readonly store?: DataStore; } /** Strategies that need server-side callbacks (OAuth code-exchange, * magic-link landing, etc.) ALSO export this companion via the * `routes` factory below. Kept separate from `AuthStrategy.resolve` * so apps that only need pure JWT-verify strategies don't pay the * callback-wiring cost. */ export declare interface AuthStrategyWithCallback extends AuthStrategy { readonly mountRoutes: (router: AuthCallbackRouter) => void; } /** * Decide what to do for an incoming (scope, key) in ONE atomic store call. On * `fresh` the caller runs the handler then calls `finish`/`fail`; on * `replay`/`conflict` it returns immediately without touching the handler. */ export declare const beginIdempotent: (store: IdempotencyStore, scope: string, key: string, ttlMs: number, now: number) => Promise; /** * A cross-table business rule was violated inside a mutation's transaction; the * write was rolled back. * * - `rule` — the declared rule name (`.`-ish, the author's choice). * - `params` — i18n params for the violation message (the offending values). * - `field` — the field path the violation pinpoints, when the rule can. * - `severity` — always `'error'` on the wire (a `'warning'` rule does not * fail the mutation, so it never reaches the client as an error). */ export declare class BusinessRuleViolation extends BusinessRuleViolation_base { } declare const BusinessRuleViolation_base: Schema.TaggedErrorClass; } & { rule: typeof Schema.String; params: Schema.optional>; field: Schema.optional; message: Schema.optional; severity: Schema.Literal<["error", "warning"]>; }>; /** The result of a cached resolution. `fresh` distinguishes a resolution that * actually ran from a replayed verdict — the audit hook fires on the former * only, so a narrowed subject logs once per window instead of once per * request. */ export declare interface CachedScopeDecision { readonly decision: ScopeDecision; readonly fresh: boolean; } /** * Validate `plugin.framework` (npm-semver range) against the running * voltro version. Returns either `{ ok: true }` or * `{ ok: false, reason }` so the boot path can log a clear warning. * * Intentionally NOT throwing — incompat is a soft signal in v1. * Aborting boot on a missed minor would freeze ecosystem evolution. * The framework logs WARN and proceeds; the operator gets visibility * + decides whether to pin or upgrade. * * Supports the subset apps in the wild actually use: * - `^x.y.z` — caret: same major * - `~x.y.z` — tilde: same major+minor * - `>=x.y.z` — minimum version * - `>=x.y { ok: true; } | { ok: false; reason: string; }; export declare const checkGuards: (subject: Subject, guards: ReadonlyArray | undefined, options?: GuardCheckOptions) => ScopeError | Unauthenticated | null; /** * The resource-aware counterpart of `checkGuards`. Identical semantics when no * resource-scope resolver is registered (or no guard carries a `resource` * extractor) — it delegates to the pure `checkGuards` and succeeds/denies * exactly the same, allocation-light. When BOTH a resolver is registered AND a * guard has a `resource`, it extracts the resource id from `input` and asks the * resolver whether the caller holds each scope on THAT resource. A globally-held * scope (or the `admin:full` bypass) still satisfies without a resolver call; * only the gap ("no global grant") falls through to the resolver. A failed * resolver Effect is treated as a denial (fail-closed). * * Returns a typed `ScopeError` naming the first unmet scope, or `null` when * every guard passes. The runtime calls this in the dispatch spine BEFORE the * executor (mutations: before the transaction opens). */ export declare const checkGuardsEffect: (subject: Subject, guards: ReadonlyArray | undefined, input: unknown, options?: GuardCheckOptions) => Effect.Effect; export declare interface ClientDescriptor { readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'workflow'; /** Query only: table(s) the cache should match this subscription against. */ readonly source?: string | ReadonlyArray | undefined; /** Mutation only: tables this mutation writes, with op. */ readonly targets?: ReadonlyArray | undefined; /** The procedure's input `Schema`, carried to the client so schema-driven * UI (forms, query-bound pickers) and the capability map can introspect * it with no extra fetch. Browser-safe: the schema is already loaded * value-level via the rpc group, so this is the same object by reference. */ readonly input?: Schema.Schema.Any | undefined; /** The procedure's output `Schema` — carried so schema-driven tables * (`` derives its columns from it) + the capability map can * introspect it. Same browser-safety rationale as `input`. */ readonly output?: Schema.Schema.Any | undefined; } /** * Client-side view of a target. Carries the same fields as `TargetSpec` * but with input/row generics erased — the client sees concrete runtime * row shapes, not Schema instances. Optional `shape`/`identify` flow * through because the codegen references the live descriptor (rather * than emitting JSON), so function refs survive the ES-module boundary. */ export declare interface ClientTarget { readonly table: string; readonly op: 'insert' | 'update' | 'delete'; readonly order?: 'prepend' | 'append' | undefined; readonly shape?: ((input: Record, optimisticIdOrCurrent?: unknown) => Record) | undefined; readonly identify?: ((input: Record) => string | ReadonlyArray) | undefined; /** Nested/path-targeted optimistic (see `NestedTargetFields`). Carried to the * client so item-level patches into a JSON array / computed value happen * automatically. Functions survive because the codegen references the live * descriptor by value. */ readonly path?: string | undefined; readonly by?: string | undefined; readonly match?: ((value: unknown, input: Record) => boolean) | undefined; /** Nested-item shaper (erased). The client uses this instead of `shape` when * `path` is set. 2nd arg is the optimistic id (insert) or the current item * (update) — `unknown` here since the erased view spans both; normalize casts. */ readonly shapeItem?: ((input: Record, currentOrOptimisticId: unknown) => Record) | undefined; } export declare const composeAuthStrategies: (strategies: ReadonlyArray, options?: { /** Fallback when no strategy matched. Default: returns the input's * tenant from `x-tenant` header or null. */ readonly fallback?: (input: AuthStrategyInput) => Subject; /** Hook for logging — called on every `failed` resolution. */ readonly onStrategyFailed?: (event: { strategyId: string; reason: string; }) => void; /** * Reject anonymous callers that carry no tenant. When `true` and no * strategy matched AND no `x-tenant` header is present, the resolver * throws `Unauthenticated` instead of returning a null-tenant * anonymous Subject. Use it on multi-tenant apps where every call — * even an anonymous one — must be scoped to a tenant; without it a * tenant-less anonymous Subject can read across the whole DB on * tables that aren't `tenant()`-scoped. Ignored when a custom * `fallback` is supplied (the fallback owns that decision). */ readonly anonymousTenantRequired?: boolean; /** * Add scopes to a resolved Subject from a source the strategy could not * see — typically a role stored in the app's own database. * * **The gap this closes.** An app whose authorization is a DB ROLE * (`requireCallerAdmin(ctx)` reading an `employees.role` column) is * invisible to every static analysis the framework has: `voltro check`'s * `rbac/unguarded-mutation` reports its writes as unguarded, and it is * right to — nothing about that authorization is declared. But the * declarative alternative was unusable for them: their subjects come from * an external IdP's JWTs and carry no scopes, so `guards: [{ scope: * 'employee:admin' }]` would lock out every real user. One app measured * 1566 findings it had no way to act on. * * Lifting roles into `subject.scopes` here makes the SAME authorization * declarable — `requireScope('employee:admin')` on the descriptor, visible * in the manifest, checkable by CI. That is the framework's own * scope-vs-filter argument one level up: only what is declared can be * checked. * * **Deliberately narrow: scopes only.** It cannot return a Subject. A hook * that could rewrite `id` or `tenantId` would be a forgery surface — the * same shape as the api-key `metadata.userId` bug, where an app-supplied * bag could overwrite the framework's claim about who a request was. The * strategy owns identity; this owns authority. * * **It is now the ONLY place a session's authority comes from.** The * framework's session cookie carries `SubjectIdentity` — no scopes — so for * a cookie-authenticated caller the strategy establishes nothing to union * with, and whatever this returns IS the caller's authority. Remove a role * and the next resolution reflects it; there is nothing frozen left to * override. An app that gates on scopes and wires no resolver has callers * with no scopes, which is the fail-closed direction. * * **Return shape.** A bare `ReadonlyArray` means * `{ kind: 'grant' }` — unioned, exactly as before. Return * `{ kind: 'authoritative', scopes }` to make this resolver the complete * answer, which is how you narrow a subject whose scopes came from a TOKEN * (a JWT's `scopesFromClaims`, an api key's record) rather than a cookie. * Return `{ kind: 'unavailable', reason }` when the lookup itself failed — * the request then fails closed with `Unauthenticated` and the reason * reaches `onStrategyFailed`, instead of an empty array being mistaken for * a policy decision. * * Runs only on a MATCHED subject — never for anonymous, where there is no * identity to look a role up for. It is on the request path and the * framework caches it for you (`scopeCache`, 30s by default, invalidatable * — see below); a resolver whose answer depends on anything other than the * subject's identity must set `scopeCache: false`. */ readonly resolveScopes?: (subject: Subject, input: AuthStrategyInput) => Promise | ScopeResolverResult; /** * How resolved authority is cached. Omit for the default window * (`DEFAULT_SCOPE_CACHE_TTL_MS`, 30s — the same window the session * revocation check uses, so the two store reads miss together). * * - `ScopeCacheOptions` — tune the window in place: `{ ttlMs: 0 }` * resolves on every request, staleness zero. * - a `ScopeCache` from `makeScopeCache()` — you keep the handle and call * `invalidate(scopeCacheKey(subject))` from whatever changes a role. * Zero staleness on this process, `ttlMs` on other replicas. * - `false` — no caching at all. Required when the resolver reads * anything beyond the subject's identity (a header, a request path), * because the cache key is the identity and nothing else. */ readonly scopeCache?: ScopeCache | ScopeCacheOptions | false; /** * Called when an `authoritative` resolution REMOVED scopes the strategy had * established — the audit trail for narrowing. * * Narrowing is a security-relevant event and it must not be inferable only * by its absence. It fires on a fresh resolution, not on a cache replay, so * a narrowed caller logs once per window rather than once per request. */ readonly onScopesNarrowed?: (event: { strategyId: string; subjectType: Subject["type"]; subjectId: string | null; removed: ReadonlyArray; granted: ReadonlyArray; }) => void; /** * Hands every strategy the app's store on `input.store`. * * A GETTER rather than a value, and that is the whole reason this composes * across both boot paths unchanged: `voltro dev` builds the store AFTER the * auth chain and `voltro serve` builds it BEFORE. A value captured here * would be `undefined` forever in dev and correct in serve — a capability * present in production and missing in development, which is the drift * class this repo has been bitten by most. */ readonly getStore?: () => unknown; }) => ((input: AuthStrategyInput) => Promise); /** * Compose a list of Effect-native interceptors into ONE function the * runtime calls. Reduces right-to-left so the array order reads * naturally as outer → inner: `[A, B, C]` produces A(B(C(next))). The * output is `undefined` when the list is empty so callers can skip the * wrap entirely. * * Used for mutation / query / action chains alike. */ export declare const composeRpcInterceptors: (interceptors: ReadonlyArray) => RpcInterceptor | undefined; export declare const CONNECTION_DISCONNECT_TAG: "__voltro.connections.disconnect"; export declare const CONNECTION_START_TAG: "__voltro.connections.start"; export declare const CONNECTION_SUBMIT_TOKEN_TAG: "__voltro.connections.submitToken"; export declare interface ConnectionCredential { /** Cookies to set or replace INSIDE the connection's `Cookie` header. Other * cookies on the connection are left alone. */ readonly cookies?: Readonly>; /** Headers to set or replace outright (`authorization`, a custom token * header). Matched case-insensitively. */ readonly headers?: Readonly>; } /** `__voltro.connections.disconnect` — forget the calling subject's credential * for this connection. Deletes the row; the provider-side grant (if any) is * the provider's to revoke. */ export declare const connectionDisconnectDescriptor: MutationProcedureDescriptor<"__voltro.connections.disconnect", Schema.Struct<{ connectionId: typeof Schema.String; }>, Schema.Struct<{ ok: typeof Schema.Boolean; }>, Schema.Union<[typeof ConnectionNotDeclared, typeof ConnectionSubjectRequired, typeof ConnectionKindMismatch, typeof ConnectionHandshakeFailed]>>; /** The provider rejected the token / the handshake failed. `transient` * distinguishes "try again" from "the user must re-consent". */ export declare class ConnectionHandshakeFailed extends ConnectionHandshakeFailed_base { } declare const ConnectionHandshakeFailed_base: Schema.TaggedErrorClass; } & { connectionId: typeof Schema.String; reason: typeof Schema.String; transient: typeof Schema.Boolean; }>; export declare class ConnectionInfo extends ConnectionInfo_base { } declare const ConnectionInfo_base: Context.TagClass; /** * Second middleware on every RpcGroup that wants the * `ConnectionInfo` service available to handlers. Resolves from the * per-call `{clientId}` the @effect/rpc transport passes in. Handlers * that re-bind the connection's subject (e.g. `auth.signin` writing * an override after a successful credential check) read clientId * from this service. * * Apps without a soft-reauth flow can skip attaching this middleware. * The framework provides a default-live Layer in `@voltro/runtime` * which the rpcServer wires alongside AuthMiddleware. */ export declare class ConnectionInfoMiddleware extends ConnectionInfoMiddleware_base { } declare const ConnectionInfoMiddleware_base: RpcMiddleware.TagClass; export declare interface ConnectionInfoValue { /** Per-connection identifier assigned by the rpc transport. Stable for the * lifetime of the underlying WebSocket connection; reused as the key for * the per-connection subject override map. */ readonly clientId: number; /** The per-CALL `idempotency-key` request header, when the client attached one * (every `useMutation` call does). Read by `bindMutation` to dedupe a retried * mutation. Absent for callers that don't send it. */ readonly idempotencyKey?: string; /** * Unix-SECONDS expiry of the credential that authorized this call, when it * has one. Absent for credentials with no expiry (anonymous, a non-expiring * strategy) — and absent means "no bound", so the failure direction is the * behaviour that already existed. * * It exists for LONG-LIVED work. A request is checked once and is over in * milliseconds, so expiry never mattered; an event subscription is a * standing state that reconnects forever by design, so one opened a minute * before the token dies would otherwise keep delivering for days on a * credential that is long gone. `bindEvent` ends the stream here, and * `useEvent`'s existing reconnect immediately re-opens it — which is a NEW * request, so it re-resolves the subject and re-runs the guards for real. * That is what makes the bound seamless rather than a disconnection the app * has to handle: still entitled, it continues; no longer entitled, it fails * loudly instead of quietly continuing. * * This bounds EXPIRY, not revocation. A role revoked mid-session is not * observed until the credential runs out — do not let this field grow a * doc comment that claims otherwise. */ readonly credentialExpiresAt?: number; } /** The two credential shapes a connection can hold. `oauth2` = an * authorization-code grant the framework refreshes; `pat` = a long-lived * personal token the user pastes and the framework only stores. */ export declare const ConnectionKind: Schema.Literal<["oauth2", "pat"]>; export declare type ConnectionKind = Schema.Schema.Type; /** The operation is not valid for this connection's kind (e.g. submitting a * pasted token to an oauth2 connection, or starting a redirect flow for a * pat one). */ export declare class ConnectionKindMismatch extends ConnectionKindMismatch_base { } declare const ConnectionKindMismatch_base: Schema.TaggedErrorClass; } & { connectionId: typeof Schema.String; expected: Schema.Literal<["oauth2", "pat"]>; actual: Schema.Literal<["oauth2", "pat"]>; }>; /** No connection with that id is declared in the app (no `*.connection.ts`). */ export declare class ConnectionNotDeclared extends ConnectionNotDeclared_base { } declare const ConnectionNotDeclared_base: Schema.TaggedErrorClass; } & { connectionId: typeof Schema.String; }>; export declare const CONNECTIONS_LIST_TAG: "__voltro.connections.list"; /** * `__voltro.connections.list` — every DECLARED connection, projected for the * calling subject. Reactive (source `_voltro_connections`) so a connect / * disconnect / refresh updates an open UI with no polling. * * It lists declarations, not rows: a connection the subject has never touched * comes back `status: 'disconnected'`. A UI can therefore render the whole * "connected accounts" settings page from this one subscription. */ export declare const connectionsListQueryDescriptor: QueryProcedureDescriptor<"__voltro.connections.list", Schema.Struct<{}>, Schema.Array$; /** Human label from the declaration; falls back to the id. */ label: typeof Schema.String; status: Schema.Literal<["disconnected", "connected", "expired", "revoked", "error"]>; /** Provider-side account identifier, when the declaration could resolve one. */ accountId: Schema.NullOr; /** Display name for the connected account ("mario@example.com"). */ accountLabel: Schema.NullOr; /** Granted scopes (oauth2); empty for pat. */ scopes: Schema.Array$; /** Access-token deadline as an ISO string, or null (pat / no expiry). */ expiresAt: Schema.NullOr; /** Last refresh failure message, when `status` is `error` / `revoked`. */ lastError: Schema.NullOr; /** When the credential was first stored, ISO. Null while disconnected. */ connectedAt: Schema.NullOr; }>>, typeof Schema.Never>; /** * `__voltro.connections.start` — begin an oauth2 handshake. Returns the * provider's consent URL, which the client navigates to (popup or full * redirect). An action, not a mutation: it mints a single-use handshake grant * and talks to no domain table, so it must not be optimistic-patched. */ export declare const connectionStartDescriptor: ActionProcedureDescriptor<"__voltro.connections.start", Schema.Struct<{ connectionId: typeof Schema.String; /** Where the callback should send the browser once the handshake lands. * Same-origin PATH only (validated server-side) — an absolute URL would * make the callback an open redirector. */ redirectTo: Schema.optional; }>, Schema.Struct<{ authorizeUrl: typeof Schema.String; state: typeof Schema.String; }>, Schema.Union<[typeof ConnectionNotDeclared, typeof ConnectionSubjectRequired, typeof ConnectionKindMismatch, typeof ConnectionHandshakeFailed]>>; /** One declared connection, projected for the calling subject. */ export declare const ConnectionState: Schema.Struct<{ /** The `defineConnection({ id })` value. */ connectionId: typeof Schema.String; kind: Schema.Literal<["oauth2", "pat"]>; /** Human label from the declaration; falls back to the id. */ label: typeof Schema.String; status: Schema.Literal<["disconnected", "connected", "expired", "revoked", "error"]>; /** Provider-side account identifier, when the declaration could resolve one. */ accountId: Schema.NullOr; /** Display name for the connected account ("mario@example.com"). */ accountLabel: Schema.NullOr; /** Granted scopes (oauth2); empty for pat. */ scopes: Schema.Array$; /** Access-token deadline as an ISO string, or null (pat / no expiry). */ expiresAt: Schema.NullOr; /** Last refresh failure message, when `status` is `error` / `revoked`. */ lastError: Schema.NullOr; /** When the credential was first stored, ISO. Null while disconnected. */ connectedAt: Schema.NullOr; }>; export declare type ConnectionState = Schema.Schema.Type; /** * Lifecycle of ONE subject's credential for ONE connection. * * - `disconnected` — synthesised for a declared connection with no stored * row, so the client can render every declared connection uniformly * without a second "what exists" call. * - `connected` — a usable credential is on file. * - `expired` — the access token is past its deadline and there is no * refresh token to renew it. Distinct from `revoked`: nothing was taken * away, we simply cannot renew without the user consenting again. * - `revoked` — the provider REFUSED the refresh (invalid_grant). The * stored tokens are cleared; only re-consent restores it. * - `error` — the last refresh failed transiently (5xx / network). * The tokens are retained and the next use retries. */ export declare const ConnectionStatus: Schema.Literal<["disconnected", "connected", "expired", "revoked", "error"]>; export declare type ConnectionStatus = Schema.Schema.Type; /** The caller is anonymous. A credential belongs to a SUBJECT — there is no * app-wide connection, by construction. */ export declare class ConnectionSubjectRequired extends ConnectionSubjectRequired_base { } declare const ConnectionSubjectRequired_base: Schema.TaggedErrorClass; } & { connectionId: typeof Schema.String; }>; /** * `__voltro.connections.submitToken` — store a pasted personal access token. * A mutation (it writes the credential row) targeting no user table. */ export declare const connectionSubmitTokenDescriptor: MutationProcedureDescriptor<"__voltro.connections.submitToken", Schema.Struct<{ connectionId: typeof Schema.String; token: typeof Schema.String; }>, Schema.Struct<{ ok: typeof Schema.Boolean; }>, Schema.Union<[typeof ConnectionNotDeclared, typeof ConnectionSubjectRequired, typeof ConnectionKindMismatch, typeof ConnectionHandshakeFailed]>>; /** * The database refused a write because it broke an integrity rule the SCHEMA * declares — a foreign key, a unique index, a NOT NULL, a CHECK. * * **Why this is typed at all.** Without it the driver failure is a `SqlError`, * which the rpc layer collapses to a bare `InternalError` on purpose (a * `SqlError`'s text is not safe to forward — see below). The caller then gets * "something went wrong" for a failure that is entirely about the input it just * sent, and the only way to find out which rule fired is to re-run the * statement by hand against the database. That is a real reported shape: * that. * * **Why it carries names and not the driver's sentence.** The temptation is to * forward `cause.message`, and it is measured to be wrong: postgres attaches * `Failing row contains (…)` — the complete row, every column — to a not-null * and a check violation; mysql and mssql echo the duplicate VALUE on a unique * violation. A constraint or column NAME is schema, which this framework * already puts on the wire (`TableValidationFailed.table`). A row is data, and * the caller who provoked the error is not automatically entitled to it. * * **Not every dialect can fill every field.** sqlite reports a foreign-key * failure as the bare sentence `FOREIGN KEY constraint failed` — no name, no * column, no direction — so `constraint` is absent there and the direction is * inferred from the operation. `kind` is the field that is always right. */ export declare class ConstraintViolation extends ConstraintViolation_base { /** * A sentence built from the fields above and nothing else. * * It exists because an undeclared tagged error reaches the client through * `defectMessage`, which renders `String(error)` — and a `Schema.TaggedError` * with no `message` renders as `[object Object]`. Without this getter the * caller would get a correctly-typed error carrying no information, which is * the same dead end in a different shape. */ get message(): string; } declare const ConstraintViolation_base: Schema.TaggedErrorClass; } & { /** * Which rule fired. `foreignKey` = the row you referenced does not exist; * `foreignKeyInUse` = this row may not go, others still reference it. They * are opposite situations with opposite fixes, so a UI branches on them * separately. */ kind: Schema.Literal<["foreignKey", "foreignKeyInUse", "unique", "notNull", "check"]>; /** The table the write targeted — the framework's own name for it, not the driver's. */ table: typeof Schema.String; /** Which store op was attempted: 'insert' | 'update' | 'delete' | …. */ operation: typeof Schema.String; /** The constraint / index name, when the dialect names one. */ constraint: Schema.optional; /** The column, when the dialect names one instead of (or beside) a constraint. */ column: Schema.optional; }>; /** * A handle to a cluster-coordinated periodic task the plugin armed via * `PluginBindContext.scheduleCoordinated`. Keep it to `stop()` the task * in `onDeactivate` — though the framework also stops every task a plugin * armed at shutdown, so an explicit `stop()` is only needed for tasks the * plugin wants to cancel EARLY (before shutdown). */ export declare interface CoordinatedScheduleHandle { /** Stop the periodic task. Idempotent. */ readonly stop: () => void; /** The task name (for logging / dedup diagnostics). */ readonly name: string; /** * Run a tick NOW because something arrived — the half of the contract that * makes {@link CoordinatedTickOutcome}'s backoff safe to use. * * Wire it to whatever announces work (a `store.onChange` on the plugin's own * queue table, a broadcast message). Coalesced to at most one extra tick per * `intervalMs`, so calling it per row is fine. */ readonly wake: () => void; /** The delay the next tick is currently armed for — a task at the idle * ceiling and one at the base interval are indistinguishable otherwise. */ readonly currentIntervalMs: () => number; /** `false` once the task has stopped ticking and is only waiting for * `wake()`. A disarmed task and a stopped one look identical from outside * otherwise, and only one of them comes back. */ readonly isArmed: () => boolean; } /** * What a coordinated tick learned, returned so the runner can stop polling a * queue that has nothing in it. * * Returning nothing means "assume there was work", which is the conservative * reading: a task that does not report is never slowed down on the strength of * an assumption about it. */ export declare interface CoordinatedTaskOptions { /** * Stop ticking entirely on an idle tick with no pending deadline, and come * back only on `wake()`. Default `false`. * * Pass `true` only when an arrival is GUARANTEED to call `wake()` — a change * subscription on the table your task drains, on a deployment where remote * writes are visible (Postgres LISTEN/NOTIFY, or a broadcast broker). Without * that, a disarmed task sleeps through a peer's enqueue forever, and the * backoff ceiling is the correct behaviour instead. */ readonly disarmWhenIdle?: boolean; } export declare interface CoordinatedTickOutcome { /** `true` when the tick found nothing to do. Only an idle tick backs off. */ readonly idle: boolean; /** Milliseconds until the earliest deadline this task already knows about. * Caps the backoff, so a task that is idle now but has something due in * 400 ms is armed for 400 ms rather than for the idle ceiling. */ readonly nextDueInMs?: number; } /** * What a descriptor CARRIES: the author's guards, or the erased form of their * `openAccess:` decision — never both. * * The option a user writes is still `Guards` (an `OpenAccessSpec` is not * something to hand-write into `guards:`; there is one spelling for the * decision and it is the `openAccess:` field). The DESCRIPTOR type is wider * because that is where the normalised decision lands, and because every * enforcement path reads the descriptor's array and nothing else. */ export declare type DeclaredAccess = ReadonlyArray | OpenAccessSpec>; /** Every channel declared in this process, by routing key. */ export declare const declaredReactivityChannelKeys: () => ReadonlySet; /** Default cap on cached subjects before the cache sweeps + trims. */ export declare const DEFAULT_SCOPE_CACHE_MAX_ENTRIES = 10000; /** Default scope-cache window. Matches the session-revocation window on * purpose: the two per-request store reads then expire together. */ export declare const DEFAULT_SCOPE_CACHE_TTL_MS = 30000; export declare const defineAction: (options: { readonly name: Name; readonly input: Input; readonly output: Output; readonly error?: Error; /** Declarative authorization guard(s) — the caller must hold the named * scope(s) or the action fails with a typed `ScopeError` before the executor * runs. `ScopeError` is auto-merged into the wire error union. */ readonly guards?: Guards>; /** * Declare that this procedure needs NO authorization check — and say why. * * The other half of `security.defaultDeny`. With the flag on, a procedure * that declares neither `guards:` nor this is refused at boot, by name: an * access decision nobody made is the SEC-1 hole, not a default. * * Use it for the endpoints that really are open — a health check, a public * price list, a signup precheck. Do NOT reach for a scope every caller * already holds just to satisfy the gate: that guard reads as protection and * enforces nothing, and it is the failure mode this field exists to prevent. * * The reason is required and is the point — it is what a reviewer reads and * what `voltro doctor` prints beside the tag. * * openAccess: 'public pricing page — reads no caller data' */ readonly openAccess?: string; /** Table(s) this action READS. Declaring it is what lets `voltro check` know * the table is alive — without it, a table only an action touches reads as * an orphan. See `ActionProcedureDescriptor.source`. */ readonly source?: string | ReadonlyArray; /** Table(s) this action WRITES, same shape as a mutation's `target`. */ readonly target?: TargetSpec | ReadonlyArray; /** Project this action as a public REST endpoint (innovation/11). */ readonly publicApi?: PublicApiSpec; /** Expose this action as an agent tool (innovation/07). */ readonly exposeAsTool?: ExposeAsTool; /** * Require a SECOND human to approve before this action takes effect. * * Same contract as a mutation's, and the gate runs at the same point relative * to the work: after `guards:`, BEFORE the executor. For an action that is the * only point at which nothing has happened yet — there is no transaction to * roll back an outbound HTTP call. */ readonly requiresApproval?: ApprovalPolicy>; /** * Keep this procedure OFF the wire entirely. * * It is not emitted into `rpcGroup.generated.ts`, and neither `voltro dev` * nor `voltro serve` registers a route for it — the tag is unroutable over * `/rpc` and the WebSocket. Server code calls it by importing its executor * directly, which is what a server-to-server caller already does. * * WHY THIS EXISTS. `publicApi` and `exposeAsTool` opt IN to wider surfaces, * and there was no way to opt OUT of the default one — every discovered * `*.query.ts` / `*.mutation.ts` was callable by any authenticated browser * session. An app that had grown 18 procedures named `*Internal` (the * convention a Convex port carried over for "only other server code calls * this") found all 18 in its client group, one of them accepting * `actorId` / `actorType` / `actorEmail` from the caller and writing an audit * row. Zero callers, no guard, reachable by anyone logged in. * * The same argument as `.serverOnly()` on a column, one level up: a naming * convention is not a boundary. If the only thing keeping a procedure off the * wire is that nobody wrote a client call for it, it is on the wire. * * NOT a substitute for a guard. An internal procedure still runs with * whatever authority its caller has; this removes the wire surface, not the * need to check who is asking. */ readonly internal?: boolean; /** Replace a PLUGIN route answering to this same tag. Explicit, never * inferred — see `overridesPlugin` on the descriptor. */ readonly overridesPlugin?: boolean; }) => ActionProcedureDescriptor; /** * Declare an event. * * ```ts * export const gameStarted = defineEvent({ * name: 'games.started', * key: Schema.Struct({ arenaId: Schema.String }), * payload: Schema.Struct({ gameId: Schema.String, startedAt: Schema.Number }), * guards: [{ scope: 'display:read' }], * }) * ``` */ export declare const defineEvent: (options: { /** * Wire identifier, `camelCase.dotted` like an rpc tag — and it shares the rpc * tag COLLISION SPACE, so a duplicate aborts boot rather than resolving itself * at the first delivery. Two declarations answering to one name is precisely * the failure mode a string channel has; a name that can collide silently * would reintroduce it one level up. */ readonly name: Name; /** * WHERE it goes. Part of the contract, not a filter the client applies: * a subscriber receives only events published under a key it asked for, so * the server never sends the others at all. * * ONLY ROUTING FIELDS BELONG HERE. Every field fragments the subscriber set * and costs fan-out dedup — a discriminator the handler reads (`gameType`) is * payload, an address the delivery is decided by (`arenaId`) is key. * * The tenant is NOT part of it and must never be added: it is derived from the * subject on both sides, so a cross-tenant delivery is impossible by * construction rather than by remembering to filter. */ readonly key: Key; /** WHAT happened. Decoded when publishing, so a mismatch is a typed error at * the PRODUCER instead of a broken handler at every consumer. */ readonly payload: Payload; /** * WHO MAY LISTEN — the same vocabulary as a query's guards, so a * resource-scoped rule ("may this terminal watch this arena") stays * declarative. `ScopeError` is merged into the wire error union automatically. * * Re-checked when the SUBJECT changes (a revoked role ends the stream), not on * every delivery. Per-delivery authorization is the design a comparable * product measured into a scaling wall: one check per subscriber per message * makes throughput scale with the audience instead of the publish rate. */ readonly guards?: Guards>; /** * Declare that this event needs NO authorization check — and say why. * * The other half of `security.defaultDeny`, exactly as on a procedure. With * the flag on, an event that declares neither `guards:` nor this is refused * at boot, by name: an access decision nobody made is the SEC-1 hole, not a * default — and an event with no decision is subscribable by ANY * authenticated session that can open the socket. * * Use it for the events that really are open — a public scoreboard tick, a * status broadcast, a service-health pulse. Do NOT reach for a scope every * caller already holds just to satisfy the gate: that guard reads as * protection and enforces nothing, and it is the failure mode this field * exists to prevent. * * The reason is required and is the point — it is what a reviewer reads and * what `voltro doctor` prints beside the tag. * * openAccess: 'public scoreboard — carries no caller data' */ readonly openAccess?: string; /** * Deliver recently-buffered events on a FIRST attach. Default `false`, and the * default is the interesting half. * * A fresh subscriber wanting only what happens from now on, and a RECONNECTING * subscriber wanting the messages it missed, are different requests that read * as one contradiction ("never replay history" vs "never lose a message"). They * are separated here: a first attach starts empty unless it opts in, while a * re-attach always resumes from the last serial the client saw. Mounting a * component is not the same event as a WebSocket dropping, and the framework * knows which one it is. */ readonly rewind?: boolean; /** * Deliver this event to subscribed HTTP targets as well. * * The unification the whole design is for: ONE declaration, and the audiences * are consumers of it. Without this an app that both fans an event out to its * screens and posts it to a partner declares the thing twice, in two shapes, * and the two drift — which is the defect a declaration exists to remove, one * level up from the string channel it already removed. * * Requires `@voltro/plugin-webhooks`. Absent ⇒ no outbound delivery, and no * cost. */ readonly webhook?: EventWebhookSpec; /** * `'each'` (default) — every delivery matters; a slow subscriber loses the * oldest and is told how many. * * `'latest'` — a newer delivery supersedes a pending one; a slow subscriber * gets the current value and is told nothing, because nothing was lost. * * See {@link EventDeliverySemantics}. The test is "would a deployment be wrong * to miss one?" — not "is this event frequent?". */ readonly delivery?: EventDeliverySemantics; }) => EventDescriptor; export declare const defineMutation: (options: { readonly name: Name; readonly input: Input; readonly output: Output; readonly error?: Error; /** Optional declarative target(s) — drives auto-optimistic. */ readonly target?: Target, Schema.Schema.Type>; /** Declarative authorization guard(s) — the caller must hold the named * scope(s) or the mutation fails with a typed `ScopeError` BEFORE the * transaction opens. `ScopeError` is auto-merged into the wire error union. */ readonly guards?: Guards>; /** * Declare that this procedure needs NO authorization check — and say why. * * The other half of `security.defaultDeny`. With the flag on, a procedure * that declares neither `guards:` nor this is refused at boot, by name: an * access decision nobody made is the SEC-1 hole, not a default. * * Use it for the endpoints that really are open — a health check, a public * price list, a signup precheck. Do NOT reach for a scope every caller * already holds just to satisfy the gate: that guard reads as protection and * enforces nothing, and it is the failure mode this field exists to prevent. * * The reason is required and is the point — it is what a reviewer reads and * what `voltro doctor` prints beside the tag. * * openAccess: 'public pricing page — reads no caller data' */ readonly openAccess?: string; /** Project this mutation as a public REST endpoint (innovation/11). */ readonly publicApi?: PublicApiSpec; /** Expose this mutation as an agent tool (innovation/07). */ readonly exposeAsTool?: ExposeAsTool; /** * Require a SECOND human to approve before this mutation takes effect. * * The first call records a durable pending intent and fails with a typed * `ApprovalRequired` carrying its id; the transaction never opens. Once an * authorised, DIFFERENT subject approves, the IDENTICAL call succeeds exactly * once (the approval is consumed). * * Composes with `guards:` rather than replacing them — the requester still has * to be allowed to ASK. Refused together with `openAccess:` (see * `assertApprovalPolicyCoherent`). */ readonly requiresApproval?: ApprovalPolicy>; /** * Keep this procedure OFF the wire entirely. * * It is not emitted into `rpcGroup.generated.ts`, and neither `voltro dev` * nor `voltro serve` registers a route for it — the tag is unroutable over * `/rpc` and the WebSocket. Server code calls it by importing its executor * directly, which is what a server-to-server caller already does. * * WHY THIS EXISTS. `publicApi` and `exposeAsTool` opt IN to wider surfaces, * and there was no way to opt OUT of the default one — every discovered * `*.query.ts` / `*.mutation.ts` was callable by any authenticated browser * session. An app that had grown 18 procedures named `*Internal` (the * convention a Convex port carried over for "only other server code calls * this") found all 18 in its client group, one of them accepting * `actorId` / `actorType` / `actorEmail` from the caller and writing an audit * row. Zero callers, no guard, reachable by anyone logged in. * * The same argument as `.serverOnly()` on a column, one level up: a naming * convention is not a boundary. If the only thing keeping a procedure off the * wire is that nobody wrote a client call for it, it is on the wire. * * NOT a substitute for a guard. An internal procedure still runs with * whatever authority its caller has; this removes the wire surface, not the * need to check who is asking. */ readonly internal?: boolean; /** Replace a PLUGIN route answering to this same tag. Explicit, never * inferred — see `overridesPlugin` on the descriptor. */ readonly overridesPlugin?: boolean; }) => MutationProcedureDescriptor; /** * Builder helper that returns the input object unchanged AT RUNTIME * but enforces the `VoltroPlugin` shape at the TYPE level. Use over * plain object literals when you want the type-checker to catch * misnamed hooks or missing required fields at the plugin's * declaration site rather than at the `app.config.ts` consumption * site. * * ```ts * export const myPlugin = (options: MyOptions): VoltroPlugin => * definePlugin({ * name: '@vendor/my-plugin', * framework: '^1.0.0', * interceptMutation: (next, ctx) => * next.pipe(Effect.tap(() => recordAudit(ctx))), * onActivate: (ctx) => Effect.sync(() => ctx.logger.info('warming caches')), * onDeactivate: () => Effect.void, * }) * ``` * * Identity function — kept narrow so the compiler picks excess-property * checks on the literal. */ export declare const definePlugin: (plugin: VoltroPlugin) => VoltroPlugin; /** * Helper to declare one rpc route contributed by a plugin. Returns * the input unchanged; exists for the type-level enforcement (plus * symmetry with `defineMutation` / `defineQuery` user-side). */ export declare const definePluginRoute: (route: PluginRpcRoute) => PluginRpcRoute; /** * Declarative builder for a plugin's `services` layer. Wraps the * `Context.Tag` + `Layer.succeed` pair so a plugin author doesn't * have to manage both. Use when a plugin's service interface is * straightforward (a record of functions); reach for raw Layer when * the implementation depends on other services (Layer.effect) or * needs resource acquisition (Layer.scoped). */ export declare const definePluginService: (identifier: string, implementation: S) => { readonly Tag: Context.Tag; readonly Live: Layer.Layer; }; export declare const defineQuery: (options: { readonly name: Name; readonly input: Input; readonly output: Output; readonly error?: Error; /** Optional: the store table(s) this query reads. Declaring it enables * auto-optimistic patch routing from mutations targeting any of those * tables. For a COMPUTED query (handler returns a shaped value) it is the * reactive trigger set: the handler re-runs when ANY listed table changes * — pass an array to depend on several (e.g. a matrix joining two tables). * * A `reactivityChannel(...)` is accepted here too, for state that pushes * without living in a table. Pass the CHANNEL, not its key string: the * import edge is what makes a channel `source:` impossible to leave stale, * which a table name (a bare string) can always be. */ readonly source?: ReactivitySource | ReadonlyArray; /** Opt into server-side snapshot caching with auto-invalidation. */ readonly cache?: QueryCacheConfig; /** Declarative authorization guard(s) — the caller must hold the named * scope(s) or the query fails with a typed `ScopeError` before the executor * runs. `ScopeError` is auto-merged into the wire error union. */ readonly guards?: Guards>; /** * Declare that this procedure needs NO authorization check — and say why. * * The other half of `security.defaultDeny`. With the flag on, a procedure * that declares neither `guards:` nor this is refused at boot, by name: an * access decision nobody made is the SEC-1 hole, not a default. * * Use it for the endpoints that really are open — a health check, a public * price list, a signup precheck. Do NOT reach for a scope every caller * already holds just to satisfy the gate: that guard reads as protection and * enforces nothing, and it is the failure mode this field exists to prevent. * * The reason is required and is the point — it is what a reviewer reads and * what `voltro doctor` prints beside the tag. * * openAccess: 'public pricing page — reads no caller data' */ readonly openAccess?: string; /** Project this query as a public REST endpoint (innovation/11). */ readonly publicApi?: PublicApiSpec; /** Expose this query as an agent tool (innovation/07). */ readonly exposeAsTool?: ExposeAsTool; /** * Keep this procedure OFF the wire entirely. * * It is not emitted into `rpcGroup.generated.ts`, and neither `voltro dev` * nor `voltro serve` registers a route for it — the tag is unroutable over * `/rpc` and the WebSocket. Server code calls it by importing its executor * directly, which is what a server-to-server caller already does. * * WHY THIS EXISTS. `publicApi` and `exposeAsTool` opt IN to wider surfaces, * and there was no way to opt OUT of the default one — every discovered * `*.query.ts` / `*.mutation.ts` was callable by any authenticated browser * session. An app that had grown 18 procedures named `*Internal` (the * convention a Convex port carried over for "only other server code calls * this") found all 18 in its client group, one of them accepting * `actorId` / `actorType` / `actorEmail` from the caller and writing an audit * row. Zero callers, no guard, reachable by anyone logged in. * * The same argument as `.serverOnly()` on a column, one level up: a naming * convention is not a boundary. If the only thing keeping a procedure off the * wire is that nobody wrote a client call for it, it is on the wire. * * NOT a substitute for a guard. An internal procedure still runs with * whatever authority its caller has; this removes the wire surface, not the * need to check who is asking. */ readonly internal?: boolean; /** Replace a PLUGIN route answering to this same tag. Explicit, never * inferred — see `overridesPlugin` on the descriptor. */ readonly overridesPlugin?: boolean; }) => QueryProcedureDescriptor; /** * Declare a non-reactive server→client stream. `handler(input, ctx)` * returns a `Stream` (e.g. an agent run's `AgentEvent`s); the * client consumes it via `useAgentStream`. Reusable for ANY push stream. */ export declare const defineStream: (options: { readonly name: Name; readonly input: Input; readonly element: Element; readonly error?: Error; /** Keep this stream OFF the wire entirely — no client-group entry, no route in * dev or serve. Same contract as `internal` on the other definers; a stream * without it would be a hole in the same boundary. */ readonly internal?: boolean; /** Replace a PLUGIN route answering to this same tag. Explicit, never * inferred — see `overridesPlugin` on the descriptor. */ readonly overridesPlugin?: boolean; /** * WHO MAY LISTEN. * * A stream is the same long-lived grant a subscription is, and it was the one * primitive that could not express authorization at all — queries, mutations * and actions carry `guards:`, streams did not, so any protection lived * hand-written inside an executor where nothing could verify it existed. * * Checked at subscribe and re-checked before every element, so a resource * un-shared or a membership ended stops the stream rather than continuing to * push. Same shape and same semantics as a query's. */ readonly guards?: Guards; /** * Declare that this procedure needs NO authorization check — and say why. * * The other half of `security.defaultDeny`. With the flag on, a procedure * that declares neither `guards:` nor this is refused at boot, by name: an * access decision nobody made is the SEC-1 hole, not a default. * * Use it for the endpoints that really are open — a health check, a public * price list, a signup precheck. Do NOT reach for a scope every caller * already holds just to satisfy the gate: that guard reads as protection and * enforces nothing, and it is the failure mode this field exists to prevent. * * The reason is required and is the point — it is what a reviewer reads and * what `voltro doctor` prints beside the tag. * * openAccess: 'public pricing page — reads no caller data' */ readonly openAccess?: string; }) => StreamProcedureDescriptor; /** * Declare a raw WebSocket gateway (`*.ws.ts` default export). Validation at * DEFINITION time — a bad path fails the boot that discovers it, not the * first client. */ export declare const defineWebSocket: (route: WebSocketGatewayRoute) => WebSocketGatewayRoute; export declare interface DeleteTarget extends NestedTargetFields { readonly table: string; readonly op: 'delete'; /** Identify the row(s) to remove. Default: `input.id`. Return an ARRAY to * remove MANY rows/items in one mutation. */ readonly identify?: ((input: Input) => string | ReadonlyArray) | undefined; } /** * Compute the patch that turns `prev` into `next`, keyed by row id. * * Algorithm: * - Index both sides by id. * - For each id in next: not in prev → `add`; in prev but content * differs → `replace`; in prev and equal → no op. * - For each id in prev but not in next → `remove`. * - `order` is next's id sequence verbatim, so apply can reconstruct * the exact ordering (including a pure reorder, where `ops` is empty). * * Throws if any row lacks an `id` — the caller (dispatcher) only diffs * id-keyed result sets; a result without ids must ship a full snapshot, * not a patch. */ export declare const diffRows: (prev: ReadonlyArray, next: ReadonlyArray) => RowPatch; /** * The caller's effective scope set — the merged set published by rbac if * present, else the raw `subject.scopes`. This is what declarative guards and * `ctx.access` check against. */ export declare const effectiveScopes: (subject: Subject) => ReadonlyArray; /** * Canonical string for a routing key. * * Object key ORDER must not change the routing: `{a,b}` and `{b,a}` are the same * address, and a subscriber that spelled its literal in the other order is the * kind of never-matches bug this whole design exists to make impossible. Sorting * the entries is what makes the two sides agree without either knowing about the * other. */ export declare const encodeEventKey: (key: unknown) => string; /** * Read the `_tag` off a thrown error value, or `undefined` when it has none. * * It lives here, beside `toRpc`, because `toRpc` is what puts the tag on the * wire — one file owns both ends of that contract. It used to live in * `@voltro/client`, and being there had a cost nobody could see from inside the * framework: an app's shared error handler lived in a package that pulled only * `@voltro/i18n`, so reading a tag meant taking a dependency on the whole client * for seven lines. `_tag` is a wire concept, and protocol owns the wire. * * Works on a `Schema.TaggedError` instance AND on the plain `{ _tag: … }` object * the wire actually emits — which is the distinction that matters at the call * site: `instanceof` does NOT hold on the client, because what arrives there was * decoded from JSON and never constructed. Match on the tag, not on the class. */ export declare const errorTag: (err: unknown) => string | undefined; /** * The full routing address: tenant, event, key. * * The tenant is FIRST and is supplied by the caller of this function from the * SUBJECT — never from anything a client sent. Two apps in two tenants * publishing `games.started` for `arena-1` are addressing different channels, * and no amount of key collision can make them the same one. * * Separated by `EVENT_ROUTE_SEP` — NUL, and NOBODY WRITES IT INLINE. The character is * the right separator (it cannot occur in a tenant id or an event name, so no * pair of parts can spell another pair's address), but a source file containing * a literal one is BINARY to every text tool: `grep` skips it and prints * nothing, which reads exactly like a clean file. This repo has already lost an * audit that way. * * The constant exists because escaping it correctly at each site does NOT work * in practice: writing this module produced three literals in three different * files — including inside the comment warning against them — before the * separator was hoisted. So there is exactly one place the character appears, * and `parseEventRoute` / `formatEventRoute` mean no consumer needs to name it * at all. `eventRouteHygiene` in the test file scans the whole package. */ export declare const EVENT_ROUTE_SEP = "\0"; /** First element of every subscription: the stream is live from here on. */ export declare const eventAttached: Schema.Struct<{ _tag: Schema.Literal<["attached"]>; }>; /** * What it means for a subscriber to fall behind. * * `'each'` — every delivery matters. A subscriber that cannot keep up loses the * OLDEST and is told exactly how many. This is the default because it is the * safe reading: an arena that misses a game-start signal must find out. * * `'latest'` — only the current value matters, and a newer delivery SUPERSEDES a * pending one. A subscriber that falls behind receives the current state on its * next read and is told nothing, because nothing was lost: for a 60Hz stream of * positions, frame 1 stopped being interesting the moment frame 2 existed. * * The distinction is semantic, not a performance knob. Choosing `'latest'` for a * stream where each delivery matters silently drops the ones in between; choosing * `'each'` for a per-frame stream makes a slow client work through a backlog to * reach a state it could have had immediately, and report a "loss" that was * never a loss. * * The honest test: **would a deployment be wrong to miss one?** If the next value * supersedes it, that is `'latest'` — and it is probably state rather than an * event at all. */ export declare type EventDeliverySemantics = 'each' | 'latest'; /** * A declared event: a name, a routing key, a payload, and who may listen. * * Type parameters are inferred from the Schemas, and every call site — `publish`, * `useEvent`, a workflow trigger — derives its types from this one value. */ export declare interface EventDescriptor { readonly kind: 'event'; readonly name: Name; readonly key: Key; readonly payload: Payload; /** Who may LISTEN — the author's guards, or the erased form of their * `openAccess:` decision, never both (see `DeclaredAccess`). Re-checked when * the subject changes, not per delivery — see `EventDefinition.guards` for * why that distinction is deliberate. */ readonly guards: DeclaredAccess> | undefined; /** The declared `openAccess:` reason, when there is one — what the boot gate * and `voltro doctor` read. The enforcement paths read `guards`. */ readonly openAccess: string | undefined; /** Deliver buffered events on a FIRST attach. Default false. */ readonly rewind: boolean | undefined; /** `'each'` (default) or `'latest'` — see the definer. */ readonly delivery: EventDeliverySemantics | undefined; /** Opt this event into outbound HTTP delivery. See `EventWebhookSpec`. */ readonly webhook: EventWebhookSpec | undefined; } /** * One delivery. * * `origin` + `n` are what make a lost message COUNTABLE. `n` is monotonic per * (origin, event, tenant, key), so a subscriber that sees 7 then 9 knows exactly * one delivery is gone — where a dropping buffer, which is what every * `onSlowConsumer: 'drop-oldest'` option in this space actually is, discards in * silence by definition. Silence is the one outcome you cannot build on: an * arena cannot tell "no game started" from "the start signal was dropped". * * It is per ORIGIN rather than global because two instances publishing the same * key have no shared clock and no shared counter. A global sequence would need * one owner per key — an extra hop on every publish and a failover story for * every key — which is a real architecture (a single-threaded actor per room) * and not the one this framework has. */ export declare const eventEnvelope:

(payload: P) => Schema.Struct<{ _tag: Schema.Literal<["event"]>; /** Publishing instance id — serials are only comparable within one. */ origin: typeof Schema.String; /** Monotonic per (origin, event, tenant, key). */ n: typeof Schema.Number; emittedAt: typeof Schema.Number; payload: P; }>; /** Told to the subscriber when the SERVER knows it cannot fill a gap. */ export declare const eventGap: Schema.Struct<{ _tag: Schema.Literal<["gap"]>; /** How many deliveries are known lost. Never a guess: a shortfall is computed * from the requested serial against what the ring still holds. */ missed: typeof Schema.Number; /** `buffer` — evicted before this subscriber could be served. * `resume` — the re-attach asked for a serial older than the ring. */ reason: Schema.Literal<["buffer", "resume"]>; }>; /** The routing key did not decode against the descriptor's schema. */ export declare class EventKeyInvalid extends EventKeyInvalid_base { } declare const EventKeyInvalid_base: Schema.TaggedErrorClass; } & { event: typeof Schema.String; message: typeof Schema.String; }>; /** The payload did not decode against the descriptor's schema. */ export declare class EventPayloadInvalid extends EventPayloadInvalid_base { } declare const EventPayloadInvalid_base: Schema.TaggedErrorClass; } & { event: typeof Schema.String; message: typeof Schema.String; }>; /** The encoded envelope exceeds `MAX_EVENT_ENVELOPE_BYTES`. */ export declare class EventPayloadTooLarge extends EventPayloadTooLarge_base { } declare const EventPayloadTooLarge_base: Schema.TaggedErrorClass; } & { event: typeof Schema.String; bytes: typeof Schema.Number; limit: typeof Schema.Number; }>; export declare type EventResumePoint = { readonly origin: string; readonly n: number; }; /** Where a re-attaching subscriber left off, per origin. */ export declare const eventResumePoint: Schema.Struct<{ origin: typeof Schema.String; n: typeof Schema.Number; }>; export declare const eventRoute: (tenantId: string | null, event: string, key: unknown) => string; export declare type EventStreamEvent

= { readonly _tag: 'attached'; } | { readonly _tag: 'event'; readonly origin: string; readonly n: number; readonly emittedAt: number; readonly payload: P; } | { readonly _tag: 'gap'; readonly missed: number; readonly reason: 'buffer' | 'resume'; }; /** * What a subscription emits. `attached` first, then `event`s, with `gap` * interleaved whenever the server can prove a loss. */ export declare const eventStreamEvent:

(payload: P) => Schema.Union<[Schema.Struct<{ _tag: Schema.Literal<["attached"]>; }>, Schema.Struct<{ _tag: Schema.Literal<["event"]>; /** Publishing instance id — serials are only comparable within one. */ origin: typeof Schema.String; /** Monotonic per (origin, event, tenant, key). */ n: typeof Schema.Number; emittedAt: typeof Schema.Number; payload: P; }>, Schema.Struct<{ _tag: Schema.Literal<["gap"]>; /** How many deliveries are known lost. Never a guess: a shortfall is computed * from the requested serial against what the ring still holds. */ missed: typeof Schema.Number; /** `buffer` — evicted before this subscriber could be served. * `resume` — the re-attach asked for a serial older than the ring. */ reason: Schema.Literal<["buffer", "resume"]>; }>]>; /** * The subscription payload: the key, plus where to resume. * * `resume` present ⇒ this is a RE-attach and the client is owed continuity; * absent ⇒ a first attach, which gets nothing older than itself unless the * descriptor opted into `rewind`. The distinction lives on the wire because only * the client knows whether it has seen this stream before — the server cannot * tell a reconnect from a fresh mount. */ export declare const eventSubscribeInput: (key: K) => Schema.Struct<{ key: K; resume: Schema.optional>>; }>; /** * Lift an event descriptor into its rpc. * * A `stream: true` rpc, like a query — the framework's push transport is the * WebSocket a subscription already holds open, and reusing it is why `useEvent` * shares one connection lifecycle, one reconnect policy and one devtools view * with `useSubscription` instead of being a second realtime concept in the same * app. */ export declare const eventToRpc: (descriptor: EventDescriptor, extraErrors?: ReadonlyArray) => Rpc.Rpc>>; }>, Stream; }>, Schema.Struct<{ _tag: Schema.Literal<["event"]>; /** Publishing instance id — serials are only comparable within one. */ origin: typeof Schema.String; /** Monotonic per (origin, event, tenant, key). */ n: typeof Schema.Number; emittedAt: typeof Schema.Number; payload: Payload; }>, Schema.Struct<{ _tag: Schema.Literal<["gap"]>; /** How many deliveries are known lost. Never a guess: a shortfall is computed * from the requested serial against what the ring still holds. */ missed: typeof Schema.Number; /** `buffer` — evicted before this subscriber could be served. * `resume` — the re-attach asked for a serial older than the ring. */ reason: Schema.Literal<["buffer", "resume"]>; }>]>, Schema.Schema.Any | Schema.Schema>, typeof Schema.Never, never>; /** * The WEBHOOK audience of a declared event. * * Present ⇒ `@voltro/plugin-webhooks` treats this event as an outbound one: * subscribers can register HTTP targets for it and every publish is delivered to * them, signed and retried, from the SAME call that fans it out to clients. * * Structurally typed rather than importing the plugin's own types, and that is a * dependency direction rather than a preference: `@voltro/protocol` is * browser-safe and must not reach a plugin. The plugin reads this block and maps * it onto its own knobs — the shapes are the plugin's to define, so a value here * is a plain number or string, never one of its enums. * * Namespaced under `webhook:` rather than spread across the descriptor because * these settings are meaningless to the other three audiences. A `retry` at the * top level would read as if it applied to client delivery, which is * at-most-once by design and has no retry at all. */ export declare interface EventWebhookSpec { /** Human-readable summary for the dashboard's event list. */ readonly description?: string; /** Payload schema version. Bump when subscribers must adapt. Default 1. */ readonly version?: number; /** Shared ceiling across ALL deliveries of this event — the runaway-emit * guard. Over-limit deliveries are deferred, never dropped. */ readonly rateLimit?: { readonly perMinute: number; }; } /** Normalize the `exposeAsTool` shorthand. `true` is only valid when the * descriptor carries a top-level `description`; callers pass that in. */ export declare type ExposeAsTool = boolean | ExposeAsToolSpec; export declare interface ExposeAsToolSpec { /** Shown to the model — REQUIRED to expose (a tool with no description is * unusable). What the tool does + when to call it. */ readonly description: string; /** Require human confirmation of the concrete call before it executes. * Default posture: writes (mutation/action) confirm, reads don't. */ readonly confirm?: boolean; /** Cap how many times the agent may call this tool per run. */ readonly maxPerRun?: number; } /** * Cross-cutting error schemas a plugin contributes to EVERY procedure's * wire error union (see `VoltroPlugin.errorSchemas`). Unioned into each * rpc's `error:` so an interceptor that fails with one of these (e.g. a * rate-limit `RateLimited`) decodes as a TYPED error on the client * instead of crossing the wire as an untyped defect. Passed identically * by the server group (cli/dev.ts) and the generated client group * (cli/codegen.ts) so both ends agree on the wire shape. */ export declare type ExtraErrors = ReadonlyArray; /** Release a claim after the handler errored, so a retry can re-process. */ export declare const failIdempotent: (store: IdempotencyStore, scope: string, key: string) => Promise; /** * Report descriptors whose `guards:` declare a per-resource check that nothing * will enforce. * * `guards: [{ scope: 'teams:write', resource: (i) => i.teamId }]` READS as "may * you write THIS team". Without a registered `setResourceScopeResolver` the * extractor is advisory and the check is against the caller's GLOBAL scopes — * so an app whose authorization is per-team (roles held on a membership row, * subjects carrying no global scopes) gets a guard that is simply wrong, and * nothing says so. * * That is a security-shaped silent downgrade: the declaration looks stricter * than the enforcement. It is also exactly what a downstream app hit, then * reached for the heavier `defineResourcePolicy` path having concluded the * declarative one could not express per-team authorization. * * Returns the offending `` list so a boot can print it. Empty when a * resolver IS registered (the extractors are live) or when no descriptor * declares one. */ export declare const findAdvisoryResourceGuards: (procedures: ReadonlyArray<{ readonly tag: string; readonly guards?: ReadonlyArray; }>) => ReadonlyArray; /** Record a completed response for replay. */ export declare const finishIdempotent: (store: IdempotencyStore, scope: string, key: string, response: IdempotencyResponse, now: number) => Promise; /** A route rendered for humans — logs, the inspect surface, the dashboard. */ export declare const formatEventRoute: (route: string) => string; /** Close code a gateway connection receives when its credential expires — * the same session-expiry contract the rpc socket has (SEC-16), spelled as * an application close code so foreign clients can reauth + reconnect. */ export declare const GATEWAY_CREDENTIAL_EXPIRED_CLOSE_CODE = 4001; /** The currently-registered policy-guard resolver, or `undefined`. */ export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefined; /** The currently-registered resource-scope resolver, or `undefined`. */ export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined; /** Ask for default-deny semantics on one `checkGuards` call. */ export declare interface GuardCheckOptions { /** Refuse a procedure that declares no access decision at all. */ readonly defaultDeny?: boolean; /** The procedure's tag, so the refusal can name it. */ readonly procedure?: string; } export declare interface GuardCheckSpec { readonly scope: string | ReadonlyArray; readonly mode?: 'all' | 'any'; /** * PURE `input → resource id` extractor (the erased runtime form of * `GuardSpec.resource`). When present AND a resource-aware resolver is * registered (`setResourceScopeResolver`), the guard is checked against THAT * resource, not just the caller's global scopes. Absent / no resolver → * global-scope check (the extractor is advisory), exactly as before. */ readonly resource?: (input: unknown) => string | undefined; } /** One or more declarative guards on a descriptor. All must pass (AND across * entries; `mode` controls AND/OR WITHIN one entry's scope array). */ export declare type Guards = ReadonlyArray>; export declare interface GuardSpec { /** Required permission scope(s). A single string, or an array combined by * `mode`. Scope strings are the same values `hasScope` / rbac roles use. */ readonly scope: string | ReadonlyArray; /** How an array of scopes combines. `'all'` (default) = AND (hold every * scope); `'any'` = OR (hold at least one). Ignored for a single scope. */ readonly mode?: 'all' | 'any'; /** * PURE `input → resource id` extractor. Browser-safe (no DB, no server * import) — exactly like `target.identify`. Omit for a plain subject-scope * guard. * * **On a `GuardSpec` this id is ADVISORY.** A scope guard answers "what may * this subject do at all", against the subject's global scope set; the id is * carried for logging and for a future subject-scope resolver that narrows by * resource. It does not, on its own, make the check per-resource. * * **If your authority is per-resource, you want {@link PolicyGuardSpec}, not * this field** — `guards: [{ action, resourceType, resource }]`, backed by * `defineResourcePolicy` + a tuple source you register. That is built, wired * on both boot paths, fail-closed without a resolver, and documented under * *Authentication → Authorization*. An app whose relationships already live * in its own tables (a `teamMembers` row, say) registers its own tuple source * rather than copying data across; see `policyGuardResolver.ts`. * * That paragraph is here because its absence cost a deployment their access * gate. This comment used to describe the resolver as "a future ReBAC / * `accessPolicy()` resolver" — written before the ReBAC path shipped and * never updated. They read the type, quoted the sentence, concluded there was * "nothing in between" declaring an untruth and turning the gate off, and set * `security: { defaultDeny: false }` on an app with 565 undecided procedures. * The capability they needed was two fields away. A doc comment that says * "future" about something shipped is not a small inaccuracy: it is the only * thing a careful reader has, and it argued them out of a feature. */ readonly resource?: (input: Input) => string | undefined; } /** Does this descriptor declare an access decision — a guard, or a deliberate * `openAccess:`? The boot gate's predicate; `false` is the SEC-1 shape. */ export declare const hasAccessDecision: (descriptor: { readonly guards?: ReadonlyArray | undefined; readonly openAccess?: string | undefined; }) => boolean; export declare const hasCallbackRoutes: (s: AuthStrategy) => s is AuthStrategyWithCallback; /** True if the subject holds `scope` (or the `admin:full` bypass), checking * the EFFECTIVE set (role-derived scopes included). Prefer this over * `hasScope` anywhere rbac roles should count. */ export declare const hasEffectiveScope: (subject: Subject, scope: string) => boolean; /** * Does this procedure carry a guard that can actually REFUSE? * * A declared `openAccess:` is an access decision, not a check — nothing can fail * with a `ScopeError`, so it neither widens the wire union nor makes a decode * failure worth annotating. * * One function because it now has two readers, and this repo's per-seam scar is * exactly a predicate that got copied: `withGuardError` decides the wire error * union, and `strictInput`'s label decides whether a decode failure says the * guard did not run. Two copies would eventually disagree about `openAccess:`, * and the disagreement would surface as a procedure that says "guarded" while * declaring no `ScopeError` — or the reverse. */ export declare const hasEnforcedGuard: (guards: DeclaredAccess | undefined) => boolean; /** True if the subject holds `scope` (or the `admin:full` bypass). Checks only * the RAW subject scopes — use `hasEffectiveScope` to include rbac roles. */ export declare const hasScope: (subject: Subject, scope: string) => boolean; /** * Short-circuit response shape. Returning a `HttpInterceptResponse` * from the interceptor halts the pipeline — the framework sends the * response and never invokes the downstream handler. * * Returning `null` (or calling `next()` and returning its result) * lets the request continue to the normal pipeline. */ export declare interface HttpInterceptResponse { readonly status: number; readonly body?: string; readonly headers?: Readonly>; } /** * Per-call context passed to HTTP-request interceptors. Fires BEFORE * auth-resolution, BEFORE rpc-routing, BEFORE inspect — at the very * top of the HTTP pipeline. Use for pre-auth concerns: rate-limit * (token bucket per IP), geo-block (451), bot-detection, header * injection for downstream observability, CORS-override. * * The interceptor is NOT an auth replacement — `AuthMiddleware` still * runs on the rpc path. If you need to GATE auth, wrap a different * surface (`interceptAction` for unary calls, `interceptMutation` for * writes). HTTP-interceptors run on EVERY HTTP request including * inspect endpoints and the rpc websocket upgrade. */ export declare interface HttpRequestContext { /** HTTP method (`'GET'`, `'POST'`, etc.). */ readonly method: string; /** Request path (no query string). */ readonly path: string; /** Lowercased request headers. */ readonly headers: Readonly>; /** * The client address, resolved through the app's `security.trustedProxies` * policy — the SAME value `PluginHttpRouteRequest.remoteAddr`, the rate * limiter, the geo-block and every audit row use (`resolveClientAddress`). * Use this, never `headers['x-forwarded-for']`. * * This is a pre-auth shield's whole key, so getting it from the header is * not a smaller mistake here than elsewhere — it is the one place it is * worst. `x-forwarded-for` is a request header: any client can write it, so * a token bucket keyed on it is bypassed by one extra header per request. * The resolution here ignores the header entirely unless a trusted proxy is * declared, and then believes only the hops that are one. * * `undefined` when the socket address is unavailable (a unix socket, an * in-process test harness that constructs the request by hand). */ readonly remoteAddr: string | undefined; } /** * Wraps every HTTP request before the framework's auth + routing. * Composed across plugins in declaration order — first listed = * outermost. The chain short-circuits as soon as a plugin returns * a `HttpInterceptResponse`; later plugins don't see the request. * * Plugin MUST declare the `http:intercept` permission or boot fails * loudly. Plugins without that permission get their `onHttpRequest` * hook stripped at activation (so the rest of the plugin still * works). */ export declare type HttpRequestInterceptor = (next: () => Promise, ctx: HttpRequestContext) => Promise; export declare type IdempotencyOutcome = { readonly kind: 'fresh'; } | { readonly kind: 'replay'; readonly response: IdempotencyResponse; } | { readonly kind: 'conflict'; }; export declare interface IdempotencyRecord { readonly scope: string; readonly key: string; readonly status: 'in_flight' | 'completed'; readonly response: IdempotencyResponse | null; /** epoch ms */ readonly createdAt: number; } export declare interface IdempotencyResponse { readonly status: number; readonly body: unknown; } /** Build the scope key — keep keys from colliding across tenants + endpoints. */ export declare const idempotencyScope: (tenantId: string | null | undefined, method: string, path: string) => string; export declare interface IdempotencyStore { /** Current record for (scope, key), or null. Read-side (inspect/dashboard). */ readonly get: (scope: string, key: string) => Promise; /** * Atomically claim (scope, key) IF absent OR stale (older than `ttlMs`): * write a fresh `in_flight` row and return `'claimed'`. Otherwise return the * EXISTING (still-fresh) record. The atomicity here is the whole game — two * concurrent same-key requests both hit this, exactly one gets `'claimed'`, * the loser reads the winner's record. In SQL this is one * `INSERT … ON CONFLICT DO UPDATE … WHERE existing.createdAt < now - ttl`. */ readonly claim: (scope: string, key: string, now: number, ttlMs: number) => Promise<'claimed' | IdempotencyRecord>; /** Flip the claim to `completed` and store the response. */ readonly complete: (scope: string, key: string, response: IdempotencyResponse, now: number) => Promise; /** Drop the claim (handler errored → a retry may re-process). */ readonly release: (scope: string, key: string) => Promise; } /** `/` — the op path for a row id. Ids are JSON-pointer-escaped * (`~` → `~0`, `/` → `~1`) so a slash or tilde inside a string id can't * corrupt the pointer. */ export declare const idToPath: (id: RowId) => string; /** * `Subject.metadata` is the APP's bag — opaque to the framework, stored * verbatim — with exactly one reserved key: this one. It carries the mark that * says the session is an impersonated one. * * **Why it lives in protocol and not in `@voltro/plugin-auth`, which mints it.** * Two packages need the answer to "is this session impersonated" and they must * not depend on each other: plugin-auth WRITES the mark, and plugin-audit reads * it to stamp `AuditEvent.impersonation` before redaction runs. A key spelled * out in both is a second definition that no guard is watching — the shape this * repo has been bitten by often enough to have a rule about it. `Subject` is * protocol's type, so its reserved key is protocol's too. */ export declare const IMPERSONATION_METADATA_KEY: "impersonation"; /** * The title a decode failure carries — and, on a GUARDED procedure, the one * sentence that stops the failure being read as "the guard did not fire". * * ── Why this sentence exists ─────────────────────────────────────────────── * * The ordering above is not fixable at this seam: the payload decodes before * the handler, so a guard on a procedure with a malformed payload never runs. A * consumer checking a guard on 2026-08-16 called such a procedure with an * incomplete payload, got a decode error instead of a `ScopeError`, and * concluded the guard was not being applied. It was; it never got the chance. * With a complete payload the picture flipped immediately. * * That is the more dangerous of the two misreadings — it says "unprotected" * about something protected — and it is the one the ordering makes easy. So a * guarded procedure says so in the title: * * workAreas.create input (guarded — the guard did NOT run: the payload * failed to decode first, so this says nothing about access) * └─ ["type"] └─ is missing * * It discloses nothing new. That a procedure is guarded is already visible to * any caller who sends a VALID payload and receives `ScopeError`, and the * message names no scope, no resource and no field type. */ export declare const inputLabel: (procedure?: string, guarded?: boolean) => string; export declare interface InsertTarget> extends NestedTargetFields { readonly table: string; readonly op: 'insert'; readonly order?: 'prepend' | 'append' | undefined; /** * Build the optimistic row from the mutation input. The framework * injects `id` (from `optimisticId`) and the `optimistic: true` flag * around the return — your `shape` returns ONLY the user-controllable * row body. Default when omitted: `{ ...input, id, optimistic: true }`. * * Return type intentionally excludes `id`: it's server-generated; * the optimistic id placeholder is the framework's responsibility. * Excluding `optimistic` (also framework-injected) follows the same * principle — `Omit` lets you describe * EVERYTHING ELSE without re-stating the bookkeeping fields. * * The `optimisticId` parameter is exposed for rare cases where the * shape function needs to reference it (e.g., setting a foreign key * on a child row spread in the same optimistic insert). For a NESTED * (`path`) insert, use `shapeItem` instead — it is typed to the item. */ readonly shape?: ((input: Input, optimisticId: string) => Omit) | undefined; /** NESTED (`path`) insert: build the ITEM to insert, typed to the item (not * the output). 2nd arg is the optimistic id. */ readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined; } export declare const isEventDescriptor: (value: unknown) => value is AnyEventDescriptor; /** True when every row in the set carries an `id`. The dispatcher uses * this to decide patch-vs-full-snapshot: a result whose rows aren't * id-keyed (rare — a custom projection that drops the id) can't be * diffed by id and falls back to shipping the full data. */ export declare const isIdKeyed: (rows: ReadonlyArray>>) => rows is ReadonlyArray; /** Runtime narrowing for the declared-open variant. */ export declare const isOpenAccess: (g: AnyCheckSpec) => g is OpenAccessSpec; /** Runtime narrowing for the relationship variant. */ export declare const isPolicyCheck: (g: AnyCheckSpec) => g is PolicyCheckSpec; /** Narrow a guard entry to the relationship variant. */ export declare const isPolicyGuard: (g: AnyGuardSpec) => g is PolicyGuardSpec; /** Whether `value` is a channel object (not its key). */ export declare const isReactivityChannel: (value: unknown) => value is ReactivityChannel; /** * Whether `key` is addressed to the channel namespace. * * A PREFIX test, deliberately not a registry lookup — the two answer different * questions and only one of them belongs on the delivery path. At runtime the * question is "is this a table name or a channel key", and getting it wrong in * the strict direction drops a real push. Whether the channel was DECLARED is a * boot question, and `undeclaredChannelKeys` answers it there — the same * asymmetry tables already have (an unregistered table name still delivers; * the boot audit is what reports it). */ export declare const isReactivityChannelKey: (key: string) => boolean; export declare const isSystemSubject: (subject: Subject) => boolean; /** * Is this descriptor allowed on the wire at all? * * ONE predicate, imported by every place that assembles an rpc group — the * codegen's client group, `voltro dev`'s, and `voltro serve`'s. Those are three * INDEPENDENT assembly paths, and a security boundary honoured by two of them is * worse than one honoured by none: the docs would say "internal", the browser * would agree, and production would still route the tag. That is the dev/serve * drift class this repo already has a maintainer rule about, applied to a * surface where the failure is silent and exploitable. * * `procedureWireReachabilityParity.test.ts` reads the source of all three and * fails if any assembles a group without consulting this. */ export declare const isWireReachable: (descriptor: { readonly internal?: boolean | undefined; }) => boolean; /** * Build a scope cache. Hand it to `composeAuthStrategies({ scopeCache })` and * keep the handle: `invalidate(scopeCacheKey(subject))` from the mutation that * grants or removes a role is what makes the staleness window zero. * * `composeAuthStrategies` builds one internally when you don't, so the default * posture is cached-with-a-window rather than a store read per request — but a * cache nobody holds cannot be invalidated, which is why this is exported. */ export declare const makeScopeCache: (options?: ScopeCacheOptions) => ScopeCache; /** * How large one encoded envelope may be, on EVERY dialect. * * Postgres `NOTIFY` dies past 8000 bytes and the other transports have no such * bound. Enforcing the limit only where the wire imposes it would make "switch * the broadcast provider" a silent behaviour change — an app developed against * Redis would start failing when it moved to the pg-native path, at the worst * possible moment. So the smallest transport's ceiling is the framework's * ceiling, minus room for the envelope around the payload. * * An oversized payload is also a design smell in its own right: an event says * that something happened, so `photo.added` carries a photo REFERENCE, and the * consumer fetches the photo through a route that can stream, cache and * authorize it. */ export declare const MAX_EVENT_ENVELOPE_BYTES = 7500; /** In-memory store — the default for single-process dev + the test double. */ export declare const memoryIdempotencyStore: () => IdempotencyStore; /** The refusal a missing access decision produces. Exported so the boot gate * and the call-time path cannot describe the same defect differently. */ export declare const missingAccessDecision: (procedure?: string) => ScopeError; export declare interface MutationProcedureDescriptor { readonly kind: 'mutation'; readonly name: Name; readonly input: Input; readonly output: Output; readonly error: Error; /** Declarative target(s) — drives auto-optimistic on the client AND * surfaces which tables this mutation touches (debug, future * query-invalidation analytics). Mutations without a target run * normally but skip auto-optimistic. */ readonly target: Target | undefined; /** Declarative authorization guard(s) — enforced before the transaction * opens, failing with a typed `ScopeError`. Absent → no framework-level * authz (author gates in-handler, or the mutation is unguarded). */ readonly guards: DeclaredAccess | undefined; /** The declared reason this procedure needs NO authorization check — * `openAccess: ''`. Mutually exclusive with `guards`; together they are * the only two shapes `security.defaultDeny` accepts. */ readonly openAccess: string | undefined; /** Opt this mutation into a public REST endpoint (innovation/11). */ readonly publicApi: PublicApiSpec | undefined; /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */ readonly exposeAsTool: ExposeAsTool | undefined; /** Require a SECOND human to approve before this mutation takes effect. The * gate runs in the dispatch spine after `guards:` and before the transaction * opens; the pending intent is a durable `_voltro_approvals` row. */ readonly requiresApproval: AnyApprovalPolicy | undefined; /** True when the procedure is kept OFF the wire — no client-group entry and no * route in dev or serve. See `internal` on the definer's options. */ /** * Replace a PLUGIN route that answers to this same tag. * * Without it, a user route and a plugin route sharing a tag is a hard error, * and correctly so — two handlers behind one name is not a thing a caller can * reason about. But refusing is the wrong answer when the app deliberately * wants its own version: the two escapes available otherwise are to rename * your procedure (so the split runs along "who built it" rather than along a * domain boundary) or to `alias` the whole plugin away (same, one level up). * For a frontend developer that is the worst possible partition. * * A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose * surface is richer than theirs, add `archive`/`unarchive` beside it — which * already composes, since the collision check compares FULL tags and not * prefixes — and replace `markRead`, because theirs maintains archive state. * * Explicit, never inferred. Silently letting the app win would mean a plugin * upgrade that adds a route could shadow an app procedure with no diff to * read; declaring it makes the intent reviewable and puts the override in the * file that performs it. */ readonly overridesPlugin: boolean | undefined; readonly internal: boolean | undefined; } export declare const mutationToRpc: (descriptor: MutationProcedureDescriptor, extraErrors?: ExtraErrors) => Rpc.Rpc : Input, Output, Schema.Schema.All, never>; export declare interface NestedTargetFields { /** Dot-path to the nested array within the query VALUE to patch (e.g. * `'snapshot.projects'`). Absent → patch the top-level row array (default). */ readonly path?: string | undefined; /** Item key within the nested array (default `'id'`). Only meaningful with * `path`. */ readonly by?: string | undefined; /** Guard WHICH cached query entries this target patches: only entries whose * CURRENT value satisfies the predicate. Pure + browser-safe. Prevents a * patch from bleeding across sibling subscriptions that share a source table * (the guard AWB hand-writes as `roadmap.id === input.roadmapId`). Absent → * every entry matching the target `table` is patched. */ readonly match?: ((value: unknown, input: Input) => boolean) | undefined; } /** * Project a runtime descriptor down to the subset the client needs. * Functions (`shape`, `identify`) are passed through by reference — * the client's auto-optimistic uses them as-is when present. */ export declare const normalizeDescriptor: (descriptor: ProcedureDescriptor) => ClientDescriptor; /** * A `source:` as the DESCRIPTOR stores it — channels resolved to their keys, * with the caller's shape preserved. * * Shape-preserving on purpose: a single `source: 'notes'` must stay the string * `'notes'` and not become `['notes']`. It is serialised into the capability * manifest and into every api golden, so widening the shape would rewrite those * artefacts for every query in every app to express nothing. */ export declare const normalizeSource: (source: ReactivitySource | ReadonlyArray | undefined) => string | ReadonlyArray | undefined; /** * What a plugin contributes to the OTel layer via `contributeObservability`. * The OTel types are intentionally `unknown` so this browser-safe protocol * package never imports `@opentelemetry/*` (the cli casts to the concrete * `SpanProcessor` / `MetricReader` / `Sampler` when wiring `buildTracingLayer`). */ export declare interface ObservabilityContribution { /** Merged into the OTel Resource (e.g. `service.version`, * `deployment.environment`). */ readonly resourceAttributes?: Record; /** Additional OTel `SpanProcessor`s installed alongside the framework's * exporter + buffer sink (e.g. a vendor OTLP exporter or `SentrySpanProcessor`). */ readonly spanProcessors?: ReadonlyArray; /** Additional OTel `MetricReader`s (e.g. a vendor OTLP metric reader). */ readonly metricReaders?: ReadonlyArray; /** OTel `Sampler` (e.g. Sentry's `SentrySampler` honouring `tracesSampleRate`). */ readonly sampler?: unknown; } /** * The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision * that this procedure needs no authorization check, and the reason. * * It is a guard entry rather than a bare descriptor field on purpose. Every * enforcement path in the framework — `servePipeline`'s `enforceGuards`, * `bindStream`, `bindEvent`, `@voltro/testing`'s `invoke` — is handed the * `guards` ARRAY and nothing else. A decision that does not live in that array * is invisible to all of them, so "guarded" and "deliberately open" would be * distinguishable in the source and identical at the point that enforces. * * It always passes. The value is the WHY, and the why is the point: it is what * a reviewer reads, what `voltro doctor` prints, and what makes an open * procedure a decision somebody made rather than a field somebody forgot. */ export declare interface OpenAccessSpec { /** Why this procedure is callable without an authorization check. Non-empty * by construction — `defineQuery` & co. refuse an empty reason. */ readonly open: string; } /** Build the erased `openAccess:` entry. The definers call this; an app writes * `openAccess: ''` on the descriptor and never sees the spec. */ export declare const openAccessSpec: (reason: string) => OpenAccessSpec; /** Split a route back into its three parts. */ export declare const parseEventRoute: (route: string) => { readonly tenantId: string | null; readonly event: string; readonly key: string; }; /** A single row in a subscription result. Must carry an `id`; everything * else is opaque to the patch layer. */ export declare type PatchRow = Readonly> & { readonly id: RowId; }; /** Inverse of `idToPath` for the string case. Numeric ids round-trip via * the `order` array (which preserves the original type), so apply never * needs to re-parse a number out of a path. */ export declare const pathToId: (path: string) => string; export declare const PendingApproval: Schema.Struct<{ id: typeof Schema.String; /** The rpc tag whose execution is pending. */ procedure: typeof Schema.String; kind: Schema.Literal<["mutation", "action"]>; /** Subject id of whoever asked. Never the approver. */ requestedBy: Schema.NullOr; status: Schema.Literal<["pending", "approved", "rejected", "expired", "consumed"]>; /** The declared reason from the descriptor, if any. */ reason: Schema.NullOr; /** Scope(s) an approver must hold. */ requiredScopes: Schema.Array$; requestedAt: typeof Schema.String; expiresAt: typeof Schema.String; decidedBy: Schema.NullOr; decidedAt: Schema.NullOr; /** The approver's note, when they left one. */ note: Schema.NullOr; /** * How this row relates to the CALLING subject — the whole point of the feed. * `'to-decide'` = they may act on it; `'requested'` = they asked for it. * A row is never both: self-approval is refused, so a requester never * qualifies as its approver. */ relation: Schema.Literal<["to-decide", "requested"]>; }>; export declare type PendingApproval = Schema.Schema.Type; /** * Called once at app boot, after `voltro dev` resolves the plugin * list and before the rpc server starts accepting connections. Use * for warming caches, opening connection pools, registering metrics, * or any one-shot setup the plugin needs. Throws abort boot — * malformed plugin config should surface as a clear, immediate error. */ export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect.Effect | Promise | void; /** * Server-only context delivered to `bindDataStore`, ALONGSIDE the store, * AFTER the framework has opened its store + pool. Gives a plugin the * framework's ALREADY-OPEN handles so it never rebuilds a pg pool from * raw env (the anti-pattern `@voltro/plugin-ratelimit` used to hand-roll) * and never runs an un-coordinated per-replica `setInterval` sweep (what * presence + governance used to do). * * These fields are SERVER-only — they are TYPES only in this browser-safe * protocol package (the `DataStore` / `SqlClient` imports are erased), and * the live values exist only inside the serve pipeline that calls * `bindDataStore`. Nothing in the browser-loaded descriptor/schema graph * touches them. */ export declare interface PluginBindContext { /** * This process's replica identity — the same value the event bus stamps as * its publish `origin` and the membership registry announces under. * * ONE id across all three on purpose: a plugin holding state per replica * (presence is the case) must be able to correlate "who owns this" with "is * that one still alive", and three identities for one process would make the * correlation quietly wrong rather than obviously broken. */ readonly instanceId?: string; /** * Live instance membership — subscribe to learn when a replica joins, leaves * or restarts. * * Present only when the host provides one. A plugin holding per-replica state * needs it and cannot derive it: pub/sub delivers messages, it does not report * who is on the channel, and an instance that dies simply goes quiet. */ readonly membership?: { readonly onChange: (listener: (event: { readonly kind: 'joined' | 'left' | 'restarted'; readonly instanceId: string; }) => void) => () => void; }; /** * The app's cross-replica broadcast transport, when one is configured. * * Absent ⇒ single instance, which is a complete answer rather than a * degraded one. A plugin must not branch on "do we have a cluster" — that * branch is how a feature comes to work in dev and differ in production. */ readonly broadcast?: { readonly publish: (channel: string, payload: string) => unknown; readonly subscribe: (channel: string, handler: (payload: string) => void) => unknown; }; /** * The framework's already-open `SqlClient` (from `@effect/sql`) — the * SAME pool the app's store uses. A plugin runs raw SQL through this * instead of standing up its OWN `ManagedRuntime` + pool from env. Only * present for SQL-backed stores; `undefined` on the in-memory store * (there is no SQL engine) — guard with `if (ctx.sql)`. */ readonly sql?: SqlClient.SqlClient; /** * Run `effect` every `intervalMs` on ONLY ONE replica per tick — * cluster-coordinated via the framework's existing claim-table * exactly-once gate (the same seam the cron scheduler uses). Replaces * the hand-rolled `setInterval` a plugin used to run inside * `bindDataStore`, which fired on EVERY replica un-coordinated. On a * single-process / memory / sqlite deployment it simply runs every tick * locally (correct — one process needs no fan-out dedup). * * `name` is the task's stable id (namespace it, e.g. `presence.sweep`); * it becomes the claim key. Returns a handle whose `stop()` cancels the * task early. The framework also stops every armed task at shutdown. */ readonly scheduleCoordinated: (name: string, intervalMs: number, effect: () => void | CoordinatedTickOutcome | Promise, options?: CoordinatedTaskOptions) => CoordinatedScheduleHandle; } /** * Post-commit change event delivered to a plugin's `onChangeEvent` tap. * Minimal, browser-safe shape (protocol must not depend on the database * package's concrete `ChangeEvent`) — the framework maps its internal event * to this before fan-out. */ export declare interface PluginChangeEvent { readonly table: string; readonly op: 'insert' | 'update' | 'delete'; /** Row after the change — present on insert + update, null on delete. */ readonly new: Record | null; /** Row before the change — present on update + delete, null on insert. */ readonly old: Record | null; /** * Set when the transport could not carry this change's images and they were * RECONSTRUCTED — a row over postgres' 8000-byte `pg_notify` cap. Absent on * every ordinary event. * * A tap must read it before trusting an image as a snapshot: * * - `'rehydrated'` — `new` is the row RE-READ from the database. Correct to * index, mirror or forward; NOT necessarily the image the write that * fired this event produced (a later write may already have landed). * - `'tombstone'` — a delete whose `old` is the PRIMARY KEY and nothing * else. Enough to remove the row; never a record of what it contained. * A history/versioning tap must not store it as a snapshot. * - `'unrecovered'` — both images are null and the content is gone. The * change happened; re-read or resync if you need it. */ readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered'; /** How the event reached this process: absent/'inline' = this process's * own write; 'injected' = delivered over a cross-instance transport * (broadcast bus / CDC consumer). Combine with `changeScope` to act * exactly once per change fleet-wide. */ readonly origin?: 'inline' | 'injected'; /** * The trace this write happened under — the SAME id `auditPlugin` records as * `AuditEvent.traceId`. This is the correlation bridge: it is what lets a tap * that records WHAT changed be joined to the audit trail that records WHO * called and whether they were refused. * * Absent means the write had no request behind it (a seed, a schedule, a * startup hook) or arrived from another replica, where stamping the local * ambient trace would attribute a remote write to a local call. */ readonly traceId?: string; /** * The acting identity behind the write — the same one `audit()` stamps into * `createdBy`/`updatedBy`, so a row's stamp and its change event agree. * `null` = a resolved subject with no acting user; `undefined` = no request. */ readonly subjectId?: string | null; /** * The rpc tag of the call that caused the write (`teams.removeSubTeamMember`). * * The THIRD copy of this shape (`WriteAttribution` → `ChangeEvent` → here), so * a field added to the others and forgotten here is invisible to every plugin * — which is the tap's whole audience. `traceId` says which call; this says * which call it WAS, and a row diff cannot supply it: the same delete on a * join table is a member removal, a cascade or an expiry. */ readonly procedure?: string; /** * The write was made BY AN AGENT acting as `subjectId` — the fourth member of * the shape the comment above tracks (`WriteAttribution` → `ChangeEvent` → * here). A tap that logs "who changed this" reads WRONG without it: the * subject is the person the agent acted as, by construction, so an agent * write and a human write are otherwise identical rows. */ readonly via?: 'agent'; /** The store's change visibility (see `DataStore.changeScope`): * 'local' — own writes inline (skip origin 'injected' for * exactly-once-on-the-writer); 'fleet' — every replica sees the full * stream (elect one worker instead). */ readonly changeScope: 'local' | 'fleet'; } /** * Plugin-supplied codegen contribution. Emitted into the user app's * `rpcGroup.generated.ts` as an extra section AFTER the framework's * own output. Use for typed bindings that don't fit the rpc wire — * helper exports, type re-exports, accessor functions that wrap the * plugin's runtime apis. * * The framework calls this function once per codegen run. Returns: * - a string → emitted verbatim * - `null` → contributes nothing * * The function MUST be pure: it gets the plugin name + the api name * + the list of discovered rpc-tag-and-kind tuples (for plugins that * want to emit per-rpc bindings). No filesystem / network access — * codegen runs as a build step on every dev restart. * * Typical pattern: emit a typed accessor for the plugin's service Tag: * * codegen: (ctx) => ` * export const use${capitalize(ctx.pluginAlias)} = () => * (ctx: AppContext) => ctx[${JSON.stringify(ctx.pluginAlias)}] * ` * * The output is wrapped between `// ` / `// ` * markers in the generated file so the user can identify the source. */ export declare type PluginCodegen = (ctx: PluginCodegenContext) => string | null; export declare interface PluginCodegenContext { /** The plugin's full name (`@voltro/plugin-audit`). */ readonly pluginName: string; /** The derived alias (`audit`). */ readonly pluginAlias: string; /** The api app's name from `app.config.ts`. */ readonly apiName: string; /** Flat list of rpc tags discovered in the app (user routes only). */ readonly rpcTags: ReadonlyArray<{ readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'workflow'; readonly tag: string; }>; } /** * A remote-mounted dashboard surface a plugin contributes. The * dashboard-host (voltro-cloud / voltro-devtools) renders the mount * by `import()`ing the ESM module at `bundleUrl` and calling the * exported component with host-provided context (auth subject, * routing, theme tokens). * * Not iframe — true in-process mount. Trade-offs: * - Plugin's bundle MUST be ESM with React/Effect as peerDeps; the * host pins the versions. Mismatch logs a warning + still mounts. * - No CSS isolation. Plugin authors are expected to use scoped * Tailwind classes (the dashboard ships the framework token set) * or CSS modules. Inline `style` on a top-level wrapper avoids * bleeding host styles in. * - Plugin code shares the host JS realm — honor-system sandboxing. * Mounts are gated by the `dashboard:mount` permission to keep * the operator's audit trail explicit. * * The framework surfaces declared mounts via * `/_voltro/inspect/plugins/dashboard-mounts`. The actual mount * runtime ships in voltro-cloud-dashboard + voltro-devtools. */ export declare interface PluginDashboardMount { /** Stable id within the plugin (kebab-case). Full id surfaces as * `:` in the dashboard's mount registry. */ readonly id: string; /** * Where the dashboard host should mount this contribution: * - `'page'` — full-page mount under `/plugins//` * - `'widget'` — card-shaped tile on the dashboard home * - `'nav'` — sidebar nav item linking to a `page`-mount route */ readonly slot: 'page' | 'widget' | 'nav'; /** Required for `slot: 'page'`. Path is mounted under * `/plugins//`. Ignored for widget/nav. */ readonly route?: string; /** Human-readable label (nav text, page title, widget header). */ readonly label: string; /** Optional icon — emoji or a `lucide-react` icon name. */ readonly icon?: string; /** * Absolute URL the host fetches at mount time. Must serve an ESM * module. CDN-hosted or self-hosted (`https://my-plugin.example/v1/bundle.mjs`). * The plugin author is responsible for HTTPS + immutable versioning. */ readonly bundleUrl: string; /** Export name to read from the imported module. Defaults to * `'default'`. The export must be a React component. */ readonly exportName?: string; /** Optional dashboard-framework version range the mount supports * (npm-semver). Host logs a warning on mismatch but mounts. */ readonly dashboardVersion?: string; } /** * Called once at graceful shutdown (SIGTERM in production; HMR-reload * in dev). Mirror of `onActivate`: close connections, flush buffers, * cancel timers. The framework waits up to 5s for `onDeactivate` to * settle before SIGKILL falls through. */ export declare type PluginDeactivateHook = (ctx: PluginLifecycleContext) => Effect.Effect | Promise | void; /** * An environment variable a plugin reads. DECLARATION ONLY — metadata, not a * read path: the plugin still reads the value itself (typically * `options.X ?? process.env.X`). Declaring it makes the plugin's env needs * visible to the env manifest (`/_voltro/inspect/env`), the generated * `.env.example`, and the dashboard Env panel — so an operator can see, before * running, exactly what each plugin expects. * * Structurally identical to `@voltro/env`'s `DeclaredEnvVar` (kept local here * to avoid a protocol → env package dependency). */ export declare interface PluginEnvVar { /** The env var name, e.g. `DD_API_KEY`. */ readonly name: string; /** True when the plugin cannot function without it (no sensible default). */ readonly required: boolean; /** True for tokens / passwords / credential URLs — the manifest then * reports `isSet` only, never the value. */ readonly secret: boolean; /** One-line description — shown in `.env.example` + the manifest. */ readonly description?: string; /** Example value for `.env.example`. Never a real secret. */ readonly example?: string; } /** * A cross-cutting error schema a plugin merges into EVERY procedure's * wire error union. Use when a plugin interceptor can fail with a typed * error (e.g. a rate-limit `RateLimited`) that must decode TYPED on the * client rather than crossing as an untyped defect. * * Two halves because the error must be applied in two places that can't * share a value: * - `schema`: the live `effect/Schema` — used at runtime when the cli * assembles the SERVER rpc group. * - `import`: where to import the same schema from — used by codegen to * emit the import into the generated CLIENT rpc group, so both ends * agree on the wire shape. `name` must be the exported identifier. * * Requires the plugin to ship the schema as a NAMED export from `module`. */ export declare interface PluginErrorSchema { readonly schema: Schema.Schema.All; readonly import: { readonly module: string; readonly name: string; }; } /** A public raw-HTTP route a plugin serves on the framework listener. */ export declare interface PluginHttpRoute { /** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS * are first-class — the REST desugar used to mount `'*'` partly BECAUSE * this union lacked PATCH; that reason is gone (the `'*'` mount remains * for its other job: one dispatcher per shared path + a precise 405). */ readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; /** Absolute path prefix, e.g. `/_voltro/storage`. Matches the path AND * any sub-path (`/_voltro/storage/abc123`). */ readonly path: string; readonly handle: (req: PluginHttpRouteRequest) => Promise; /** Per-route body cap override (bytes) — wins over the listener's shared * `maxBodyBytes`. NOTE: routes SHARING a path share one body read, so the * widest override on the path's group applies to the whole group. */ readonly maxBodyBytes?: number; /** * Opt this route's path OUT of the listener's cross-site origin check. * * Every state-changing request (anything but GET/HEAD/OPTIONS) is * origin-checked by default, because the default assumption has to be that a * route can be reached with the browser's ambient session cookie — and a * route that can is CSRF-reachable. Declaring `'exempt'` is a claim that this * route CANNOT be: its caller must present something a browser will not * attach cross-site. * * The test to apply, and it is the only one: * * > If an attacker's page makes a browser send this request with the * > victim's cookies attached, does anything happen? * * If the answer is "no, the request still needs a signature / a bearer token * / a signed ticket the attacker does not have", the route is exempt. * Otherwise it is not, and no amount of "but it is behind the dashboard" * makes it so. * * The first-party exemptions and why each qualifies: * - `@voltro/plugin-sso-saml` `/saml` — the IdP delivers the assertion as a * genuine cross-site browser form POST; authority is the signed * SAMLResponse, not the cookie. * - `@voltro/plugin-storage` `/…/upload` + `/…/upload/resumable` — a signed * upload ticket in the query string, and the route ships its own CORS * allowlist because a cross-origin upload is the point. * - `@voltro/plugin-billing` `/billing/webhook` — an HMAC-verified provider * callback. * - `@voltro/plugin-scim` `/scim/v2` — bearer-only, refuses to mount without * a token. * * Granularity is the PATH PREFIX the route mounts, not the sub-path its * handler branches on: exempting `/saml` exempts `POST /saml/anything`. */ readonly originGuard?: 'exempt'; } /** * A binary streaming body — the download/export shape. The serve layer pipes * the Web ReadableStream to the socket without buffering, so a response * larger than the heap is fine; the LAZY thunk form defers opening the * source (a provider connection, a file handle) until the response actually * streams. */ export declare interface PluginHttpRouteByteStream { readonly stream: ReadableStream | (() => ReadableStream); /** Declared up front when known — lets the client render progress. */ readonly contentLength?: number; /** e.g. `attachment; filename="export.zip"`. */ readonly contentDisposition?: string; } export declare interface PluginHttpRouteRequest { readonly method: string; /** Path WITHOUT query string. */ readonly path: string; /** Query string WITHOUT the leading `?` (empty when absent). Parse with * `new URLSearchParams(req.query)`. */ readonly query: string; readonly headers: Record; readonly rawBody: Uint8Array; /** * The app's DataStore, for a route that must read or write to do its job. * * The same seam as `AuthStrategyInput.store`, one layer over — and the report * that produced it is the same file. An adopter's `auth/db.ts` has five * consumers: two are strategies and collapsed onto `input.store`; three are * plugin HTTP routes and could not, so the second `ManagedRuntime` + * `MysqlClient` stayed for them. * * Login is the sharpest case and it is not exotic: it MUST write (the session * row), it cannot be an rpc mutation because it is what mints the cookie, and * it is a documented first-class pattern — `@voltro/plugin-auth` ships * `handleSignIn` / `handleSignUp` and the reference consumer mounts them here. * Every app that does so needed a store this contract did not give it. * * Unlike a handler's `ctx.store`, this is the BOOT store: routes are mounted * before any request exists, and it arrives through the same lazy getter the * auth chain uses. `undefined` only while the store is still being built and * on an app with no store — answer the request rather than throwing. * * The line is **everything that does not need a Subject** — not "less than * `ctx.store`". `AuthStrategyInput.store` draws it the same way. Spelled out, * because naming only ONE of the absences invites the reader to assume the * rest are present, and a team porting raw SQL onto this seam did exactly * that: * * | Behaviour | here | * |--------------------------------------------|------| * | `.encrypted()` columns decrypt / encrypt | YES | * | Array columns round-trip on non-native dialects | YES | * | Tenant scope | no | * | **Soft-delete filter (`deletedAt IS NULL`)** | **no** | * | Audit-column stamping | no | * | Row-level security | no | * * The four `no`s need a resolved Subject and a route serves raw HTTP without * one, so a route reading tenant-owned rows must derive and apply that scope * itself. That is the price of the surface being raw, and it is why an rpc * procedure remains the better place for anything that CAN be one. * * The soft-delete row is the one worth reading twice if you are porting: a * read here behaves like your raw SQL did and returns tombstones. Nothing * silently starts hiding rows, so a lookup that must see a soft-deleted user * — a login that revives one, say — needs no opt-out. (On the REQUEST store, * where the filter is applied, `.withDeleted()` is the opt-out.) * * Reading a plugin's OWN tables here is a supported use: they are declared * through `extendSchema` like any other, so `getTable(name)` finds them and * this store reads them. */ readonly store?: DataStore; /** * The client address, resolved through the app's `security.trustedProxies` * policy — the SAME value the rate limiter, the geo-block and every audit row * use (`resolveClientAddress`). Use this, never `headers['x-forwarded-for']`. * * `x-forwarded-for` is a request header: any client can write it. Reading it * raw means a caller picks the IP that lands in your `sessions.ipAddress` * column, which is the one field a breach investigation leans on. Three * first-party routes did exactly that until SEC-8 was extended down to this * surface. The resolution here ignores the header entirely unless a trusted * proxy is declared, and then believes only the hops that are one. * * `undefined` when the socket address is unavailable (a unix socket, an * in-process test harness that constructs the request by hand). */ readonly remoteAddr?: string | undefined; } /** * One HTTP endpoint a plugin contributes under the framework's * `/_voltro/inspect/*` introspection surface. Plugins use this to * surface tooling / dashboards that don't fit the rpc wire (e.g. * Server-Sent-Event streams of plugin-internal state, on-demand * health probes, plugin-specific debug dumps). * * Path convention: `/_voltro/inspect/plugins//`. * The plugin slug (derived from `plugin.name` with `@scope/` + * `plugin-` stripped, KEBAB-CASE — `plugin-cdc-out` → `cdc-out`, * instance suffix `#x` → `--x`) is prepended by the framework so * plugin endpoints never collide with the framework's own inspect * endpoints OR with another plugin's. Kebab (not the camelCase rpc * alias) because it's a URL every dashboard fetches. The dashboard * lists every plugin's endpoints grouped by plugin in the manifest. * * Plugin endpoints inherit the SAME auth resolver the framework's * own inspect endpoints use (`VOLTRO_INSPECT_TOKEN` by default; * customisable by passing an `authResolver` at the http-handler * layer). The plugin can NOT bypass auth — that's the framework's * job, not the plugin's. */ /** What a plugin HTTP route returns. `body` may be bytes (e.g. a served * blob) or text; `headers` carries redirects (302 `location`) + cache * policy. */ export declare interface PluginHttpRouteResult { readonly status: number; readonly body?: string | Uint8Array; readonly contentType?: string; readonly headers?: Record; /** Stream the response (SSE) instead of sending `body`. See * {@link PluginHttpRouteStream}. */ readonly stream?: PluginHttpRouteStream; /** Stream a BINARY response (a download, an export) instead of sending * `body` — see {@link PluginHttpRouteByteStream}. Never buffered by the * serve layer; never compressed (flush timing + Content-Length are the * contract). Takes precedence over `body`; do not set both `stream` and * `byteStream`. */ readonly byteStream?: PluginHttpRouteByteStream; } /** * A long-lived Server-Sent-Events body. When a route result carries this, the * serve layer streams the response until the client disconnects instead of * sending a buffered body — `body` is ignored. * * `subscribe` receives an `emit` that takes ONE already-SSE-framed chunk (e.g. * `` `event: snapshot\ndata: ${json}\n\n` ``) and MUST return an unsubscribe * function. The serve layer runs that unsubscribe when the client goes away, so * whatever the route opened (a dispatcher subscription, an interval) is released * — a stream route that leaks its subscription leaks it per connection. */ export declare interface PluginHttpRouteStream { readonly subscribe: (emit: (chunk: string) => void) => () => void; /** Keep-alive comment interval in ms (default 15000; `0` disables). Without * it an idle SSE connection is dropped by proxies after ~30–60s. */ readonly keepAliveMs?: number; } export declare interface PluginInspectEndpoint { /** HTTP method. */ readonly method: 'GET' | 'POST' | 'DELETE'; /** * Path relative to the plugin's inspect prefix. Leading slash * optional. `health` → `/_voltro/inspect/plugins//health`. */ readonly path: string; /** Optional human-readable description; surfaced in the dashboard. */ readonly description?: string; /** * Effect-based handler. Receives the raw request + headers, * returns the response shape. Body parsing is the handler's * job — the framework just hands the string through. JSON/CORS * are sugar on top: returning `{ json: ... }` from the response * shape sets the right content-type + serialises. * * Long-running responses (SSE, streaming) are NOT supported in * v1 — every endpoint returns one buffered response. SSE plugins * can route to a separate path mounted by the plugin's own * `routes:` array via the rpc layer, which DOES stream. */ readonly handler: (request: PluginInspectRequest) => Effect.Effect; } export declare interface PluginInspectRequest { readonly method: 'GET' | 'POST' | 'DELETE'; /** Full URL the client requested (path + query). */ readonly url: string; /** Lowercased headers. */ readonly headers: Readonly>; /** Raw body (empty string on GET / DELETE). */ readonly body: string; } export declare type PluginInspectResponse = { readonly kind: 'json'; readonly status?: number; readonly data: unknown; } | { readonly kind: 'text'; readonly status?: number; readonly contentType?: string; readonly body: string; }; /** * Called ONCE per host across the plugin's lifetime — the first time * it's seen by the host. Used for one-time schema migrations + resource * provisioning that survive across deactivate/activate cycles. Skipped * on subsequent boots once the host has recorded the install. * * v1 model: install state is in-process only (in-memory marker keyed by * plugin name + version). A future slice will persist it to * `_voltro_plugin_installs` so install/uninstall idempotency survives * process restarts. For now, treat `onInstall` as "what runs on a fresh * dev process the first time this plugin is wired" — sufficient for the * "create a schema table on first wire-up" use case. */ export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.Effect | Promise | void; /** * The two things a plugin's `name` is asked to encode — and they are NOT the * same question, which is why they are two fields. * * A plugin's name decides its rpc-tag prefix (`pluginAlias`) and its inspect * URL slug (`pluginSlug`). Two different app-side problems land on it: * * - **`alias` — "your namespace collides with mine."** An app that already * publishes `notifications.*` routes cannot install a plugin that also wants * `notifications.*`; the collision is fatal at codegen. `alias` REPLACES the * namespace, so the app keeps its own name and the plugin moves. * - **`instance` — "I want two of these."** A second cdc-out pipeline, a * second mail transport. The base name stays (so it is still recognisably * that plugin) and gains a `#suffix` discriminator. * * Both were already in the tree, one of them eleven times. The `#suffix` * ternary was copy-pasted verbatim into eleven plugins, and `alias` existed on * exactly one (`ai-flows`) with its own hand-rolled shape — so the two * mechanisms had no defined interaction at all. This is the one implementation. * * **What an alias costs, stated because it is not obvious and nothing else * says it:** the local and cloud dashboards fetch a plugin's inspect panel at * `/_voltro/inspect/plugins//…` with the DEFAULT slug compiled in. Alias * a plugin that ships `inspectEndpoints` and the endpoints keep working, the * rpc tags move as intended, and the dashboard panel 404s — because the panel * is in a different repository and cannot follow. Alias to dodge a tag * collision; do not alias a plugin whose dashboard panel you use. * * @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'` * @param alias replaces the whole namespace — an app-chosen name * @param instance discriminates one installation from another (`#suffix`) * * ```ts * pluginInstanceName({ base: '@voltro/plugin-cdc-out' }) * //=> '@voltro/plugin-cdc-out' tag `cdcOut.*` slug `cdc-out` * pluginInstanceName({ base: '@voltro/plugin-cdc-out', instance: 'analytics' }) * //=> '@voltro/plugin-cdc-out#analytics' tag `cdcOut.*` slug `cdc-out--analytics` * pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror' }) * //=> 'mirror' tag `mirror.*` slug `mirror` * pluginInstanceName({ base: '@voltro/plugin-cdc-out', alias: 'mirror', instance: 'analytics' }) * //=> 'mirror#analytics' tag `mirror.*` slug `mirror--analytics' * ``` */ export declare const pluginInstanceName: (args: { readonly base: string; readonly alias?: string | undefined; readonly instance?: string | undefined; }) => string; /** * Per-app context handed to lifecycle hooks. The shape is intentionally * thin — the framework's deeper services (DataStore, Logger, etc.) are * available via the per-request `AppContext` plugins see in their rpc * interceptors. Lifecycle hooks run OUTSIDE any request, so they * receive a minimal slice: * * - `app`: the resolved app-config metadata * (`{ name, type, voltroVersion }`). * - `logger`: a scoped `@voltro/logger` instance keyed to the * plugin's `name`, so log lines are attributable. * - `env`: the process env (alias of `process.env` for typing). * - `config`: the plugin's own validated config (if `configSchema` * declared) — already decoded against the plugin's Schema, ready * for the hook to consume. Unknown otherwise. */ export declare interface PluginLifecycleContext { readonly app: { readonly name: string; readonly type: 'api' | 'web'; readonly voltroVersion: string; }; readonly logger: { info: (message: string, fields?: Record) => void; warn: (message: string, fields?: Record) => void; error: (message: string, fields?: Record) => void; debug: (message: string, fields?: Record) => void; }; readonly env: NodeJS.ProcessEnv; /** Validated plugin config (if `configSchema` was declared). */ readonly config: unknown; } export declare interface PluginMigration { readonly id: string; readonly description?: string; readonly up: (sql: PluginMigrationSqlClient) => Effect.Effect; readonly down?: (sql: PluginMigrationSqlClient) => Effect.Effect; } /** * A plugin-supplied custom migration. Runs ONCE per app DB, tracked in * the framework's `_voltro_plugin_migrations` ledger so re-runs of * `voltro dev` skip already-applied steps. Use for setup the * declarative-tables path can't express: extension installs (`CREATE * EXTENSION pgcrypto`), seed data, DDL for pre-existing / * externally-owned tables the plugin wraps, etc. * * Ids are stable strings the plugin author owns. The framework * namespaces them as `__` in the ledger * so two plugins can each ship a `'001-init'` without collision. * * The `up` Effect runs against the live `SqlClient` after the * framework's own schema apply. Failures abort boot (loud + * recoverable: fix + restart). Down-migrations are optional and not * auto-run — they exist for ops tooling. */ /** * The `sql` parameter the framework hands the migration is the live * `SqlClient.SqlClient` from `@effect/sql`. The reference is type-only, * so it adds no runtime dependency — `@voltro/protocol` still ships * without pulling `@effect/sql` into a plugin's bundle. */ export declare type PluginMigrationSqlClient = SqlClient.SqlClient; /** * Declared permission scope. The framework gates hook surfaces on * the declared set: plugins that ship `onHttpRequest` without * declaring `'http:intercept'` are rejected at boot. Pattern-perms * support a `:*` wildcard suffix (`'secrets:read:auth0:*'` covers * `'secrets:read:auth0:clientSecret'`). * * The static surface is a typed union so plugin authors get autocomplete; * pattern perms fall through the template-literal arm. Surfaced in the * boot log + `/_voltro/inspect/plugins` so operators audit what each * plugin asks for before installing. */ 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}`; /** * Per-call context handed to a plugin-contributed rpc route's * executor. Mirror of the user-handler `AppContext` shape but typed * loosely so the plugin doesn't need to depend on `@voltro/runtime`. */ export declare interface PluginRouteContext { readonly request: { readonly subject: Subject; readonly traceId: string; }; } /** * Client-facing declaration of a plugin RPC route's descriptor, so the codegen * can emit it into the generated client rpc group (`appGroup` + `appDescriptors`) * — without this, a plugin route is mounted server-side but the browser client * can't resolve its tag ("not reachable on client"). * * `import` MUST point at a BROWSER-SAFE module that exports the route's descriptor * value (tag + input/output/error Schema, NO executor, NO `node:*`/server deps) — * the same discipline `PluginErrorSchema.import` follows (e.g. * `@voltro/plugin-storage/rpc`). The generated file is loaded value-level by the * web client + guarded by `assertBrowserSafeRpcGroup`, so a server import here is * a boot failure. `tag` is the route's EFFECTIVE tag (`.`); `kind` * selects `queryToRpc` / `mutationToRpc` / `actionToRpc`. */ export declare interface PluginRpcClientDescriptor { readonly tag: string; readonly kind: RpcKind; readonly import: { readonly module: string; readonly name: string; }; } /** * One descriptor + executor pair a plugin contributes to the host's * rpc surface. Same shape as a user-authored mutation/query/action, * but registered programmatically by the plugin instead of via file * discovery. * * The plugin's name is prepended to the rpc tag at registration time * unless the descriptor already carries a dot — e.g. a plugin named * `@voltro/plugin-audit` exporting `route({ name: 'list' })` lands * as `audit.list`; exporting `route({ name: 'admin.events' })` lands * unchanged. This guarantees plugin routes never collide with * user-authored routes (user routes don't ship the plugin prefix) and * lets the dashboard show them grouped by plugin. */ export declare interface PluginRpcRoute { /** 'mutation' | 'query' | 'action' — determines the runtime path. */ readonly kind: RpcKind; /** RPC tag relative to the plugin (e.g. `'list'`, `'admin.events'`). */ readonly name: string; /** Optional human-readable description; surfaced in the dashboard. */ readonly description?: string; /** Input schema — same `effect/Schema` shape user routes use. */ readonly input: Schema.Schema.Any; /** Output schema. */ readonly output: Schema.Schema.Any; /** Optional typed-error schema (TaggedError union). */ readonly error?: Schema.Schema.All; /** * REACTIVE source table(s) — ONLY for `kind: 'query'`. When set, the * framework drives the query REACTIVELY: it re-runs the executor and pushes * a fresh result over the SAME subscription/WS transport app reactive * queries use, every time one of the named tables changes. This makes a * plugin query PUSH-DRIVEN — no client polling — by reusing the framework's * computed-reactive-query machinery (the descriptor is browser-safe, the * executor runs server-side). * * The executor returns the query's VALUE (an array / object) — the same * shape it returns for a poll; the framework recomputes it on change. Omit * for a plain (poll-only) plugin query, a mutation, or an action. For a * query that mirrors a plugin table, set this to that table's name. * * For plugin state that is NOT in a table, declare a * `reactivityChannel(...)` and pass the CHANNEL here — do not declare a table * you never write in order to own the name. `@voltro/plugin-presence` did * exactly that for a release, and the empty `_voltro_presence` table it left * in every user's database is what the channel primitive replaced. * * `| undefined` is explicit so a plugin can spread a browser-safe query * DESCRIPTOR (which always carries `source: … | undefined`) into a route * literal under `exactOptionalPropertyTypes` without a cast. * * Deliberately the WIDE type, where an app's own `source:` is narrowed to its * generated {@link TableName} union. A plugin ships against many apps and * cannot know any of their tables; its own come from its `extendSchema`, which * the consuming app's generated names do include but which the plugin's own * compilation has no access to. Narrow where an APP author writes, stay wide * where a plugin declares — the same rule the runtime readers follow, for the * same reason: a type that refuses a name it cannot check is asserting, not * checking. */ readonly source?: ReactivitySourceValue | ReadonlyArray | undefined; /** * The route's ACCESS DECISION — the same two-field vocabulary user * procedures carry, because a plugin route is dispatched through the exact * same spine. Spreading a browser-safe descriptor * (`{ ...listDescriptor, execute }`) carries the decision automatically; the * lift (`pluginRoutes.ts`) forwards it onto the descriptor the runtime * enforces. * * `| undefined` on both, like `source`: a spread descriptor always carries * the properties, and `exactOptionalPropertyTypes` would otherwise refuse * the spread without a cast. * * A route that declares NEITHER is the SEC-1 shape — callable by any * authenticated session, and REFUSED per-request by the dispatch spine when * the app runs with `security.defaultDeny` (the default). First-party * plugins declare a decision on every route; third-party plugins must too. */ readonly guards?: DeclaredAccess | undefined; /** Deliberate no-check marker with the REASON — see `openAccess:` on * `defineQuery`/`defineMutation`/`defineAction`. Mutually exclusive with a * non-empty `guards`. */ readonly openAccess?: string | undefined; /** * Effect-only executor. The base layer is provided by the framework * (DataStore, HttpClient, the plugin's own service Tags) — the * executor declares its requirements through Effect's R channel like * any other handler. Mutations land inside `store.transactional()` * automatically; queries return a `QueryDescriptor`-shaped result; * actions run without a transaction. */ readonly execute: (input: never, ctx: PluginRouteContext) => Effect.Effect; } /** * What a plugin contributes to the host app's schema. `tables` join * the user's table set + go through the same `applySchema()` path * (idempotent CREATE TABLE IF NOT EXISTS). `migrations` run after * `applySchema()` against the live SqlClient, tracked in * `_voltro_plugin_migrations`. * * Plugin MUST declare `store:write` for `tables`, `migration:run` * for `migrations`. Boot fails if the declared-permission set is * insufficient. */ export declare interface PluginSchemaContribution { readonly tables?: ReadonlyArray; readonly migrations?: ReadonlyArray; } /** * A scaffold template a plugin contributes to `voltro init` / * `voltro add-app`. Templates are listed under the plugin's name in * `voltro list-templates`; the operator picks one and the CLI seeds * the project tree. * * The framework keeps the template registry per-plugin scoped so two * plugins can each ship an `auth` template without collision: the * full template id is `:`. * * v1 ships the registration shape + listing; the actual * `voltro init --from-plugin` invocation lands once a real consumer * needs it. Until then, templates are surfaced in * `/_voltro/inspect/plugins` so the dashboard can preview them. */ export declare interface PluginTemplate { /** Stable id within the plugin (kebab-case). The framework * prepends the plugin alias when surfacing — `audit:starter` / * `webhooks:stripe-receiver`. */ readonly id: string; /** Human-readable label for the picker. */ readonly title: string; /** One-line description; surfaced in `voltro list-templates`. */ readonly description: string; /** Which kind of app this template scaffolds. */ readonly kind: 'api' | 'web' | 'fullstack'; /** * Source directory inside the plugin's package. The CLI resolves * it relative to the plugin's npm-resolved root. Files inside are * copied verbatim into the target project; `package.json` is * patched to point dependencies at the plugin. */ readonly sourcePath: string; /** Optional list of post-install hints surfaced after scaffolding. */ readonly postInstallSteps?: ReadonlyArray; } /** * Called ONCE when the host removes the plugin. Mirror of `onInstall`. * Used to clean up the plugin's persisted resources (drop tables, * delete queue topics) so an uninstall fully reverses an install. * * v1 model: triggered explicitly by `voltro plugins uninstall ` * (CLI command — see manifest plan). NOT triggered on plain shutdown * (that's `onDeactivate`). Plugins are responsible for making * `onUninstall` idempotent — repeating it should be a no-op once the * resources are gone. */ export declare type PluginUninstallHook = (ctx: PluginLifecycleContext) => Effect.Effect | Promise | void; /** The runtime-erased form of `PolicyGuardSpec` — a relationship check. */ export declare interface PolicyCheckSpec { readonly action: string; readonly resourceType: string; readonly resource: (input: unknown) => string | undefined; } /** The question the runtime asks a registered policy-guard resolver. */ export declare interface PolicyGuardRequest { readonly subject: Subject; /** The action named in the resource policy's `actions` map. */ readonly action: string; readonly resourceType: string; /** The id the guard's pure `resource` extractor produced. */ readonly resourceId: string; } /** * Answers "does `subject` have `action` on `:`". * Registered once at boot via `setPolicyGuardResolver`. A failed Effect is * treated as a DENIAL — a resolver wanting another posture catches its own * errors. */ export declare type PolicyGuardResolver = (req: PolicyGuardRequest) => Effect.Effect; /** * A relationship (ReBAC) guard — "may this subject perform ACTION on THIS row?" * * The scope guard above answers "what may this subject do at all"; this answers * "on which row". Both live in the same `guards:` array on purpose, because the * alternative is what apps actually built: a hand-maintained map from rpc tag → * policy rule, installed as an interceptor. That map is FAIL-OPEN BY OMISSION — * add an rpc, forget the entry, and it is silently unguarded. A declaration on * the descriptor cannot be forgotten for an rpc that exists, because it IS the * rpc. * * Browser-safe by the same construction as `GuardSpec`: strings plus a PURE * `resource` extractor. Resolution — reading relationship tuples, applying the * policy's `implies` closure — happens server-side through the registered * policy resolver. With no resolver registered the check FAILS CLOSED: an * unanswerable authorization question is a denial, never a pass. * * Because it is data, the same declaration compiles into the capability * manifest the client reads, so a UI gate and the server check cannot drift. */ export declare interface PolicyGuardSpec { /** The action to authorize, as named in the resource policy's `actions`. */ readonly action: string; /** Which registered resource policy governs the check. */ readonly resourceType: string; /** PURE `input → resource id`. Returning undefined DENIES — an unidentifiable * resource is not a reason to skip the check. */ readonly resource: (input: Input) => string | undefined; } export declare type ProcedureDescriptor = QueryProcedureDescriptor | MutationProcedureDescriptor | ActionProcedureDescriptor | StreamProcedureDescriptor; export declare const PROTOCOL_VERSION: 1; export declare type ProtocolVersion = typeof PROTOCOL_VERSION; export declare interface PublicApiSpec { /** Derived by kind when omitted: query→GET, mutation/action→POST. */ readonly method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; /** Derived from the tag + version when omitted: `//`. */ readonly path?: string; /** Path version segment; multiple versions coexist as separate descriptors. */ readonly version?: string; /** Additional API-key scopes required ON TOP of the handler's RBAC. */ readonly scopes?: ReadonlyArray; /** Per-endpoint rate limit (rides @voltro/plugin-ratelimit when mounted). */ readonly rateLimit?: { readonly limit: number; readonly window: string; }; /** Honor `Idempotency-Key` for mutating methods (existing HTTP idempotency). */ readonly idempotent?: boolean; /** Replacement hint → `Deprecation: true` header. */ readonly deprecated?: string; /** ISO date → `Sunset:` header, `410` past the date. */ readonly sunset?: string; readonly summary?: string; readonly description?: string; /** Streaming queries over request/response: 'snapshot' (default — first * snapshot, like POST /rpc) | 'sse' (Server-Sent Events of snapshot+deltas). */ readonly stream?: 'snapshot' | 'sse'; } /** * Push every subscriber of `channel`. * * Returns whether the store could deliver it. A store that cannot inject (a * limited fake, a transactional view) is a no-op rather than a throw: a missing * seam must not be able to take down the write that called this, and the read * that follows is still correct. * * This exists so no caller hand-builds the event. The one that did wrote * `injectExternalChange({ … } as never)` — and `as never` on a wiring object is * how the webhook trigger context came to differ between the two boot paths * while both compiled. */ export declare const publishReactivity: (store: ReactivityPublisher | undefined, channel: ReactivityChannel) => boolean; /** Publish a server-primitive error. Cheap + sync; a broken reporter can * never break the caller (each listener is isolated). Safe no-op when * nothing is subscribed — primitives call it unconditionally. */ export declare const publishServerError: (event: ServerErrorEvent) => void; /** * Server-side snapshot caching for a query (see `defineQuery`). When set, * the dispatcher caches the initial snapshot result and auto-invalidates it * when a mutation writes any table the query depends on. */ export declare interface QueryCacheConfig { /** Fresh window — seconds (number) or a duration string (`'30s'`, * `'5m'`, `'1h'`). */ readonly ttl: number | string; /** Stale-while-revalidate window past `ttl` (same units). While stale, * the cached value is served immediately and refreshed in the * background. */ readonly swr?: number | string | undefined; /** * Cross-subject safety — REQUIRED, no default, because guessing wrong leaks * rows. * * - `'subject'` keys by the caller's subject id. Always safe, and recomputes * per PERSON — for a figure that is identical for everyone in an org, that * is one identical computation per employee. * - `'tenant'` keys by the caller's `tenantId`: one entry per org, none * shared across orgs. The right answer for anything derived from * `subject.tenantId` — a rollup, a dashboard figure, a count. * - `'global'` shares ONE entry across every caller. Legal ONLY when the * resolved query is caller-independent (reference data). * * Rubric: does the resolved predicate depend on the caller? On the PERSON → * `subject`; on their ORG only → `tenant`; not at all → `global`. * * `'tenant'` exists because the other two were the only options and neither * fits an org-wide figure: `subject` recomputes it per person, and `global` * is not a cache but a cross-tenant leak. A deployment reported computing the * same nine-table statistic up to 18 times for 18 employees rather than take * the second option, which was the correct call. * * A caller with NO tenant (anonymous) BYPASSES a `'tenant'` cache rather than * falling back — falling back to `global` would be the leak this option * exists to avoid, and falling back to `subject` would silently change the * cardinality of a cache the author sized per org. */ readonly scope: 'subject' | 'tenant' | 'global'; } export declare interface QueryProcedureDescriptor { readonly kind: 'query'; readonly name: Name; readonly input: Input; readonly output: Output; readonly error: Error; /** Which DataStore table(s) this query reads. Drives auto-optimistic patch * routing — mutations that target ANY of these tables patch caches keyed * to queries with a matching `source`. A computed query re-runs when ANY * listed table changes (pass an array to depend on several). Optional: * queries without a source never receive auto-patches (joins, aggregates). */ readonly source: string | ReadonlyArray | undefined; /** Server-side snapshot cache config. Absent → never cached (the * default; the dispatcher already keeps live subscriptions fresh). */ readonly cache: QueryCacheConfig | undefined; /** Declarative authorization guard(s) — enforced before the executor runs, * failing with a typed `ScopeError`. Absent → no framework-level authz. */ readonly guards: DeclaredAccess | undefined; /** The declared reason this procedure needs NO authorization check — * `openAccess: ''`. Mutually exclusive with `guards`; together they are * the only two shapes `security.defaultDeny` accepts. */ readonly openAccess: string | undefined; /** Opt this query into a public REST endpoint (innovation/11). */ readonly publicApi: PublicApiSpec | undefined; /** Opt this query into the auto-synthesized agent toolset (innovation/07). */ readonly exposeAsTool: ExposeAsTool | undefined; /** True when the procedure is kept OFF the wire — no client-group entry and no * route in dev or serve. See `internal` on the definer's options. */ /** * Replace a PLUGIN route that answers to this same tag. * * Without it, a user route and a plugin route sharing a tag is a hard error, * and correctly so — two handlers behind one name is not a thing a caller can * reason about. But refusing is the wrong answer when the app deliberately * wants its own version: the two escapes available otherwise are to rename * your procedure (so the split runs along "who built it" rather than along a * domain boundary) or to `alias` the whole plugin away (same, one level up). * For a frontend developer that is the worst possible partition. * * A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose * surface is richer than theirs, add `archive`/`unarchive` beside it — which * already composes, since the collision check compares FULL tags and not * prefixes — and replace `markRead`, because theirs maintains archive state. * * Explicit, never inferred. Silently letting the app win would mean a plugin * upgrade that adds a route could shadow an app procedure with no diff to * read; declaring it makes the intent reviewable and puts the override in the * file that performs it. */ readonly overridesPlugin: boolean | undefined; readonly internal: boolean | undefined; } export declare const queryToRpc: (descriptor: QueryProcedureDescriptor, extraErrors?: ExtraErrors) => Rpc.Rpc : Input, Stream; revision: typeof Schema.Number; data: Output; computed: Schema.optional; }>, Schema.Struct<{ _tag: Schema.Literal<["delta"]>; revision: typeof Schema.Number; emittedAt: typeof Schema.Number; patch: Schema.Struct<{ ops: Schema.Array$; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["replace"]>; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["remove"]>; path: typeof Schema.String; }>]>>; order: Schema.Array$>; }>; }>, Schema.Struct<{ _tag: Schema.Literal<["error"]>; error: typeof Schema.Unknown; revision: Schema.optional; }>]>, Schema.Schema.All>, typeof Schema.Never, never>; /** * The RAW mark, unvalidated, or `null`. * * Deliberately `unknown`: protocol owns the key, not the payload's shape. * `@voltro/plugin-auth` mints the mark and validates it back into an * `ImpersonationMark`; an auditor only needs to copy it out before a redactor * can reach it, and giving protocol an opinion about the fields would make the * shape need to agree in two places again — the exact thing the key moved here * to avoid. */ export declare const rawImpersonationMark: (subject: Subject) => unknown; /** The `channel:` namespace. Every channel key starts with it. */ export declare const REACTIVITY_CHANNEL_PREFIX = "channel:"; /** * A declared reactivity channel — a push target with no table behind it. * * Create one with `reactivityChannel(name)`; pass it as a query's `source:`. */ export declare interface ReactivityChannel { readonly kind: 'reactivity-channel'; /** The name as declared, without the namespace. */ readonly name: string; /** The routing key — what the dispatcher indexes and `source:` resolves to. */ readonly key: string; /** The key, so a channel interpolates into a message as its routing key. */ toString(): string; } /** * Declare a reactivity channel. * * Idempotent by name: calling it twice returns the SAME object. A module * evaluated twice (hot reload, a dual-instance resolve) must not produce two * channels that compare unequal while routing to one key. * * export const presenceRoster = reactivityChannel('presence') * // … * defineQuery({ name: 'presence.list', source: presenceRoster, … }) * // … * publishReactivity(store, presenceRoster) */ export declare const reactivityChannel: (name: string) => ReactivityChannel; /** * The one method of a store this needs — structural, so publishing does not * pull `@voltro/database`'s `DataStore` into a caller that had no reason for it. */ export declare interface ReactivityPublisher { readonly injectExternalChange?: (event: { readonly table: string; readonly op: 'insert' | 'update' | 'delete'; readonly new: Record; readonly old: Record; readonly origin?: 'inline' | 'injected'; }) => void; } /** What a query may declare as its reactive source. */ export declare type ReactivitySource = TableName | ReactivityChannel; /** * The shape a `source:` has once it is DATA rather than something being written. * * Deliberately wider than {@link ReactivitySource}, and the difference is the * whole discipline behind the typed `source:`: **narrow where an author writes, * stay wide where the framework reads.** A descriptor that arrived over the * wire, was loaded from a generated file, or came from a plugin carries whatever * string it carries — a runtime reader that refused an unknown one would be * asserting a fact it cannot check, and the first thing it would reject is a * STALE name, which is the exact case these readers exist to report. */ export declare type ReactivitySourceValue = string | ReactivityChannel; /** Gate a handler on a scope; fails with a typed `ScopeError` if missing. * Checks the EFFECTIVE set so role-derived scopes count. */ export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect; /** The question the runtime asks a registered resource-scope resolver. */ export declare interface ResourceScopeRequest { /** The authenticated caller. */ readonly subject: Subject; /** The single scope being checked (guards with a scope array ask once per scope). */ readonly scope: string; /** The resource id the guard's `resource` extractor produced (e.g. a teamId). */ readonly resource: string; } /** * A process-global resolver that answers "does `subject` hold `scope` on * `resource`". Registered once at boot via `setResourceScopeResolver`. Returns * an Effect so it can read the app's role tables. It should NOT leak errors — * `checkGuardsEffect` treats a failed resolver as a DENIAL (fail-closed), so a * resolver that wants a different posture must catch its own errors. */ export declare type ResourceScopeResolver = (req: ResourceScopeRequest) => Effect.Effect; /** The id of a row, addressed in op paths as `/`. */ export declare type RowId = string | number; /** The full delta payload: the per-row ops plus the authoritative id * ordering of the resulting set. */ export declare interface RowPatch { readonly ops: ReadonlyArray; /** Id sequence of the resulting (next) row set, in order. */ readonly order: ReadonlyArray; } /** * One RFC-6902-style op against an id-keyed row set. `path` is always * `/` (the row's id), NOT an array index — this is the deliberate * departure that makes the diff reorder-stable. * * - `replace` — the row with this id exists in both prev and next but * its content changed; `value` is the full next row. * - `add` — the row with this id is new in next; `value` is it. * - `remove` — the row with this id is gone in next. */ export declare type RowPatchOp = { readonly op: 'add'; readonly path: string; readonly value: PatchRow; } | { readonly op: 'replace'; readonly path: string; readonly value: PatchRow; } | { readonly op: 'remove'; readonly path: string; }; export declare const rowPatchOpSchema: Schema.Union<[Schema.Struct<{ op: Schema.Literal<["add"]>; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["replace"]>; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["remove"]>; path: typeof Schema.String; }>]>; export declare const rowPatchSchema: Schema.Struct<{ ops: Schema.Array$; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["replace"]>; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["remove"]>; path: typeof Schema.String; }>]>>; order: Schema.Array$>; }>; /** * Effect-native interceptor. Receives the rest of the chain as an * `Effect`; the interceptor returns an `Effect` that wraps it (or * substitutes a different one). Whatever the returned Effect produces * becomes the final result. * * Typical patterns: * * // Pre-only authz: short-circuit BEFORE the executor runs. * const guard: RpcInterceptor = (next, ctx) => * ctx.tag.startsWith('admin.') && ctx.subject.type !== 'user' * ? Effect.fail(new Forbidden({ tag: ctx.tag })) * : next * * // Post-only side effects: tap the success/failure channels. * const log: RpcInterceptor = (next, ctx) => * next.pipe( * Effect.tap((result) => recordAudit(ctx.tag, ctx.input, result, ctx.subject)), * Effect.tapError((err) => recordAuditError(ctx.tag, err)), * ) * * // Tracing: wrap with a span; OTel spans nest automatically. * const trace: RpcInterceptor = (next, ctx) => * next.pipe(Effect.withSpan(`plugin.audit.${ctx.kind}`, { attributes: { tag: ctx.tag } })) * * The `next` Effect MUST be flowed through somehow — either yielded, * piped, or returned — for the executor to run. Returning a different * Effect substitutes the result entirely (rare, e.g. cache hits). */ export declare type RpcInterceptor = (next: Effect.Effect, context: RpcInterceptorContext) => Effect.Effect; /** * Per-call context passed to every rpc interceptor. The fields are * deliberately shallow — plugins shouldn't need the full AppContext + * DataStore to do their job. For runtime services (DataStore, custom * Tags, etc.) declare them via `definePlugin({ services })` and read * via `yield* MyTag` in the interceptor body; the framework provides * them automatically. */ export declare interface RpcInterceptorContext { /** Procedure tag, e.g. `todos.create`. */ readonly tag: string; /** Discriminator — `'mutation' | 'query' | 'action'`. */ readonly kind: RpcKind; /** The validated input the executor is about to (or already did) receive. */ readonly input: unknown; /** The resolved subject (user / apiKey / serviceAccount / anonymous). */ readonly subject: Subject; /** Per-call trace id (matches OTel spans + inspect events). */ readonly traceId: string; /** Active OTel span id (16-hex), when tracing is on. Lets observability * plugins (Sentry/Datadog) attach an error to the exact span, not just * the trace. Absent when no tracer is installed. */ readonly spanId?: string; } /** * Which kind of rpc the interceptor is wrapping. The runtime passes * `kind: 'mutation'` to interceptors registered as `interceptMutation`, * `kind: 'query'` to `interceptQuery`, and `kind: 'action'` to * `interceptAction` — one plugin can implement any combination. */ export declare type RpcKind = 'mutation' | 'query' | 'action'; /** * Per-firing context passed to schedule-fire interceptors. Shape * parallels `RpcInterceptorContext` so plugin authors can lift the * same patterns across rpc + schedule surfaces. */ export declare interface ScheduleFireContext { /** Schedule name (e.g. `'nightlyBilling'`). */ readonly name: string; /** When the firing was scheduled by the cron expression. */ readonly scheduledAt: Date; /** When the firing actually started. */ readonly firedAt: Date; /** Run id recorded in `_voltro_schedule_runs` (`'unrecorded'` if * the run-row insert failed). */ readonly runId: string; /** What triggered this firing. `'self'`/`'external'`/`'manual'`. */ readonly trigger: 'self' | 'external' | 'manual'; } /** * Wraps a single schedule firing. Receives `next` as a thunk so the * interceptor can choose to skip the firing entirely (don't call * `next()`), wrap it with side effects, or substitute behaviour. * Throwing/rejecting propagates as a handler failure recorded in * `_voltro_schedule_runs.status = 'failed'`. The watchdog * (`maxRuntimeMs`) wraps the WHOLE chain — exceeding it aborts. * * Use for: per-schedule observability (custom metrics/traces beyond * the framework's defaults), durable-lock extension, feature-flag- * gated suppression, suspend/resume integration with external * orchestrators. * * Typical pattern: * * onScheduleFire: async (next, ctx) => { * if (await isFeatureKilled(ctx.name)) return // suppress * const start = Date.now() * try { * await next() * } finally { * recordCustomMetric({ name: ctx.name, durationMs: Date.now() - start }) * } * } */ export declare type ScheduleFireInterceptor = (next: () => Promise, ctx: ScheduleFireContext) => Promise; export declare interface ScopeCache { /** The effective window in milliseconds. */ readonly ttlMs: number; /** Resolve through the cache. `compute` runs only on a miss. */ readonly resolve: (key: string, compute: () => Promise) => Promise; /** Drop one subject's cached authority — call this from whatever changes a * role, and the change is live on this process immediately. */ readonly invalidate: (key: string) => void; /** Drop every cached verdict — for a change that alters a ROLE's definition * rather than one user's membership, where the affected subjects aren't * enumerable. */ readonly invalidateAll: () => void; } /** * The cache key for a subject's authority: type, tenant, id. * * `tenantId` is in the key and it is load-bearing. A user who switches tenants * keeps their `id`, and their authority is per-tenant — key on the id alone and * a switch serves the previous tenant's scopes for a whole window. Use this * when calling `invalidate` so the key you drop is the key that was written. */ export declare const scopeCacheKey: (subject: Subject | SubjectIdentity) => string; export declare interface ScopeCacheOptions { /** Cache window in milliseconds. Default 30 000. `0` disables caching (every * request resolves). Overridden by `VOLTRO_AUTH_SCOPE_CACHE_TTL_MS` only * when this is left unset — an explicit number in code wins over the env. */ readonly ttlMs?: number; /** Upper bound on cached subjects. Default 10 000. */ readonly maxEntries?: number; /** Clock override (epoch ms). Testing seam. */ readonly now?: () => number; } /** Compose multiple strategies into a single resolver function. The * composer evaluates them in declaration order; first `matched` * wins; first `failed` short-circuits to anonymous (does NOT fall * through — see StrategyResolution doc). * * NOTHING may answer ahead of this chain. The per-connection soft-reauth * override used to (`getConnectionSubject`, `@voltro/runtime`), and that is * precisely what made a rebound connection immune to session revocation, to * `resolveScopes` and to the scope cache. It patches the connection's HEADERS * now and this chain runs unchanged — see `ConnectionCredential` above. * * Returns an async resolver `(input) => Promise`. */ /** A resolver's verdict on a subject's authority. */ export declare type ScopeDecision = /** Union these onto what the strategy established. */ { readonly kind: 'grant'; readonly scopes: ReadonlyArray; } /** These are the caller's COMPLETE authority — anything else is removed. */ | { readonly kind: 'authoritative'; readonly scopes: ReadonlyArray; } /** The authority source could not be reached. Fails the request closed. */ | { readonly kind: 'unavailable'; readonly reason: string; }; export declare class ScopeError extends ScopeError_base { } declare const ScopeError_base: Schema.TaggedErrorClass; } & { required: typeof Schema.String; message: typeof Schema.String; }>; /** What `resolveScopes` may return. A bare array is the shorthand for * `{ kind: 'grant', scopes }` — the common case, and the reading an existing * resolver already has. */ export declare type ScopeResolverResult = ReadonlyArray | ScopeDecision; export declare interface ServerErrorEvent { /** The thrown value (Error | tagged error | anything). */ readonly error: unknown; /** Which primitive caught it. */ readonly source: ServerErrorSource; /** Human identifier: route path / aggregate name / schedule name / * workflow name / webhook id / startup id / subscriber table. */ readonly name?: string; /** Distributed-trace id when one is in scope (workflows carry it). */ readonly traceId?: string; /** Extra context — op, runId, executionId, http status, etc. */ readonly fields?: Record; } declare type ServerErrorListener = (event: ServerErrorEvent) => void; export declare type ServerErrorSource = 'rest' | 'aggregate' | 'subscriber' | 'reaction' | 'schedule' | 'workflow' | 'webhook' | 'startup'; /** * A generated CRUD write (`crud.create` / `crud.update`) was handed a * `.serverOnly()` column in its INPUT. * * `.serverOnly()` is the WIRE-exposure axis: the column never crosses the * boundary in EITHER direction. Reads strip it; a write that accepts it is the * same violation mirrored — mass assignment of a column the schema declared the * client may not see, let alone set. * * Refused rather than silently stripped: a stripped field makes an attack * indistinguishable from a no-op and leaves an honest caller wondering why the * value it sent never landed. `columns` names what was rejected so the fix * (drop the field from the descriptor's input schema, or from the caller) is * mechanical. */ export declare class ServerOnlyColumnWrite extends ServerOnlyColumnWrite_base { } declare const ServerOnlyColumnWrite_base: Schema.TaggedErrorClass; } & { /** The table the write targeted. */ table: typeof Schema.String; /** The `.serverOnly()` columns the input tried to set. */ columns: Schema.Array$; }>; /** * Publish the caller's fully-resolved scope set for this request (raw subject * scopes ∪ role-derived scopes ∪ any extra grants). Called by the rbac * interceptor; safe to call more than once (last write wins). */ export declare const setEffectiveScopes: (subject: Subject, scopes: ReadonlyArray) => void; /** Register (or clear) the process-global policy-guard resolver. Last write * wins. The runtime installs one backed by the resource-policy registry and * the registered tuple source. */ export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | undefined) => void; /** * Register (or clear, with `undefined`) the process-global resource-scope * resolver. Call once at boot — from `@voltro/plugin-rbac`'s wiring, or directly * from an app that resolves per-resource permissions against its own tables. * Last write wins. */ export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void; /** * Every routing key a `source:` names, flattened. * * The one normaliser. `source` has been read with an inline * `typeof s === 'string' ? [s] : s` at nine sites, which was already a decision * written nine times; adding a second accepted shape to each of them is how the * ninth ends up handling channels differently from the first. */ export declare const sourceKeys: (source: ReactivitySourceValue | ReadonlyArray | undefined) => ReadonlyArray; /** * Discriminated union of all framework-owned store errors. Use it on * a mutation's `error:` schema when you want every kind surfaced * typed to the client. */ export declare type StoreError = TenantScopeViolation | TenantRowNotFound | ServerOnlyColumnWrite | StoreOperationFailed | TableValidationFailed | ConstraintViolation; /** * Anything else the underlying `DataStore` raised during a write or * query. `cause` is the stringified original error — sufficient for * logs + UI, doesn't try to serialise complex error chains across the * rpc boundary. The `_tag` discriminator stays cheap to pattern-match. */ export declare class StoreOperationFailed extends StoreOperationFailed_base { } declare const StoreOperationFailed_base: Schema.TaggedErrorClass; } & { /** Which DataStore op was attempted: 'query' | 'insert' | 'update' | 'delete' | 'hardDelete'. */ operation: typeof Schema.String; /** The table the op targeted. */ table: typeof Schema.String; /** `String(originalError)` — round-trip-safe diagnostic form. */ cause: typeof Schema.String; }>; export declare type StrategyResolution = { readonly kind: 'matched'; readonly subject: Subject; /** * When this credential expires, in unix SECONDS — if the strategy knows. * * The strategy is the ONLY place in the system that has verified the token * and holds its `exp`, and until now it could not say so. The credential * bound therefore read one source: the `voltro:session` cookie. For an app * authenticating with Bearer JWTs — six of our own catalog strategies do, * and every one of them verifies an `exp` — it was silently `undefined`, * so "a subscription can no longer outlive the credential that authorized * it" was a no-op that read as a guarantee. * * A reporter found it by expecting black screens an hour after a deploy * and getting none. Their conclusion is the one to keep: the guarantee was * not false, it was scoped to an auth shape the sentence did not name. * * Optional, and absent still means no bound — the failure direction is the * behaviour that already existed. */ readonly credentialExpiresAt?: number; } | { readonly kind: 'skip'; } | { readonly kind: 'failed'; readonly reason: string; }; /** * A non-reactive server→client stream — the load-bearing distinction from * a query: a query streams a reactive `SubscriptionEvent` (snapshot+delta) * envelope over a table; a stream emits PLAIN `element`s (e.g. an agent * run's `AgentEvent`s), one-shot, no table, no snapshot. Backs A3's * agent runtime + any other server-push stream. */ export declare interface StreamProcedureDescriptor { readonly kind: 'stream'; readonly name: Name; readonly input: Input; readonly element: Element; readonly error: Error; /** True when the stream is kept OFF the wire — no client-group entry and no * route in dev or serve. See `internal` on the definer's options. */ readonly internal: boolean | undefined; /** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element, * the same as a query's — a stream is a long-lived grant and the scopes that * justified it can be withdrawn while it is still open. */ readonly guards: DeclaredAccess | undefined; /** The declared reason this procedure needs NO authorization check — * `openAccess: ''`. Mutually exclusive with `guards`; together they are * the only two shapes `security.defaultDeny` accepts. */ readonly openAccess: string | undefined; } export declare const streamToRpc: (descriptor: StreamProcedureDescriptor, extraErrors?: ExtraErrors) => Rpc.Rpc : Input, Stream, typeof Schema.Never, never>; /** * Annotate a procedure's input schema so an undeclared field fails the decode * instead of vanishing from it. * * Applies to the schema handed to `Rpc.make` — the PAYLOAD only. Not the * success schema, and not `descriptor.input`, which the client still reads * unannotated for schema-driven UI (`normalizeDescriptor`). * * Measured, because none of it follows from the annotation's name: it * propagates into NESTED structs and through a UNION's members, and a * non-struct payload (`Schema.Void`, a scalar) is unaffected — there is no * excess property for it to have. */ export declare const strictInput: (input: S, procedure?: string, guarded?: boolean) => S; export declare const Subject: Schema.Union<[Schema.Struct<{ type: Schema.Literal<["user"]>; id: typeof Schema.String; tenantId: typeof Schema.String; scopes: Schema.optional>; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["apiKey"]>; id: typeof Schema.String; tenantId: typeof Schema.String; scopes: Schema.optional>; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["serviceAccount"]>; id: typeof Schema.String; tenantId: typeof Schema.String; scopes: Schema.optional>; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["anonymous"]>; id: typeof Schema.Null; tenantId: Schema.NullOr; /** * Set when this caller PRESENTED a credential and it was rejected — an * expired token above all. Absent when they presented none. * * The two are the same Subject and must not be the same ANSWER. A deployment * measured the cost: a user's tab outlived their IdP's token lifetime, the * strategy logged `supabase jwt expired`, the caller fell through to * anonymous, and the guard then refused with `missing required scope * 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and * it sent everyone who read it into the permissions system while the problem * was an expired session. They did that round. * * It stays a FALLBACK rather than a hard failure on purpose: a stale cookie * must not break an `openAccess` procedure that needs no session at all. The * fact travels, and only a guard that actually refuses spends it. */ credentialRejected: Schema.optional; }>, Schema.Struct<{ type: Schema.Literal<["system"]>; id: typeof Schema.String; tenantId: typeof Schema.Null; scopes: Schema.optional>; metadata: Schema.optional>; }>]>; export declare type Subject = typeof Subject.Type; export declare const SubjectIdentity: Schema.Union<[Schema.Struct<{ id: typeof Schema.String; type: Schema.Literal<["user"]>; tenantId: typeof Schema.String; metadata: Schema.optional>; }>, Schema.Struct<{ id: typeof Schema.String; type: Schema.Literal<["apiKey"]>; tenantId: typeof Schema.String; metadata: Schema.optional>; }>, Schema.Struct<{ id: typeof Schema.String; type: Schema.Literal<["serviceAccount"]>; tenantId: typeof Schema.String; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["anonymous"]>; id: typeof Schema.Null; tenantId: Schema.NullOr; /** * Set when this caller PRESENTED a credential and it was rejected — an * expired token above all. Absent when they presented none. * * The two are the same Subject and must not be the same ANSWER. A deployment * measured the cost: a user's tab outlived their IdP's token lifetime, the * strategy logged `supabase jwt expired`, the caller fell through to * anonymous, and the guard then refused with `missing required scope * 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and * it sent everyone who read it into the permissions system while the problem * was an expired session. They did that round. * * It stays a FALLBACK rather than a hard failure on purpose: a stale cookie * must not break an `openAccess` procedure that needs no session at all. The * fact travels, and only a guard that actually refuses spends it. */ credentialRejected: Schema.optional; }>, Schema.Struct<{ id: typeof Schema.String; type: Schema.Literal<["system"]>; tenantId: typeof Schema.Null; metadata: Schema.optional>; }>]>; export declare type SubjectIdentity = typeof SubjectIdentity.Type; /** * Drop a Subject's authority, keeping everything that identifies it. * * Total and lossy on purpose — there is no variant it can fail on and no * option to keep the scopes. A caller that wants to mint a credential from a * Subject goes through here, so "did this cookie carry authority?" has one * answer at every mint site instead of one per site. */ export declare const subjectIdentity: (subject: Subject) => SubjectIdentity; /** A strategy's verdict on a request. * - `matched`: this strategy claims the request; here's the Subject. * - `skip`: not my request (e.g. cookie absent); try next strategy. * - `failed`: this IS my request BUT validation failed (signature * mismatch, expired JWT). Composer bails to anonymous + logs; * silently falling through would mask attacks. */ /** What the composed chain returns: the subject, plus what it learned about the * credential's lifetime on the way. */ export declare interface SubjectResolution { readonly subject: Subject; /** Unix SECONDS, when the matching strategy could tell. */ readonly credentialExpiresAt?: number; } /** The subject's scopes (empty for anonymous / unscoped). */ export declare const subjectScopes: (subject: Subject) => ReadonlyArray; export declare class SubjectService extends SubjectService_base { } declare const SubjectService_base: Context.TagClass; /** Subscribe to server-primitive errors. Returns an unsubscribe fn. */ export declare const subscribeServerErrors: (listener: ServerErrorListener) => (() => void); export declare type SubscriptionEvent = { readonly _tag: 'snapshot'; readonly revision: number; readonly data: T; readonly computed?: boolean; } | { readonly _tag: 'delta'; readonly revision: number; readonly emittedAt: number; readonly patch: RowPatch; } | { readonly _tag: 'error'; readonly error: unknown; readonly revision?: number; }; export declare const subscriptionEvent: (data: D) => Schema.Union<[Schema.Struct<{ _tag: Schema.Literal<["snapshot"]>; revision: typeof Schema.Number; data: D; computed: Schema.optional; }>, Schema.Struct<{ _tag: Schema.Literal<["delta"]>; revision: typeof Schema.Number; emittedAt: typeof Schema.Number; patch: Schema.Struct<{ ops: Schema.Array$; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["replace"]>; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["remove"]>; path: typeof Schema.String; }>]>>; order: Schema.Array$>; }>; }>, Schema.Struct<{ _tag: Schema.Literal<["error"]>; error: typeof Schema.Unknown; revision: Schema.optional; }>]>; export declare const systemSubject: (id: string, scopes?: ReadonlyArray) => Subject; /** * A table name this app declares, or `string` when none are generated yet. * * Still a string LITERAL at runtime, so nothing about the browser/server * boundary changes — a descriptor carrying these is as loadable in the browser * as one carrying bare strings, which is the property that ruled out passing the * table VALUE here. * * ── `_voltro_*` stays WIDE, and that is not an escape hatch ──────────────── * * A framework table's presence in the declared set can be DEPLOYMENT-DEPENDENT, * so narrowing framework names against a file generated by ONE machine would * make `source: '…'` compile there and fail on a colleague's — a type error that * depends on an environment. That is the class `declaredSchemaGates.ts` exists * to forbid, one layer up. * * **How much of that is still true has changed, and the difference matters if * you are tempted to narrow this.** The original measurement was three tables * moving with three inputs: `_voltro_traces` and `_voltro_undo_log` followed * `NODE_ENV`, `_voltro_cdc_offsets` follows the dialect. The first two do not * move any more — they are declared in every environment, because one source * tree declaring two schemas broke a cross-environment data transfer. What is * left is `_voltro_cdc_offsets` and the source-tree-derived families (an app * with no workflows declares no workflow tables), and those are per-APP rather * than per-machine. * * The remaining reason to stay wide is a different one, and it is the one that * actually broke a build: the FRAMEWORK's own descriptors name `_voltro_*` * tables, and they are compiled inside programs that carry an app's * augmentation — an app which legitimately declares none of them. See * `frameworkSourcesUnderAugmentation` in the cli's type tests. * * **This is not the only check.** A `source:` naming a table that does not * exist is caught at BOOT (`unresolvedSources`), for framework names as much as * for an app's own, with a did-you-mean — that is the check which covers what * this type deliberately does not, and `sourceResolution.test.ts` pins the * `_voltro_` case so it cannot acquire the type's exemption by sympathy. * * It is also what broke the framework's OWN build the first time an augmentation * was ever present in this repo: three framework descriptors name a `_voltro_*` * table, and the fixture that generated the file naturally declares none of * them. Before that moment `keyof VoltroTableNames` was always `never` here, so * the whole narrow/wide split had never once been exercised in our own * typecheck — see `frameworkSourcesUnderAugmentation` in the type tests, which * compiles framework sources WITH an augmentation so this cannot go quiet again. * * The app-authored half loses nothing: an app's OWN table is not `_voltro_` * prefixed (`validateTableName` reserves that space), so every name a user * writes for their own data is narrowed exactly as before. */ export declare type TableName = keyof VoltroTableNames extends never ? string : (keyof VoltroTableNames & string) | `_voltro_${string}`; /** * Pre-INSERT row validation against a table's `.validate(Schema)` * decoder failed. The MutationStore runs the table's `insertSchema` * AFTER defaults + computed + audit/tenant stamps; failure means * the stamped row didn't satisfy the decoder. `issues` is the * formatted ArrayFormatter output (cheap to render in UI, doesn't * leak the original Schema instance). */ export declare class TableValidationFailed extends TableValidationFailed_base { } declare const TableValidationFailed_base: Schema.TaggedErrorClass; } & { /** The table the row targeted. */ table: typeof Schema.String; /** Top-level explanation for logs + UI ("user.email failed pattern check"). */ summary: typeof Schema.String; /** Flat list of `{ path, message }` per failing leaf. Path uses dot-notation. */ issues: Schema.Array$>; }>; export declare type Target = TargetSpec | ReadonlyArray>; export declare type TargetSpec = InsertTarget | UpdateTarget | DeleteTarget; /** * A keyed-by-primary-key write (`ctx.store.update(table, id, patch)`, * `delete(table, id)`, `hardDelete(table, id)`, `patchJson(table, id, …)`) on a * `tenant()`-scoped table did not resolve to a row inside the CALLER's tenant. * * **One error for two situations, on purpose.** It is raised identically when * the row does not exist at all and when it exists but belongs to another * tenant, and it carries no field that separates them. That is the whole point: * * - Reporting "forbidden" for a foreign row and "not found" for a missing one * turns any keyed write into a cross-tenant EXISTENCE ORACLE — an attacker * walks ids and learns which ones are real in someone else's tenant, which * is exactly the isolation the `tenant()` mixin exists to provide. * - Collapsing the other way — silently affecting zero rows — is worse than * either: the handler reads it as "the row is gone", not "you may not touch * it", so a genuine isolation breach shows up in an app as a confusing * absent-row branch and never as a security signal. * * So both cases fail LOUDLY and IDENTICALLY. `id` is the key the caller itself * supplied — never another tenant's data. */ export declare class TenantRowNotFound extends TenantRowNotFound_base { } declare const TenantRowNotFound_base: Schema.TaggedErrorClass; } & { /** The table the keyed write targeted. */ table: typeof Schema.String; /** The primary key the CALLER supplied. Echoing it leaks nothing. */ id: typeof Schema.String; /** Human-readable explanation for diagnostics + UI. */ reason: typeof Schema.String; }>; /** * The subject a `storeForTenant(id)` view runs as — the caller's identity, * re-pointed at ONE explicit tenant. * * Lives here, next to the other subject constructors, because BOTH the serve * context builder (`@voltro/cli`) and the test harness (`@voltro/testing`) * must produce the identical subject. When it lived in the CLI, the harness * could not reach it, and `ctx.storeForTenant` was simply absent under test — * a handler that used it had no way to be tested at all. * * The narrowed return type is the point, not decoration: it states at the type * level that this can never hand back a `system` subject, which is the variant * whose null tenant means "all tenants". Producing one here would silently * widen a deliberately narrow view back to every tenant. */ export declare const tenantScopedSubject: (subject: Subject, tenantId: string) => Extract; /** * A write was attempted against a `tenant()`-scoped table, but the * authenticated subject's `tenantId` is null — either anonymous, or * an apiKey / serviceAccount without a tenant binding. Refusing the * write at the boundary is the safe default; a future "system writes" * subject type can opt out. */ export declare class TenantScopeViolation extends TenantScopeViolation_base { } declare const TenantScopeViolation_base: Schema.TaggedErrorClass; } & { /** The table the violating write targeted. */ table: typeof Schema.String; /** Human-readable explanation for diagnostics + UI. */ reason: typeof Schema.String; }>; export declare const toRpc: (descriptor: QueryProcedureDescriptor | MutationProcedureDescriptor | ActionProcedureDescriptor | StreamProcedureDescriptor) => Rpc.Rpc : Input, Stream; revision: typeof Schema.Number; data: Output; computed: Schema.optional; }>, Schema.Struct<{ _tag: Schema.Literal<["delta"]>; revision: typeof Schema.Number; emittedAt: typeof Schema.Number; patch: Schema.Struct<{ ops: Schema.Array$; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["replace"]>; path: typeof Schema.String; value: Schema.refine<{ readonly [x: string]: unknown; } & Readonly> & { readonly id: RowId; }, Schema.Schema<{ readonly [x: string]: unknown; }, { readonly [x: string]: unknown; }, never>>; }>, Schema.Struct<{ op: Schema.Literal<["remove"]>; path: typeof Schema.String; }>]>>; order: Schema.Array$>; }>; }>, Schema.Struct<{ _tag: Schema.Literal<["error"]>; error: typeof Schema.Unknown; revision: Schema.optional; }>]>, Schema.Schema.All>, typeof Schema.Never, never> | Rpc.Rpc : Input, Output, Schema.Schema.All, never> | Rpc.Rpc : Input, Stream, typeof Schema.Never, never>; /** * Coerce a timestamp read (`Date` / epoch-ms number / ISO string) to * epoch milliseconds. Unrecognised inputs collapse to `0`. */ export declare const tsMs: (v: unknown) => number; /** * Typed error every handler can throw to signal "you must be signed * in to perform this action". The framework's @effect/rpc layer * preserves the `_tag` across the wire, and `@voltro/client`'s error * bus surfaces it to the consuming app so the dashboard can * auto-redirect to the sign-in page (cleared session + UI swap). * * Distinct from TenantMismatch (which means "you ARE signed in but * touching the wrong tenant"). Unauthenticated means "no real subject * resolved at all" — anonymous, expired token, missing cookie, etc. */ export declare class Unauthenticated extends Unauthenticated_base { } declare const Unauthenticated_base: Schema.TaggedErrorClass; } & { /** Optional context — e.g. "auth.signin required", "session expired". */ reason: Schema.optional; }>; /** * Channel keys named by a `source:` that no `reactivityChannel()` declared. * * The channel half of the stale-`source:` audit. It should be structurally * unreachable when the channel is authored as an object — you cannot import a * declaration that does not exist — so it exists for the two ways round that: * a hand-written `source: 'channel:presence'` string, and a channel whose * declaring module the boot did not load. */ export declare const undeclaredChannelKeys: (procedures: ReadonlyArray<{ readonly name: string; readonly source: string | ReadonlyArray | undefined; }>) => ReadonlyArray<{ readonly procedure: string; readonly key: string; }>; export declare const UNDO_APPLY_TAG: "__voltro.undo.apply"; export declare const UNDO_LOG_TAG: "__voltro.undo.log"; export declare const UNDO_REDO_TAG: "__voltro.undo.redo"; /** `__voltro.undo.apply` — undo one invocation (synthesize + apply its inverse). * No `target` (it writes whatever tables the change-set touched — its effects * reach the client via those tables' own reactive deltas, not optimistic). */ export declare const undoApplyDescriptor: MutationProcedureDescriptor<"__voltro.undo.apply", Schema.Struct<{ invocationId: typeof Schema.String; }>, Schema.Struct<{ ok: typeof Schema.Boolean; }>, Schema.Union<[typeof UndoNotFound, typeof UndoForbidden, typeof UndoConflict]>>; /** The row changed since the action (a concurrent writer), OR the action * crossed an external side effect — a blind restore would clobber/lie, so * undo refuses. `reason` distinguishes the two (mirrors the engine's * UndoBoundaryError boundary). */ export declare class UndoConflict extends UndoConflict_base { } declare const UndoConflict_base: Schema.TaggedErrorClass; } & { invocationId: typeof Schema.String; reason: Schema.Literal<["conflict", "action"]>; }>; /** The action belongs to a different subject — undo is per-actor. */ export declare class UndoForbidden extends UndoForbidden_base { } declare const UndoForbidden_base: Schema.TaggedErrorClass; } & { invocationId: typeof Schema.String; }>; /** One undoable action in the calling subject's recent history. The heavy * `changes` payload is server-only — the client list needs only this. */ export declare const UndoLogEntry: Schema.Struct<{ /** The invocation id (= the `_voltro_undo_log` row id) to pass to apply/redo. */ id: typeof Schema.String; /** Rpc tag of the captured mutation (e.g. `todos.create`). */ tag: typeof Schema.String; /** Human label ("create todo") — falls back to the tag. */ label: Schema.NullOr; /** True once undone (so the UI shows it as redo-able). */ undone: typeof Schema.Boolean; /** True when the action crossed an external side effect → not undoable. */ crossesAction: typeof Schema.Boolean; /** Display-only creation time (stringified). */ createdAt: typeof Schema.String; }>; export declare type UndoLogEntry = Schema.Schema.Type; /** `__voltro.undo.log` — the calling subject's recent undoable actions, newest * first. Reactive (source `_voltro_undo_log`) so the list updates live as the * subject makes + undoes changes. */ export declare const undoLogQueryDescriptor: QueryProcedureDescriptor<"__voltro.undo.log", Schema.Struct<{ limit: Schema.optional; }>, Schema.Array$; /** True once undone (so the UI shows it as redo-able). */ undone: typeof Schema.Boolean; /** True when the action crossed an external side effect → not undoable. */ crossesAction: typeof Schema.Boolean; /** Display-only creation time (stringified). */ createdAt: typeof Schema.String; }>>, typeof Schema.Never>; /** No undo-log row for that invocation (already pruned, or wrong id). */ export declare class UndoNotFound extends UndoNotFound_base { } declare const UndoNotFound_base: Schema.TaggedErrorClass; } & { invocationId: typeof Schema.String; }>; /** `__voltro.undo.redo` — re-apply a previously-undone invocation's forward changes. */ export declare const undoRedoDescriptor: MutationProcedureDescriptor<"__voltro.undo.redo", Schema.Struct<{ invocationId: typeof Schema.String; }>, Schema.Struct<{ ok: typeof Schema.Boolean; }>, Schema.Union<[typeof UndoNotFound, typeof UndoForbidden, typeof UndoConflict]>>; export declare interface UpdateTarget> extends NestedTargetFields { readonly table: string; readonly op: 'update'; /** Identify the row(s) to patch. Default: `input.id`. Return an ARRAY to patch * MANY rows/items in one mutation (a bulk edit — where the per-item * parallel-write race lived). */ readonly identify?: ((input: Input) => string | ReadonlyArray) | undefined; /** Build the patch for a FLAT target (`current` is the OUTPUT row). Default: * merges input over current. For a NESTED (`path`) target use `shapeItem`. */ readonly shape?: ((input: Input, current: Row) => Row) | undefined; /** NESTED (`path`) update: build the item patch. `current` is the existing * ITEM (not the mutation output), so no cast is needed. */ readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined; } export declare interface VoltroPlugin { /** * Plugin identifier — printed in `voltro dev` boot logs + surfaced in * the inspect manifest. Use a stable string scoped to the package * (e.g. `@voltro/audit`, `acme.invoicing`). Required so multiple * instances of the same package can be disambiguated by suffix * (`@voltro/audit#analytics`). */ readonly name: string; /** * The plugin's CANONICAL name, before the app renamed it — set this whenever * `name` can come from an app-supplied `alias`. * * It exists because without it an alias silently half-works, and that was the * shipped state. `effectiveRouteTag` prefixes a route with the plugin's * alias UNLESS the route name already contains a dot — an escape hatch for a * plugin wanting a deeper namespace. But every first-party plugin declares * its routes fully qualified (`name: 'notifications.inbox'`, 96 such * declarations across seven plugins), so the escape hatch fires on all of * them and the alias moves NOTHING on the rpc surface. A user aliasing * `notifications` to escape a collision with their own `notifications.*` * routes would still collide — and would additionally lose the dashboard * panel, which fetches the default slug. Worse than not shipping the field. * * With `baseName` set, both tag derivations strip a leading * `.` before applying the EFFECTIVE alias, so * `notifications.inbox` under `alias: 'inbox'` becomes `inbox.inbox` rather * than staying put. A dotted name that does NOT start with the default alias * is left alone — that is the genuine escape hatch, and it survives. * * Derived, not declared per route, so the 96 declarations stay as they are * and cannot drift out of step with the strip. */ readonly baseName?: string; /** * Plugin version — the package's own semver. Distinct from * `framework` (the FRAMEWORK range the plugin works with). Used by * the manifest endpoint + future signing + install/uninstall * idempotency. Optional today; recommended. */ readonly version?: string; /** Optional human-readable description; surfaced by the dashboard. */ readonly description?: string; /** * Compatible framework version range, in npm-semver syntax * (`'^1.0.0'`, `'>=2.3.0 <3'`). The runtime checks the running * framework against this at boot — incompatible plugins log a * boot-time warning naming the constraint + the running version, * but DON'T abort boot. App author makes the call; surfacing the * mismatch is the framework's job. * * Absent → no compat check (plugin opts out of versioning). Suitable * for in-tree plugins that ship lockstep with the framework. */ readonly framework?: string; /** * Declared permission scopes. v1 advisory; future slice enforces. * Surfaced in the boot log + `/_voltro/inspect/plugins` endpoint * so operators audit what each plugin is asking for before granting. */ readonly permissions?: ReadonlyArray; /** * Environment variables this plugin reads (declaration only — metadata, not * a read path). The plugin still reads its values directly (`options.X ?? * process.env.X`); declaring them here surfaces the plugin's env needs in the * manifest (`/_voltro/inspect/env`), the generated `.env.example`, and the * dashboard Env panel. Mirror exactly what the plugin reads. */ readonly declaredEnv?: ReadonlyArray; /** * The authorization scopes this plugin DEFINES for the app — its scope * vocabulary. Distinct from `permissions` above, which is what the plugin * itself asks to be granted. * * `@voltro/plugin-rbac` fills this with every scope its `roles` map grants, * because that map already IS the app's declared vocabulary. The capability * manifest unions these, and `voltro check` compares each handler's required * scopes against the union: a guard demanding a scope no role can grant (a * typo, a rename) makes that procedure permanently uncallable, silently. * * Only declare this when the list is EXHAUSTIVE. A plugin that also grants * scopes from a dynamic source (a custom resolver, per-row ACLs) must leave * it undefined — the check treats "no declared scopes" as "cannot conclude" * and stays dormant, which is the honest outcome. A partial list would flag * correct code, and a check that cries wolf gets ignored. */ readonly declaredScopes?: ReadonlyArray; /** * Optional `effect/Schema` describing the plugin's user-supplied * config. When present the framework decodes the operator's config * payload against this schema at boot — invalid config aborts boot * with a typed error message. The decoded value lands in * `PluginLifecycleContext.config` so lifecycle hooks consume the * already-validated shape. * * For plugins that take options via a factory function (the * idiomatic TS pattern), `configSchema` is optional — the factory * already validates at the type level. Declare `configSchema` when * the plugin will surface a config form in the dashboard, or when * the plugin reads dynamic config from env / a config file. */ readonly configSchema?: Schema.Schema.Any; /** * Wraps every MUTATION handler call. Composed in declaration order * across the plugin list: outermost = first listed in app.config.ts. * Absent → the plugin contributes nothing at the mutation boundary * (schema-only / lifecycle-only plugins are fine). */ readonly interceptMutation?: RpcInterceptor; /** * Wraps every QUERY (streaming subscription handler) call. Same * composition semantics as `interceptMutation`. Most observability * plugins want to install this AND `interceptMutation` so the audit * trail covers both read + write. */ readonly interceptQuery?: RpcInterceptor; /** * Wraps every ACTION (unary non-transactional handler) call. Same * composition semantics as `interceptMutation`. Useful for plugins * that need to govern outbound HTTP / file IO calls (rate limiting, * tenant-scoped IO quotas). */ readonly interceptAction?: RpcInterceptor; /** * Effect Layer that contributes services into the per-request * context. Anything declared here is yieldable from any handler in * the host app (`const myTag = yield* MyTag`). Use this when a * plugin needs to expose APIs to handlers without forcing every * handler to import the plugin directly — handlers depend on the * Tag, the plugin owns the Live implementation. * * The layer is provided ONCE at boot — it must be self-contained * (`RIn = never`). For per-call data, expose a `Service` whose * methods return Effects that consume per-call inputs. */ readonly services?: Layer.Layer; /** * RPC routes the plugin contributes. Registered alongside the * user-authored routes — same wire protocol, same dashboard * surface. The plugin's name is prepended to each route's `name` * field unless the name already carries a dot. * * Use for plugin-shaped endpoints the user app shouldn't have to * re-implement: webhook ingestion targets, OAuth callbacks, plugin * admin queries, etc. */ readonly routes?: ReadonlyArray; /** * Plugin-contributed inspect endpoints. Mounted under * `/_voltro/inspect/plugins//` — see * `PluginInspectEndpoint`. Use for plugin-specific tooling that * doesn't fit the rpc wire (health probes, debug dumps, * on-demand stats exports). */ readonly inspectEndpoints?: ReadonlyArray; /** * Public raw-HTTP routes the plugin serves directly (not rpc, not * inspect). Mounted by BOTH `voltro dev` and the production `serveApi` * on the same listener — so e.g. `@voltro/plugin-storage` can serve * `GET /_voltro/storage/:id` (302-presigned or streamed bytes) in dev * AND prod. The handler owns its own auth + returns a status, an * optional body (string or bytes), content-type, and headers (so it can * 302-redirect or set immutable cache headers). */ readonly httpRoutes?: ReadonlyArray; /** * Called once by the serve pipeline (dev + serveApi) AFTER the app's * DataStore is built — earlier than this it doesn't exist (dev activates * plugins before the store). Lets a plugin bind a DB-backed resource it * couldn't construct from static config: e.g. `@voltro/plugin-storage` * swaps its in-memory ref store for `dataStoreRefStore(store)` so blob * metadata persists in `_voltro_storage_refs`. * * `store` is the framework `DataStore` — TYPED now (was `unknown`); a * `(s) => s as DataStore` implementation still compiles (DataStore → * DataStore is a no-op cast) so the tightening is additive. `ctx` is the * post-store bind context: the framework's already-open `SqlClient` * (`ctx.sql`) so a plugin reuses the app's pool instead of rebuilding one * from env, and `ctx.scheduleCoordinated(name, intervalMs, effect)` for * a cluster-coordinated sweep that runs on ONE replica per tick instead * of an un-coordinated per-replica `setInterval`. * * `ctx` is OPTIONAL in the type so the tightening stays additive: the * framework ALWAYS passes it, but existing single-arg call sites (a * plugin's `bindDataStore(store)` in a unit test) still compile. A plugin * that wants the enablers declares `(store, ctx)` and reads * `ctx?.sql` / `ctx?.scheduleCoordinated(...)`; one that ignores `ctx` * keeps working unchanged. */ readonly bindDataStore?: (store: DataStore, ctx?: PluginBindContext) => void; /** * Post-commit ChangeEvent tap — an `Effect` the runtime SUPERVISES. * Fires for EVERY committed store change (insert/update/delete on any table), * AFTER the transaction commits and AFTER the framework's own subscribers. * * The runtime forks the returned Effect under the plugin's supervision scope * and routes its failure channel to the plugin-scoped logger — so a change-tap * gets a REAL typed error channel (`Effect.retry`, `Effect.timeout`, * `Effect.catchTag`, durable enqueue) instead of fire-and-forget * `.catch(warn)` glue. The fork keeps the tap non-blocking: a slow or failing * tap can't back-pressure or break the change stream, and a failure is logged * scoped to the plugin, never a defect that crosses into the stream. * * Still NOT durable at the framework layer — a crash between commit and the * fork loses the event; a durable consumer builds durability INSIDE the * Effect (a `store.insert` into an outbox the way `@voltro/plugin-cdc-out` * does, retried against the typed error channel). This is the seam * `@voltro/plugin-search` uses to mirror tables into an external index * without a user-authored `*.subscribe.ts`. Requires the * `store:changes:read` permission. * * Exactly-once / change-scope semantics are unchanged: read * `event.origin` + `event.changeScope` inside the Effect to act once per * change fleet-wide (skip `origin:'injected'` on `'local'` scope; elect one * worker on `'fleet'`). */ readonly onChangeEvent?: (event: PluginChangeEvent) => Effect.Effect; /** * Wraps every schedule firing. Composed across plugins in * declaration order (first-in-array = outermost wrapper). Runs * INSIDE the framework's `maxRuntimeMs` watchdog — the watchdog * applies to the whole chain. See `ScheduleFireInterceptor`. */ readonly onScheduleFire?: ScheduleFireInterceptor; /** * Wraps every `step()` (== `Activity.make`) invocation inside a * workflow body. Composed across plugins. The interceptor sees * the user's `execute` Effect AS an Effect — it can `Effect.tap`, * `Effect.retry`, `Effect.withSpan`, etc. See * `WorkflowStepInterceptor`. Bypassed on workflow REPLAY (only * fires on the first execution per step). */ readonly onWorkflowStep?: WorkflowStepInterceptor; /** * Plugin-supplied codegen contribution. The framework calls this * during `voltro dev`'s codegen step and emits the returned string * into `rpcGroup.generated.ts` as a marked section. Use for typed * accessor bindings that wrap the plugin's runtime apis. See * `PluginCodegen`. */ readonly codegen?: PluginCodegen; /** * Templates this plugin contributes to `voltro init` / `voltro add-app`. * Each entry is a self-contained directory tree the CLI seeds into * a new project. The framework lists them under the plugin's name * in `voltro list-templates`. */ readonly templates?: ReadonlyArray; /** * Pre-auth HTTP-pipeline interceptor. Fires at the very top of the * HTTP request handler, BEFORE auth resolution, BEFORE rpc routing, * BEFORE inspect. Use for rate-limit, geo-block, bot-detection, * outbound header injection. See `HttpRequestInterceptor`. * * Requires `'http:intercept'` permission — boot fails loudly if * the hook is declared without the matching permission. */ readonly onHttpRequest?: HttpRequestInterceptor; /** * Dashboard mount contributions — remote-loaded ESM modules the * dashboard host renders alongside framework pages. See * `PluginDashboardMount` for the trade-offs vs iframe. * * Requires `'dashboard:mount'` permission. Surfaces under * `/_voltro/inspect/plugins/dashboard-mounts`; the actual mount * runtime lives in voltro-cloud-dashboard + voltro-devtools. */ readonly dashboard?: ReadonlyArray; /** * Schema + migration contribution. `tables` are merged with the * user's table set and run through the same idempotent * `applySchema()` path. `migrations` run after the schema apply * against the live SqlClient and are tracked in * `_voltro_plugin_migrations` so they execute exactly once per * app database. * * `tables` requires `'store:write'`; `migrations` requires * `'migration:run'`. Either one missing → boot fails with the * specific permission name in the error. */ readonly extendSchema?: PluginSchemaContribution; /** * Cross-cutting error schemas merged into every procedure's wire error * union. Lets a plugin interceptor fail with a typed error (e.g. * `RateLimited`) that decodes TYPED on the client instead of as an * untyped defect. The cli applies these to the server rpc group; codegen * emits the matching imports into the generated client group. See * `PluginErrorSchema`. */ readonly errorSchemas?: ReadonlyArray; /** * Client-facing RPC route descriptors for `routes` the web client calls * (e.g. `storage.mintUploadTicket` behind `useUpload`). The codegen emits each * into the generated client rpc group so its tag resolves in the browser. Each * `import` MUST resolve to a BROWSER-SAFE module (descriptor/schema only) — see * `PluginRpcClientDescriptor`. Omit for routes only ever called server-side. */ readonly rpcClientDescriptors?: ReadonlyArray; /** * One-time setup invoked the FIRST time this plugin is seen by the * host. Use for schema migrations + resource provisioning that * survive across deactivate/activate cycles. See `PluginInstallHook`. */ readonly onInstall?: PluginInstallHook; /** * One-time teardown invoked when the host removes the plugin * (`voltro plugins uninstall `). Mirror of `onInstall`. */ readonly onUninstall?: PluginUninstallHook; /** * One-shot setup invoked at app boot. Throws abort boot. * Lifecycle hooks run OUTSIDE any request — receive `PluginLifecycleContext`, * not `AppContext`. */ readonly onActivate?: PluginActivateHook; /** * One-shot teardown invoked at app shutdown. The framework waits * up to 5s for it to resolve before SIGKILL. */ readonly onDeactivate?: PluginDeactivateHook; /** * Contribute to the app's OpenTelemetry observability layer — extra * span processors / metric readers / resource attributes / a sampler. * Gathered at boot (before the tracer builds) and merged into the * framework's NodeSdk ALONGSIDE its own exporter + buffer sink (the * framework keeps owning the tracer — vendor SDKs run as OTel * CONSUMERS, never the global provider, so the in-app Traces dashboard * stays intact). This is how `@voltro/plugin-sentry` / * `@voltro/plugin-datadog` route the framework's spans to the vendor * with zero `OTEL_*` env. The hook may be async (lazy-init the vendor * SDK here). Returns of `undefined`/empty are no-ops. */ readonly contributeObservability?: (ctx: PluginLifecycleContext) => Promise | ObservabilityContribution | undefined; } /** * The table names THIS app declares — filled in by codegen, empty here. * * ── Why an interface, and why it may be empty ────────────────────────────── * * `source:` is matched by NAME against change events, so a typo or a missed * rename produces a subscription that is permanently QUIET rather than broken: * the query compiles, boots, serves its first snapshot and never updates again. * The boot warns about it, but a warning is read once and a compile error cannot * be read past — and the compiler cannot help while the field is `string`. * * It cannot be a fixed union either: `@voltro/protocol` has no idea what tables * an app declares. So the app's own codegen augments this interface, and * {@link TableName} resolves to those names. * * **Empty means `string`, deliberately.** Before the first codegen run — and in * any consumer that never generates — `keyof` this is `never`, and a `never` * here would reject every `source:` in the codebase with an error about a type * nobody wrote. The fallback keeps that from being a cliff: you get exactly * today's behaviour until the generated names exist, and the tightening is * silent and automatic. * * Augment it as: * * ```ts * declare module '@voltro/protocol' { * interface VoltroTableNames { tasks: true; task_sub_tasks: true } * } * ``` */ export declare interface VoltroTableNames { } /** What a gateway's connection handler receives. Transport-agnostic on * purpose — the runtime adapts the platform socket to this. */ export declare interface WebSocketGatewayConnection { /** Send a text or binary frame. */ readonly send: (data: string | Uint8Array) => void; /** Close the connection (application close codes 4000-4999 are yours). */ readonly close: (code?: number, reason?: string) => void; /** Register a message listener (binary-safe; text arrives as bytes). */ readonly onMessage: (listener: (data: Uint8Array) => void) => void; /** The authenticated subject — `null` only on an `auth: 'public'` route. */ readonly subject: Subject | null; /** Lowercased request headers of the upgrade. */ readonly headers: Readonly>; /** The mounted path. */ readonly path: string; } export declare interface WebSocketGatewayRoute { /** Absolute upgrade path (`/gateways/yjs`). Must not collide with the rpc * socket (`/ws` or the configured `transport.wsPath`) or `/rpc`. */ readonly path: `/${string}`; /** * REQUIRED, no default: who may connect. * - `'subject'` — the upgrade resolves a Subject through the SAME auth * chain as rpc/SSR (cookie/bearer); an unauthenticated upgrade is a 401 * BEFORE any socket exists, and the connection closes with * {@link GATEWAY_CREDENTIAL_EXPIRED_CLOSE_CODE} when the credential * expires. * - `'public'` — deliberately unauthenticated (a device fleet with its own * protocol-level auth). A decision somebody wrote down, not a default. */ readonly auth: 'subject' | 'public'; /** * Runs once per accepted connection. The returned function is the * connection's TEARDOWN — taken at construction (the `startOutboxRunner` * rule): it runs on client disconnect, on credential expiry, and on * server shutdown, so whatever the handler opened cannot outlive the * socket. */ readonly onConnection: (connection: WebSocketGatewayConnection) => void | (() => void) | Promise void)>; } /** * The error union a procedure ACTUALLY puts on the wire — `descriptor.error` * plus everything the framework can produce for it before or around the * executor. * * ── Why this is exported rather than inlined in the lifters ──────────────── * * Because a second reader needs the same answer, and it was computing a * different one. `runMutationEffect` refuses to ship a TAGGED error that this * descriptor cannot represent — it collapses it to `InternalError` rather than * letting the encoder emit a raw defect tree. Right rule. It was handed * `descriptor.error`, the RAW declaration, while the wire union it is protecting * is the WIDENED one. So the server judged against a narrower set than it had * advertised, and the framework's own errors failed that judgement: * * - a guard's `ScopeError` — reported by a deployment, who measured it on every * relationship-guarded mutation in a real app: a permissions refusal reached * their client as `InternalError`, so their UI showed "something went wrong" * where it should have shown "forbidden"; * - a cross-table `rule()`'s `BusinessRuleViolation` — unconditional for * mutations, and `withRuleError`'s own comment says it MUST be in the union * "or the violation crosses the wire as an untyped defect". It was in the * union and collapsed before it got there; * - the `requiresApproval` refusals, for the same reason. * * The consumer diagnosed it as "the merge only happens on the streaming path". * The merge happens on BOTH — `withGuardError` is called by every lifter. What * differs is that a QUERY is delivered through `wireErrorFromCause`, which * preserves the tag and never consults a declared set, while a mutation goes * through the check above. Their observation was exact and their fix — declaring * `ScopeError` themselves — is what made the two sets agree by hand. * * One function now, so they cannot disagree again. */ export declare const wireErrorUnion: (descriptor: { readonly error: Schema.Schema.All; readonly guards?: DeclaredAccess | undefined; readonly requiresApproval?: AnyApprovalPolicy | undefined; }, kind: "query" | "mutation" | "action" | "stream") => Schema.Schema.All; export declare const workflowCancelDescriptor: ActionProcedureDescriptor<"__voltro.workflow.cancel", Schema.Struct<{ workflowName: typeof Schema.String; executionId: typeof Schema.String; }>, Schema.Struct<{ ok: typeof Schema.Boolean; }>, typeof Schema.Never>; export declare const WorkflowControlInputSchema: Schema.Struct<{ workflowName: typeof Schema.String; executionId: typeof Schema.String; }>; export declare type WorkflowDomainEventRow = Schema.Schema.Type; export declare const WorkflowDomainEventRowSchema: Schema.Struct<{ id: typeof Schema.String; name: typeof Schema.String; payload: typeof Schema.Unknown; source: typeof Schema.String; subject: Schema.NullOr; traceId: Schema.NullOr; occurredAt: typeof Schema.Date; }>; export declare const WorkflowDomainEventsInputSchema: Schema.Struct<{ name: Schema.optional; limit: Schema.optional; }>; export declare const workflowDomainEventsQueryDescriptor: QueryProcedureDescriptor<"__voltro.workflow.domainEvents", Schema.Struct<{ name: Schema.optional; limit: Schema.optional; }>, Schema.Array$; traceId: Schema.NullOr; occurredAt: typeof Schema.Date; }>>, typeof Schema.Never>; export declare const WorkflowEventDeliveriesInputSchema: Schema.Struct<{ eventId: typeof Schema.String; }>; export declare const workflowEventDeliveriesQueryDescriptor: QueryProcedureDescriptor<"__voltro.workflow.event.deliveries", Schema.Struct<{ eventId: typeof Schema.String; }>, Schema.Array$; status: Schema.Literal<["starting", "started", "skipped", "failed"]>; idempotencyKey: typeof Schema.String; skipped: typeof Schema.Boolean; errorMessage: Schema.NullOr; createdAt: typeof Schema.Date; completedAt: Schema.NullOr; }>>, typeof Schema.Never>; export declare type WorkflowEventDeliveryRow = Schema.Schema.Type; export declare const WorkflowEventDeliveryRowSchema: Schema.Struct<{ id: typeof Schema.String; eventId: typeof Schema.String; eventName: typeof Schema.String; triggerId: typeof Schema.String; workflowName: typeof Schema.String; executionId: Schema.NullOr; status: Schema.Literal<["starting", "started", "skipped", "failed"]>; idempotencyKey: typeof Schema.String; skipped: typeof Schema.Boolean; errorMessage: Schema.NullOr; createdAt: typeof Schema.Date; completedAt: Schema.NullOr; }>; export declare type WorkflowParentClosePolicy = Schema.Schema.Type; export declare const WorkflowParentClosePolicySchema: Schema.Literal<["cancel", "terminate", "abandon"]>; export declare const workflowResumeDescriptor: ActionProcedureDescriptor<"__voltro.workflow.resume", Schema.Struct<{ workflowName: typeof Schema.String; executionId: typeof Schema.String; }>, Schema.Struct<{ ok: typeof Schema.Boolean; }>, typeof Schema.Never>; export declare type WorkflowRunEventRow = Schema.Schema.Type; export declare const WorkflowRunEventRowSchema: Schema.Struct<{ id: typeof Schema.String; runId: typeof Schema.String; eventType: typeof Schema.String; payload: Schema.NullOr; occurredAt: typeof Schema.Date; stepName: Schema.NullOr; attempt: Schema.NullOr; }>; export declare const workflowRunEventsQueryDescriptor: QueryProcedureDescriptor<"__voltro.workflow.run.events", Schema.Struct<{ /** `_voltro_workflow_runs.id`; step/event rows reference this id. */ runId: typeof Schema.String; }>, Schema.Array$; occurredAt: typeof Schema.Date; stepName: Schema.NullOr; attempt: Schema.NullOr; }>>, typeof Schema.Never>; export declare type WorkflowRunHandle = Schema.Schema.Type; export declare const WorkflowRunHandleSchema: Schema.Struct<{ /** Stable handle callers pass to `useWorkflowRun(...)`. The durable execution * id for a started run; the pending-intent id for a queued one. */ id: typeof Schema.String; /** Workflow tag, e.g. `notes.summarise`. */ workflowName: typeof Schema.String; /** * Deterministic durable id derived from workflow name + idempotency key. * * NULL when flow control deferred, dropped, or skipped the start — because * there is no execution yet, and there may never be one. It used to be a * required string, and keeping it that way would have meant inventing a value: * either an empty string or the id the run WOULD have had. Both are polls that * return `status: 'unknown'` forever, which is the failure mode this whole * feature set exists to remove. * * For a `skipped` singleton it is the INCUMBENT's execution id — a real, * pollable run, which is the entire point of `mode: 'skip'`. */ executionId: Schema.NullOr; /** * `running` — the engine has it; starting is fire-and-return. * `queued` — flow control deferred it; see `deferral.dueAt`. * `dropped` — over a `rateLimit` cap. It will NOT run. * `skipped` — a `singleton: { mode: 'skip' }` key was held; `executionId` * names the run that holds it. */ status: Schema.Literal<["running", "queued", "dropped", "skipped"]>; deferral: Schema.optional; /** How long until the rate window reopens. Null unless dropped. */ retryAfterMs: Schema.NullOr; /** The `_voltro_workflow_pending` row holding this start. Null unless queued. */ intentId: Schema.NullOr; }>>; }>; export declare const workflowRunQueryDescriptor: QueryProcedureDescriptor<"__voltro.workflow.run", Schema.Struct<{ /** `_voltro_workflow_runs.id` OR durable `executionId`. */ id: typeof Schema.String; }>, Schema.Array$; payload: typeof Schema.Unknown; workflowVersion: Schema.NullOr; workflowPatches: Schema.NullOr; output: Schema.NullOr; subject: Schema.NullOr; source: Schema.NullOr; errorTag: Schema.NullOr; errorMessage: Schema.NullOr; cancelled: typeof Schema.Boolean; startedAt: typeof Schema.Date; completedAt: Schema.NullOr; durationMs: Schema.NullOr; traceId: Schema.NullOr; parentExecutionId: Schema.NullOr; parentClosePolicy: Schema.NullOr>; }>>, typeof Schema.Never>; export declare const WorkflowRunRefSchema: Schema.Struct<{ /** `_voltro_workflow_runs.id` OR durable `executionId`. */ id: typeof Schema.String; }>; export declare type WorkflowRunRow = Schema.Schema.Type; export declare const WorkflowRunRowSchema: Schema.Struct<{ id: typeof Schema.String; tag: typeof Schema.String; executionId: typeof Schema.String; status: Schema.Literal<["running", "succeeded", "failed", "cancelled", "suspended"]>; payload: typeof Schema.Unknown; workflowVersion: Schema.NullOr; workflowPatches: Schema.NullOr; output: Schema.NullOr; subject: Schema.NullOr; source: Schema.NullOr; errorTag: Schema.NullOr; errorMessage: Schema.NullOr; cancelled: typeof Schema.Boolean; startedAt: typeof Schema.Date; completedAt: Schema.NullOr; durationMs: Schema.NullOr; traceId: Schema.NullOr; parentExecutionId: Schema.NullOr; parentClosePolicy: Schema.NullOr>; }>; export declare type WorkflowRunsInput = Schema.Schema.Type; export declare const WorkflowRunsInputSchema: Schema.Struct<{ tag: Schema.optional; status: Schema.optional>; limit: Schema.optional; }>; export declare const workflowRunsQueryDescriptor: QueryProcedureDescriptor<"__voltro.workflow.runs", Schema.Struct<{ tag: Schema.optional; status: Schema.optional>; limit: Schema.optional; }>, Schema.Array$; payload: typeof Schema.Unknown; workflowVersion: Schema.NullOr; workflowPatches: Schema.NullOr; output: Schema.NullOr; subject: Schema.NullOr; source: Schema.NullOr; errorTag: Schema.NullOr; errorMessage: Schema.NullOr; cancelled: typeof Schema.Boolean; startedAt: typeof Schema.Date; completedAt: Schema.NullOr; durationMs: Schema.NullOr; traceId: Schema.NullOr; parentExecutionId: Schema.NullOr; parentClosePolicy: Schema.NullOr>; }>>, typeof Schema.Never>; export declare type WorkflowRunStatus = Schema.Schema.Type; export declare const WorkflowRunStatusSchema: Schema.Literal<["running", "succeeded", "failed", "cancelled", "suspended"]>; export declare type WorkflowRunStepRow = Schema.Schema.Type; export declare const WorkflowRunStepRowSchema: Schema.Struct<{ id: typeof Schema.String; runId: typeof Schema.String; stepName: typeof Schema.String; attempt: typeof Schema.Number; status: Schema.Literal<["running", "succeeded", "failed"]>; input: Schema.NullOr; retryPolicy: Schema.NullOr; output: Schema.NullOr; errorTag: Schema.NullOr; errorMessage: Schema.NullOr; errorCause: Schema.NullOr; startedAt: typeof Schema.Date; completedAt: Schema.NullOr; durationMs: Schema.NullOr; }>; export declare const workflowRunStepsQueryDescriptor: QueryProcedureDescriptor<"__voltro.workflow.run.steps", Schema.Struct<{ /** `_voltro_workflow_runs.id`; step/event rows reference this id. */ runId: typeof Schema.String; }>, Schema.Array$; input: Schema.NullOr; retryPolicy: Schema.NullOr; output: Schema.NullOr; errorTag: Schema.NullOr; errorMessage: Schema.NullOr; errorCause: Schema.NullOr; startedAt: typeof Schema.Date; completedAt: Schema.NullOr; durationMs: Schema.NullOr; }>>, typeof Schema.Never>; export declare const WorkflowRunTableRefSchema: Schema.Struct<{ /** `_voltro_workflow_runs.id`; step/event rows reference this id. */ runId: typeof Schema.String; }>; export declare const workflowSignalDescriptor: ActionProcedureDescriptor<"__voltro.workflow.signal", Schema.Struct<{ id: typeof Schema.String; signalName: typeof Schema.String; payload: Schema.optional; }>, Schema.Struct<{ eventId: typeof Schema.String; }>, typeof Schema.Never>; export declare const WorkflowSignalInputSchema: Schema.Struct<{ id: typeof Schema.String; signalName: typeof Schema.String; payload: Schema.optional; }>; export declare type WorkflowStartDeferral = Schema.Schema.Type; /** * What flow control did to a start that did not become a run. * * Present only when `status !== 'running'`. Every field is a fact the caller * would otherwise have to guess at: a drop with no `retryAfterMs` is * indistinguishable from a failure, and a queued start with no `dueAt` is * indistinguishable from one that was lost. */ export declare const WorkflowStartDeferralSchema: Schema.Struct<{ /** `debounce` | `batch` | `throttle` | `concurrency` | `paused` | * `rateLimit` | `singleton`. */ mode: typeof Schema.String; /** Epoch ms this may first be admitted. Null for a drop or a skip. */ dueAt: Schema.NullOr; /** How long until the rate window reopens. Null unless dropped. */ retryAfterMs: Schema.NullOr; /** The `_voltro_workflow_pending` row holding this start. Null unless queued. */ intentId: Schema.NullOr; }>; /** * Per-step plugin interceptor. Wraps every `step()` (== * `Activity.make`) invocation inside a workflow body. Composed * across plugins at boot. The interceptor sees the user's `execute` * Effect AS an Effect — it can wrap with `Effect.tap`, * `Effect.retry`, `Effect.withSpan`, etc. * * Useful for: per-step audit beyond the framework's row-recorder, * step-level retry-policy override, step-level resource scoping, * external observability injection. * * The interceptor runs INSIDE `@effect/workflow`'s replay logic — * on a workflow resume, the framework replays cached Activity * outputs without re-running the user effect, and the interceptor * is bypassed on replay (only fires on the first execution). */ export declare interface WorkflowStepContext { /** Run id from `_voltro_workflow_runs` (`'unrecorded'` outside * the framework's wrapper, e.g. in unit tests). */ readonly runId: string; /** Step name (== first arg of `Activity.make({ name })`). */ readonly stepName: string; /** Attempt number — 1 on first try, 2+ on retry. */ readonly attempt: number; } export declare type WorkflowStepInterceptor = (next: Effect.Effect, ctx: WorkflowStepContext) => Effect.Effect; export declare const workflowUpdateDescriptor: ActionProcedureDescriptor<"__voltro.workflow.update", Schema.Struct<{ id: typeof Schema.String; updateName: typeof Schema.String; payload: Schema.optional; timeoutMs: Schema.optional; }>, Schema.Struct<{ eventId: typeof Schema.String; updateId: typeof Schema.String; completedEventId: typeof Schema.String; result: typeof Schema.Unknown; }>, typeof Schema.Never>; export declare const WorkflowUpdateInputSchema: Schema.Struct<{ id: typeof Schema.String; updateName: typeof Schema.String; payload: Schema.optional; timeoutMs: Schema.optional; }>; export declare type WorkflowUpdateResult = Schema.Schema.Type; export declare const WorkflowUpdateResultSchema: Schema.Struct<{ eventId: typeof Schema.String; updateId: typeof Schema.String; completedEventId: typeof Schema.String; result: typeof Schema.Unknown; }>; /** * Scope for a WS-rpc MUTATION. Unlike the REST scope (tenant + method + path), * this includes the acting SUBJECT id: a WS app is often single-tenant with many * users, and a key from subject A must never replay for subject B. The tag is the * effective rpc tag (e.g. `notes.create`), so the same key on two different * mutations stays independent. */ export declare const wsMutationIdempotencyScope: (tenantId: string | null | undefined, subjectId: string | null | undefined, mutationTag: string) => string; export { }