@voltro/runtime 0.80.0 → 0.81.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 +90 -0
- package/dist/index.d.ts +200 -12
- package/dist/index.js +2746 -2600
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,96 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.81.0] — 2026-09-25
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **The compose baseline gives each api app its own database** — `@voltro/cli`
|
|
47
|
+
|
|
48
|
+
`voltro baseline sync` now gives every api app in a `project.json` its own database, login and migrate service. The primary api (the app named `api`, else the first api app) keeps the project's database; every other api app connects to `<project>_<app>` with `<PROJECT>_<APP>_DB_PASSWORD`, and each api app gets `VOLTRO_API_<APP>_URL` for its sibling api apps by service name. Migration: sync, move each secondary app's own tables into its new database, and set the new password variables in production.
|
|
49
|
+
|
|
50
|
+
**`voltro update` carries you across this** — codemod `0.81.0/03_compose-baseline-one-database-per-api-app`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
51
|
+
- **A crashed process blocks its idempotency key for 30 seconds, not a day** — `@voltro/protocol`, `@voltro/runtime`, `@voltro/client`, `@voltro/voltro`
|
|
52
|
+
|
|
53
|
+
A process that died inside an idempotent mutation left its key `in_flight` for the whole replay window — 24 hours by default — so every retry with that key was refused with `MutationInFlight` (REST: `409`), including the retry of a workflow step whose callee had simply restarted. A running claim now holds on a 30-second lease that the server renews while the handler runs, over the socket and on REST alike; a dead process stops renewing, and the next send with the key takes over and runs the mutation once — the dead call's transaction never committed. A completed response still replays for the whole window. A takeover is conditional on the claim still being stale, so a claim renewed a moment earlier is never taken from a live handler. `ctx.apis` calls treat `MutationInFlight` as no answer yet: the same key is re-sent when the callee says, until it answers or the call's deadline passes (then `ApiUnavailable`). BREAKING only for an app that implements `IdempotencyStore` itself: `claim` takes the lease, and `renew(scope, key, now)` is required.
|
|
54
|
+
|
|
55
|
+
**`voltro update` carries you across this** — codemod `0.81.0/05_idempotency-store-renews-its-lease`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
56
|
+
- **Each api app owns its database schema; a second app on it refuses to boot** — `@voltro/database`, `@voltro/cli`
|
|
57
|
+
|
|
58
|
+
The first boot that migrates a database records which app owns it (`_voltro_schema_owner`). A different app booting against the same database and schema now refuses — naming both apps — before it plans anything, under `voltro dev`, `voltro serve` and `VOLTRO_AUTO_MIGRATE=0` alike; `voltro db apply` refuses the same way and claims an unclaimed database — as its first write, after every other check has passed, so an apply refused for drift or a blocked operation leaves the database as it found it. `voltro data` export and import treat the owner row as the deployment's own: it never travels to another database. Two apps on one schema each diffed it against their own declarations and planned to drop the other's tables. Migration: give every api app its own database (`DB_URL`) or schema (`DB_SCHEMA`) and reach the other app's data through `apis:` and `service:` procedures; `voltro doctor` lists api apps of a project that resolve the same target. `voltro db claim --yes` hands a schema to a RENAMED app.
|
|
59
|
+
|
|
60
|
+
**`voltro update` carries you across this** — codemod `0.81.0/01_one-database-per-app`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
61
|
+
- **ReactionDefinition is the union of the table form and the query form** — `@voltro/runtime`, `@voltro/voltro`
|
|
62
|
+
|
|
63
|
+
With reactions that watch a live query's rows, `ReactionDefinition` is now `EdgeReactionDefinition | LevelReactionDefinition`. Code holding a value of that type and reading a member only the table form has — `watch.table`, `watch.on`, `guards.dedupeKey`, `when(event)` — no longer compiles: type it `EdgeReactionDefinition`, or narrow with `isLevelReaction(def)` first. `defineReaction({ … })` is unchanged: an object literal resolves to its own form, and a value typed as the union is still accepted and validated by its `watch`.
|
|
64
|
+
|
|
65
|
+
**`voltro update` carries you across this** — codemod `0.81.0/04_reaction-definition-is-a-union`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
66
|
+
- **Store calls on a table the app does not declare now fail** — `@voltro/runtime`, `@voltro/cli`
|
|
67
|
+
|
|
68
|
+
Every store verb — in handlers, startups, schedules and workflows — now fails with `UndeclaredTableAccess` on a table that is neither declared by the app (its `table()`s, a plugin's `extendSchema.tables`) nor framework-owned (`_voltro_*`), in every environment. A string table name used to reach another app's table past its guards, audit and tenant rules. `store.raw(sql)` is unchanged. Migration: call the owning app through `apis:` and a `service:` procedure, or declare the table if it is this app's own.
|
|
69
|
+
|
|
70
|
+
**`voltro update` carries you across this** — codemod `0.81.0/02_store-refuses-undeclared-tables`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **A plugin can bring app-shaped building blocks with `sources`** — `@voltro/protocol`, `@voltro/cli`
|
|
75
|
+
|
|
76
|
+
`definePlugin({ sources: fileURLToPath(new URL('./app', import.meta.url)) })` points at a directory of the same file conventions an app uses — queries and mutations with their executors, `schema.ts` tables, `*.workflow.tsx`, `*.cron.tsx`, subscribers, reactions — and every app that installs the plugin discovers them exactly as it discovers its own: the same codegen, browser-safety guard, access gate, migrations, and both boot paths. A reusable building block (a ledger, an audit trail, an approval flow) is shared between apps of a project without copying it; each installing app gets its own copy of the block's tables in its own database. `voltro build` keys the block's modules by plugin and path, so the serve bundle finds them wherever the block lives after a deploy.
|
|
77
|
+
- **A reaction can watch the rows of a live query, including another app's** — `@voltro/runtime`, `@voltro/cli`
|
|
78
|
+
|
|
79
|
+
`defineReaction({ watch: { query: 'deployments.pending', api: 'cloud' }, key: (row) => row.id, act: { kind: 'workflow', workflow: 'deploy.run' } })` reacts to every ROW of a live query's current result — the set of open work — instead of to each change of a table. Every delivery is the whole set, so a row that appeared while the reacting process was down, restarting or deploying is acted on when it comes back; nothing is lost. The workflow's `idempotencyKey` makes each row's act exactly-once across every replica, so no claim table is involved and a start that failed is tried again on the next delivery or on `resync` (default 5 minutes). `resync` and `stuckAfter` take milliseconds or a duration from `'30s'` to `'8 days'`. With `api:` the query is another app's `service:` query, subscribed over `ctx.apis` — push without the watched app knowing its watcher; without it, one of this app's own queries, read through the same guards and row filter as its REST projection. A row still in the set long after its workflow started is reported as `stuck` and never restarted. `voltro_reaction_pending`, `voltro_reaction_oldest_seconds` and `voltro_reaction_connected` expose the queue per reaction. The table form (`watch: { table }`) is unchanged; `runReaction` now takes the table form's type, `EdgeReactionDefinition`.
|
|
80
|
+
- **Apps of one project call each other through typed service procedures** — `@voltro/voltro`, `@voltro/protocol`, `@voltro/runtime`, `@voltro/client`, `@voltro/cli`, `@voltro/testing`, `@voltro/workflow`
|
|
81
|
+
|
|
82
|
+
An api app can now call another app of the same project, typed, over the socket, without touching its tables. The called app declares `service: { callers: ['worker'] }` on a query, mutation or action and lists the caller under `serviceCallers: { worker: {} }`; codegen emits those procedures into `serviceGroup.generated.ts` and never into the browser's `rpcGroup.generated.ts`. The calling app binds it under `apis: { cloud: { package: '@acme/api' } }` and calls `ctx.apis.cloud.deployments.activate(input)` (Promise) or `.effect(input)` (Effect), with every input, output and typed error inferred from the callee's declarations. A service authenticates with a per-edge credential (`VOLTRO_API_<BINDING>_TOKEN` on the caller, `VOLTRO_SERVICE_CALLER_<CALLER>_TOKEN` on the callee, `_NEXT` for rotation); `voltro dev` derives and mints it into both apps' `.env.local`, production supplies it (`voltro secret generate service`) and a missing one fails boot. A caller name is letters, digits and dashes, at most 56 characters, so its `service:<name>` identity fits an `id()` column. The called app plants an `actors` row for each accepted caller at boot (`service:<name>`, `kind: 'serviceAccount'`), so `audit()`-stamped writes name the calling app instead of failing on the foreign key. A service subject holds no scopes: it reaches exactly the procedures that name it, is refused everywhere else — framework built-ins included — and no user, API key or `admin:full` session reaches a service procedure. A write made inside a workflow step carries an idempotency key derived from the step, so a step retried after a lost reply replays instead of writing twice; an app with `serviceCallers` deduplicates mutations by default. A call without an answer fails with `ApiUnavailable` within a deadline (10 s by default); calls are measured as `voltro_api_call_duration_seconds` and `voltro_api_call_errors_total`. `voltro doctor` reports a caller listed without a procedure, a procedure naming an unlisted caller and a binding the callee does not accept. `@voltro/testing` adds `makeServiceApiMock`, which decodes every input and output through the callee's real schemas.
|
|
83
|
+
|
|
84
|
+
`apiSurface: compatible` — the replaced golden lines in `@voltro/protocol` (and the `@voltro/voltro` aggregate re-exporting it) are `AnyCheckSpec` and `DeclaredAccess` gaining the member `ServiceAccessSpec`. Every existing member is unchanged and an app keeps writing the guards it wrote; the framework is the only producer of the new member, which it normalises from `service:`. Only a switch exhaustive over guard specs needs an arm.
|
|
85
|
+
- **startTestApps boots several apps of one project, each service edge wired** — `@voltro/testing`
|
|
86
|
+
|
|
87
|
+
`startTestApps({ apps, edges })` from `@voltro/testing` boots the apps of one project as real `voltro` processes (`dev` by default, `serve` with `mode: 'serve'`), each on a free port — every callee before its callers, because a caller imports the contract its callee's codegen writes — and resolves once every app answers `/internal/readiness`; a cycle of edges is refused before anything starts. Every listed edge gets a freshly minted credential written to both ends, under the variables the apps read (`VOLTRO_API_<BINDING>_TOKEN` / `_URL` on the caller, `VOLTRO_SERVICE_CALLER_<CALLER>_TOKEN` on the callee); `apps.token(caller, callee)` returns it so a test can call the callee as the caller. An app that exits or is not ready in time stops the others and the call rejects with its log. Processes rather than one process, because apps with their own tables cannot share one process's table registry.
|
|
88
|
+
- **makeTestApp requests carry the caller's address** — `@voltro/testing`
|
|
89
|
+
|
|
90
|
+
A route that reads `ctx.remoteAddr` (a per-client rate limit, an audit field) could not be tested per client: every `makeTestApp` request arrived without an address. A request's options now take `remoteAddr`, the address the connection comes from, defaulting to `127.0.0.1`. The route sees it as `ctx.remoteAddr`, resolved by the function the server's listener uses, and a `publicApi` procedure's plugin interceptors see the same address, as on the server. `makeTestApp({ security: { trustedProxies, clientAddressHeader } })` takes the api's own settings, so a forwarding header is believed in a test exactly when it would be in production.
|
|
91
|
+
|
|
92
|
+
`markRestInvocation` takes the request's address as an optional third argument, and `restInvocationOf` returns it beside the spec; both only gain an optional member, so existing calls and reads are unchanged.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- **Codegen removes an rpc group the app no longer has procedures for** — `@voltro/cli`
|
|
97
|
+
|
|
98
|
+
An app whose last client-reachable procedure went away — deleted, or marked `internal: true` — kept its `rpcGroup.generated.ts`: codegen reported "no rpc primitives found — nothing to write" and left the old file, a typed contract for procedures the server no longer served. The next `voltro dev` or `voltro codegen` now removes it, as it already removed an empty `serviceGroup.generated.ts` and `apis.generated.ts`. A hand-written file of that name is left alone.
|
|
99
|
+
- **The auto-launched dashboard binds `VOLTRO_DASHBOARD_PORT`** — `@voltro/cli`
|
|
100
|
+
|
|
101
|
+
`VOLTRO_DASHBOARD_PORT` was read by `voltro dashboard` only. The dashboard that `voltro dev` and `voltro start` launch on their own — api and web app alike — always came up on `:5179`, and the launcher wrote `5179` into the child's `VOLTRO_DASHBOARD_PORT`, overriding the value the developer had set. The launcher and `voltro dashboard` now resolve the port through one reader: the variable, else `5179`. A value that is not a port (`1..65535`) is refused with a warning naming it instead of silently becoming `5179`.
|
|
102
|
+
- **The db pool boot line counts a connection outside the pool once** — `@voltro/cli`
|
|
103
|
+
|
|
104
|
+
With change data capture on, the line read `max=5 … + 1 = 6` and then, separately, `PLUS 1 outside the pool (CDC LISTEN consumer)`, which reads as 7 when the process holds 6. The outside connection is now named once, inside the sum it belongs to: `max=5 per replica (DB_MAX_CONNECTIONS) + 1 outside the pool (CDC LISTEN consumer) = 6 per process`.
|
|
105
|
+
- **A declared `utf8mb4` store reaches the workflow engine's tables** — `@voltro/database`, `@voltro/workflow`, `@voltro/cli`
|
|
106
|
+
|
|
107
|
+
`store: { charset: 'utf8mb4' }` converted every table the app declares and left the tables the framework creates without declaring them in the database default: the durable workflow engine's `cluster_*` storage, which holds every workflow call's input and result as JSON text, and the deploy runner's `_voltro_deploys` / `_voltro_migration_ops`. On a `utf8mb3` database a workflow whose input carried an emoji was refused there. Every table the framework creates is now created in the declared charset: the runner's ledgers directly, and the workflow engine's tables by the process that creates them on its first boot, while they are still empty. A workflow table a booting process finds is never rewritten at boot; tables from before the declaration are converted by `voltro db apply` (also when the plan has nothing to do, and with `--plan`) once the deploy has closed and by the `voltro dev` auto-migrate, keeping type, nullability and column CHECKs, and `voltro db plan` lists them beside the plan (under `--json` too, on the log, never in the plan file). The database's own default is left alone. The step only ever widens, and a deploy that fails and is undone never runs it.
|
|
108
|
+
- **The MySQL/MariaDB pool keeps its connections instead of reconnecting** — `@voltro/sql-mysql`
|
|
109
|
+
|
|
110
|
+
The driver pool was created with `maxIdle: 0`, so every connection released to it was closed within a second. A process reconnected after each second of quiet: a TCP handshake per burst, plus a TLS one where configured. Measured on MySQL and MariaDB: a query after two quiet seconds ran on a new connection. The pool now keeps the connections it opened, up to the limit.
|
|
111
|
+
- **Workflows started by a reaction or an incoming webhook start again** — `@voltro/cli`
|
|
112
|
+
|
|
113
|
+
A workflow started by a `*.reaction.ts` act or an incoming `*.webhook.tsx` was refused with `Workflow placement requires explicit caller identity`, under `voltro dev` and `voltro serve`: neither path named a caller, and placement refuses a start without one. Both now start as the framework itself, placed in the changed row's tenant when it has one.
|
|
114
|
+
- **The first boot on an empty database no longer warns about replica observations** — `@voltro/cli`
|
|
115
|
+
|
|
116
|
+
`voltro dev` started publishing this replica's observations before its auto-migrate, so the first write on an empty database hit a table that did not exist yet and printed `could not record this replica's observation — fleet views will report this replica as stale` on the first boot of every new project. The publisher now writes its first observation once the schema is applied — at once on `voltro serve`, whose fingerprint check runs before it — and never when the boot stops earlier.
|
|
117
|
+
- **A 405 on a shared REST path names every method the path serves** — `@voltro/protocol`
|
|
118
|
+
|
|
119
|
+
A `DELETE` to a path served by `GET` and `POST` answered `405` with `Allow: POST`: each route refused with its own method, and the last refusal won. `Allow` and the error body's `allow` now list every method declared on the path, `GET, POST`, as the REST documentation already promised.
|
|
120
|
+
- **A large REST surface no longer holds the boot for seconds registering routes** — `@voltro/runtime`, `@voltro/protocol`
|
|
121
|
+
|
|
122
|
+
Every REST route used to be mounted for all 34 HTTP methods the router knows, at its path and at its sub-path, and each insert compares itself against every entry already held. A public REST surface of about 1000 procedures therefore held about 34 000 router entries, and building them kept the thread busy for over a second after the port was bound — long enough for a liveness probe to time out. A path is now mounted for exactly the methods its routes declare: the same 1000 procedures hold 4 000 entries. Behaviour is unchanged. A method the path does not declare still reaches its routes and gets the precise `405` with `Allow`, a path nobody declared is still a `404`, and `HEAD` still answers from `GET`. For this, a REST route now declares its real `method` instead of `'*'` and sets the new `PluginHttpRoute.answersOtherMethods: true`. A plugin route that declares one method without that flag never sees another method, as before. A server of your own that mounts `restRoutesToHttpRoutes` output by method must hand the other methods to routes with `answersOtherMethods`, or they become `404` instead of `405`.
|
|
123
|
+
- **The testing guide's tsconfig example no longer sets baseUrl** — `@voltro/cli`
|
|
124
|
+
|
|
125
|
+
The `paths` alias example in the testing guide set `"baseUrl": "."`, an option TypeScript 7 removed. It now maps `paths` alone, which resolves against the tsconfig's own directory. `voltro test` still reads a `baseUrl` an older config sets.
|
|
126
|
+
- **voltro update lists a note's code and config lines before prose** — `@voltro/cli`
|
|
127
|
+
|
|
128
|
+
A manual note lists the lines it found in code and config first, under `── found in code or config (N lines)`. Lines in Markdown and text files follow in their own `── mentioned in prose (N lines)` block, and a note that matched only prose says `nothing in code or config matched`. Past the lines shown, the rest are named by file with a count instead of a bare `… and N more`. Before, the lines were sorted by path, and 40 lines of documentation hid a Helm env entry, the one line to change. The guide `voltro agents-md` copies into `agent-docs/` is no longer searched. It names every change of a release, so refreshing it made notes report "found" for changes the project never used.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
42
132
|
## [0.80.0] — 2026-09-24
|
|
43
133
|
|
|
44
134
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -909,6 +909,12 @@ export declare const API_CSP = "default-src 'none'; frame-ancestors 'none'; base
|
|
|
909
909
|
|
|
910
910
|
export declare const API_KEYS_TABLE = "_voltro_api_keys";
|
|
911
911
|
|
|
912
|
+
/** Caller side: the token this app presents to the api it binds as `name`. */
|
|
913
|
+
export declare const apiBindingTokenEnv: (name: string) => string;
|
|
914
|
+
|
|
915
|
+
/** Caller side: where the api it binds as `name` listens (production). */
|
|
916
|
+
export declare const apiBindingUrlEnv: (name: string) => string;
|
|
917
|
+
|
|
912
918
|
/** What `ApiKeyStore.ensureActor` is handed for the row it plants. */
|
|
913
919
|
export declare interface ApiKeyActor {
|
|
914
920
|
readonly id: string;
|
|
@@ -1107,6 +1113,19 @@ export declare interface AppAccess {
|
|
|
1107
1113
|
readonly scopes: ReadonlyArray<string>;
|
|
1108
1114
|
}
|
|
1109
1115
|
|
|
1116
|
+
/**
|
|
1117
|
+
* The typed clients of the apps this app calls — augmented per app by the
|
|
1118
|
+
* codegen'd `apis.generated.ts`:
|
|
1119
|
+
*
|
|
1120
|
+
* declare module '@voltro/runtime' {
|
|
1121
|
+
* interface AppApis { readonly cloud: ServiceApi<CloudServiceProcedures> }
|
|
1122
|
+
* }
|
|
1123
|
+
*
|
|
1124
|
+
* Empty until an app declares `apis:`.
|
|
1125
|
+
*/
|
|
1126
|
+
export declare interface AppApis {
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1110
1129
|
export declare interface AppContext {
|
|
1111
1130
|
/** Run database work with a freshly transaction-bound context. Reuses an
|
|
1112
1131
|
* existing mutation transaction (no savepoint). The callback may be retried;
|
|
@@ -1204,6 +1223,19 @@ export declare interface AppContext {
|
|
|
1204
1223
|
* subject", by construction.
|
|
1205
1224
|
*/
|
|
1206
1225
|
readonly connections?: ConnectionsFacade;
|
|
1226
|
+
/**
|
|
1227
|
+
* The other apps of the project this app calls (`ctx.apis.<name>`) — one
|
|
1228
|
+
* typed client per `apis:` binding in `app.config.ts`, typed from the
|
|
1229
|
+
* callee's `service:` procedures by the codegen'd `apis.generated.ts`.
|
|
1230
|
+
*
|
|
1231
|
+
* await ctx.apis.cloud.deployments.activate({ deploymentId, tenantId })
|
|
1232
|
+
* yield* ctx.apis.cloud.deployments.activate.effect(input) // in a workflow step
|
|
1233
|
+
*
|
|
1234
|
+
* Always present — an app with no bindings gets an empty object, so the field
|
|
1235
|
+
* never reads as "maybe wired". A call without an answer fails with
|
|
1236
|
+
* `ApiUnavailable`; every answer the callee gives arrives as itself.
|
|
1237
|
+
*/
|
|
1238
|
+
readonly apis: AppApis;
|
|
1207
1239
|
/**
|
|
1208
1240
|
* Request-scoped batching (`ctx.load` / `ctx.loadMany`). Coalesces
|
|
1209
1241
|
* same-tick reads of one table into a single `WHERE id IN (...)`.
|
|
@@ -1460,6 +1492,9 @@ export declare const assertCan: (subject: RebacSubject, action: string, resource
|
|
|
1460
1492
|
*/
|
|
1461
1493
|
export declare const assertConnectionCipherConfigured: (definitions: ReadonlyArray<ConnectionDefinition>) => void;
|
|
1462
1494
|
|
|
1495
|
+
/** Refuse a caller map that could not be what the author meant. */
|
|
1496
|
+
export declare const assertServiceCallersConfig: (callers: ServiceCallersConfig) => void;
|
|
1497
|
+
|
|
1463
1498
|
/**
|
|
1464
1499
|
* Boot gate: every mounted incoming-webhook route must carry a verification
|
|
1465
1500
|
* declaration. Throws `UnverifiedWebhookRoute` for the first that does not.
|
|
@@ -1640,6 +1675,19 @@ export declare interface AuditableQuery {
|
|
|
1640
1675
|
|
|
1641
1676
|
export declare interface AuthMiddlewareLayerOptions {
|
|
1642
1677
|
readonly resolveSubject: ResolveSubjectFn;
|
|
1678
|
+
/**
|
|
1679
|
+
* The apps allowed to call one rpc tag, or `undefined` when the tag is not a
|
|
1680
|
+
* `service:` procedure. Both boot paths pass the same lookup over the
|
|
1681
|
+
* procedures they registered.
|
|
1682
|
+
*
|
|
1683
|
+
* This is the OUTER wall for a service credential: the guard check refuses a
|
|
1684
|
+
* service subject at every procedure that does not name it, but framework
|
|
1685
|
+
* built-ins (`__voltro.*`) and plugin routes reach their handlers without a
|
|
1686
|
+
* descriptor guard. Here, before any handler, a service subject reaches only
|
|
1687
|
+
* the tags that name it — so a leaked service token opens exactly the doors
|
|
1688
|
+
* the callee declared and nothing the framework adds.
|
|
1689
|
+
*/
|
|
1690
|
+
readonly serviceCallersFor?: (tag: string) => ReadonlyArray<string> | undefined;
|
|
1643
1691
|
}
|
|
1644
1692
|
|
|
1645
1693
|
/**
|
|
@@ -3303,6 +3351,10 @@ export declare const DEFAULT_HSTS = "max-age=15552000; includeSubDomains";
|
|
|
3303
3351
|
*/
|
|
3304
3352
|
export declare const DEFAULT_KEEP_ALIVE_MS = 72000;
|
|
3305
3353
|
|
|
3354
|
+
export declare const DEFAULT_LEVEL_RESYNC_MS: number;
|
|
3355
|
+
|
|
3356
|
+
export declare const DEFAULT_LEVEL_STUCK_MS: number;
|
|
3357
|
+
|
|
3306
3358
|
/** Ceiling the idle backoff climbs to. Deliberately short enough to be a
|
|
3307
3359
|
* FLOOR under a missed wake rather than a substitute for one. */
|
|
3308
3360
|
export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
|
|
@@ -3541,7 +3593,12 @@ export declare const defineOutboxHandler: <R = never>(definition: OutboxHandlerD
|
|
|
3541
3593
|
|
|
3542
3594
|
/** Declare a reaction. Validates that the MANDATORY guard is present — an
|
|
3543
3595
|
* ungated reaction is a spend-storm footgun, so this fails LOUD at boot. */
|
|
3544
|
-
export declare
|
|
3596
|
+
export declare function defineReaction(def: LevelReactionDefinition): LevelReactionDefinition;
|
|
3597
|
+
|
|
3598
|
+
export declare function defineReaction(def: EdgeReactionDefinition): EdgeReactionDefinition;
|
|
3599
|
+
|
|
3600
|
+
/** A value already typed as either form — narrowed at runtime by its `watch`. */
|
|
3601
|
+
export declare function defineReaction(def: ReactionDefinition): ReactionDefinition;
|
|
3545
3602
|
|
|
3546
3603
|
/** Register a resource policy (the static, discoverable relationship registry
|
|
3547
3604
|
* the capability map exposes). Validates + fails LOUD on a malformed or
|
|
@@ -4064,6 +4121,15 @@ export declare interface DrainResult {
|
|
|
4064
4121
|
*/
|
|
4065
4122
|
export declare const dueWakeups: (store: DataStore, now: Date, limit?: number) => Promise<ReadonlyArray<Wakeup>>;
|
|
4066
4123
|
|
|
4124
|
+
/** A reaction to each CHANGE of a table (edge-triggered). */
|
|
4125
|
+
export declare interface EdgeReactionDefinition extends ReactionBase {
|
|
4126
|
+
/** The table whose changes trigger the reaction (+ optional op filter). */
|
|
4127
|
+
readonly watch: ReactionEdgeWatch;
|
|
4128
|
+
/** Optional predicate over the change — skip when it returns false. */
|
|
4129
|
+
readonly when?: (event: ReactionEvent) => boolean;
|
|
4130
|
+
readonly guards: ReactionGuards;
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4067
4133
|
/** The minimal Effect-store surface `insertIgnoreRowOutcomeEffect` needs. */
|
|
4068
4134
|
export declare interface EffectInsertIgnoreOutcomeStore {
|
|
4069
4135
|
readonly insertIgnoreWithOutcome: (table: string, row: Row, options: {
|
|
@@ -5577,6 +5643,8 @@ export declare interface GroupState {
|
|
|
5577
5643
|
readonly extreme?: number;
|
|
5578
5644
|
}
|
|
5579
5645
|
|
|
5646
|
+
export declare const guardUndeclaredTables: (underlying: DataStore, isKnownTable: (table: string) => boolean) => DataStore;
|
|
5647
|
+
|
|
5580
5648
|
/** What a user handler may return. */
|
|
5581
5649
|
export declare type HandlerBody<R = never> = void | Promise<unknown> | Effect.Effect<unknown, unknown, R>;
|
|
5582
5650
|
|
|
@@ -6016,6 +6084,12 @@ export declare const isIdleNow: (opts: IdleCheckOptions) => Promise<boolean>;
|
|
|
6016
6084
|
|
|
6017
6085
|
export declare const isInfraError: (e: unknown) => boolean;
|
|
6018
6086
|
|
|
6087
|
+
/** Narrow a reaction definition to the level form. */
|
|
6088
|
+
export declare const isLevelReaction: (def: ReactionDefinition) => def is LevelReactionDefinition;
|
|
6089
|
+
|
|
6090
|
+
/** Narrow a reaction to the level form. */
|
|
6091
|
+
export declare const isLevelWatch: (watch: ReactionWatch) => watch is ReactionLevelWatch;
|
|
6092
|
+
|
|
6019
6093
|
export declare const isObservingGraph: () => boolean;
|
|
6020
6094
|
|
|
6021
6095
|
/**
|
|
@@ -6123,6 +6197,8 @@ export declare const isTransactionReadConflict: (err: unknown) => err is Transac
|
|
|
6123
6197
|
/** Is `address` one of the peers we are willing to take a forwarded chain from? */
|
|
6124
6198
|
export declare const isTrustedProxy: (address: string | undefined, config: TrustedProxyConfig) => boolean;
|
|
6125
6199
|
|
|
6200
|
+
export declare const isWellFormedServiceToken: (value: string) => boolean;
|
|
6201
|
+
|
|
6126
6202
|
export declare const isWorkflowEventTriggerDefinition: (value: unknown) => value is WorkflowEventTriggerDefinition;
|
|
6127
6203
|
|
|
6128
6204
|
/** A maintainable aggregate op. (countDistinct / window funcs are NOT here —
|
|
@@ -6246,6 +6322,34 @@ export declare const lastWriteProvenance: (row: Record<string, unknown> | null |
|
|
|
6246
6322
|
readonly traceId?: string | null;
|
|
6247
6323
|
}) => ProvenanceResult;
|
|
6248
6324
|
|
|
6325
|
+
/**
|
|
6326
|
+
* A reaction to every ROW of a live query's current result (level-triggered).
|
|
6327
|
+
*
|
|
6328
|
+
* The act is a workflow, and the workflow's `idempotencyKey` is what makes it
|
|
6329
|
+
* exactly-once across the fleet: every replica that sees the row may try to
|
|
6330
|
+
* start it, and the engine collapses them into one execution. So there is no
|
|
6331
|
+
* `dedupeKey` here, and the workflow's key must identify the unit of work — a
|
|
6332
|
+
* row that can come BACK into the set (an org suspended, unsuspended, suspended
|
|
6333
|
+
* again) needs a key carrying that generation (`suspend:${orgId}:${suspendedAt}`).
|
|
6334
|
+
*
|
|
6335
|
+
* The workflow must move the row OUT of the set (its first step flips a status,
|
|
6336
|
+
* its failure path records a terminal one). A row still in the set long after
|
|
6337
|
+
* it was started is reported as `stuck`, never silently restarted.
|
|
6338
|
+
*/
|
|
6339
|
+
export declare interface LevelReactionDefinition extends ReactionBase {
|
|
6340
|
+
readonly watch: ReactionLevelWatch;
|
|
6341
|
+
/** The identity of a row in the set — what `stuck` reports and the runner tracks. */
|
|
6342
|
+
readonly key: (row: never) => string;
|
|
6343
|
+
/** Only rows it returns true for act. */
|
|
6344
|
+
readonly when?: (row: never) => boolean;
|
|
6345
|
+
/** Re-read the whole set this often even when nothing changed, so a start that
|
|
6346
|
+
* failed is retried. Default 5 minutes, minimum 30 seconds. */
|
|
6347
|
+
readonly resync?: number | string;
|
|
6348
|
+
/** Report a row still in the set this long after it was started. Default 15 minutes. */
|
|
6349
|
+
readonly stuckAfter?: number | string;
|
|
6350
|
+
readonly guards?: Omit<ReactionGuards, 'dedupeKey'>;
|
|
6351
|
+
}
|
|
6352
|
+
|
|
6249
6353
|
export declare class LinearScanMatcher implements Matcher {
|
|
6250
6354
|
private readonly byTable;
|
|
6251
6355
|
register(sub: SubscriptionRecord): void;
|
|
@@ -7069,6 +7173,11 @@ export declare type MetricsMode = 'off' | 'console' | 'otlp' | 'memory';
|
|
|
7069
7173
|
/** The lowest cadence any platform scheduler can express. */
|
|
7070
7174
|
export declare const MIN_EXTERNALIZABLE_INTERVAL_MS = 60000;
|
|
7071
7175
|
|
|
7176
|
+
export declare const MIN_LEVEL_RESYNC_MS = 30000;
|
|
7177
|
+
|
|
7178
|
+
/** Mint a fresh service token. `voltro dev` and `voltro secret generate` call this. */
|
|
7179
|
+
export declare const mintServiceToken: () => string;
|
|
7180
|
+
|
|
7072
7181
|
/**
|
|
7073
7182
|
* Ordering token for one mirrored write.
|
|
7074
7183
|
*
|
|
@@ -8282,17 +8391,23 @@ export declare type ReactionAct = {
|
|
|
8282
8391
|
readonly payload?: (event: ReactionEvent) => unknown;
|
|
8283
8392
|
};
|
|
8284
8393
|
|
|
8285
|
-
|
|
8394
|
+
declare interface ReactionBase {
|
|
8286
8395
|
readonly name: string;
|
|
8287
|
-
/** The table whose changes trigger the reaction (+ optional op filter). */
|
|
8288
|
-
readonly watch: {
|
|
8289
|
-
readonly table: string;
|
|
8290
|
-
readonly on?: ReactionOp | ReadonlyArray<ReactionOp>;
|
|
8291
|
-
};
|
|
8292
|
-
/** Optional predicate over the change — skip when it returns false. */
|
|
8293
|
-
readonly when?: (event: ReactionEvent) => boolean;
|
|
8294
8396
|
readonly act: ReactionAct;
|
|
8295
|
-
|
|
8397
|
+
}
|
|
8398
|
+
|
|
8399
|
+
export declare type ReactionDefinition = EdgeReactionDefinition | LevelReactionDefinition;
|
|
8400
|
+
|
|
8401
|
+
/** `'5 minutes'` / `'30s'` / `'8 days'` / `300000` → milliseconds. */
|
|
8402
|
+
export declare const reactionDurationMs: (value: number | string, field: string, reaction: string) => number;
|
|
8403
|
+
|
|
8404
|
+
/**
|
|
8405
|
+
* The EDGE form: react to each change of one table, as it happens.
|
|
8406
|
+
* Best-effort — a change made while the reacting process is down is not seen.
|
|
8407
|
+
*/
|
|
8408
|
+
export declare interface ReactionEdgeWatch {
|
|
8409
|
+
readonly table: string;
|
|
8410
|
+
readonly on?: ReactionOp | ReadonlyArray<ReactionOp>;
|
|
8296
8411
|
}
|
|
8297
8412
|
|
|
8298
8413
|
/** The change a reaction reacts to (the framework's CDC shape). */
|
|
@@ -8338,6 +8453,23 @@ export declare interface ReactionGuards {
|
|
|
8338
8453
|
};
|
|
8339
8454
|
}
|
|
8340
8455
|
|
|
8456
|
+
/**
|
|
8457
|
+
* The LEVEL form: react to every ROW of a live query's current result — the set
|
|
8458
|
+
* of open work — for as long as it is in the set.
|
|
8459
|
+
*
|
|
8460
|
+
* Each delivery of the query is the whole current set, so work that appeared
|
|
8461
|
+
* while the reacting process was down, restarting or deploying is seen on the
|
|
8462
|
+
* first delivery after it comes back: nothing is lost. `api` names a binding in
|
|
8463
|
+
* `apis:` (the query is another app's `service:` query); omit it to watch one
|
|
8464
|
+
* of this app's own queries.
|
|
8465
|
+
*/
|
|
8466
|
+
export declare interface ReactionLevelWatch {
|
|
8467
|
+
readonly query: string;
|
|
8468
|
+
readonly api?: string;
|
|
8469
|
+
/** The query's input. Default `{}`. */
|
|
8470
|
+
readonly input?: unknown;
|
|
8471
|
+
}
|
|
8472
|
+
|
|
8341
8473
|
export declare type ReactionOp = 'insert' | 'update' | 'delete';
|
|
8342
8474
|
|
|
8343
8475
|
export declare type ReactionOutcome = 'acted' | 'skipped-op' | 'skipped-when' | 'skipped-dedupe' | 'skipped-ratelimit' | 'skipped-budget';
|
|
@@ -8396,6 +8528,8 @@ export declare interface ReactionRunDeps {
|
|
|
8396
8528
|
readonly now?: () => number;
|
|
8397
8529
|
}
|
|
8398
8530
|
|
|
8531
|
+
export declare type ReactionWatch = ReactionEdgeWatch | ReactionLevelWatch;
|
|
8532
|
+
|
|
8399
8533
|
/** The reactive tunables a caller may declare. Every field optional; every
|
|
8400
8534
|
* default above. */
|
|
8401
8535
|
export declare interface ReactiveConfigInput {
|
|
@@ -9522,7 +9656,8 @@ export declare type ResolveSubjectFn = (input: {
|
|
|
9522
9656
|
export declare const resolveTimelineMode: (env?: Record<string, string | undefined>) => TimelineMode;
|
|
9523
9657
|
|
|
9524
9658
|
/** Select identity before opening a transaction, never while one owns writes.
|
|
9525
|
-
* Only the framework's system subject
|
|
9659
|
+
* Only a cross-tenant machine subject — the framework's system subject, or a
|
|
9660
|
+
* service calling a `service:` procedure — may select a different tenant. */
|
|
9526
9661
|
export declare const resolveTransactionSubject: (subject: Subject, tenantId: string | undefined, transactionActive: boolean) => Subject;
|
|
9527
9662
|
|
|
9528
9663
|
/**
|
|
@@ -10035,6 +10170,9 @@ export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
|
|
|
10035
10170
|
* boot-path parity rule asks for.
|
|
10036
10171
|
*/
|
|
10037
10172
|
readonly publishSurfaceDigest?: boolean;
|
|
10173
|
+
/** The bound tags that are `service:` procedures — published as
|
|
10174
|
+
* `serviceSurface`, kept out of the client `surface`. */
|
|
10175
|
+
readonly serviceSurfaceTags?: ReadonlySet<string>;
|
|
10038
10176
|
/** Non-sensitive fleet identity written on the rpc WebSocket open/close
|
|
10039
10177
|
* records. Defaults to the process identity resolver (POD_NAME/HOSTNAME).
|
|
10040
10178
|
* Embedders may state it explicitly when they own process identity. */
|
|
@@ -10248,7 +10386,7 @@ export declare const runProvidedEffect: <A>(effect: Effect.Effect<A, unknown, ne
|
|
|
10248
10386
|
* `agentActor(via:'agent')` scoped to the row's tenant. Idempotent: the same
|
|
10249
10387
|
* dedupe key never acts twice.
|
|
10250
10388
|
*/
|
|
10251
|
-
export declare const runReaction: (reaction:
|
|
10389
|
+
export declare const runReaction: (reaction: EdgeReactionDefinition, event: ReactionEvent, deps: ReactionRunDeps) => Promise<ReactionOutcome>;
|
|
10252
10390
|
|
|
10253
10391
|
/**
|
|
10254
10392
|
* Start the full single-node orchestrator: bind the router on the public
|
|
@@ -11208,6 +11346,49 @@ export declare type ServeRpcInterceptor = (base: Effect.Effect<unknown, unknown,
|
|
|
11208
11346
|
/** The published tracer, or `undefined` when tracing is off / no server booted. */
|
|
11209
11347
|
export declare const serverTracer: () => Tracer.Tracer | undefined;
|
|
11210
11348
|
|
|
11349
|
+
/** The bearer prefix every service credential carries. */
|
|
11350
|
+
export declare const SERVICE_TOKEN_PREFIX = "vsvc_";
|
|
11351
|
+
|
|
11352
|
+
/**
|
|
11353
|
+
* One accepted caller. Declared in the callee's `app.config.ts`:
|
|
11354
|
+
*
|
|
11355
|
+
* serviceCallers: { worker: {} }
|
|
11356
|
+
*
|
|
11357
|
+
* The object is the per-caller configuration slot; today it carries nothing,
|
|
11358
|
+
* because the token comes from the environment by convention and the grant
|
|
11359
|
+
* comes from the procedures. It is an object rather than `true` so a later
|
|
11360
|
+
* per-caller option (a rate limit, an mTLS identity) is an addition, not a
|
|
11361
|
+
* change of shape.
|
|
11362
|
+
*/
|
|
11363
|
+
export declare type ServiceCallerConfig = Readonly<Record<string, never>>;
|
|
11364
|
+
|
|
11365
|
+
/** Callee side: the second accepted value during a rotation. */
|
|
11366
|
+
export declare const serviceCallerNextTokenEnv: (caller: string) => string;
|
|
11367
|
+
|
|
11368
|
+
export declare type ServiceCallersConfig = Readonly<Record<string, ServiceCallerConfig>>;
|
|
11369
|
+
|
|
11370
|
+
/**
|
|
11371
|
+
* The strategy an app with `serviceCallers` puts FIRST in its chain.
|
|
11372
|
+
*
|
|
11373
|
+
* It claims only `Bearer vsvc_…`. Every other request is a `skip`, so an app's
|
|
11374
|
+
* users never pay more than a prefix check.
|
|
11375
|
+
*/
|
|
11376
|
+
export declare const serviceCallerStrategy: (options: ServiceCallerStrategyOptions) => AuthStrategy;
|
|
11377
|
+
|
|
11378
|
+
export declare interface ServiceCallerStrategyOptions {
|
|
11379
|
+
readonly callers: ServiceCallersConfig;
|
|
11380
|
+
/** Read lazily — at request time and in `verifyConfig` — never while the
|
|
11381
|
+
* config is imported, so `voltro build` needs no production secret. */
|
|
11382
|
+
readonly env?: () => Readonly<Record<string, string | undefined>>;
|
|
11383
|
+
}
|
|
11384
|
+
|
|
11385
|
+
/** Callee side: the env var holding the token a caller presents. */
|
|
11386
|
+
export declare const serviceCallerTokenEnv: (caller: string) => string;
|
|
11387
|
+
|
|
11388
|
+
/** `worker` → `WORKER`, `build-runner` → `BUILD_RUNNER`, `voltroCloudWorker` →
|
|
11389
|
+
* `VOLTRO_CLOUD_WORKER`. The env-var spelling of an app name. */
|
|
11390
|
+
export declare const serviceEnvSegment: (name: string) => string;
|
|
11391
|
+
|
|
11211
11392
|
export declare const setBufferedBytes: (label: string, bytes: number) => void;
|
|
11212
11393
|
|
|
11213
11394
|
/** Register the process-wide connection resolver (or clear with `undefined`).
|
|
@@ -12365,6 +12546,13 @@ declare interface TypedUpsertOptions<T extends AnyWritableTable> {
|
|
|
12365
12546
|
*/
|
|
12366
12547
|
export declare const unbindConnection: (clientId: number) => void;
|
|
12367
12548
|
|
|
12549
|
+
export declare class UndeclaredTableAccess extends Error {
|
|
12550
|
+
readonly table: string;
|
|
12551
|
+
readonly operation: string;
|
|
12552
|
+
readonly _tag = "UndeclaredTableAccess";
|
|
12553
|
+
constructor(table: string, operation: string);
|
|
12554
|
+
}
|
|
12555
|
+
|
|
12368
12556
|
/** The store surface an inverse op needs (a subset of the framework DataStore). */
|
|
12369
12557
|
export declare interface UndoApplyStore {
|
|
12370
12558
|
insert(table: string, row: Record<string, unknown>): Promise<unknown> | unknown;
|