@voltro/protocol 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -39,6 +39,55 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.4.0] — 2026-07-18
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/protocol, @voltro/plugin-rbac, @voltro/runtime** — **Resource-aware declarative guards + one denial tag.** `guards: [{ scope, resource }]` now actually scopes to a resource. Register a resolver — either `setResourceScopeResolver((req) => Effect<boolean>)` from `@voltro/protocol`, or `rbacPlugin({ resolveResourceRoles: (subject, resourceId) => roleSlugs })` — and the framework asks it, per request, whether the caller holds the scope **on the extracted resource** (a team, workspace, document), before the executor runs (mutations: before the transaction opens). A globally-held scope or `admin:full` still short-circuits without a resolver call; a resolver error fails **closed**. With no resolver registered, `resource` stays advisory (global check) exactly as before, so single-tenant apps are unaffected. **BREAKING — `@voltro/plugin-rbac` no longer exports `Forbidden`.** rbac's `permission()` / `assertPermission()` / `anyPermission()` now fail with protocol's `ScopeError` — the SAME typed error the declarative `guards:` enforcement already raised. One denial tag across the framework: a client that matches `_tag === 'ScopeError'` (or `errorTag(err)`) recognizes both a descriptor-guard denial and a `permission()` denial with one branch. The `@voltro/plugin-rbac/errors` subpath is removed. Migration (the `voltro update` codemod rewrites the mechanical part): - `import { Forbidden } from '@voltro/plugin-rbac/errors'` → `import { ScopeError } from '@voltro/protocol'`; `import { Forbidden } from '@voltro/plugin-rbac'` → `import { ScopeError } from '@voltro/plugin-rbac'` (re-exported from the root). - Every `Forbidden` identifier → `ScopeError` (incl. `error: Forbidden` on a descriptor). - By hand: a constructed `new Forbidden({ required, reason })` becomes `new ScopeError({ required, message })` (the field is `message` and is REQUIRED); and any `Effect.catchTag('Forbidden', …)` / `err._tag === 'Forbidden'` string becomes `'ScopeError'`. Guards are normally caught, not constructed, so most apps only need the import rewrite the codemod does. Also note: `requireScope` / declarative `guards:` enforcement is now async-capable (it awaits the resource resolver when one is registered) — no change to handler code, which never called the internal enforcement directly.
47
+ - **@voltro/client, @voltro/plugin-rbac** — **`@voltro/plugin-rbac/web` is removed — the UI permission gate lives in `@voltro/client`.** The framework shipped `useCan` TWICE, with the same name and the same semantics, over two different React contexts. An app that mounted `<PermissionProvider>` and imported the plugin's `useCan` got an empty scope set and silently hid every gated affordance — no error, just a UI where nothing is permitted. One of the two had to go. `@voltro/client` survives because scopes are a FRAMEWORK concept: `@voltro/protocol` owns `ScopeError`, `guards: [{ scope }]` and `ctx.access`, and rbac is only one way to PRODUCE scopes — an app can register its own `setResourceScopeResolver` over its own tables and never install the plugin. The hook consumes scopes, which is generic; producing them is rbac-specific. Keeping the hook behind the plugin would force an rbac dependency on apps that deliberately don't use rbac, purely to gate a button. `@voltro/plugin-rbac` keeps everything that IS rbac: the roles map, role→scope compilation, the `permission()` / `assertPermission()` / `anyPermission()` server guards, the role tables, and `resolveResourceRoles`. The codemod rewrites every mechanical site: `RbacScopeProvider` → `PermissionProvider`, `canFromScopes` → `canCall`, `canAnyFromScopes` → `canCallAny`, and repoints `useCan` / `useCanAny` / `ADMIN_SCOPE` to `@voltro/client`. A raw `useContext(RbacScopeContext)` is ANNOTATED rather than rewritten — the client equivalent is `usePermissions()`, which returns `{ scopes }` instead of the bare array, so a silent rewrite would compile and then hand back the wrong shape.
48
+
49
+ ### Added
50
+
51
+ - **@voltro/runtime, @voltro/cli** — **`read({ where })` on aggregates.** Aggregate reads were all-or-nothing, so every consumer that wanted one slice pulled the whole materialised set over the wire and filtered in the component — the cost scaling with the aggregate, not the slice. `where` entries are ANDed; a scalar means strict equality, an array means IN; filtering runs before `orderBy`/`limit`. Deliberately a data filter over already materialised rows, not a predicate language — aggregates stay aggregates.
52
+ - **@voltro/client** — **Two ergonomics that remove the two most-repeated shapes in a Voltro frontend.** - **`mutate(input, options)` — `onSuccess` / `onError` / `notify`.** Every write site used to re-type `try { await mutate() } catch { toast.error(…) } finally { setSubmitting(false) }`. `pending` already replaced the `finally`; these replace the rest. Load-bearing semantic: supplying an error handler (`onError` or `notify.error`) marks the failure HANDLED — `mutate` resolves with `undefined` instead of rejecting, which is what actually deletes the try/catch. With no handler it rejects exactly as before, so unhandled failures stay loud. `notify` routes to an app-registered sink (`setMutationNotifier`) — the framework stays unbound to any toast library. - **`useSubscription` now returns `loading` and `isEmpty`, and accepts `fallback`.** Call sites branched on `data === undefined`, conflating "no snapshot yet" with "zero rows" — the cause of a flash of empty-state before the first snapshot. They are now distinct, derived once. `fallback` fills `data` while loading without lying about `loading`. Also documented: `error` carries a COLD-START stream failure (check it to avoid an infinite skeleton); a failure after data arrived deliberately does not blank good data — those reach the error bus (`useOnRpcError`).
53
+ - **@voltro/cli** — **Make the client hook surface discoverable — and keep it that way.** A downstream app built its whole frontend on three transport hooks (`useSubscription` / `useMutation` / `useAction`) and hand-rolled forms, tables, upload, permission gates, debounce and pagination, because the agent guide documented ~none of the ~40 client hooks that already ship. Three changes: - **The always-loaded core now carries a "Pick the CLIENT primitive" rubric** — the same decision-rubric treatment the backend primitives get, plus a "you're about to write X → reach for Y" table (per-field `useState` → `useFormBinding`, hand-rolled table → `useDataTable`, `FileReader` → `useUpload`, `useMemo` fan-in → `useDerived`, …). - **The hook reference is complete.** `reference/hooks-overview.md` presented itself as "the client-side hook surface" while documenting 13 of 44 hooks; it now enumerates the schema-driven-UI, files/permissions/utilities, and AI families (en + de). - **A coverage GATE** (`agentsMdTemplate.test.ts`): every exported `use*` hook of `@voltro/client` must appear in the composed agent-docs corpus, or the test fails with the list. Docs can no longer silently fall behind the export surface. Plus **`voltro doctor` now flags hand-rolls**: it scans an app's source for the patterns a shipped primitive covers (hand-rolled form/table, `FileReader` upload, `setTimeout` debounce, `useMemo` fan-in, local Next.js compat shims, hand-rolled presence) and names the primitive to use. Advisory, never fatal — a false-positive lecture must not fail a build.
54
+ - **@voltro/cli** — **Generated `matchError` / `AppError` / `AppErrorTag`.** Codegen now emits a per-app exhaustive error matcher into `rpcGroup.generated.ts`, derived BY REFERENCE from every descriptor's `error:` schema plus the cross-cutting plugin errors. `matchError(err, { [tag]: handler }, fallback?)` dispatches on `_tag` with the handler keys constrained to the app's ACTUAL error tags — so a hand-maintained tag list that silently drifts (dead/renamed tags) is gone; a stale tag is a compile error. Browser-safe (reads `_tag` structurally, no runtime dep). Workflows keep their existing `WorkflowErrors`.
55
+ - **@voltro/cli** — **`voltro doctor` + `voltro update --codemods-only`/`--from`** — two upgrade-path ergonomics from AWB adoption feedback: - **`voltro doctor [app]`** (and **`voltro serve --preflight`**) preflights an API app for production serve: it verifies the precompiled serve bundle exists and, if not, prints the exact fix and exits 1 — so a Dockerfile / CI step catches "prod `voltro serve` with no `voltro build`" at BUILD time instead of at cold-start (0.3.0 made an unbuilt production serve fatal). - **`voltro update --codemods-only` (alias `--run-codemods`) + `--from <version>`** — re-apply the codemods + manual notes for an explicit `[from, to]` delta WITHOUT bumping `package.json` or installing. The recovery path for a hand-edited version bump (`bump package.json` + install first), which otherwise makes `voltro update` report "already on X — nothing to do" and silently skip the codemods. `--from` also overrides the auto-detected source version on a normal update.
56
+ - **@voltro/runtime** — **`encryptField` / `decryptField` — standalone field cipher for raw-SQL paths.** `.encrypted()` columns encrypt/decrypt transparently inside the `ctx.store` middleware, so a code path that reaches the DB by RAW SQL (an auth strategy with no store handle reading/writing a session token, a one-off backfill) bypasses it. `@voltro/runtime` now exposes the SAME registered cipher standalone: `encryptField` encrypts a value; `decryptField` decrypts an `enc:v1:…` value and passes a non-ciphertext value through unchanged (so a raw-SQL path can adopt encryption while pre-existing plaintext rows keep working). Both throw a clear error when no cipher is registered. Encryption stops being all-or-nothing tied to going through `ctx.store`.
57
+ - **@voltro/testing** — **`makeVoltroTestClient` — the missing half of the test story.** The framework shipped a strong BACKEND test story (`makeTestContext`, `mockStore`, `mockAi`, the workflow runner) and nothing for the client. A Voltro frontend is a reactive-data app: with no way to render a component against mocked `useSubscription` / `useMutation` — and to inject a loading state, a stream error, or a failing write — it is structurally untestable, which is why real apps end up with zero frontend tests. `import { makeVoltroTestClient } from '@voltro/testing/client'` returns a `Provider` plus `setSubscription` / `failSubscription` / `resetSubscription` and a `calls` log of every write with its input. A tag ABSENT from the fixture stays in the loading state — the distinction from an empty result is exactly what you want to assert. Deliberately NOT a renderer: it hands you a Provider, so it works with `react-dom/client` + `act`, testing-library, or your own harness, and locks you into none of them.
58
+ - **@voltro/i18n** — **Plurals, Intl formatters, and catalogs a bundler can actually split.** - **`plural(locale, count, forms)` + `usePlural`** — real CLDR categories via `Intl.PluralRules`. Replaces the hardcoded `"{count} epic(s)"` pattern, whose literal `(s)` is simply wrong outside English, and which cannot express languages that distinguish few from many (Polish 2–4 vs 5+). An explicit `zero` form is honoured for exactly 0; ordinals via `{ type: 'ordinal' }`. - **`useFormatDate` / `useRelativeTime` / `useFormatNumber` / `useFormatCurrency` / `useFormatters`** — locale-bound `Intl` wrappers, so an app stops carrying several divergent hand-rolled "X minutes ago" helpers that each round differently. - **`defineCatalogs` + `LazyI18nProvider`** — code-split catalogs. `pickCatalog({ en, de })` is a STATIC import map: every locale is a value-level import, so the bundler must emit them in one chunk and a German-only visitor downloads English too, growing linearly per locale. A map of `() => import()` loaders is the only shape a bundler treats as a chunk boundary. `preload(locale)` before hydration keeps first paint synchronous via `peek()`; the provider's `fallback` is for a locale SWITCH, not first paint. Unknown locales degrade to the base catalog, and a failed chunk load stays retryable.
59
+ - **@voltro/protocol, @voltro/client** — **Nested optimistic: `shapeItem` + bulk (multi-item) targets.** Two additive improvements to path-targeted auto-optimistic: - **`shapeItem`** — the nested counterpart of `shape`, typed to the ITEM of the nested array (not the mutation's output), so a path-target patch reads/returns the item without casting `current`. (Previously the nested shaper was the flat `shape`, pinned to the output row — every adopter had to cast. `shape` stays bound to the output for flat targets; a single field can't be both, so the nested shaper is its own.) - **Bulk `identify`** — a target's `identify` may now return `string[]` to patch/delete MANY items (or top-level rows) in one mutation — the group-drag / batch-edit case where per-item parallel writes used to race. Each patched item keeps its own key. Runtime-compatible: a nested target that still uses `shape` falls back transparently. No codemod — additive.
60
+ - **@voltro/web, @voltro/client** — **Two hooks apps kept hand-rolling, because nothing shipped them.** - **`useTheme` (`@voltro/web`)** — returns `{ theme, resolvedTheme, setTheme }` over the framework's own `voltro:theme` cookie and the `html.dark` class. The bug it removes is specific: an app that hand-rolls a second theme store ends up with two writers on one class, so the toggle's state and the rendered theme can disagree, and the pre-paint script flashes the wrong one. `'system'` resolves through `matchMedia` and tracks live OS changes. - **`useConnectionStatus` (`@voltro/client`)** — `'connected' | 'degraded' | 'offline'`, derived from the only two signals the client honestly has: `navigator.onLine` (browser says the network is gone) and the rpc error bus (a call actually failed). `degraded` means "we saw a failure and no success since"; coming back online clears it, because the failures counted during an offline window ARE that window. Deliberately no polling ping just to colour an indicator.
61
+ - **@voltro/database** — **`.uniqueActive([cols])` now works on mysql / mariadb** (it previously failed loud at migrate — those engines have no partial index). It lowers automatically to a STORED generated column per key column — `CASE WHEN <predicate> THEN CAST(<col> AS CHAR(255)) ELSE NULL END` — plus a UNIQUE over them. NULL-distinct uniqueness means a soft-deleted row's generated columns are all NULL and never collide, so re-creating the key just works — the same resurrection-safe semantics the partial index gives on postgres/sqlite/mssql. The lowering is applied identically on the emit side and the declared-snapshot side, so it round-trips through the declarative differ (verified live on MariaDB: declare→migrate→introspect→re-plan is a no-diff, and a duplicate among active rows is rejected while soft-deleting one frees the key). You write the same `.uniqueActive([...])` on every dialect.
62
+
63
+ ### Fixed
64
+
65
+ - **@voltro/cli** — **Standalone `voltro codegen` now emits the SAME `rpcGroup.generated.ts` as `voltro dev` / `voltro build`.** Previously the standalone command skipped the app's plugins, so it dropped every plugin's cross-cutting error union AND its client RPC routes — an inconsistent generated file (typed plugin errors gone, plugin route tags unresolved) vs the dev/serve boot path. The command now loads `app.config` and gathers the plugins' codegen inputs through the SAME `gatherCodegenPluginImports` the boot path uses (single-sourced, so the three paths can't drift).
66
+ - **@voltro/cli** — **The hook-coverage gate was too weak, and it let dead documentation through.** It asked only whether a hook's NAME appeared in the agent-docs corpus — which a one-line row in a link table satisfies. Ten client hooks had exactly that, and six of those rows linked to a page that never mentions the hook, so a reader following the link learned nothing. Two sharper checks replace it: - **Substance** (`agentsMdTemplate.test.ts`): a hook must appear at least once OUTSIDE a table row, so a bare row no longer counts as documentation. - **Dead references** (`gen-agent-docs.mjs`): a row promising `[`useX`](/docs/…)` must point at a page that actually mentions `useX`. This lives in the generator because that is the only thing which reads the docs tree — and it walks BOTH language trees, since an en-only check leaves every German dead link invisible.
67
+ - **@voltro/cli** — - **Serve bundle: inline the pure-JS SQL drivers (`pg`/`mysql2`/`tedious`) instead of runtime-shimming them.** A production `voltro serve` with a postgres/mysql/mssql store crashed at boot with `Pg.Pool is not a constructor` (and the mysql/mssql equivalents): the driver's `@effect/sql-*` consumer links its leaf as a namespace (`import * as Pg from 'pg'; new Pg.Pool()`), but the native-leaf CJS shim (`module.exports = <leaf>`) did not surface the named members through esbuild's `import * as` interop, so `Pg.Pool` was `undefined`. These drivers are pure JS (no `.node`), so they are now inlined into the serve bundle like the rest of the framework — esbuild links the real module and the namespace resolves correctly. Only genuinely-native leaves (`better-sqlite3`, the turso/libsql addons, `pg-native`) and the dynamically-imported `ioredis`/`nodemailer` still resolve via the runtime shim. Verified end-to-end from a relocated prod tree (a real `POST /rpc` through the inlined `pg`).
68
+
69
+ ---
70
+
71
+ ## [0.3.0] — 2026-07-18
72
+
73
+ ### ⚠ BREAKING
74
+
75
+ - **@voltro/cli** — `voltro build` precompiles the whole serve path (framework + effect + `@voltro` inlined, app modules as lazy chunks) into a single **serve bundle**, and `voltro serve` boots from it in-process — cutting `serve: ready` from ~1000 ms to ~180 ms (5–6×; the win is larger on a cold scale-to-zero container). The app's declared SQL driver is inlined (only the native leaf like `pg` stays external, resolved at runtime so it survives the deploy relocation). **Production now serves ONLY from the bundle and NEVER transpiles on demand:** the bundle build externalises unresolvable optional peers (e.g. `@react-email/render` behind `@voltro/plugin-mail`) so it always builds; a bundle-build failure is **fatal** (`voltro build` exits non-zero); and an unbuilt production `voltro serve` fails loud instead of falling back to tsx. The build toolchain (`tsx`, `esbuild`, `vite`, `@vitejs/plugin-react`, `@tailwindcss/vite` + their native tree: rolldown/lightningcss/postcss/jiti) moves to **`optionalDependencies`** of `@voltro/cli`, so `pnpm --prod --no-optional deploy` yields a serve image with none of it — a prod API image's `node_modules` drops ~305 MB → ~131 MB, structurally, with no fragile prune list. `voltro dev` and a non-production local `voltro serve` are unchanged (still tsx). **Migration:** in production (`NODE_ENV=production`) run `voltro build` before `voltro serve`. The generated Dockerfiles already do; a custom Dockerfile / start script adds a `voltro build .` step before `voltro serve .` (`voltro update` prints this — see the 0.3.0 codemod note).
76
+
77
+ ### Added
78
+
79
+ - **@voltro/protocol, @voltro/runtime, @voltro/plugin-rbac** — Declarative authorization `guards:` on `defineMutation` / `defineQuery` / `defineAction`. The framework enforces the declared scope(s) in the dispatch spine BEFORE the executor (for a mutation, before the transaction opens), fails with a typed `ScopeError`, and auto-merges `ScopeError` into the wire error union so the client decodes the denial typed. Guards are browser-safe DATA (scope strings + a pure `resource: (input) => id` extractor). Checks run against the caller's EFFECTIVE scope set — raw subject scopes ∪ `@voltro/plugin-rbac` role-derived scopes — via a new canonical effective-scope seam in `@voltro/protocol` (`effectiveScopes` / `setEffectiveScopes` / `checkGuards`), which rbac now publishes to (so a role-granted scope satisfies a `guards:` entry and the in-handler `permission()` identically). Adds `ctx.access` (`has` / `hasAny` / `require` / `scopes`) — the cast-free typed authorization slice on every handler context. Enforcement is single-sourced in the shared serve pipeline, so `voltro dev` and `voltro serve` can't drift.
80
+ - **@voltro/protocol, @voltro/client** — Nested / path-targeted auto-optimistic. A mutation `target` can now patch a nested array INSIDE a query's value — a JSON array column (`snapshot.projects`) or a computed/shaped result — at item granularity, via `path` (dot-path to the array), `by` (item key, default `id`), and `match` (a pure predicate that scopes the patch to the entries whose current value satisfies it, preventing a patch bleeding across sibling subscriptions that share a source table). Previously auto-optimistic only patched the flat top-level row array keyed by `id`; nested values needed a hand-written `.withOptimistic` reducer. `path`/`by`/`match` are browser-safe descriptor data (a dot-path string + pure predicate), same discipline as `identify`/`shape`. A path insert is applied even on a computed entry (it targets a known document, not a blind top-level add).
81
+ - **@voltro/runtime** — `ctx.store.applyDefined(input, keys)` (and a standalone `applyDefined` export from `@voltro/runtime`) — builds a partial-update patch keeping only the listed keys whose value the caller actually provided (`!== undefined`; a defined falsy value like `0`/`''`/`false` is kept). Collapses the per-field `if (input.x !== undefined) patch.x = input.x` idiom every partial-update mutation hand-writes.
82
+ - **@voltro/database** — `.uniqueActive([cols], opts?)` on the table builder — a portable partial-UNIQUE constraint that holds only among the rows matching a predicate (default `"deletedAt" IS NULL`, pairing with `.softDelete()`). Emits `CREATE UNIQUE INDEX … WHERE` on postgres / sqlite / mssql, so a soft-deleted row leaves the active set and a NEW row with the same key inserts cleanly — no hand-written `generatedAs("CASE WHEN …")` column and no resurrection footgun. On mysql / mariadb (no partial-index support) it FAILS LOUDLY at migrate time rather than silently emitting a full unique index that would forbid re-creating a soft-deleted key — the generated-STORED-column lowering for those dialects is a follow-up. Kept out of the declarative index snapshot (the incremental planner is predicate-blind and would misclassify a unique+partial index as a full constraint), so the fresh-schema DDL path is its sole emitter and there is no re-diff churn. Live-verified against postgres.
83
+ - **@voltro/cli** — `voltro update` upgrades an app to the latest framework: it bumps every `@voltro/*` dependency, installs with the detected package manager, and runs the codemods shipped with the target version. Codemods are authored with `defineCodemod` + an import-scoped ts-morph helper toolkit (`renameImport`, `renameModuleSpecifier`, `renameJsxProp`, `renameObjectKey`, `add`/`removeImport`, structural `changeCallArgs`/`wrapCall`, `annotate`) and run against the app source; a `manual` kind surfaces written steps for changes that can't be automated. Breaking public-API changes now ship a codemod (or an explicit `codemod: none`), enforced by the changelog gate. Framework-owned `_voltro_*` table changes continue to ride the declarative differ on `voltro db apply` / `voltro dev` boot — `update` does not touch the database.
84
+
85
+ ### Fixed
86
+
87
+ - **@voltro/database** — The core-table registry (`registerCoreTables` / `requireActors` / `requireTenants`) now stores its state on a process-global `Symbol.for` singleton, the same mechanism the main table registry already uses — instead of module-local `let` bindings. Module-local state splits when the `@voltro/database` module is duplicated in a process (e.g. resolved through both the `.` and `./sql` entry points, or a bundled framework copy alongside an externally-resolved one): one instance's `registerCoreTables` becomes invisible to the instance that reads it, surfacing as a spurious `core 'actors' table not registered` at store construction. Pinning it to `globalThis` makes every copy share one store, matching the table registry's already-global behaviour.
88
+
89
+ ---
90
+
42
91
  ## [0.2.2] — 2026-07-17
