@voltro/protocol 0.3.0 → 0.5.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 +50 -0
- package/dist/index.d.ts +156 -11
- package/dist/index.js +44 -43
- package/dist/rest.d.ts +53 -9
- package/dist/rest.js +1 -1
- package/dist/serverErrorBus-XDdIJk0c.js +0 -0
- package/package.json +2 -2
- package/dist/serverErrorBus-BrqfEV84.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,56 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.5.0] — 2026-07-18
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/runtime** — `.one()` now fails when a query matches MORE than one row, not only when it matches none. It previously probed with `LIMIT 1`, so a filter that quietly stopped being unique returned an arbitrary row while the thrown error still claimed "expected exactly one row". It now probes with `LIMIT 2` and fails with `NoRowFound({ found: 2 })`. Migration: a `.one()` whose filter is not unique and that meant "any match" becomes `.first()` / `.maybeOne()`; one that meant "the unique row" stays as-is and now fails loudly when that assumption breaks. `NoRowFound` and `OptimisticLockError` are now `Schema.TaggedError`s, so they can be declared directly in a descriptor's `error:` union and caught with `Effect.catchTag`. Previously they carried a `_tag` field that looked declarable but did not typecheck there, forcing every caller to catch and re-wrap them in a hand-written tagged error — that wrapper can be deleted. Constructing one directly now takes an object: `new NoRowFound({ table, found })`, `new OptimisticLockError({ table, expected })`. `instanceof` and `_tag` checks are unaffected.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/mcp** — Relationship (ReBAC) authorization is now declarative. A descriptor can carry `guards: [{ action, resourceType, resource: (input) => input.id }]` alongside its scope guards; the framework resolves it before the executor (for a mutation, before the transaction opens) and fails with a typed `ScopeError` naming `<resourceType>:<action>`. `defineResourcePolicy` previously enforced nothing on its own. Making it bite required hand-building a map from rpc tag to policy rule and installing an interceptor — undocumented plumbing that nobody wired, and **fail-open by omission**: an rpc missing from the map passed unchecked, with no type error and no boot warning. A guard on the descriptor cannot be forgotten for an rpc that exists, because it is part of the rpc. `setTupleSource` registers where relations are read from — a real registry, not a per-call parameter. Both entrypoints register a default reading `_voltro_rebac_tuples`; an app whose relations already live in its own tables (a `teamMembers` row) registers its own instead of copying data into a framework table. Every unanswerable case DENIES: no tuple source, no policy for the type, an input that does not identify a resource, or a tuple source that throws. An authorization question nobody can answer is a refusal. The capability manifest now carries the declared guards, so a client gates UI on the same declaration the server enforces instead of a hand-kept copy. Only the data crosses the wire — the pure `resource` extractor stays server-side and is reported as `resourceScoped: true`, so a client knows the real answer is per-row and asks rather than assuming. Because guards are re-checked on every subscription delivery, a relationship revoked mid-session now ends the stream rather than continuing to serve it.
|
|
51
|
+
- **@voltro/database, @voltro/runtime, @voltro/cli** — `paginateBy(descriptor, column, cursor, limit, direction?)` generalises `paginateById` to any orderable column. `paginateById` hardcoded `id`, which is right for a sortable key and useless for what feeds actually need — "the next page by `createdAt`" — so apps fell back to hand-rolled `limit + 1` / slice / `hasMore` triples on cursors the helper could not express. `paginateById` is now literally `paginateBy(descriptor, 'id', …)`, so the two cannot drift. `direction` flips the comparison as well as the sort: a `desc` feed pages with `<`. Mismatching those is the classic keyset bug — an ascending comparison under a descending sort returns the same first page forever. The docs example previously demonstrated a related trap (ordering by `createdAt desc` and then calling `paginateById`, which silently re-orders by `id asc`); it now shows the correct form and names the trap. `ctx.load` / `ctx.loadMany` add request-scoped batching. Same-tick reads of one table coalesce into a single `WHERE id IN (...)`, so a breadth-first walk costs one query per LEVEL rather than per node — the assembly shape `relations()` + `.with()` cannot express, because each level's ids come from the level above. Misses are cached too (a repeated dangling reference is fetched once) and a failed batch rejects its waiters without poisoning the cache, so a transient store error does not become "these rows do not exist" for the rest of the request. The cache is request-scoped as a correctness requirement, not a tuning choice: anything longer-lived would serve one subject's rows to another. Workflow steps can now `yield* EffectStore`. Handlers always could; workflow executors could not, so a step reading the store had to lift `ctx.store` with `Effect.tryPromise` — the idiom the rest of the framework tells you to avoid, because it discards the typed `StoreError` channel. The asymmetry was an oversight; the layer is now provided from the workflow context's own store.
|
|
52
|
+
- **@voltro/cli** — `voltro capabilities [--json]` enumerates the framework's export surface by reading the `.d.ts` files in the project's own `node_modules`, so the answer to "what does this framework export" can be verified rather than recalled. The `--json` form is locale-independent and byte-stable, so it can be diffed across upgrades. Symbols that ship but appear nowhere in the project's seeded agent guide are flagged — limited to primitives and hooks, because counting every undocumented export on a real tree gave 1,088 (mostly types and internal Layers), a number too large to act on. `voltro doctor`'s hand-roll detector gained server rules: a hand-written not-found branch on `rows[0]` (→ `.one()`), several sequential `store.query` calls assembling related data (→ `relations()` + `.with()`), `ctx.store` lifted with `Effect.promise` (→ `EffectStore`), an imperative scope check at the top of an executor (→ `guards:`), a credential-shaped column with no `.encrypted()`, a notify/webhook helper at a mutation's tail (→ `defineSubscriber` / `defineReaction`), and hand-rolled cursor pagination (→ `paginateById`). Its scan roots now include the API app directories (`queries/`, `mutations/`, `database/`, …) — without that the server rules could never have fired. The always-loaded agent core now carries a "Pick the SERVER primitive" rubric alongside the client one, and a doc-coverage gate keeps the server surface from drifting out of it.
|
|
53
|
+
- **@voltro/runtime, @voltro/cli** — `ctx.outbox.enqueue(effect, payload, options?)` — a reliable external side effect from a mutation. The enqueue writes through `ctx.store`, which inside a mutation IS the transactional view, so the intent to deliver commits in the same transaction as the domain write or not at all. That closes the window a post-commit tap cannot: `@voltro/plugin-cdc-out` is at-least-once *from enqueue* (its own docs say so — a crash between commit and tap loses the event). Here the enqueue cannot be lost, because losing it means the domain write rolled back too. Delivery after commit remains at-least-once, so handlers must be idempotent; `idempotencyKey` makes that easy to honour. Delivery is declared per effect in a `*.outbox.ts` via `defineOutboxHandler`. The worker is nudged on commit for the fast path and polls every 5s — the poll is the contract, not the optimisation: it recovers rows whose nudge was lost to a crash, rows from another replica, and rows waiting out a backoff. Exponential backoff capped at 5 minutes, configurable `maxAttempts` (default 8), and a dead-letter that stays queryable in `_voltro_outbox` with its `lastError`. An effect with no registered handler is left PENDING rather than dead-lettered — the usual cause is a deploy where the enqueuing code shipped ahead of its handler, and discarding those would turn a rollout ordering detail into permanent loss of a side effect the app believes happened. Two handlers claiming one effect are refused at boot with both filenames. `_voltro_outbox` rides the declarative differ and is created only when the app declares at least one handler. Wired in both `voltro dev` and `voltro serve`.
|
|
54
|
+
- **@voltro/database, @voltro/runtime** — `ctx.store.query()` now returns the row type instead of `Readonly<Record<string, unknown>>`. `QueryDescriptor<R>` carries a phantom row type, so the shape the typed builder already knew survives `.descriptor` into the store: ```ts const rows = await ctx.store.query(database.notes.where(eq('id', id)).descriptor) rows[0].title // string — previously `rows[0]['title'] as string` ``` The type was always available; it was dropped at exactly this boundary, which is why reading a field meant casting. One downstream app accumulated 2,032 of those casts against a surface that could have typed them all along. `EffectStore.query` is typed the same way, so an Effect-form handler keeps both the row type and the `StoreError` channel. A hand-built descriptor still resolves to `Row` — this never types less than before, so it is additive: existing code keeps compiling, and the casts it contains simply become redundant. The driver-level `DataStore.query` stays untyped deliberately. It is the SPI every dialect store and transactional view implements, and those genuinely do return untyped rows off the wire; the type is re-applied one layer up, at the handler-facing `FluentStore`.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **@voltro/runtime, @voltro/cli** — Declarative `guards:` are now re-checked before EVERY subscription delivery, not only when the subscription is opened. A subscription is a long-lived authorization grant, and the scopes that justified it can be withdrawn while it is still open — a role revoked, a resource un-shared, a membership ended. Previously the gate ran once at subscribe and every later delivery re-ran the query and pushed rows out without re-asking, so a revoked subject kept receiving live updates until their socket happened to drop. A denial now ends the subscription with the typed `ScopeError` rather than silently freezing the subscriber on its last authorized value, and the re-check runs BEFORE the read, so a revoked subject's rows are never materialised. Both the descriptor and computed-query paths are covered, in both `voltro dev` and `voltro serve`. Non-authorization failures (a bad predicate, a dropped connection) still leave the subscriber on its last good snapshot as before.
|
|
59
|
+
- **@voltro/testing, @voltro/cli** — - **`@voltro/testing`'s context now provides `ctx.load` / `ctx.loadMany`.** The request-scoped batching helpers were added to the handler context but not to the test harness, so `makeTestContext` no longer satisfied `AppContext` — any suite building a context failed to typecheck, and a handler calling `ctx.load` could not be tested at all. The harness now builds a loader over the same underlying store (one per context, so `withSubject` / `withTenant` can't serve one subject's cached rows to another), mirroring the real builder. - **The `.one()` codemod is declared for 0.5.0, not 0.4.0.** It was authored while 0.4.0 was unreleased; since codemods are selected with `from < version <= to`, leaving it at 0.4.0 would have silently skipped it for everyone upgrading from 0.4.0 — exactly the users who need it.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## [0.4.0] — 2026-07-18
|
|
64
|
+
|
|
65
|
+
### ⚠ BREAKING
|
|
66
|
+
|
|
67
|
+
- **@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.
|
|
68
|
+
- **@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.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **@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.
|
|
73
|
+
- **@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`).
|
|
74
|
+
- **@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.
|
|
75
|
+
- **@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`.
|
|
76
|
+
- **@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.
|
|
77
|
+
- **@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`.
|
|
78
|
+
- **@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.
|
|
79
|
+
- **@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.
|
|
80
|
+
- **@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.
|
|
81
|
+
- **@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.
|
|
82
|
+
- **@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.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **@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).
|
|
87
|
+
- **@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.
|
|
88
|
+
- **@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`).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
42
92
|
## [0.3.0] — 2026-07-18
|
|
43
93
|
|
|
44
94
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export declare const ADMIN_SCOPE = "admin:full";
|
|
|
29
29
|
|
|
30
30
|
export declare const anonymousSubject: (tenantId: string | null) => Subject;
|
|
31
31
|
|
|
32
|
+
/** A guard entry is either a scope check or a relationship check. */
|
|
33
|
+
export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec;
|
|
34
|
+
|
|
35
|
+
/** A guard is either a scope check or a relationship check. */
|
|
36
|
+
export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* Apply a `RowPatch` to `prev`, producing `next`. Exact inverse of
|
|
34
40
|
* `diffRows`: `applyRowPatch(prev, diffRows(prev, next))` deep-equals
|
|
@@ -171,7 +177,24 @@ export declare const checkFrameworkCompat: (pluginName: string, range: string |
|
|
|
171
177
|
* mutations, before the transaction opens). The `admin:full` bypass passes
|
|
172
178
|
* everything.
|
|
173
179
|
*/
|
|
174
|
-
export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<
|
|
180
|
+
export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined) => ScopeError | null;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The resource-aware counterpart of `checkGuards`. Identical semantics when no
|
|
184
|
+
* resource-scope resolver is registered (or no guard carries a `resource`
|
|
185
|
+
* extractor) — it delegates to the pure `checkGuards` and succeeds/denies
|
|
186
|
+
* exactly the same, allocation-light. When BOTH a resolver is registered AND a
|
|
187
|
+
* guard has a `resource`, it extracts the resource id from `input` and asks the
|
|
188
|
+
* resolver whether the caller holds each scope on THAT resource. A globally-held
|
|
189
|
+
* scope (or the `admin:full` bypass) still satisfies without a resolver call;
|
|
190
|
+
* only the gap ("no global grant") falls through to the resolver. A failed
|
|
191
|
+
* resolver Effect is treated as a denial (fail-closed).
|
|
192
|
+
*
|
|
193
|
+
* Returns a typed `ScopeError` naming the first unmet scope, or `null` when
|
|
194
|
+
* every guard passes. The runtime calls this in the dispatch spine BEFORE the
|
|
195
|
+
* executor (mutations: before the transaction opens).
|
|
196
|
+
*/
|
|
197
|
+
export declare const checkGuardsEffect: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined, input: unknown) => Effect.Effect<ScopeError | null>;
|
|
175
198
|
|
|
176
199
|
export declare interface ClientDescriptor {
|
|
177
200
|
readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'workflow';
|
|
@@ -202,7 +225,7 @@ export declare interface ClientTarget {
|
|
|
202
225
|
readonly op: 'insert' | 'update' | 'delete';
|
|
203
226
|
readonly order?: 'prepend' | 'append' | undefined;
|
|
204
227
|
readonly shape?: ((input: Record<string, unknown>, optimisticIdOrCurrent?: unknown) => Record<string, unknown>) | undefined;
|
|
205
|
-
readonly identify?: ((input: Record<string, unknown>) => string) | undefined;
|
|
228
|
+
readonly identify?: ((input: Record<string, unknown>) => string | ReadonlyArray<string>) | undefined;
|
|
206
229
|
/** Nested/path-targeted optimistic (see `NestedTargetFields`). Carried to the
|
|
207
230
|
* client so item-level patches into a JSON array / computed value happen
|
|
208
231
|
* automatically. Functions survive because the codegen references the live
|
|
@@ -210,6 +233,10 @@ export declare interface ClientTarget {
|
|
|
210
233
|
readonly path?: string | undefined;
|
|
211
234
|
readonly by?: string | undefined;
|
|
212
235
|
readonly match?: ((value: unknown, input: Record<string, unknown>) => boolean) | undefined;
|
|
236
|
+
/** Nested-item shaper (erased). The client uses this instead of `shape` when
|
|
237
|
+
* `path` is set. 2nd arg is the optimistic id (insert) or the current item
|
|
238
|
+
* (update) — `unknown` here since the erased view spans both; normalize casts. */
|
|
239
|
+
readonly shapeItem?: ((input: Record<string, unknown>, currentOrOptimisticId: unknown) => Record<string, unknown>) | undefined;
|
|
213
240
|
}
|
|
214
241
|
|
|
215
242
|
/** Compose multiple strategies into a single resolver function. The
|
|
@@ -417,8 +444,9 @@ export declare const defineStream: <const Name extends string, Input extends Sch
|
|
|
417
444
|
export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
|
|
418
445
|
readonly table: string;
|
|
419
446
|
readonly op: 'delete';
|
|
420
|
-
/** Identify the row to remove. Default: `input.id`.
|
|
421
|
-
|
|
447
|
+
/** Identify the row(s) to remove. Default: `input.id`. Return an ARRAY to
|
|
448
|
+
* remove MANY rows/items in one mutation. */
|
|
449
|
+
readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
|
|
422
450
|
}
|
|
423
451
|
|
|
424
452
|
/**
|
|
@@ -477,14 +505,28 @@ export declare const failIdempotent: (store: IdempotencyStore, scope: string, ke
|
|
|
477
505
|
/** Record a completed response for replay. */
|
|
478
506
|
export declare const finishIdempotent: (store: IdempotencyStore, scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
|
|
479
507
|
|
|
508
|
+
/** The currently-registered policy-guard resolver, or `undefined`. */
|
|
509
|
+
export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefined;
|
|
510
|
+
|
|
511
|
+
/** The currently-registered resource-scope resolver, or `undefined`. */
|
|
512
|
+
export declare const getResourceScopeResolver: () => ResourceScopeResolver | undefined;
|
|
513
|
+
|
|
480
514
|
export declare interface GuardCheckSpec {
|
|
481
515
|
readonly scope: string | ReadonlyArray<string>;
|
|
482
516
|
readonly mode?: 'all' | 'any';
|
|
517
|
+
/**
|
|
518
|
+
* PURE `input → resource id` extractor (the erased runtime form of
|
|
519
|
+
* `GuardSpec.resource`). When present AND a resource-aware resolver is
|
|
520
|
+
* registered (`setResourceScopeResolver`), the guard is checked against THAT
|
|
521
|
+
* resource, not just the caller's global scopes. Absent / no resolver →
|
|
522
|
+
* global-scope check (the extractor is advisory), exactly as before.
|
|
523
|
+
*/
|
|
524
|
+
readonly resource?: (input: unknown) => string | undefined;
|
|
483
525
|
}
|
|
484
526
|
|
|
485
527
|
/** One or more declarative guards on a descriptor. All must pass (AND across
|
|
486
528
|
* entries; `mode` controls AND/OR WITHIN one entry's scope array). */
|
|
487
|
-
export declare type Guards<Input = unknown> = ReadonlyArray<
|
|
529
|
+
export declare type Guards<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input>>;
|
|
488
530
|
|
|
489
531
|
export declare interface GuardSpec<Input = unknown> {
|
|
490
532
|
/** Required permission scope(s). A single string, or an array combined by
|
|
@@ -617,7 +659,7 @@ export declare interface IdempotencyStore {
|
|
|
617
659
|
* corrupt the pointer. */
|
|
618
660
|
export declare const idToPath: (id: RowId) => string;
|
|
619
661
|
|
|
620
|
-
export declare interface InsertTarget<Input = unknown, Row = unknown
|
|
662
|
+
export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
|
|
621
663
|
readonly table: string;
|
|
622
664
|
readonly op: 'insert';
|
|
623
665
|
readonly order?: 'prepend' | 'append' | undefined;
|
|
@@ -635,9 +677,13 @@ export declare interface InsertTarget<Input = unknown, Row = unknown> extends Ne
|
|
|
635
677
|
*
|
|
636
678
|
* The `optimisticId` parameter is exposed for rare cases where the
|
|
637
679
|
* shape function needs to reference it (e.g., setting a foreign key
|
|
638
|
-
* on a child row spread in the same optimistic insert).
|
|
680
|
+
* on a child row spread in the same optimistic insert). For a NESTED
|
|
681
|
+
* (`path`) insert, use `shapeItem` instead — it is typed to the item.
|
|
639
682
|
*/
|
|
640
683
|
readonly shape?: ((input: Input, optimisticId: string) => Omit<Row, 'id' | 'optimistic'>) | undefined;
|
|
684
|
+
/** NESTED (`path`) insert: build the ITEM to insert, typed to the item (not
|
|
685
|
+
* the output). 2nd arg is the optimistic id. */
|
|
686
|
+
readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
|
|
641
687
|
}
|
|
642
688
|
|
|
643
689
|
/** True when every row in the set carries an `id`. The dispatcher uses
|
|
@@ -646,6 +692,12 @@ export declare interface InsertTarget<Input = unknown, Row = unknown> extends Ne
|
|
|
646
692
|
* diffed by id and falls back to shipping the full data. */
|
|
647
693
|
export declare const isIdKeyed: (rows: ReadonlyArray<Readonly<Record<string, unknown>>>) => rows is ReadonlyArray<PatchRow>;
|
|
648
694
|
|
|
695
|
+
/** Runtime narrowing for the relationship variant. */
|
|
696
|
+
export declare const isPolicyCheck: (g: AnyCheckSpec) => g is PolicyCheckSpec;
|
|
697
|
+
|
|
698
|
+
/** Narrow a guard entry to the relationship variant. */
|
|
699
|
+
export declare const isPolicyGuard: <I>(g: AnyGuardSpec<I>) => g is PolicyGuardSpec<I>;
|
|
700
|
+
|
|
649
701
|
export declare const isSystemSubject: (subject: Subject) => boolean;
|
|
650
702
|
|
|
651
703
|
/** In-memory store — the default for single-process dev + the test double. */
|
|
@@ -1304,6 +1356,61 @@ export declare interface PluginTemplate {
|
|
|
1304
1356
|
*/
|
|
1305
1357
|
export declare type PluginUninstallHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
|
|
1306
1358
|
|
|
1359
|
+
/** The runtime-erased form of `PolicyGuardSpec` — a relationship check. */
|
|
1360
|
+
export declare interface PolicyCheckSpec {
|
|
1361
|
+
readonly action: string;
|
|
1362
|
+
readonly resourceType: string;
|
|
1363
|
+
readonly resource: (input: unknown) => string | undefined;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/** The question the runtime asks a registered policy-guard resolver. */
|
|
1367
|
+
export declare interface PolicyGuardRequest {
|
|
1368
|
+
readonly subject: Subject;
|
|
1369
|
+
/** The action named in the resource policy's `actions` map. */
|
|
1370
|
+
readonly action: string;
|
|
1371
|
+
readonly resourceType: string;
|
|
1372
|
+
/** The id the guard's pure `resource` extractor produced. */
|
|
1373
|
+
readonly resourceId: string;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Answers "does `subject` have `action` on `<resourceType>:<resourceId>`".
|
|
1378
|
+
* Registered once at boot via `setPolicyGuardResolver`. A failed Effect is
|
|
1379
|
+
* treated as a DENIAL — a resolver wanting another posture catches its own
|
|
1380
|
+
* errors.
|
|
1381
|
+
*/
|
|
1382
|
+
export declare type PolicyGuardResolver = (req: PolicyGuardRequest) => Effect.Effect<boolean, unknown>;
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* A relationship (ReBAC) guard — "may this subject perform ACTION on THIS row?"
|
|
1386
|
+
*
|
|
1387
|
+
* The scope guard above answers "what may this subject do at all"; this answers
|
|
1388
|
+
* "on which row". Both live in the same `guards:` array on purpose, because the
|
|
1389
|
+
* alternative is what apps actually built: a hand-maintained map from rpc tag →
|
|
1390
|
+
* policy rule, installed as an interceptor. That map is FAIL-OPEN BY OMISSION —
|
|
1391
|
+
* add an rpc, forget the entry, and it is silently unguarded. A declaration on
|
|
1392
|
+
* the descriptor cannot be forgotten for an rpc that exists, because it IS the
|
|
1393
|
+
* rpc.
|
|
1394
|
+
*
|
|
1395
|
+
* Browser-safe by the same construction as `GuardSpec`: strings plus a PURE
|
|
1396
|
+
* `resource` extractor. Resolution — reading relationship tuples, applying the
|
|
1397
|
+
* policy's `implies` closure — happens server-side through the registered
|
|
1398
|
+
* policy resolver. With no resolver registered the check FAILS CLOSED: an
|
|
1399
|
+
* unanswerable authorization question is a denial, never a pass.
|
|
1400
|
+
*
|
|
1401
|
+
* Because it is data, the same declaration compiles into the capability
|
|
1402
|
+
* manifest the client reads, so a UI gate and the server check cannot drift.
|
|
1403
|
+
*/
|
|
1404
|
+
export declare interface PolicyGuardSpec<Input = unknown> {
|
|
1405
|
+
/** The action to authorize, as named in the resource policy's `actions`. */
|
|
1406
|
+
readonly action: string;
|
|
1407
|
+
/** Which registered resource policy governs the check. */
|
|
1408
|
+
readonly resourceType: string;
|
|
1409
|
+
/** PURE `input → resource id`. Returning undefined DENIES — an unidentifiable
|
|
1410
|
+
* resource is not a reason to skip the check. */
|
|
1411
|
+
readonly resource: (input: Input) => string | undefined;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1307
1414
|
export declare type ProcedureDescriptor = QueryProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | MutationProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | ActionProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | StreamProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All>;
|
|
1308
1415
|
|
|
1309
1416
|
export declare const PROTOCOL_VERSION: 1;
|
|
@@ -1440,6 +1547,25 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Sche
|
|
|
1440
1547
|
* Checks the EFFECTIVE set so role-derived scopes count. */
|
|
1441
1548
|
export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
|
|
1442
1549
|
|
|
1550
|
+
/** The question the runtime asks a registered resource-scope resolver. */
|
|
1551
|
+
export declare interface ResourceScopeRequest {
|
|
1552
|
+
/** The authenticated caller. */
|
|
1553
|
+
readonly subject: Subject;
|
|
1554
|
+
/** The single scope being checked (guards with a scope array ask once per scope). */
|
|
1555
|
+
readonly scope: string;
|
|
1556
|
+
/** The resource id the guard's `resource` extractor produced (e.g. a teamId). */
|
|
1557
|
+
readonly resource: string;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* A process-global resolver that answers "does `subject` hold `scope` on
|
|
1562
|
+
* `resource`". Registered once at boot via `setResourceScopeResolver`. Returns
|
|
1563
|
+
* an Effect so it can read the app's role tables. It should NOT leak errors —
|
|
1564
|
+
* `checkGuardsEffect` treats a failed resolver as a DENIAL (fail-closed), so a
|
|
1565
|
+
* resolver that wants a different posture must catch its own errors.
|
|
1566
|
+
*/
|
|
1567
|
+
export declare type ResourceScopeResolver = (req: ResourceScopeRequest) => Effect.Effect<boolean, unknown>;
|
|
1568
|
+
|
|
1443
1569
|
/** The id of a row, addressed in op paths as `/<id>`. */
|
|
1444
1570
|
export declare type RowId = string | number;
|
|
1445
1571
|
|
|
@@ -1680,6 +1806,19 @@ export declare type ServerErrorSource = 'rest' | 'aggregate' | 'subscriber' | 'r
|
|
|
1680
1806
|
*/
|
|
1681
1807
|
export declare const setEffectiveScopes: (subject: Subject, scopes: ReadonlyArray<string>) => void;
|
|
1682
1808
|
|
|
1809
|
+
/** Register (or clear) the process-global policy-guard resolver. Last write
|
|
1810
|
+
* wins. The runtime installs one backed by the resource-policy registry and
|
|
1811
|
+
* the registered tuple source. */
|
|
1812
|
+
export declare const setPolicyGuardResolver: (resolver: PolicyGuardResolver | undefined) => void;
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Register (or clear, with `undefined`) the process-global resource-scope
|
|
1816
|
+
* resolver. Call once at boot — from `@voltro/plugin-rbac`'s wiring, or directly
|
|
1817
|
+
* from an app that resolves per-resource permissions against its own tables.
|
|
1818
|
+
* Last write wins.
|
|
1819
|
+
*/
|
|
1820
|
+
export declare const setResourceScopeResolver: (resolver: ResourceScopeResolver | undefined) => void;
|
|
1821
|
+
|
|
1683
1822
|
/** A strategy's verdict on a request.
|
|
1684
1823
|
* - `matched`: this strategy claims the request; here's the Subject.
|
|
1685
1824
|
* - `skip`: not my request (e.g. cookie absent); try next strategy.
|
|
@@ -2028,13 +2167,19 @@ export declare const undoRedoDescriptor: MutationProcedureDescriptor<"__voltro.u
|
|
|
2028
2167
|
ok: typeof Schema.Boolean;
|
|
2029
2168
|
}>, Schema.Union<[typeof UndoNotFound, typeof UndoForbidden, typeof UndoConflict]>>;
|
|
2030
2169
|
|
|
2031
|
-
export declare interface UpdateTarget<Input = unknown, Row = unknown
|
|
2170
|
+
export declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
|
|
2032
2171
|
readonly table: string;
|
|
2033
2172
|
readonly op: 'update';
|
|
2034
|
-
/** Identify the row to patch. Default: `input.id`.
|
|
2035
|
-
|
|
2036
|
-
|
|
2173
|
+
/** Identify the row(s) to patch. Default: `input.id`. Return an ARRAY to patch
|
|
2174
|
+
* MANY rows/items in one mutation (a bulk edit — where the per-item
|
|
2175
|
+
* parallel-write race lived). */
|
|
2176
|
+
readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
|
|
2177
|
+
/** Build the patch for a FLAT target (`current` is the OUTPUT row). Default:
|
|
2178
|
+
* merges input over current. For a NESTED (`path`) target use `shapeItem`. */
|
|
2037
2179
|
readonly shape?: ((input: Input, current: Row) => Row) | undefined;
|
|
2180
|
+
/** NESTED (`path`) update: build the item patch. `current` is the existing
|
|
2181
|
+
* ITEM (not the mutation output), so no cast is needed. */
|
|
2182
|
+
readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined;
|
|
2038
2183
|
}
|
|
2039
2184
|
|
|
2040
2185
|
export declare interface VoltroPlugin {
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import { Context as
|
|
1
|
+
import { S as e, _ as t, a as n, b as r, c as i, d as a, f as o, g as ee, h as te, i as ne, l as s, m as c, n as re, o as ie, p as ae, r as oe, s as se, t as ce, u as le, v as ue, x as de, y as fe } from "./serverErrorBus-XDdIJk0c.js";
|
|
2
|
+
import { a as pe, c as me, d as he, f as ge, i as _e, l as ve, n as ye, o as be, r as xe, s as Se, t as Ce, u as we } from "./auth-DCE6m7Bo.js";
|
|
3
|
+
import { Context as Te, Layer as Ee, Schema as l } from "effect";
|
|
4
4
|
import { Rpc as u } from "@effect/rpc";
|
|
5
5
|
//#region src/rowPatch.ts
|
|
6
|
-
var
|
|
6
|
+
var De = l.Union(l.String, l.Number), d = l.Record({
|
|
7
7
|
key: l.String,
|
|
8
8
|
value: l.Unknown
|
|
9
9
|
}).pipe(l.filter((e) => "id" in e, { message: () => "patch row must carry an id" })), f = l.Union(l.Struct({
|
|
@@ -19,12 +19,12 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
19
19
|
path: l.String
|
|
20
20
|
})), p = l.Struct({
|
|
21
21
|
ops: l.Array(f),
|
|
22
|
-
order: l.Array(
|
|
23
|
-
}), m = (e) => `/${String(e).replace(/~/g, "~0").replace(/\//g, "~1")}`,
|
|
22
|
+
order: l.Array(De)
|
|
23
|
+
}), m = (e) => `/${String(e).replace(/~/g, "~0").replace(/\//g, "~1")}`, Oe = (e) => (e.startsWith("/") ? e.slice(1) : e).replace(/~1/g, "/").replace(/~0/g, "~"), h = (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
|
-
},
|
|
27
|
+
}, ke = (e, t) => h(e) === h(t), Ae = (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();
|
|
@@ -35,7 +35,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
35
35
|
op: "add",
|
|
36
36
|
path: m(e.id),
|
|
37
37
|
value: e
|
|
38
|
-
}) :
|
|
38
|
+
}) : ke(t, e) || r.push({
|
|
39
39
|
op: "replace",
|
|
40
40
|
path: m(e.id),
|
|
41
41
|
value: e
|
|
@@ -49,10 +49,10 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
49
49
|
ops: r,
|
|
50
50
|
order: i
|
|
51
51
|
};
|
|
52
|
-
},
|
|
52
|
+
}, je = (e) => e.every((e) => {
|
|
53
53
|
let t = e.id;
|
|
54
54
|
return typeof t == "string" || typeof t == "number";
|
|
55
|
-
}),
|
|
55
|
+
}), Me = (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,7 +62,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
62
62
|
t !== void 0 && r.push(t);
|
|
63
63
|
}
|
|
64
64
|
return r;
|
|
65
|
-
},
|
|
65
|
+
}, g = (e) => l.Union(l.Struct({
|
|
66
66
|
_tag: l.Literal("snapshot"),
|
|
67
67
|
revision: l.Number,
|
|
68
68
|
data: e,
|
|
@@ -76,7 +76,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
76
76
|
_tag: l.Literal("error"),
|
|
77
77
|
error: l.Unknown,
|
|
78
78
|
revision: l.optional(l.Number)
|
|
79
|
-
})),
|
|
79
|
+
})), Ne = (e) => e.action !== void 0 && e.resourceType !== void 0, _ = (e) => ({
|
|
80
80
|
kind: "query",
|
|
81
81
|
name: e.name,
|
|
82
82
|
input: e.input,
|
|
@@ -87,7 +87,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
87
87
|
guards: e.guards,
|
|
88
88
|
publicApi: e.publicApi,
|
|
89
89
|
exposeAsTool: e.exposeAsTool
|
|
90
|
-
}),
|
|
90
|
+
}), v = (e) => ({
|
|
91
91
|
kind: "mutation",
|
|
92
92
|
name: e.name,
|
|
93
93
|
input: e.input,
|
|
@@ -97,7 +97,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
97
97
|
guards: e.guards,
|
|
98
98
|
publicApi: e.publicApi,
|
|
99
99
|
exposeAsTool: e.exposeAsTool
|
|
100
|
-
}),
|
|
100
|
+
}), y = (e) => ({
|
|
101
101
|
kind: "action",
|
|
102
102
|
name: e.name,
|
|
103
103
|
input: e.input,
|
|
@@ -106,7 +106,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
106
106
|
guards: e.guards,
|
|
107
107
|
publicApi: e.publicApi,
|
|
108
108
|
exposeAsTool: e.exposeAsTool
|
|
109
|
-
}),
|
|
109
|
+
}), b = (e) => ({
|
|
110
110
|
kind: "stream",
|
|
111
111
|
name: e.name,
|
|
112
112
|
input: e.input,
|
|
@@ -114,7 +114,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
114
114
|
error: e.error ?? l.Never
|
|
115
115
|
}), x = (e, t) => t && t.length > 0 ? l.Union(e, ...t) : e, S = (e, t) => t && t.length > 0 ? l.Union(e, s) : e, C = (e, t) => u.make(e.name, {
|
|
116
116
|
payload: e.input,
|
|
117
|
-
success:
|
|
117
|
+
success: g(e.output),
|
|
118
118
|
error: x(S(e.error, e.guards), t),
|
|
119
119
|
stream: !0
|
|
120
120
|
}), w = (e, t) => u.make(e.name, {
|
|
@@ -130,14 +130,14 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
130
130
|
success: e.element,
|
|
131
131
|
error: x(e.error, t),
|
|
132
132
|
stream: !0
|
|
133
|
-
}),
|
|
133
|
+
}), Pe = (e) => {
|
|
134
134
|
switch (e.kind) {
|
|
135
135
|
case "query": return C(e);
|
|
136
136
|
case "mutation": return w(e);
|
|
137
137
|
case "action": return T(e);
|
|
138
138
|
case "stream": return E(e);
|
|
139
139
|
}
|
|
140
|
-
},
|
|
140
|
+
}, Fe = (e) => {
|
|
141
141
|
let t = e.input, n = t === void 0 ? {} : { input: t }, r = e.output, i = r === void 0 ? {} : { output: r };
|
|
142
142
|
if (e.kind === "query") return {
|
|
143
143
|
kind: "query",
|
|
@@ -174,18 +174,19 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
174
174
|
..."identify" in e && e.identify !== void 0 ? { identify: e.identify } : {},
|
|
175
175
|
..."path" in e && e.path !== void 0 ? { path: e.path } : {},
|
|
176
176
|
..."by" in e && e.by !== void 0 ? { by: e.by } : {},
|
|
177
|
-
..."match" in e && e.match !== void 0 ? { match: e.match } : {}
|
|
177
|
+
..."match" in e && e.match !== void 0 ? { match: e.match } : {},
|
|
178
|
+
..."shapeItem" in e && e.shapeItem !== void 0 ? { shapeItem: e.shapeItem } : {}
|
|
178
179
|
}))
|
|
179
180
|
};
|
|
180
|
-
},
|
|
181
|
+
}, Ie = (e) => e, Le = (e) => e, Re = (e) => {
|
|
181
182
|
if (e.length !== 0) return (t, n) => e.reduceRight((e, t) => t(e, n), t);
|
|
182
|
-
},
|
|
183
|
-
let n =
|
|
183
|
+
}, ze = (e, t) => {
|
|
184
|
+
let n = Te.GenericTag(e);
|
|
184
185
|
return {
|
|
185
186
|
Tag: n,
|
|
186
|
-
Live:
|
|
187
|
+
Live: Ee.succeed(n, t)
|
|
187
188
|
};
|
|
188
|
-
},
|
|
189
|
+
}, D = (e, t, n) => {
|
|
189
190
|
if (!t) return { ok: !0 };
|
|
190
191
|
let r = O(n);
|
|
191
192
|
if (!r) return {
|
|
@@ -195,7 +196,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
195
196
|
let i = t.trim();
|
|
196
197
|
if (i === "*" || i === "") return { ok: !0 };
|
|
197
198
|
let a = i.split(/\s+/).filter((e) => e.length > 0);
|
|
198
|
-
for (let i of a) if (!
|
|
199
|
+
for (let i of a) if (!Be(i, r)) return {
|
|
199
200
|
ok: !1,
|
|
200
201
|
reason: `plugin "${e}" requires framework ${t}, running ${n}`
|
|
201
202
|
};
|
|
@@ -208,7 +209,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
208
209
|
patch: Number(t[3]),
|
|
209
210
|
pre: t[4] ?? ""
|
|
210
211
|
} : null;
|
|
211
|
-
}, 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,
|
|
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, Be = (e, t) => {
|
|
212
213
|
if (e === "*") return !0;
|
|
213
214
|
if (e.startsWith("^")) {
|
|
214
215
|
let n = O(e.slice(1));
|
|
@@ -230,7 +231,7 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
230
231
|
}
|
|
231
232
|
let r = O(e);
|
|
232
233
|
return r ? k(t, r) === 0 : !1;
|
|
233
|
-
}, A = l.Literal("running", "succeeded", "failed", "cancelled", "suspended"), j = l.Literal("cancel", "terminate", "abandon"),
|
|
234
|
+
}, A = l.Literal("running", "succeeded", "failed", "cancelled", "suspended"), j = l.Literal("cancel", "terminate", "abandon"), Ve = l.Struct({
|
|
234
235
|
id: l.String,
|
|
235
236
|
workflowName: l.String,
|
|
236
237
|
executionId: l.String,
|
|
@@ -323,49 +324,49 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
323
324
|
updateId: l.String,
|
|
324
325
|
completedEventId: l.String,
|
|
325
326
|
result: l.Unknown
|
|
326
|
-
}),
|
|
327
|
+
}), He = _({
|
|
327
328
|
name: "__voltro.workflow.run",
|
|
328
329
|
source: "_voltro_workflow_runs",
|
|
329
330
|
input: R,
|
|
330
331
|
output: l.Array(M)
|
|
331
|
-
}),
|
|
332
|
+
}), Ue = _({
|
|
332
333
|
name: "__voltro.workflow.runs",
|
|
333
334
|
source: "_voltro_workflow_runs",
|
|
334
335
|
input: N,
|
|
335
336
|
output: l.Array(M)
|
|
336
|
-
}),
|
|
337
|
+
}), We = _({
|
|
337
338
|
name: "__voltro.workflow.run.steps",
|
|
338
339
|
source: "_voltro_workflow_run_steps",
|
|
339
340
|
input: z,
|
|
340
341
|
output: l.Array(P)
|
|
341
|
-
}),
|
|
342
|
+
}), Ge = _({
|
|
342
343
|
name: "__voltro.workflow.run.events",
|
|
343
344
|
source: "_voltro_workflow_run_events",
|
|
344
345
|
input: z,
|
|
345
346
|
output: l.Array(F)
|
|
346
|
-
}),
|
|
347
|
+
}), Ke = _({
|
|
347
348
|
name: "__voltro.workflow.domainEvents",
|
|
348
349
|
source: "_voltro_workflow_events",
|
|
349
350
|
input: B,
|
|
350
351
|
output: l.Array(I)
|
|
351
|
-
}),
|
|
352
|
+
}), qe = _({
|
|
352
353
|
name: "__voltro.workflow.event.deliveries",
|
|
353
354
|
source: "_voltro_workflow_event_deliveries",
|
|
354
355
|
input: V,
|
|
355
356
|
output: l.Array(L)
|
|
356
|
-
}),
|
|
357
|
+
}), Je = y({
|
|
357
358
|
name: "__voltro.workflow.cancel",
|
|
358
359
|
input: H,
|
|
359
360
|
output: l.Struct({ ok: l.Boolean })
|
|
360
|
-
}),
|
|
361
|
+
}), Ye = y({
|
|
361
362
|
name: "__voltro.workflow.resume",
|
|
362
363
|
input: H,
|
|
363
364
|
output: l.Struct({ ok: l.Boolean })
|
|
364
|
-
}),
|
|
365
|
+
}), Xe = y({
|
|
365
366
|
name: "__voltro.workflow.signal",
|
|
366
367
|
input: U,
|
|
367
368
|
output: l.Struct({ eventId: l.String })
|
|
368
|
-
}),
|
|
369
|
+
}), Ze = y({
|
|
369
370
|
name: "__voltro.workflow.update",
|
|
370
371
|
input: W,
|
|
371
372
|
output: G
|
|
@@ -379,24 +380,24 @@ var xe = l.Union(l.String, l.Number), d = l.Record({
|
|
|
379
380
|
}), X = class extends l.TaggedError()("UndoNotFound", { invocationId: l.String }) {}, Z = class extends l.TaggedError()("UndoForbidden", { invocationId: l.String }) {}, Q = class extends l.TaggedError()("UndoConflict", {
|
|
380
381
|
invocationId: l.String,
|
|
381
382
|
reason: l.Literal("conflict", "action")
|
|
382
|
-
}) {}, $ = l.Union(X, Z, Q),
|
|
383
|
+
}) {}, $ = l.Union(X, Z, Q), Qe = _({
|
|
383
384
|
name: K,
|
|
384
385
|
source: "_voltro_undo_log",
|
|
385
386
|
input: l.Struct({ limit: l.optional(l.Number) }),
|
|
386
387
|
output: l.Array(Y)
|
|
387
|
-
}),
|
|
388
|
+
}), $e = v({
|
|
388
389
|
name: q,
|
|
389
390
|
input: l.Struct({ invocationId: l.String }),
|
|
390
391
|
output: l.Struct({ ok: l.Boolean }),
|
|
391
392
|
error: $
|
|
392
|
-
}),
|
|
393
|
+
}), et = v({
|
|
393
394
|
name: J,
|
|
394
395
|
input: l.Struct({ invocationId: l.String }),
|
|
395
396
|
output: l.Struct({ ok: l.Boolean }),
|
|
396
397
|
error: $
|
|
397
|
-
}),
|
|
398
|
+
}), tt = (e) => {
|
|
398
399
|
let t = e instanceof Date ? e.getTime() : typeof e == "number" ? e : typeof e == "string" ? new Date(e).getTime() : 0;
|
|
399
400
|
return Number.isNaN(t) ? 0 : t;
|
|
400
|
-
},
|
|
401
|
+
}, nt = 1;
|
|
401
402
|
//#endregion
|
|
402
|
-
export {
|
|
403
|
+
export { i as ADMIN_SCOPE, Ce as AuthMiddleware, ye as ConnectionInfo, xe as ConnectionInfoMiddleware, nt as PROTOCOL_VERSION, s as ScopeError, _e as Subject, pe as SubjectService, q as UNDO_APPLY_TAG, K as UNDO_LOG_TAG, J as UNDO_REDO_TAG, be 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, Ve 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, T as actionToRpc, Se as anonymousSubject, Me as applyRowPatch, me as assertAuthenticated, oe as beginIdempotent, D as checkFrameworkCompat, le as checkGuards, a as checkGuardsEffect, ve as composeAuthStrategies, Re as composeRpcInterceptors, y as defineAction, v as defineMutation, Ie as definePlugin, Le as definePluginRoute, ze as definePluginService, _ as defineQuery, b as defineStream, Ae as diffRows, o as effectiveScopes, ne as failIdempotent, n as finishIdempotent, ae as getPolicyGuardResolver, c as getResourceScopeResolver, we as hasCallbackRoutes, te as hasEffectiveScope, ee as hasScope, m as idToPath, ie as idempotencyScope, je as isIdKeyed, t as isPolicyCheck, Ne as isPolicyGuard, he as isSystemSubject, se as memoryIdempotencyStore, w as mutationToRpc, Fe as normalizeDescriptor, Oe as pathToId, ce as publishServerError, C as queryToRpc, ue as requireScope, f as rowPatchOpSchema, p as rowPatchSchema, fe as setEffectiveScopes, r as setPolicyGuardResolver, de as setResourceScopeResolver, E as streamToRpc, e as subjectScopes, re as subscribeServerErrors, g as subscriptionEvent, ge as systemSubject, Pe as toRpc, tt as tsMs, $e as undoApplyDescriptor, Qe as undoLogQueryDescriptor, et as undoRedoDescriptor, Je as workflowCancelDescriptor, Ke as workflowDomainEventsQueryDescriptor, qe as workflowEventDeliveriesQueryDescriptor, Ye as workflowResumeDescriptor, Ge as workflowRunEventsQueryDescriptor, He as workflowRunQueryDescriptor, We as workflowRunStepsQueryDescriptor, Ue as workflowRunsQueryDescriptor, Xe as workflowSignalDescriptor, Ze as workflowUpdateDescriptor };
|
package/dist/rest.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ declare interface ActionProcedureDescriptor<Name extends string, Input extends S
|
|
|
15
15
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** A guard is either a scope check or a relationship check. */
|
|
19
|
+
declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* Project every descriptor carrying a `publicApi` annotation into a REST
|
|
20
23
|
* route, in stable (path) order. The boot layer feeds these straight into
|
|
@@ -32,8 +35,9 @@ export declare const defineRestRoute: <I, O>(descriptor: RestRouteDescriptor<I,
|
|
|
32
35
|
declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
|
|
33
36
|
readonly table: string;
|
|
34
37
|
readonly op: 'delete';
|
|
35
|
-
/** Identify the row to remove. Default: `input.id`.
|
|
36
|
-
|
|
38
|
+
/** Identify the row(s) to remove. Default: `input.id`. Return an ARRAY to
|
|
39
|
+
* remove MANY rows/items in one mutation. */
|
|
40
|
+
readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/** query→GET, mutation/action→POST, unless the spec overrides. */
|
|
@@ -76,7 +80,7 @@ declare interface ExposeAsToolSpec {
|
|
|
76
80
|
|
|
77
81
|
/** One or more declarative guards on a descriptor. All must pass (AND across
|
|
78
82
|
* entries; `mode` controls AND/OR WITHIN one entry's scope array). */
|
|
79
|
-
declare type Guards<Input = unknown> = ReadonlyArray<
|
|
83
|
+
declare type Guards<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input>>;
|
|
80
84
|
|
|
81
85
|
declare interface GuardSpec<Input = unknown> {
|
|
82
86
|
/** Required permission scope(s). A single string, or an array combined by
|
|
@@ -129,7 +133,7 @@ declare interface IdempotencyStore {
|
|
|
129
133
|
readonly release: (scope: string, key: string) => Promise<void>;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
declare interface InsertTarget<Input = unknown, Row = unknown
|
|
136
|
+
declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
|
|
133
137
|
readonly table: string;
|
|
134
138
|
readonly op: 'insert';
|
|
135
139
|
readonly order?: 'prepend' | 'append' | undefined;
|
|
@@ -147,9 +151,13 @@ declare interface InsertTarget<Input = unknown, Row = unknown> extends NestedTar
|
|
|
147
151
|
*
|
|
148
152
|
* The `optimisticId` parameter is exposed for rare cases where the
|
|
149
153
|
* shape function needs to reference it (e.g., setting a foreign key
|
|
150
|
-
* on a child row spread in the same optimistic insert).
|
|
154
|
+
* on a child row spread in the same optimistic insert). For a NESTED
|
|
155
|
+
* (`path`) insert, use `shapeItem` instead — it is typed to the item.
|
|
151
156
|
*/
|
|
152
157
|
readonly shape?: ((input: Input, optimisticId: string) => Omit<Row, 'id' | 'optimistic'>) | undefined;
|
|
158
|
+
/** NESTED (`path`) insert: build the ITEM to insert, typed to the item (not
|
|
159
|
+
* the output). 2nd arg is the optimistic id. */
|
|
160
|
+
readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
/** Extract `:name` path params by aligning the route PATTERN with the request
|
|
@@ -248,6 +256,36 @@ declare interface PluginHttpRouteResult {
|
|
|
248
256
|
readonly headers?: Record<string, string>;
|
|
249
257
|
}
|
|
250
258
|
|
|
259
|
+
/**
|
|
260
|
+
* A relationship (ReBAC) guard — "may this subject perform ACTION on THIS row?"
|
|
261
|
+
*
|
|
262
|
+
* The scope guard above answers "what may this subject do at all"; this answers
|
|
263
|
+
* "on which row". Both live in the same `guards:` array on purpose, because the
|
|
264
|
+
* alternative is what apps actually built: a hand-maintained map from rpc tag →
|
|
265
|
+
* policy rule, installed as an interceptor. That map is FAIL-OPEN BY OMISSION —
|
|
266
|
+
* add an rpc, forget the entry, and it is silently unguarded. A declaration on
|
|
267
|
+
* the descriptor cannot be forgotten for an rpc that exists, because it IS the
|
|
268
|
+
* rpc.
|
|
269
|
+
*
|
|
270
|
+
* Browser-safe by the same construction as `GuardSpec`: strings plus a PURE
|
|
271
|
+
* `resource` extractor. Resolution — reading relationship tuples, applying the
|
|
272
|
+
* policy's `implies` closure — happens server-side through the registered
|
|
273
|
+
* policy resolver. With no resolver registered the check FAILS CLOSED: an
|
|
274
|
+
* unanswerable authorization question is a denial, never a pass.
|
|
275
|
+
*
|
|
276
|
+
* Because it is data, the same declaration compiles into the capability
|
|
277
|
+
* manifest the client reads, so a UI gate and the server check cannot drift.
|
|
278
|
+
*/
|
|
279
|
+
declare interface PolicyGuardSpec<Input = unknown> {
|
|
280
|
+
/** The action to authorize, as named in the resource policy's `actions`. */
|
|
281
|
+
readonly action: string;
|
|
282
|
+
/** Which registered resource policy governs the check. */
|
|
283
|
+
readonly resourceType: string;
|
|
284
|
+
/** PURE `input → resource id`. Returning undefined DENIES — an unidentifiable
|
|
285
|
+
* resource is not a reason to skip the check. */
|
|
286
|
+
readonly resource: (input: Input) => string | undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
251
289
|
/** A descriptor kind that can be projected to REST (streams are not, in v1). */
|
|
252
290
|
export declare type PublicApiDescriptor = QueryProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | MutationProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | ActionProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All>;
|
|
253
291
|
|
|
@@ -486,13 +524,19 @@ declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, Row> | R
|
|
|
486
524
|
|
|
487
525
|
declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
|
|
488
526
|
|
|
489
|
-
declare interface UpdateTarget<Input = unknown, Row = unknown
|
|
527
|
+
declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
|
|
490
528
|
readonly table: string;
|
|
491
529
|
readonly op: 'update';
|
|
492
|
-
/** Identify the row to patch. Default: `input.id`.
|
|
493
|
-
|
|
494
|
-
|
|
530
|
+
/** Identify the row(s) to patch. Default: `input.id`. Return an ARRAY to patch
|
|
531
|
+
* MANY rows/items in one mutation (a bulk edit — where the per-item
|
|
532
|
+
* parallel-write race lived). */
|
|
533
|
+
readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
|
|
534
|
+
/** Build the patch for a FLAT target (`current` is the OUTPUT row). Default:
|
|
535
|
+
* merges input over current. For a NESTED (`path`) target use `shapeItem`. */
|
|
495
536
|
readonly shape?: ((input: Input, current: Row) => Row) | undefined;
|
|
537
|
+
/** NESTED (`path`) update: build the item patch. `current` is the existing
|
|
538
|
+
* ITEM (not the mutation output), so no cast is needed. */
|
|
539
|
+
readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined;
|
|
496
540
|
}
|
|
497
541
|
|
|
498
542
|
export { }
|
package/dist/rest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-
|
|
1
|
+
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-XDdIJk0c.js";
|
|
2
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
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.
|
|
56
|
+
"@voltro/database": "0.5.0",
|
|
57
57
|
"jose": "^6.2.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
Binary file
|