43
92
 
44
93
  ### Added
package/dist/index.d.ts CHANGED
@@ -14,6 +14,9 @@ export declare interface ActionProcedureDescriptor<Name extends string, Input ex
14
14
  readonly input: Input;
15
15
  readonly output: Output;
16
16
  readonly error: Error;
17
+ /** Declarative authorization guard(s) — enforced before the executor runs,
18
+ * failing with a typed `ScopeError`. Absent → no framework-level authz. */
19
+ readonly guards: Guards | undefined;
17
20
  /** Opt this action into a public REST endpoint (innovation/11). */
18
21
  readonly publicApi: PublicApiSpec | undefined;
19
22
  /** Opt this action into the auto-synthesized agent toolset (innovation/07). */
@@ -160,6 +163,33 @@ export declare const checkFrameworkCompat: (pluginName: string, range: string |
160
163
  reason: string;
161
164
  };
162
165
 
166
+ /**
167
+ * Check a descriptor's declared `guards:` against a subject's EFFECTIVE scopes.
168
+ * Returns a typed `ScopeError` naming the first unmet scope, or `null` when
169
+ * every guard is satisfied (or there are no guards). Pure + synchronous — the
170
+ * runtime calls it in the dispatch spine BEFORE the executor (and, for
171
+ * mutations, before the transaction opens). The `admin:full` bypass passes
172
+ * everything.
173
+ */
174
+ export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<GuardCheckSpec> | undefined) => ScopeError | null;
175
+
176
+ /**
177
+ * The resource-aware counterpart of `checkGuards`. Identical semantics when no
178
+ * resource-scope resolver is registered (or no guard carries a `resource`
179
+ * extractor) — it delegates to the pure `checkGuards` and succeeds/denies
180
+ * exactly the same, allocation-light. When BOTH a resolver is registered AND a
181
+ * guard has a `resource`, it extracts the resource id from `input` and asks the
182
+ * resolver whether the caller holds each scope on THAT resource. A globally-held
183
+ * scope (or the `admin:full` bypass) still satisfies without a resolver call;
184
+ * only the gap ("no global grant") falls through to the resolver. A failed
185
+ * resolver Effect is treated as a denial (fail-closed).
186
+ *
187
+ * Returns a typed `ScopeError` naming the first unmet scope, or `null` when
188
+ * every guard passes. The runtime calls this in the dispatch spine BEFORE the
189
+ * executor (mutations: before the transaction opens).
190
+ */
191
+ export declare const checkGuardsEffect: (subject: Subject, guards: ReadonlyArray<GuardCheckSpec> | undefined, input: unknown) => Effect.Effect<ScopeError | null>;
192
+
163
193
  export declare interface ClientDescriptor {
164
194
  readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'workflow';
165
195
  /** Query only: table(s) the cache should match this subscription against. */
@@ -189,7 +219,18 @@ export declare interface ClientTarget {
189
219
  readonly op: 'insert' | 'update' | 'delete';
190
220
  readonly order?: 'prepend' | 'append' | undefined;
191
221
  readonly shape?: ((input: Record<string, unknown>, optimisticIdOrCurrent?: unknown) => Record<string, unknown>) | undefined;
192
- readonly identify?: ((input: Record<string, unknown>) => string) | undefined;
222
+ readonly identify?: ((input: Record<string, unknown>) => string | ReadonlyArray<string>) | undefined;
223
+ /** Nested/path-targeted optimistic (see `NestedTargetFields`). Carried to the
224
+ * client so item-level patches into a JSON array / computed value happen
225
+ * automatically. Functions survive because the codegen references the live
226
+ * descriptor by value. */
227
+ readonly path?: string | undefined;
228
+ readonly by?: string | undefined;
229
+ readonly match?: ((value: unknown, input: Record<string, unknown>) => boolean) | undefined;
230
+ /** Nested-item shaper (erased). The client uses this instead of `shape` when
231
+ * `path` is set. 2nd arg is the optimistic id (insert) or the current item
232
+ * (update) — `unknown` here since the erased view spans both; normalize casts. */
233
+ readonly shapeItem?: ((input: Record<string, unknown>, currentOrOptimisticId: unknown) => Record<string, unknown>) | undefined;
193
234
  }
194
235
 
195
236
  /** Compose multiple strategies into a single resolver function. The
@@ -287,6 +328,10 @@ export declare const defineAction: <const Name extends string, Input extends Sch
287
328
  readonly input: Input;
288
329
  readonly output: Output;
289
330
  readonly error?: Error;
331
+ /** Declarative authorization guard(s) — the caller must hold the named
332
+ * scope(s) or the action fails with a typed `ScopeError` before the executor
333
+ * runs. `ScopeError` is auto-merged into the wire error union. */
334
+ readonly guards?: Guards<Schema.Schema.Type<Input>>;
290
335
  /** Project this action as a public REST endpoint (innovation/11). */
291
336
  readonly publicApi?: PublicApiSpec;
292
337
  /** Expose this action as an agent tool (innovation/07). */
@@ -300,6 +345,10 @@ export declare const defineMutation: <const Name extends string, Input extends S
300
345
  readonly error?: Error;
301
346
  /** Optional declarative target(s) — drives auto-optimistic. */
302
347
  readonly target?: Target<Schema.Schema.Type<Input>, Schema.Schema.Type<Output>>;
348
+ /** Declarative authorization guard(s) — the caller must hold the named
349
+ * scope(s) or the mutation fails with a typed `ScopeError` BEFORE the
350
+ * transaction opens. `ScopeError` is auto-merged into the wire error union. */
351
+ readonly guards?: Guards<Schema.Schema.Type<Input>>;
303
352
  /** Project this mutation as a public REST endpoint (innovation/11). */
304
353
  readonly publicApi?: PublicApiSpec;
305
354
  /** Expose this mutation as an agent tool (innovation/07). */
@@ -364,6 +413,10 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
364
413
  readonly source?: string | ReadonlyArray<string>;
365
414
  /** Opt into server-side snapshot caching with auto-invalidation. */
366
415
  readonly cache?: QueryCacheConfig;
416
+ /** Declarative authorization guard(s) — the caller must hold the named
417
+ * scope(s) or the query fails with a typed `ScopeError` before the executor
418
+ * runs. `ScopeError` is auto-merged into the wire error union. */
419
+ readonly guards?: Guards<Schema.Schema.Type<Input>>;
367
420
  /** Project this query as a public REST endpoint (innovation/11). */
368
421
  readonly publicApi?: PublicApiSpec;
369
422
  /** Expose this query as an agent tool (innovation/07). */
@@ -382,11 +435,12 @@ export declare const defineStream: <const Name extends string, Input extends Sch
382
435
  readonly error?: Error;
383
436
  }) => StreamProcedureDescriptor<Name, Input, Element, Error>;
384
437
 
385
- export declare interface DeleteTarget<Input = unknown> {
438
+ export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
386
439
  readonly table: string;
387
440
  readonly op: 'delete';
388
- /** Identify the row to remove. Default: `input.id`. */
389
- readonly identify?: ((input: Input) => string) | undefined;
441
+ /** Identify the row(s) to remove. Default: `input.id`. Return an ARRAY to
442
+ * remove MANY rows/items in one mutation. */
443
+ readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
390
444
  }
391
445
 
392
446
  /**
@@ -406,6 +460,13 @@ export declare interface DeleteTarget<Input = unknown> {
406
460
  */
407
461
  export declare const diffRows: (prev: ReadonlyArray<PatchRow>, next: ReadonlyArray<PatchRow>) => RowPatch;
408
462
 
463
+ /**
464
+ * The caller's effective scope set — the merged set published by rbac if
465
+ * present, else the raw `subject.scopes`. This is what declarative guards and
466
+ * `ctx.access` check against.
467
+ */
468
+ export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string>;
469
+
409
470
  /** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
410
471
  * descriptor carries a top-level `description`; callers pass that in. */
411
472
  export declare type ExposeAsTool = boolean | ExposeAsToolSpec;
@@ -438,9 +499,54 @@ export declare const failIdempotent: (store: IdempotencyStore, scope: string, ke
438
499
  /** Record a completed response for replay. */
439
500
  export declare const finishIdempotent: (store: IdempotencyStore, scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
440
501
 
502
+ /** The currently-registered resource-scope resolver, or `undefined`. */
503
+ export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
504
+
505
+ export declare interface GuardCheckSpec {
506
+ readonly scope: string | ReadonlyArray<string>;
507
+ readonly mode?: 'all' | 'any';
508
+ /**
509
+ * PURE `input → resource id` extractor (the erased runtime form of
510
+ * `GuardSpec.resource`). When present AND a resource-aware resolver is
511
+ * registered (`setResourceScopeResolver`), the guard is checked against THAT
512
+ * resource, not just the caller's global scopes. Absent / no resolver →
513
+ * global-scope check (the extractor is advisory), exactly as before.
514
+ */
515
+ readonly resource?: (input: unknown) => string | undefined;
516
+ }
517
+
518
+ /** One or more declarative guards on a descriptor. All must pass (AND across
519
+ * entries; `mode` controls AND/OR WITHIN one entry's scope array). */
520
+ export declare type Guards<Input = unknown> = ReadonlyArray<GuardSpec<Input>>;
521
+
522
+ export declare interface GuardSpec<Input = unknown> {
523
+ /** Required permission scope(s). A single string, or an array combined by
524
+ * `mode`. Scope strings are the same values `hasScope` / rbac roles use. */
525
+ readonly scope: string | ReadonlyArray<string>;
526
+ /** How an array of scopes combines. `'all'` (default) = AND (hold every
527
+ * scope); `'any'` = OR (hold at least one). Ignored for a single scope. */
528
+ readonly mode?: 'all' | 'any';
529
+ /**
530
+ * PURE `input → resource id` extractor for a row/resource-scoped guard.
531
+ * Browser-safe (no DB, no server import) — exactly like `target.identify`.
532
+ * The framework passes the extracted id to a resource-aware scope resolver
533
+ * (a future ReBAC / `accessPolicy()` resolver) so the check can be scoped to
534
+ * THAT resource. With only the default (subject-global) resolver installed
535
+ * the id is advisory and the guard checks the subject's global scopes. Omit
536
+ * for a plain subject-scope guard.
537
+ */
538
+ readonly resource?: (input: Input) => string | undefined;
539
+ }
540
+
441
541
  export declare const hasCallbackRoutes: (s: AuthStrategy) => s is AuthStrategyWithCallback;
442
542
 
443
- /** True if the subject holds `scope` (or the `admin:full` bypass). */
543
+ /** True if the subject holds `scope` (or the `admin:full` bypass), checking
544
+ * the EFFECTIVE set (role-derived scopes included). Prefer this over
545
+ * `hasScope` anywhere rbac roles should count. */
546
+ export declare const hasEffectiveScope: (subject: Subject, scope: string) => boolean;
547
+
548
+ /** True if the subject holds `scope` (or the `admin:full` bypass). Checks only
549
+ * the RAW subject scopes — use `hasEffectiveScope` to include rbac roles. */
444
550
  export declare const hasScope: (subject: Subject, scope: string) => boolean;
445
551
 
446
552
  /**
@@ -544,7 +650,7 @@ export declare interface IdempotencyStore {
544
650
  * corrupt the pointer. */
545
651
  export declare const idToPath: (id: RowId) => string;
546
652
 
547
- export declare interface InsertTarget<Input = unknown, Row = unknown> {
653
+ export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
548
654
  readonly table: string;
549
655
  readonly op: 'insert';
550
656
  readonly order?: 'prepend' | 'append' | undefined;
@@ -562,9 +668,13 @@ export declare interface InsertTarget<Input = unknown, Row = unknown> {
562
668
  *
563
669
  * The `optimisticId` parameter is exposed for rare cases where the
564
670
  * shape function needs to reference it (e.g., setting a foreign key
565
- * on a child row spread in the same optimistic insert).
671
+ * on a child row spread in the same optimistic insert). For a NESTED
672
+ * (`path`) insert, use `shapeItem` instead — it is typed to the item.
566
673
  */
567
674
  readonly shape?: ((input: Input, optimisticId: string) => Omit<Row, 'id' | 'optimistic'>) | undefined;
675
+ /** NESTED (`path`) insert: build the ITEM to insert, typed to the item (not
676
+ * the output). 2nd arg is the optimistic id. */
677
+ readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
568
678
  }
569
679
 
570
680
  /** True when every row in the set carries an `id`. The dispatcher uses
@@ -589,6 +699,10 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
589
699
  * query-invalidation analytics). Mutations without a target run
590
700
  * normally but skip auto-optimistic. */
591
701
  readonly target: Target | undefined;
702
+ /** Declarative authorization guard(s) — enforced before the transaction
703
+ * opens, failing with a typed `ScopeError`. Absent → no framework-level
704
+ * authz (author gates in-handler, or the mutation is unguarded). */
705
+ readonly guards: Guards | undefined;
592
706
  /** Opt this mutation into a public REST endpoint (innovation/11). */
593
707
  readonly publicApi: PublicApiSpec | undefined;
594
708
  /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
@@ -597,6 +711,21 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
597
711
 
598
712
  export declare const mutationToRpc: <Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: MutationProcedureDescriptor<Name, Input, Output, Err>, extraErrors?: ExtraErrors) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Output, Schema.Schema.All, never>;
599
713
 
714
+ export declare interface NestedTargetFields<Input = unknown> {
715
+ /** Dot-path to the nested array within the query VALUE to patch (e.g.
716
+ * `'snapshot.projects'`). Absent → patch the top-level row array (default). */
717
+ readonly path?: string | undefined;
718
+ /** Item key within the nested array (default `'id'`). Only meaningful with
719
+ * `path`. */
720
+ readonly by?: string | undefined;
721
+ /** Guard WHICH cached query entries this target patches: only entries whose
722
+ * CURRENT value satisfies the predicate. Pure + browser-safe. Prevents a
723
+ * patch from bleeding across sibling subscriptions that share a source table
724
+ * (the guard AWB hand-writes as `roadmap.id === input.roadmapId`). Absent →
725
+ * every entry matching the target `table` is patched. */
726
+ readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
727
+ }
728
+
600
729
  /**
601
730
  * Project a runtime descriptor down to the subset the client needs.
602
731
  * Functions (`shape`, `identify`) are passed through by reference —
@@ -1289,6 +1418,9 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
1289
1418
  /** Server-side snapshot cache config. Absent → never cached (the
1290
1419
  * default; the dispatcher already keeps live subscriptions fresh). */
1291
1420
  readonly cache: QueryCacheConfig | undefined;
1421
+ /** Declarative authorization guard(s) — enforced before the executor runs,
1422
+ * failing with a typed `ScopeError`. Absent → no framework-level authz. */
1423
+ readonly guards: Guards | undefined;
1292
1424
  /** Opt this query into a public REST endpoint (innovation/11). */
1293
1425
  readonly publicApi: PublicApiSpec | undefined;
1294
1426
  /** Opt this query into the auto-synthesized agent toolset (innovation/07). */
@@ -1341,9 +1473,29 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Sche
1341
1473
  revision: Schema.optional<typeof Schema.Number>;
1342
1474
  }>]>, Schema.Schema.All>, typeof Schema.Never, never>;
1343
1475
 
1344
- /** Gate a handler on a scope; fails with a typed `ScopeError` if missing. */
1476
+ /** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
1477
+ * Checks the EFFECTIVE set so role-derived scopes count. */
1345
1478
  export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
1346
1479
 
1480
+ /** The question the runtime asks a registered resource-scope resolver. */
1481
+ export declare interface ResourceScopeRequest {
1482
+ /** The authenticated caller. */
1483
+ readonly subject: Subject;
1484
+ /** The single scope being checked (guards with a scope array ask once per scope). */
1485
+ readonly scope: string;
1486
+ /** The resource id the guard's `resource` extractor produced (e.g. a teamId). */
1487
+ readonly resource: string;
1488
+ }
1489
+
1490
+ /**
1491
+ * A process-global resolver that answers "does `subject` hold `scope` on
1492
+ * `resource`". Registered once at boot via `setResourceScopeResolver`. Returns
1493
+ * an Effect so it can read the app's role tables. It should NOT leak errors —
1494
+ * `checkGuardsEffect` treats a failed resolver as a DENIAL (fail-closed), so a
1495
+ * resolver that wants a different posture must catch its own errors.
1496
+ */
1497
+ export declare type ResourceScopeResolver = (req: ResourceScopeRequest) => Effect.Effect<boolean, unknown>;
1498
+
1347
1499
  /** The id of a row, addressed in op paths as `/<id>`. */
1348
1500
  export declare type RowId = string | number;
1349
1501
 
@@ -1577,6 +1729,21 @@ declare type ServerErrorListener = (event: ServerErrorEvent) => void;
1577
1729
 
1578
1730
  export declare type ServerErrorSource = 'rest' | 'aggregate' | 'subscriber' | 'reaction' | 'schedule' | 'workflow' | 'webhook' | 'startup';
1579
1731
 
1732
+ /**
1733
+ * Publish the caller's fully-resolved scope set for this request (raw subject
1734
+ * scopes ∪ role-derived scopes ∪ any extra grants). Called by the rbac
1735
+ * interceptor; safe to call more than once (last write wins).
1736
+ */
1737
+ export declare const setEffectiveScopes: (subject: Subject, scopes: ReadonlyArray<string>) => void;
1738
+
1739
+ /**
1740
+ * Register (or clear, with `undefined`) the process-global resource-scope
1741
+ * resolver. Call once at boot — from `@voltro/plugin-rbac`'s wiring, or directly
1742
+ * from an app that resolves per-resource permissions against its own tables.
1743
+ * Last write wins.
1744
+ */
1745
+ export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
1746
+
1580
1747
  /** A strategy's verdict on a request.
1581
1748
  * - `matched`: this strategy claims the request; here's the Subject.
1582
1749
  * - `skip`: not my request (e.g. cookie absent); try next strategy.
@@ -1925,13 +2092,19 @@ export declare const undoRedoDescriptor: MutationProcedureDescriptor<"__voltro.u
1925
2092
  ok: typeof Schema.Boolean;
1926
2093
  }>, Schema.Union<[typeof UndoNotFound, typeof UndoForbidden, typeof UndoConflict]>>;
1927
2094
 
1928
- export declare interface UpdateTarget<Input = unknown, Row = unknown> {
2095
+ export declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
1929
2096
  readonly table: string;
1930
2097
  readonly op: 'update';
1931
- /** Identify the row to patch. Default: `input.id`. */
1932
- readonly identify?: ((input: Input) => string) | undefined;
1933
- /** Build the patch. Default: merges input over current. */
2098
+ /** Identify the row(s) to patch. Default: `input.id`. Return an ARRAY to patch
2099
+ * MANY rows/items in one mutation (a bulk edit where the per-item
2100
+ * parallel-write race lived). */
2101
+ readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
2102
+ /** Build the patch for a FLAT target (`current` is the OUTPUT row). Default:
2103
+ * merges input over current. For a NESTED (`path`) target use `shapeItem`. */
1934
2104
  readonly shape?: ((input: Input, current: Row) => Row) | undefined;
2105
+ /** NESTED (`path`) update: build the item patch. `current` is the existing
2106
+ * ITEM (not the mutation output), so no cast is needed. */
2107
+ readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined;
1935
2108
  }
1936
2109
 
1937
2110
  export declare interface VoltroPlugin {
package/dist/index.js CHANGED
@@ -1,30 +1,30 @@
1
- import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as ee, r as te, s, t as ne, u as re } from "./auth-DCE6m7Bo.js";
2
- import { a as ie, c as ae, d as oe, f as se, i as ce, l as le, n as ue, o as de, r as fe, s as pe, t as me, u as he } from "./serverErrorBus-C1KVL0dr.js";
3
- import { Context as ge, Layer as _e, Schema as c } from "effect";
4
- import { Rpc as l } from "@effect/rpc";
1
+ import { _ as e, a as t, c as n, d as r, f as i, g as a, h as o, i as ee, l as s, m as te, n as ne, o as re, p as ie, r as ae, s as oe, t as c, u as se, v as ce, y as le } from "./serverErrorBus-0wjvKwGO.js";
2
+ import { a as ue, c as de, d as fe, f as pe, i as me, l as he, n as ge, o as _e, r as ve, s as ye, t as be, u as l } from "./auth-DCE6m7Bo.js";
3
+ import { Context as u, Layer as xe, Schema as d } from "effect";
4
+ import { Rpc as f } from "@effect/rpc";
5
5
  //#region src/rowPatch.ts
6
- var ve = c.Union(c.String, c.Number), u = c.Record({
7
- key: c.String,
8
- value: c.Unknown
9
- }).pipe(c.filter((e) => "id" in e, { message: () => "patch row must carry an id" })), d = c.Union(c.Struct({
10
- op: c.Literal("add"),
11
- path: c.String,
12
- value: u
13
- }), c.Struct({
14
- op: c.Literal("replace"),
15
- path: c.String,
16
- value: u
17
- }), c.Struct({
18
- op: c.Literal("remove"),
19
- path: c.String
20
- })), f = c.Struct({
21
- ops: c.Array(d),
22
- order: c.Array(ve)
23
- }), p = (e) => `/${String(e).replace(/~/g, "~0").replace(/\//g, "~1")}`, ye = (e) => (e.startsWith("/") ? e.slice(1) : e).replace(/~1/g, "/").replace(/~0/g, "~"), m = (e) => {
6
+ var Se = d.Union(d.String, d.Number), p = d.Record({
7
+ key: d.String,
8
+ value: d.Unknown
9
+ }).pipe(d.filter((e) => "id" in e, { message: () => "patch row must carry an id" })), m = d.Union(d.Struct({
10
+ op: d.Literal("add"),
11
+ path: d.String,
12
+ value: p
13
+ }), d.Struct({
14
+ op: d.Literal("replace"),
15
+ path: d.String,
16
+ value: p
17
+ }), d.Struct({
18
+ op: d.Literal("remove"),
19
+ path: d.String
20
+ })), h = d.Struct({
21
+ ops: d.Array(m),
22
+ order: d.Array(Se)
23
+ }), g = (e) => `/${String(e).replace(/~/g, "~0").replace(/\//g, "~1")}`, Ce = (e) => (e.startsWith("/") ? e.slice(1) : e).replace(/~1/g, "/").replace(/~0/g, "~"), _ = (e) => {
24
24
  let t = Object.keys(e).sort(), n = {};
25
25
  for (let r of t) n[r] = e[r];
26
26
  return JSON.stringify(n);
27
- }, be = (e, t) => m(e) === m(t), xe = (e, t) => {
27
+ }, we = (e, t) => _(e) === _(t), Te = (e, t) => {
28
28
  let n = /* @__PURE__ */ new Map();
29
29
  for (let t of e) n.set(t.id, t);
30
30
  let r = [], i = [], a = /* @__PURE__ */ new Set();
@@ -33,26 +33,26 @@ var ve = c.Union(c.String, c.Number), u = c.Record({
33
33
  let t = n.get(e.id);
34
34
  t === void 0 ? r.push({
35
35
  op: "add",
36
- path: p(e.id),
36
+ path: g(e.id),
37
37
  value: e
38
- }) : be(t, e) || r.push({
38
+ }) : we(t, e) || r.push({
39
39
  op: "replace",
40
- path: p(e.id),
40
+ path: g(e.id),
41
41
  value: e
42
42
  });
43
43
  }
44
44
  for (let t of e) a.has(t.id) || r.push({
45
45
  op: "remove",
46
- path: p(t.id)
46
+ path: g(t.id)
47
47
  });
48
48
  return {
49
49
  ops: r,
50
50
  order: i
51
51
  };
52
- }, Se = (e) => e.every((e) => {
52
+ }, Ee = (e) => e.every((e) => {
53
53
  let t = e.id;
54
54
  return typeof t == "string" || typeof t == "number";
55
- }), Ce = (e, t) => {
55
+ }), De = (e, t) => {
56
56
  let n = /* @__PURE__ */ new Map();
57
57
  for (let t of e) n.set(t.id, t);
58
58
  for (let e of t.ops) (e.op === "add" || e.op === "replace") && n.set(e.value.id, e.value);
@@ -62,79 +62,82 @@ var ve = c.Union(c.String, c.Number), u = c.Record({
62
62
  t !== void 0 && r.push(t);
63
63
  }
64
64
  return r;
65
- }, h = (e) => c.Union(c.Struct({
66
- _tag: c.Literal("snapshot"),
67
- revision: c.Number,
65
+ }, v = (e) => d.Union(d.Struct({
66
+ _tag: d.Literal("snapshot"),
67
+ revision: d.Number,
68
68
  data: e,
69
- computed: c.optional(c.Boolean)
70
- }), c.Struct({
71
- _tag: c.Literal("delta"),
72
- revision: c.Number,
73
- emittedAt: c.Number,
74
- patch: f
75
- }), c.Struct({
76
- _tag: c.Literal("error"),
77
- error: c.Unknown,
78
- revision: c.optional(c.Number)
79
- })), g = (e) => ({
69
+ computed: d.optional(d.Boolean)
70
+ }), d.Struct({
71
+ _tag: d.Literal("delta"),
72
+ revision: d.Number,
73
+ emittedAt: d.Number,
74
+ patch: h
75
+ }), d.Struct({
76
+ _tag: d.Literal("error"),
77
+ error: d.Unknown,
78
+ revision: d.optional(d.Number)
79
+ })), y = (e) => ({
80
80
  kind: "query",
81
81
  name: e.name,
82
82
  input: e.input,
83
83
  output: e.output,
84
- error: e.error ?? c.Never,
84
+ error: e.error ?? d.Never,
85
85
  source: e.source,
86
86
  cache: e.cache,
87
+ guards: e.guards,
87
88
  publicApi: e.publicApi,
88
89
  exposeAsTool: e.exposeAsTool
89
- }), _ = (e) => ({
90
+ }), b = (e) => ({
90
91
  kind: "mutation",
91
92
  name: e.name,
92
93
  input: e.input,
93
94
  output: e.output,
94
- error: e.error ?? c.Never,
95
+ error: e.error ?? d.Never,
95
96
  target: e.target,
97
+ guards: e.guards,
96
98
  publicApi: e.publicApi,
97
99
  exposeAsTool: e.exposeAsTool
98
- }), v = (e) => ({
100
+ }), x = (e) => ({
99
101
  kind: "action",
100
102
  name: e.name,
101
103
  input: e.input,
102
104
  output: e.output,
103
- error: e.error ?? c.Never,
105
+ error: e.error ?? d.Never,
106
+ guards: e.guards,
104
107
  publicApi: e.publicApi,
105
108
  exposeAsTool: e.exposeAsTool
106
- }), we = (e) => ({
109
+ }), Oe = (e) => ({
107
110
  kind: "stream",
108
111
  name: e.name,
109
112
  input: e.input,
110
113
  element: e.element,
111
- error: e.error ?? c.Never
112
- }), y = (e, t) => t && t.length > 0 ? c.Union(e, ...t) : e, b = (e, t) => l.make(e.name, {
114
+ error: e.error ?? d.Never
115
+ }), S = (e, t) => t && t.length > 0 ? d.Union(e, ...t) : e, C = (e, t) => t && t.length > 0 ? d.Union(e, s) : e, w = (e, t) => f.make(e.name, {
113
116
  payload: e.input,
114
- success: h(e.output),
115
- error: y(e.error, t),
117
+ success: v(e.output),
118
+ error: S(C(e.error, e.guards), t),
116
119
  stream: !0
117
- }), x = (e, t) => l.make(e.name, {
120
+ }), T = (e, t) => f.make(e.name, {
118
121
  payload: e.input,
119
122
  success: e.output,
120
- error: y(e.error, t)
121
- }), S = (e, t) => l.make(e.name, {
123
+ error: S(C(e.error, e.guards), t)
124
+ }), E = (e, t) => f.make(e.name, {
122
125
  payload: e.input,
123
126
  success: e.output,
124
- error: y(e.error, t)
125
- }), C = (e, t) => l.make(e.name, {
127
+ error: S(C(e.error, e.guards), t)
128
+ }), D = (e, t) => f.make(e.name, {
126
129
  payload: e.input,
127
130
  success: e.element,
128
- error: y(e.error, t),
131
+ error: S(e.error, t),
129
132
  stream: !0
130
- }), Te = (e) => {
133
+ }), ke = (e) => {
131
134
  switch (e.kind) {
132
- case "query": return b(e);
133
- case "mutation": return x(e);
134
- case "action": return S(e);
135
- case "stream": return C(e);
135
+ case "query": return w(e);
136
+ case "mutation": return T(e);
137
+ case "action": return E(e);
138
+ case "stream": return D(e);
136
139
  }
137
- }, Ee = (e) => {
140
+ }, Ae = (e) => {
138
141
  let t = e.input, n = t === void 0 ? {} : { input: t }, r = e.output, i = r === void 0 ? {} : { output: r };
139
142
  if (e.kind === "query") return {
140
143
  kind: "query",
@@ -168,18 +171,22 @@ var ve = c.Union(c.String, c.Number), u = c.Record({
168
171
  op: e.op,
169
172
  ..."order" in e && e.order !== void 0 ? { order: e.order } : {},
170
173
  ..."shape" in e && e.shape !== void 0 ? { shape: e.shape } : {},
171
- ..."identify" in e && e.identify !== void 0 ? { identify: e.identify } : {}
174
+ ..."identify" in e && e.identify !== void 0 ? { identify: e.identify } : {},
175
+ ..."path" in e && e.path !== void 0 ? { path: e.path } : {},
176
+ ..."by" in e && e.by !== void 0 ? { by: e.by } : {},
177
+ ..."match" in e && e.match !== void 0 ? { match: e.match } : {},
178
+ ..."shapeItem" in e && e.shapeItem !== void 0 ? { shapeItem: e.shapeItem } : {}
172
179
  }))
173
180
  };
174
- }, w = (e) => e, T = (e) => e, E = (e) => {
181
+ }, je = (e) => e, Me = (e) => e, Ne = (e) => {
175
182
  if (e.length !== 0) return (t, n) => e.reduceRight((e, t) => t(e, n), t);
176
- }, D = (e, t) => {
177
- let n = ge.GenericTag(e);
183
+ }, Pe = (e, t) => {
184
+ let n = u.GenericTag(e);
178
185
  return {
179
186
  Tag: n,
180
- Live: _e.succeed(n, t)
187
+ Live: xe.succeed(n, t)
181
188
  };
182
- }, De = (e, t, n) => {
189
+ }, Fe = (e, t, n) => {
183
190
  if (!t) return { ok: !0 };
184
191
  let r = O(n);
185
192
  if (!r) return {
@@ -189,7 +196,7 @@ var ve = c.Union(c.String, c.Number), u = c.Record({
189
196
  let i = t.trim();
190
197
  if (i === "*" || i === "") return { ok: !0 };
191
198
  let a = i.split(/\s+/).filter((e) => e.length > 0);
192
- for (let i of a) if (!Oe(i, r)) return {
199
+ for (let i of a) if (!Ie(i, r)) return {
193
200
  ok: !1,
194
201
  reason: `plugin "${e}" requires framework ${t}, running ${n}`
195
202
  };
@@ -202,7 +209,7 @@ var ve = c.Union(c.String, c.Number), u = c.Record({
202
209
  patch: Number(t[3]),
203
210
  pre: t[4] ?? ""
204
211
  } : null;
205
- }, k = (e, t) => e.major === t.major ? e.minor === t.minor ? e.patch === t.patch ? e.pre && !t.pre ? -1 : !e.pre && t.pre ? 1 : e.pre && t.pre ? e.pre < t.pre ? -1 : +(e.pre > t.pre) : 0 : e.patch - t.patch : e.minor - t.minor : e.major - t.major, Oe = (e, t) => {
212
+ }, k = (e, t) => e.major === t.major ? e.minor === t.minor ? e.patch === t.patch ? e.pre && !t.pre ? -1 : !e.pre && t.pre ? 1 : e.pre && t.pre ? e.pre < t.pre ? -1 : +(e.pre > t.pre) : 0 : e.patch - t.patch : e.minor - t.minor : e.major - t.major, Ie = (e, t) => {
206
213
  if (e === "*") return !0;
207
214
  if (e.startsWith("^")) {
208
215
  let n = O(e.slice(1));
@@ -224,173 +231,173 @@ var ve = c.Union(c.String, c.Number), u = c.Record({
224
231
  }
225
232
  let r = O(e);
226
233
  return r ? k(t, r) === 0 : !1;
227
- }, A = c.Literal("running", "succeeded", "failed", "cancelled", "suspended"), j = c.Literal("cancel", "terminate", "abandon"), ke = c.Struct({
228
- id: c.String,
229
- workflowName: c.String,
230
- executionId: c.String,
231
- status: c.Literal("running")
232
- }), M = c.Struct({
233
- id: c.String,
234
- tag: c.String,
235
- executionId: c.String,
234
+ }, A = d.Literal("running", "succeeded", "failed", "cancelled", "suspended"), j = d.Literal("cancel", "terminate", "abandon"), Le = d.Struct({
235
+ id: d.String,
236
+ workflowName: d.String,
237
+ executionId: d.String,
238
+ status: d.Literal("running")
239
+ }), M = d.Struct({
240
+ id: d.String,
241
+ tag: d.String,
242
+ executionId: d.String,
236
243
  status: A,
237
- payload: c.Unknown,
238
- workflowVersion: c.NullOr(c.String),
239
- workflowPatches: c.NullOr(c.Unknown),
240
- output: c.NullOr(c.Unknown),
241
- subject: c.NullOr(c.Unknown),
242
- source: c.NullOr(c.String),
243
- errorTag: c.NullOr(c.String),
244
- errorMessage: c.NullOr(c.String),
245
- cancelled: c.Boolean,
246
- startedAt: c.Date,
247
- completedAt: c.NullOr(c.Date),
248
- durationMs: c.NullOr(c.Number),
249
- traceId: c.NullOr(c.String),
250
- parentExecutionId: c.NullOr(c.String),
251
- parentClosePolicy: c.NullOr(j)
252
- }), N = c.Struct({
253
- tag: c.optional(c.String),
254
- status: c.optional(A),
255
- limit: c.optional(c.Number)
256
- }), P = c.Struct({
257
- id: c.String,
258
- runId: c.String,
259
- stepName: c.String,
260
- attempt: c.Number,
261
- status: c.Literal("running", "succeeded", "failed"),
262
- input: c.NullOr(c.Unknown),
263
- retryPolicy: c.NullOr(c.Unknown),
264
- output: c.NullOr(c.Unknown),
265
- errorTag: c.NullOr(c.String),
266
- errorMessage: c.NullOr(c.String),
267
- errorCause: c.NullOr(c.Unknown),
268
- startedAt: c.Date,
269
- completedAt: c.NullOr(c.Date),
270
- durationMs: c.NullOr(c.Number)
271
- }), F = c.Struct({
272
- id: c.String,
273
- runId: c.String,
274
- eventType: c.String,
275
- payload: c.NullOr(c.Unknown),
276
- occurredAt: c.Date,
277
- stepName: c.NullOr(c.String),
278
- attempt: c.NullOr(c.Number)
279
- }), I = c.Struct({
280
- id: c.String,
281
- name: c.String,
282
- payload: c.Unknown,
283
- source: c.String,
284
- subject: c.NullOr(c.Unknown),
285
- traceId: c.NullOr(c.String),
286
- occurredAt: c.Date
287
- }), L = c.Struct({
288
- id: c.String,
289
- eventId: c.String,
290
- eventName: c.String,
291
- triggerId: c.String,
292
- workflowName: c.String,
293
- executionId: c.NullOr(c.String),
294
- status: c.Literal("starting", "started", "skipped", "failed"),
295
- idempotencyKey: c.String,
296
- skipped: c.Boolean,
297
- errorMessage: c.NullOr(c.String),
298
- createdAt: c.Date,
299
- completedAt: c.NullOr(c.Date)
300
- }), R = c.Struct({ id: c.String }), z = c.Struct({ runId: c.String }), B = c.Struct({
301
- name: c.optional(c.String),
302
- limit: c.optional(c.Number)
303
- }), V = c.Struct({ eventId: c.String }), H = c.Struct({
304
- workflowName: c.String,
305
- executionId: c.String
306
- }), U = c.Struct({
307
- id: c.String,
308
- signalName: c.String,
309
- payload: c.optional(c.Unknown)
310
- }), W = c.Struct({
311
- id: c.String,
312
- updateName: c.String,
313
- payload: c.optional(c.Unknown),
314
- timeoutMs: c.optional(c.Number)
315
- }), G = c.Struct({
316
- eventId: c.String,
317
- updateId: c.String,
318
- completedEventId: c.String,
319
- result: c.Unknown
320
- }), Ae = g({
244
+ payload: d.Unknown,
245
+ workflowVersion: d.NullOr(d.String),
246
+ workflowPatches: d.NullOr(d.Unknown),
247
+ output: d.NullOr(d.Unknown),
248
+ subject: d.NullOr(d.Unknown),
249
+ source: d.NullOr(d.String),
250
+ errorTag: d.NullOr(d.String),
251
+ errorMessage: d.NullOr(d.String),
252
+ cancelled: d.Boolean,
253
+ startedAt: d.Date,
254
+ completedAt: d.NullOr(d.Date),
255
+ durationMs: d.NullOr(d.Number),
256
+ traceId: d.NullOr(d.String),
257
+ parentExecutionId: d.NullOr(d.String),
258
+ parentClosePolicy: d.NullOr(j)
259
+ }), N = d.Struct({
260
+ tag: d.optional(d.String),
261
+ status: d.optional(A),
262
+ limit: d.optional(d.Number)
263
+ }), P = d.Struct({
264
+ id: d.String,
265
+ runId: d.String,
266
+ stepName: d.String,
267
+ attempt: d.Number,
268
+ status: d.Literal("running", "succeeded", "failed"),
269
+ input: d.NullOr(d.Unknown),
270
+ retryPolicy: d.NullOr(d.Unknown),
271
+ output: d.NullOr(d.Unknown),
272
+ errorTag: d.NullOr(d.String),
273
+ errorMessage: d.NullOr(d.String),
274
+ errorCause: d.NullOr(d.Unknown),
275
+ startedAt: d.Date,
276
+ completedAt: d.NullOr(d.Date),
277
+ durationMs: d.NullOr(d.Number)
278
+ }), F = d.Struct({
279
+ id: d.String,
280
+ runId: d.String,
281
+ eventType: d.String,
282
+ payload: d.NullOr(d.Unknown),
283
+ occurredAt: d.Date,
284
+ stepName: d.NullOr(d.String),
285
+ attempt: d.NullOr(d.Number)
286
+ }), I = d.Struct({
287
+ id: d.String,
288
+ name: d.String,
289
+ payload: d.Unknown,
290
+ source: d.String,
291
+ subject: d.NullOr(d.Unknown),
292
+ traceId: d.NullOr(d.String),
293
+ occurredAt: d.Date
294
+ }), L = d.Struct({
295
+ id: d.String,
296
+ eventId: d.String,
297
+ eventName: d.String,
298
+ triggerId: d.String,
299
+ workflowName: d.String,
300
+ executionId: d.NullOr(d.String),
301
+ status: d.Literal("starting", "started", "skipped", "failed"),
302
+ idempotencyKey: d.String,
303
+ skipped: d.Boolean,
304
+ errorMessage: d.NullOr(d.String),
305
+ createdAt: d.Date,
306
+ completedAt: d.NullOr(d.Date)
307
+ }), R = d.Struct({ id: d.String }), z = d.Struct({ runId: d.String }), B = d.Struct({
308
+ name: d.optional(d.String),
309
+ limit: d.optional(d.Number)
310
+ }), V = d.Struct({ eventId: d.String }), H = d.Struct({
311
+ workflowName: d.String,
312
+ executionId: d.String
313
+ }), U = d.Struct({
314
+ id: d.String,
315
+ signalName: d.String,
316
+ payload: d.optional(d.Unknown)
317
+ }), W = d.Struct({
318
+ id: d.String,
319
+ updateName: d.String,
320
+ payload: d.optional(d.Unknown),
321
+ timeoutMs: d.optional(d.Number)
322
+ }), G = d.Struct({
323
+ eventId: d.String,
324
+ updateId: d.String,
325
+ completedEventId: d.String,
326
+ result: d.Unknown
327
+ }), Re = y({
321
328
  name: "__voltro.workflow.run",
322
329
  source: "_voltro_workflow_runs",
323
330
  input: R,
324
- output: c.Array(M)
325
- }), je = g({
331
+ output: d.Array(M)
332
+ }), ze = y({
326
333
  name: "__voltro.workflow.runs",
327
334
  source: "_voltro_workflow_runs",
328
335
  input: N,
329
- output: c.Array(M)
330
- }), Me = g({
336
+ output: d.Array(M)
337
+ }), Be = y({
331
338
  name: "__voltro.workflow.run.steps",
332
339
  source: "_voltro_workflow_run_steps",
333
340
  input: z,
334
- output: c.Array(P)
335
- }), Ne = g({
341
+ output: d.Array(P)
342
+ }), Ve = y({
336
343
  name: "__voltro.workflow.run.events",
337
344
  source: "_voltro_workflow_run_events",
338
345
  input: z,
339
- output: c.Array(F)
340
- }), Pe = g({
346
+ output: d.Array(F)
347
+ }), He = y({
341
348
  name: "__voltro.workflow.domainEvents",
342
349
  source: "_voltro_workflow_events",
343
350
  input: B,
344
- output: c.Array(I)
345
- }), Fe = g({
351
+ output: d.Array(I)
352
+ }), Ue = y({
346
353
  name: "__voltro.workflow.event.deliveries",
347
354
  source: "_voltro_workflow_event_deliveries",
348
355
  input: V,
349
- output: c.Array(L)
350
- }), Ie = v({
356
+ output: d.Array(L)
357
+ }), We = x({
351
358
  name: "__voltro.workflow.cancel",
352
359
  input: H,
353
- output: c.Struct({ ok: c.Boolean })
354
- }), Le = v({
360
+ output: d.Struct({ ok: d.Boolean })
361
+ }), Ge = x({
355
362
  name: "__voltro.workflow.resume",
356
363
  input: H,
357
- output: c.Struct({ ok: c.Boolean })
358
- }), Re = v({
364
+ output: d.Struct({ ok: d.Boolean })
365
+ }), Ke = x({
359
366
  name: "__voltro.workflow.signal",
360
367
  input: U,
361
- output: c.Struct({ eventId: c.String })
362
- }), ze = v({
368
+ output: d.Struct({ eventId: d.String })
369
+ }), qe = x({
363
370
  name: "__voltro.workflow.update",
364
371
  input: W,
365
372
  output: G
366
- }), K = "__voltro.undo.log", q = "__voltro.undo.apply", J = "__voltro.undo.redo", Y = c.Struct({
367
- id: c.String,
368
- tag: c.String,
369
- label: c.NullOr(c.String),
370
- undone: c.Boolean,
371
- crossesAction: c.Boolean,
372
- createdAt: c.String
373
- }), X = class extends c.TaggedError()("UndoNotFound", { invocationId: c.String }) {}, Z = class extends c.TaggedError()("UndoForbidden", { invocationId: c.String }) {}, Q = class extends c.TaggedError()("UndoConflict", {
374
- invocationId: c.String,
375
- reason: c.Literal("conflict", "action")
376
- }) {}, $ = c.Union(X, Z, Q), Be = g({
373
+ }), K = "__voltro.undo.log", q = "__voltro.undo.apply", J = "__voltro.undo.redo", Y = d.Struct({
374
+ id: d.String,
375
+ tag: d.String,
376
+ label: d.NullOr(d.String),
377
+ undone: d.Boolean,
378
+ crossesAction: d.Boolean,
379
+ createdAt: d.String
380
+ }), X = class extends d.TaggedError()("UndoNotFound", { invocationId: d.String }) {}, Z = class extends d.TaggedError()("UndoForbidden", { invocationId: d.String }) {}, Q = class extends d.TaggedError()("UndoConflict", {
381
+ invocationId: d.String,
382
+ reason: d.Literal("conflict", "action")
383
+ }) {}, $ = d.Union(X, Z, Q), Je = y({
377
384
  name: K,
378
385
  source: "_voltro_undo_log",
379
- input: c.Struct({ limit: c.optional(c.Number) }),
380
- output: c.Array(Y)
381
- }), Ve = _({
386
+ input: d.Struct({ limit: d.optional(d.Number) }),
387
+ output: d.Array(Y)
388
+ }), Ye = b({
382
389
  name: q,
383
- input: c.Struct({ invocationId: c.String }),
384
- output: c.Struct({ ok: c.Boolean }),
390
+ input: d.Struct({ invocationId: d.String }),
391
+ output: d.Struct({ ok: d.Boolean }),
385
392
  error: $
386
- }), He = _({
393
+ }), Xe = b({
387
394
  name: J,
388
- input: c.Struct({ invocationId: c.String }),
389
- output: c.Struct({ ok: c.Boolean }),
395
+ input: d.Struct({ invocationId: d.String }),
396
+ output: d.Struct({ ok: d.Boolean }),
390
397
  error: $
391
- }), Ue = (e) => {
398
+ }), Ze = (e) => {
392
399
  let t = e instanceof Date ? e.getTime() : typeof e == "number" ? e : typeof e == "string" ? new Date(e).getTime() : 0;
393
400
  return Number.isNaN(t) ? 0 : t;
394
- }, We = 1;
401
+ }, Qe = 1;
395
402
  //#endregion
396
- export { ae as ADMIN_SCOPE, ne as AuthMiddleware, o as ConnectionInfo, te as ConnectionInfoMiddleware, We as PROTOCOL_VERSION, le as ScopeError, i as Subject, e as SubjectService, q as UNDO_APPLY_TAG, K as UNDO_LOG_TAG, J as UNDO_REDO_TAG, ee as Unauthenticated, Q as UndoConflict, Z as UndoForbidden, Y as UndoLogEntry, X as UndoNotFound, H as WorkflowControlInputSchema, I as WorkflowDomainEventRowSchema, B as WorkflowDomainEventsInputSchema, V as WorkflowEventDeliveriesInputSchema, L as WorkflowEventDeliveryRowSchema, j as WorkflowParentClosePolicySchema, F as WorkflowRunEventRowSchema, ke as WorkflowRunHandleSchema, R as WorkflowRunRefSchema, M as WorkflowRunRowSchema, A as WorkflowRunStatusSchema, P as WorkflowRunStepRowSchema, z as WorkflowRunTableRefSchema, N as WorkflowRunsInputSchema, U as WorkflowSignalInputSchema, W as WorkflowUpdateInputSchema, G as WorkflowUpdateResultSchema, S as actionToRpc, s as anonymousSubject, Ce as applyRowPatch, t as assertAuthenticated, fe as beginIdempotent, De as checkFrameworkCompat, a as composeAuthStrategies, E as composeRpcInterceptors, v as defineAction, _ as defineMutation, w as definePlugin, T as definePluginRoute, D as definePluginService, g as defineQuery, we as defineStream, xe as diffRows, ce as failIdempotent, ie as finishIdempotent, re as hasCallbackRoutes, he as hasScope, p as idToPath, de as idempotencyScope, Se as isIdKeyed, n as isSystemSubject, pe as memoryIdempotencyStore, x as mutationToRpc, Ee as normalizeDescriptor, ye as pathToId, me as publishServerError, b as queryToRpc, oe as requireScope, d as rowPatchOpSchema, f as rowPatchSchema, C as streamToRpc, se as subjectScopes, ue as subscribeServerErrors, h as subscriptionEvent, r as systemSubject, Te as toRpc, Ue as tsMs, Ve as undoApplyDescriptor, Be as undoLogQueryDescriptor, He as undoRedoDescriptor, Ie as workflowCancelDescriptor, Pe as workflowDomainEventsQueryDescriptor, Fe as workflowEventDeliveriesQueryDescriptor, Le as workflowResumeDescriptor, Ne as workflowRunEventsQueryDescriptor, Ae as workflowRunQueryDescriptor, Me as workflowRunStepsQueryDescriptor, je as workflowRunsQueryDescriptor, Re as workflowSignalDescriptor, ze as workflowUpdateDescriptor };
403
+ export { n as ADMIN_SCOPE, be as AuthMiddleware, ge as ConnectionInfo, ve as ConnectionInfoMiddleware, Qe as PROTOCOL_VERSION, s as ScopeError, me as Subject, ue as SubjectService, q as UNDO_APPLY_TAG, K as UNDO_LOG_TAG, J as UNDO_REDO_TAG, _e as Unauthenticated, Q as UndoConflict, Z as UndoForbidden, Y as UndoLogEntry, X as UndoNotFound, H as WorkflowControlInputSchema, I as WorkflowDomainEventRowSchema, B as WorkflowDomainEventsInputSchema, V as WorkflowEventDeliveriesInputSchema, L as WorkflowEventDeliveryRowSchema, j as WorkflowParentClosePolicySchema, F as WorkflowRunEventRowSchema, Le as WorkflowRunHandleSchema, R as WorkflowRunRefSchema, M as WorkflowRunRowSchema, A as WorkflowRunStatusSchema, P as WorkflowRunStepRowSchema, z as WorkflowRunTableRefSchema, N as WorkflowRunsInputSchema, U as WorkflowSignalInputSchema, W as WorkflowUpdateInputSchema, G as WorkflowUpdateResultSchema, E as actionToRpc, ye as anonymousSubject, De as applyRowPatch, de as assertAuthenticated, ae as beginIdempotent, Fe as checkFrameworkCompat, se as checkGuards, r as checkGuardsEffect, he as composeAuthStrategies, Ne as composeRpcInterceptors, x as defineAction, b as defineMutation, je as definePlugin, Me as definePluginRoute, Pe as definePluginService, y as defineQuery, Oe as defineStream, Te as diffRows, i as effectiveScopes, ee as failIdempotent, t as finishIdempotent, ie as getResourceScopeResolver, l as hasCallbackRoutes, te as hasEffectiveScope, o as hasScope, g as idToPath, re as idempotencyScope, Ee as isIdKeyed, fe as isSystemSubject, oe as memoryIdempotencyStore, T as mutationToRpc, Ae as normalizeDescriptor, Ce as pathToId, c as publishServerError, w as queryToRpc, a as requireScope, m as rowPatchOpSchema, h as rowPatchSchema, e as setEffectiveScopes, ce as setResourceScopeResolver, D as streamToRpc, le as subjectScopes, ne as subscribeServerErrors, v as subscriptionEvent, pe as systemSubject, ke as toRpc, Ze as tsMs, Ye as undoApplyDescriptor, Je as undoLogQueryDescriptor, Xe as undoRedoDescriptor, We as workflowCancelDescriptor, He as workflowDomainEventsQueryDescriptor, Ue as workflowEventDeliveriesQueryDescriptor, Ge as workflowResumeDescriptor, Ve as workflowRunEventsQueryDescriptor, Re as workflowRunQueryDescriptor, Be as workflowRunStepsQueryDescriptor, ze as workflowRunsQueryDescriptor, Ke as workflowSignalDescriptor, qe as workflowUpdateDescriptor };
package/dist/rest.d.ts CHANGED
@@ -6,6 +6,9 @@ declare interface ActionProcedureDescriptor<Name extends string, Input extends S
6
6
  readonly input: Input;
7
7
  readonly output: Output;
8
8
  readonly error: Error;
9
+ /** Declarative authorization guard(s) — enforced before the executor runs,
10
+ * failing with a typed `ScopeError`. Absent → no framework-level authz. */
11
+ readonly guards: Guards | undefined;
9
12
  /** Opt this action into a public REST endpoint (innovation/11). */
10
13
  readonly publicApi: PublicApiSpec | undefined;
11
14
  /** Opt this action into the auto-synthesized agent toolset (innovation/07). */
@@ -26,11 +29,12 @@ export declare const collectPublicApiRoutes: (entries: ReadonlyArray<PublicApiEn
26
29
  */
27
30
  export declare const defineRestRoute: <I, O>(descriptor: RestRouteDescriptor<I, O>) => RestRouteDescriptor<I, O>;
28
31
 
29
- declare interface DeleteTarget<Input = unknown> {
32
+ declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
30
33
  readonly table: string;
31
34
  readonly op: 'delete';
32
- /** Identify the row to remove. Default: `input.id`. */
33
- readonly identify?: ((input: Input) => string) | undefined;
35
+ /** Identify the row(s) to remove. Default: `input.id`. Return an ARRAY to
36
+ * remove MANY rows/items in one mutation. */
37
+ readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
34
38
  }
35
39
 
36
40
  /** query→GET, mutation/action→POST, unless the spec overrides. */
@@ -71,6 +75,29 @@ declare interface ExposeAsToolSpec {
71
75
  readonly maxPerRun?: number;
72
76
  }
73
77
 
78
+ /** One or more declarative guards on a descriptor. All must pass (AND across
79
+ * entries; `mode` controls AND/OR WITHIN one entry's scope array). */
80
+ declare type Guards<Input = unknown> = ReadonlyArray<GuardSpec<Input>>;
81
+
82
+ declare interface GuardSpec<Input = unknown> {
83
+ /** Required permission scope(s). A single string, or an array combined by
84
+ * `mode`. Scope strings are the same values `hasScope` / rbac roles use. */
85
+ readonly scope: string | ReadonlyArray<string>;
86
+ /** How an array of scopes combines. `'all'` (default) = AND (hold every
87
+ * scope); `'any'` = OR (hold at least one). Ignored for a single scope. */
88
+ readonly mode?: 'all' | 'any';
89
+ /**
90
+ * PURE `input → resource id` extractor for a row/resource-scoped guard.
91
+ * Browser-safe (no DB, no server import) — exactly like `target.identify`.
92
+ * The framework passes the extracted id to a resource-aware scope resolver
93
+ * (a future ReBAC / `accessPolicy()` resolver) so the check can be scoped to
94
+ * THAT resource. With only the default (subject-global) resolver installed
95
+ * the id is advisory and the guard checks the subject's global scopes. Omit
96
+ * for a plain subject-scope guard.
97
+ */
98
+ readonly resource?: (input: Input) => string | undefined;
99
+ }
100
+
74
101
  declare interface IdempotencyRecord {
75
102
  readonly scope: string;
76
103
  readonly key: string;
@@ -103,7 +130,7 @@ declare interface IdempotencyStore {
103
130
  readonly release: (scope: string, key: string) => Promise<void>;
104
131
  }
105
132
 
106
- declare interface InsertTarget<Input = unknown, Row = unknown> {
133
+ declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
107
134
  readonly table: string;
108
135
  readonly op: 'insert';
109
136
  readonly order?: 'prepend' | 'append' | undefined;
@@ -121,9 +148,13 @@ declare interface InsertTarget<Input = unknown, Row = unknown> {
121
148
  *
122
149
  * The `optimisticId` parameter is exposed for rare cases where the
123
150
  * shape function needs to reference it (e.g., setting a foreign key
124
- * on a child row spread in the same optimistic insert).
151
+ * on a child row spread in the same optimistic insert). For a NESTED
152
+ * (`path`) insert, use `shapeItem` instead — it is typed to the item.
125
153
  */
126
154
  readonly shape?: ((input: Input, optimisticId: string) => Omit<Row, 'id' | 'optimistic'>) | undefined;
155
+ /** NESTED (`path`) insert: build the ITEM to insert, typed to the item (not
156
+ * the output). 2nd arg is the optimistic id. */
157
+ readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
127
158
  }
128
159
 
129
160
  /** Extract `:name` path params by aligning the route PATTERN with the request
@@ -144,12 +175,31 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
144
175
  * query-invalidation analytics). Mutations without a target run
145
176
  * normally but skip auto-optimistic. */
146
177
  readonly target: Target | undefined;
178
+ /** Declarative authorization guard(s) — enforced before the transaction
179
+ * opens, failing with a typed `ScopeError`. Absent → no framework-level
180
+ * authz (author gates in-handler, or the mutation is unguarded). */
181
+ readonly guards: Guards | undefined;
147
182
  /** Opt this mutation into a public REST endpoint (innovation/11). */
148
183
  readonly publicApi: PublicApiSpec | undefined;
149
184
  /** Opt this mutation into the auto-synthesized agent toolset (innovation/07). */
150
185
  readonly exposeAsTool: ExposeAsTool | undefined;
151
186
  }
152
187
 
188
+ declare interface NestedTargetFields<Input = unknown> {
189
+ /** Dot-path to the nested array within the query VALUE to patch (e.g.
190
+ * `'snapshot.projects'`). Absent → patch the top-level row array (default). */
191
+ readonly path?: string | undefined;
192
+ /** Item key within the nested array (default `'id'`). Only meaningful with
193
+ * `path`. */
194
+ readonly by?: string | undefined;
195
+ /** Guard WHICH cached query entries this target patches: only entries whose
196
+ * CURRENT value satisfies the predicate. Pure + browser-safe. Prevents a
197
+ * patch from bleeding across sibling subscriptions that share a source table
198
+ * (the guard AWB hand-writes as `roadmap.id === input.roadmapId`). Absent →
199
+ * every entry matching the target `table` is patched. */
200
+ readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
201
+ }
202
+
153
203
  /** A public raw-HTTP route a plugin serves on the framework listener. */
154
204
  declare interface PluginHttpRoute {
155
205
  /** HTTP method, or `'*'` for any (the handler decides). */
@@ -294,6 +344,9 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
294
344
  /** Server-side snapshot cache config. Absent → never cached (the
295
345
  * default; the dispatcher already keeps live subscriptions fresh). */
296
346
  readonly cache: QueryCacheConfig | undefined;
347
+ /** Declarative authorization guard(s) — enforced before the executor runs,
348
+ * failing with a typed `ScopeError`. Absent → no framework-level authz. */
349
+ readonly guards: Guards | undefined;
297
350
  /** Opt this query into a public REST endpoint (innovation/11). */
298
351
  readonly publicApi: PublicApiSpec | undefined;
299
352
  /** Opt this query into the auto-synthesized agent toolset (innovation/07). */
@@ -438,13 +491,19 @@ declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, Row> | R
438
491
 
439
492
  declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
440
493
 
441
- declare interface UpdateTarget<Input = unknown, Row = unknown> {
494
+ declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
442
495
  readonly table: string;
443
496
  readonly op: 'update';
444
- /** Identify the row to patch. Default: `input.id`. */
445
- readonly identify?: ((input: Input) => string) | undefined;
446
- /** Build the patch. Default: merges input over current. */
497
+ /** Identify the row(s) to patch. Default: `input.id`. Return an ARRAY to patch
498
+ * MANY rows/items in one mutation (a bulk edit where the per-item
499
+ * parallel-write race lived). */
500
+ readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
501
+ /** Build the patch for a FLAT target (`current` is the OUTPUT row). Default:
502
+ * merges input over current. For a NESTED (`path`) target use `shapeItem`. */
447
503
  readonly shape?: ((input: Input, current: Row) => Row) | undefined;
504
+ /** NESTED (`path`) update: build the item patch. `current` is the existing
505
+ * ITEM (not the mutation output), so no cast is needed. */
506
+ readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined;
448
507
  }
449
508
 
450
509
  export { }
package/dist/rest.js CHANGED
@@ -1,5 +1,5 @@
1
- import { s as e } from "./auth-DCE6m7Bo.js";
2
- import { a as t, i as n, o as r, r as i, t as a } from "./serverErrorBus-C1KVL0dr.js";
1
+ import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-0wjvKwGO.js";
2
+ import { s as a } from "./auth-DCE6m7Bo.js";
3
3
  import { Effect as o, Schema as s } from "effect";
4
4
  //#region src/publicApi.ts
5
5
  var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.path ?? `/${t.version ?? "v1"}/${e.replace(/\./g, "/")}`, u = (e, t) => {
@@ -95,7 +95,7 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
95
95
  }, f);
96
96
  p = t.value;
97
97
  }
98
- let h = l.resolveSubject ? await l.resolveSubject(s.headers) : e(s.headers["x-tenant"] ?? null), g = {
98
+ let h = l.resolveSubject ? await l.resolveSubject(s.headers) : a(s.headers["x-tenant"] ?? null), g = {
99
99
  subject: h,
100
100
  headers: s.headers,
101
101
  store: l.store
@@ -104,9 +104,9 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
104
104
  let t = await e(g);
105
105
  if (t) return m(t.status, { error: t.message }, f);
106
106
  }
107
- let b = l.idempotency, x = b && y.has(c.method) ? s.headers[b.header.toLowerCase()] : void 0, S = b && x ? r(h.tenantId, c.method, c.path) : "";
107
+ let b = l.idempotency, x = b && y.has(c.method) ? s.headers[b.header.toLowerCase()] : void 0, S = b && x ? n(h.tenantId, c.method, c.path) : "";
108
108
  if (b && x) {
109
- let e = await i(b.store, S, x, b.ttlMs, Date.now());
109
+ let e = await r(b.store, S, x, b.ttlMs, Date.now());
110
110
  if (e.kind === "replay") return m(e.response.status, e.response.body, {
111
111
  ...f,
112
112
  "Idempotency-Replayed": "true"
@@ -114,17 +114,17 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
114
114
  if (e.kind === "conflict") return m(409, { error: "A request with this Idempotency-Key is already being processed" }, f);
115
115
  }
116
116
  try {
117
- let e = await c.handler(p, g), n = await o.runPromise(d(e));
118
- return b && x && await t(b.store, S, x, {
117
+ let t = await c.handler(p, g), n = await o.runPromise(d(t));
118
+ return b && x && await e(b.store, S, x, {
119
119
  status: 200,
120
120
  body: n
121
121
  }, Date.now()), m(200, n, f);
122
122
  } catch (e) {
123
- if (b && x && await n(b.store, S, x), typeof e == "object" && e && typeof e.status == "number") {
123
+ if (b && x && await t(b.store, S, x), typeof e == "object" && e && typeof e.status == "number") {
124
124
  let t = e;
125
125
  return m(t.status, { error: t.message ?? "Error" }, f);
126
126
  }
127
- return a({
127
+ return i({
128
128
  error: e,
129
129
  source: "rest",
130
130
  name: `${c.method} ${c.path}`,
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/protocol",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "The Voltro wire + plugin contract — defineQuery/Mutation/Action/Stream, definePlugin, sessions / JWT / API-keys, and the RPC protocol.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@effect/sql": "^0.51.1",
56
- "@voltro/database": "0.2.2",
56
+ "@voltro/database": "0.4.0",
57
57
  "jose": "^6.2.3"
58
58
  },
59
59
  "peerDependencies": {
Binary file