@voltro/plugin-sso-saml 0.79.1 → 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 +169 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,175 @@ _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
|
+
|
|
132
|
+
## [0.80.0] — 2026-09-24
|
|
133
|
+
|
|
134
|
+
### ⚠ BREAKING
|
|
135
|
+
|
|
136
|
+
- **The inspect stream's replica selection is decided before the response** — `@voltro/runtime`, `@voltro/voltro`
|
|
137
|
+
|
|
138
|
+
`InspectStream.select` returns `Effect<InspectStreamSelection | undefined>` instead of a `Stream`, where `InspectStreamSelection` is either `{ kind: 'stream', stream }` or `{ kind: 'refused', status, body }`. A stream response is committed as `200` from its first byte, so a refusal can only be expressed before it starts. The option is wired by `voltro dev`, `voltro serve` and `voltro start`; code that passes its own `inspectStream` to `startRpcServer` wraps its stream in `Effect.succeed({ kind: 'stream', stream })`. `voltro update` lists every line that sets `inspectStream`.
|
|
139
|
+
- **`JiraService.listBoards` finds boards by project, name and type** — `@voltro/plugin-atlassian`
|
|
140
|
+
|
|
141
|
+
Every board method of `JiraService` took a board id, so a board picker had nothing to list and people looked ids up in Jira by hand. `listBoards({ projectKeyOrId?, name?, type? })` calls `GET /rest/agile/1.0/board`, follows the pages itself, and returns each board as `{ id, name, type, location? }`, where `location` names the project the board is located in. `name` is a contains match on Jira's side. It runs on the caller's own credentials like every other method, so a board the caller may not see is not listed. The break is for code that provides the service itself: a test stub written against the full `JiraServiceShape` needs the new method.
|
|
142
|
+
|
|
143
|
+
**`voltro update` carries you across this** — codemod `0.80.0/03_jira-service-lists-boards`. 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.80.0).
|
|
144
|
+
- **A loader has `ctx.query` in the browser, so a clicked-to page keeps its title** — `@voltro/web`, `@voltro/client`, `@voltro/protocol`, `@voltro/cli`
|
|
145
|
+
|
|
146
|
+
`ctx.query` was `undefined` when a loader ran in the browser. A page whose loader resolved an entity's name for `meta` therefore showed it in the title on a fresh load and dropped it after a click inside the app: the same URL, two titles. The browser loader now gets `ctx.query` too. It reads through the page's own api client, so it carries the visitor's session. A query resolves to its first snapshot, as on the server, and an in-band error rejects with the typed error on `voltroError`. A newer navigation that replaces the one in flight now aborts the loader's `signal`, as `LoaderContext.signal` always promised, and that closes the read. What changes for existing code is a check on `query`'s presence: `if (query)`, `query ? … : …` and `query?.(…)` now take the resolving branch in the browser too. Branch on `ctx.isServer` where the check meant "this is the server". `voltro update` lists every such line in your pages and layouts.
|
|
147
|
+
|
|
148
|
+
Bundle size: the browser read path adds 1.6 KB gz to a web app's first load (194.6 → 196.2 KB gz on the framework's zero-procedure fixture). Because the router now imports the api client, the shared chunk that carries the Effect runtime is emitted as `router-*.js` instead of `index-*.js`; the bytes moved between the two files, they did not grow.
|
|
149
|
+
- **No framework loading spinner; useNavigation() exposes navigation state** — `@voltro/web`
|
|
150
|
+
|
|
151
|
+
`FrameworkBoot` mounted a corner `NavigationIndicator` in every web app, in development and production, so a spinner appeared at the bottom right of pages whose design never included one — also in apps that had switched the devtools off. It is no longer mounted, and `NavigationIndicator` is removed from `@voltro/web`. In development the devtools button shows navigation progress as before. To build your own cue, read the new `useNavigation()` hook: `state` is `'loading'` while the router keeps the current page on screen for a target whose page module or loaders are not ready, and `pendingPathname` names that target; a server render is always `'idle'`. For a reconnecting api use `useConnectionStatus(apiName)` from `@voltro/client`. `voltro update` lists every file that still imports `NavigationIndicator`.
|
|
152
|
+
- **DB_MAX_CONNECTIONS is what the whole process holds, not the size of each pool** — `@voltro/cli`, `@voltro/workflow`, `@voltro/database`
|
|
153
|
+
|
|
154
|
+
Every framework user of the database opened its own pool: the store, the plugin bind context, the workflow runtime, aggregates, analytics, and the boot steps that migrate or check the schema. Each was sized by `DB_MAX_CONNECTIONS`, so one process could hold several times the number its boot line printed. Measured on postgres at `DB_MAX_CONNECTIONS=3`: the line promised 4 and the database saw 8 connections from `voltro dev` and from `voltro serve`. Two pods budgeted at 5 against a pooler of 15 both failed to acquire in the same second.
|
|
155
|
+
|
|
156
|
+
The pools were built by dialect-independent code, and the repair is measured the same way on every dialect: under boot and a burst of concurrent requests, postgres, mariadb, mysql and mssql each hold exactly what the line prints.
|
|
157
|
+
|
|
158
|
+
In `voltro dev` and `voltro serve` they now share one pool per database target, so the boot line's number is what the process holds. Short-lived commands such as `voltro data restore` keep their own pool, because they close it to get fresh sessions. A value tuned per pool buys fewer connections in total. Read replicas, a separate migration URL (`DB_DIRECT_URL` / `DB_MIGRATE_URL`), a provisioned tenant's database and pools your own code builds stay separate.
|
|
159
|
+
|
|
160
|
+
The workflow runtime needs 3 connections at once (2 with `VOLTRO_WORKFLOW_RUNNER_STORAGE=memory`). Below that the boot now refuses and names the number; it used to wait out the acquire timeout and exit on `Failed to acquire connection`. `sqlPoolCapacity` is exported from `@voltro/database/sql` beside `registerSqlPoolCapacity`, an addition.
|
|
161
|
+
|
|
162
|
+
**`voltro update` carries you across this** — codemod `0.80.0/01_db-max-connections-is-the-process-budget`. 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.80.0).
|
|
163
|
+
|
|
164
|
+
### Added
|
|
165
|
+
|
|
166
|
+
- **security.clientAddressHeader reads a proxy's single-address header** — `@voltro/runtime`, `@voltro/cli`
|
|
167
|
+
|
|
168
|
+
`security.clientAddressHeader` (env `VOLTRO_CLIENT_ADDRESS_HEADER`) names a header a trusted proxy sets to the client's address, such as `cf-connecting-ip` or `x-real-ip`. It is believed only from a peer covered by `security.trustedProxies` and only when it holds one valid IP; otherwise `x-forwarded-for` applies as before. Naming a header without declaring a trusted proxy refuses the boot.
|
|
169
|
+
- **Rate-limit rules can key on the caller's address** — `@voltro/plugin-ratelimit`, `@voltro/protocol`, `@voltro/runtime`, `@voltro/cli`
|
|
170
|
+
|
|
171
|
+
A rule can now use `by: 'ip'`, one bucket per client address. Before sign-in every caller is the same anonymous subject, so `'subject'` put them all in one bucket and one client could use up a sign-in or invite endpoint for everyone. The address is the one the listener resolves through `security.trustedProxies`, and plugin interceptors receive it as `RpcInterceptorContext.remoteAddr` on WebSocket, `POST /rpc` and `publicApi` REST calls; REST handlers get it as `RestRouteContext.remoteAddr`. A client cannot set it. Calls that do not arrive over the network share one `unknown` bucket. `KeyBy` gains the `'ip'` member: every value that type-checked before still does; only code that exhaustively switches over `KeyBy` needs the new case.
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- **Server-side rpc calls carry the visitor's address** — `@voltro/cli`
|
|
176
|
+
|
|
177
|
+
A loader's `ctx.query`, a page's `preload`, and the no-JavaScript form post now send `x-forwarded-for` to the api: the chain the request arrived with plus the address that delivered it to the web server. An api whose `security.trustedProxies` covers the web tier therefore rate-limits and audits these calls per visitor instead of counting every visitor against the web server. An api that trusts no proxy ignores the header, so its behaviour is unchanged. A visitor-written `x-forwarded-for` is never forwarded on its own.
|
|
178
|
+
|
|
179
|
+
### Fixed
|
|
180
|
+
|
|
181
|
+
- **The boot's database connection line names the dialect in use** — `@voltro/cli`
|
|
182
|
+
|
|
183
|
+
A database configured with `DB_HOST` and friends was logged as `db connection: postgres://…` whatever the dialect, so a SQL Server or MySQL connection read as a postgres one. The line now starts with the dialect's name.
|
|
184
|
+
- **The dashboard's live streams share one browser connection** — `@voltro/cli`
|
|
185
|
+
|
|
186
|
+
The DevTools dashboard opened one event-stream connection per app. Over HTTP/1.1 a browser holds at most six connections per origin across all tabs, so with six apps — or three in two tabs — the logs and overview pages held every connection the dashboard had, and each later request waited indefinitely: a click on another page changed the URL, kept the old page on screen and left the navigation indicator spinning. Every stream a page holds now travels on one connection: the page posts its targets, each with its own app bearer, to `/api/dashboard/proxy/streams`, and `voltro dev` and `voltro start` fan the apps in behind it, each reconnecting on its own backoff. `/api/dashboard/proxy/stream` is removed; `@voltro/dashboard` of the same release uses the new route.
|
|
187
|
+
- **`meta` keeps the rendered language after hydration without a locale cookie** — `@voltro/web`, `@voltro/cli`
|
|
188
|
+
|
|
189
|
+
In the browser, `meta` received the `voltro:locale` cookie's locale and otherwise a fixed `'en'`. A first visit carries no cookie, so the server rendered the title in the language it negotiated (`Accept-Language`, else `defaultLocale`), and after hydration the tab switched to English. `meta` now falls back to the locale the server rendered the document with, which it stamps on `<html lang>`, and the cookie still wins when a language switch has written one. A page `voltro serve` answers with the unrendered shell (a page the prerender did not produce) declared `lang="en"` in every app, and the client took that for the server's choice. The shell now declares the app's `defaultLocale`, or its first declared locale.
|
|
190
|
+
- **The first `voltro dev` on a fresh SQL Server database boots with CDC on** — `@voltro/sql-mssql`
|
|
191
|
+
|
|
192
|
+
With `changeStrategy: 'cdc'`, the default, the Change Tracking reader started before auto-migrate created the schema and enabled tracking on tables that did not exist yet. The boot died on `Failed to execute statement`, so every new project on SQL Server failed its first `voltro dev`. The same database booted once with `CDC=0` worked afterwards.
|
|
193
|
+
|
|
194
|
+
The reader now enables tracking on the tables that exist and picks up the rest on the poll that first finds them, reading each from its own tracking floor. A table the migration creates during the boot is tracked from then on. `ensureChangeTracking` resolves to the tables it could not enable yet.
|
|
195
|
+
- **Replica observations of ended processes are deleted after a day** — `@voltro/cli`, `@voltro/runtime`, `@voltro/database`
|
|
196
|
+
|
|
197
|
+
`_voltro_replica_observations` had no retention. A replica deletes its own rows on a graceful shutdown, but a process that crashed or was killed left them behind, and a replica ID that does not survive a restart — `dev-<pid>` under `voltro dev`, a pod replaced under a new name — never overwrites them. Every such row stayed listed as `departed` in each fleet read indefinitely. The retention sweep now deletes rows not rewritten for 24 hours on both boot paths and every dialect; live replicas rewrite theirs every 30 seconds. `VOLTRO_REPLICA_OBSERVATIONS_TTL_HOURS` changes the window, with a floor of one hour, and the boot line lists the policy with the others.
|
|
198
|
+
- **A live stream for an ended replica is refused instead of reconnecting forever** — `@voltro/cli`, `@voltro/devtools-ui`
|
|
199
|
+
|
|
200
|
+
`/_voltro/inspect/stream?replica=<id>` now resolves the named replica and verifies its handshake before it answers, with the same refusals as a read: `404` for an unknown ID or one without a reachable address (no request leaves the process), `504` for a peer that does not accept the connection, `502` for a peer that answers with another process's feed. Previously the API answered `200 text/event-stream` first and found the missing peer inside the body, so a dashboard still naming a process from before a restart received a stream that closed at once, reconnected on backoff indefinitely, and left a `Fiber terminated with an unhandled error` in the target's log on every attempt. The replica selector in the local and Cloud dashboards now also removes a selected replica once the fleet no longer lists it — the process ended and its ID never answers again — names the removed IDs, and returns an emptied selection to All replicas. A replica that is only unavailable (stale, without a reachable address, or expected by the fleet without an answer) stays selected.
|
|
201
|
+
- **A controlled workflow start without an acknowledgement returns its receipt** — `@voltro/runtime`, `@voltro/voltro`, `@voltro/workflow`, `@voltro/cli`, `@voltro/protocol`
|
|
202
|
+
|
|
203
|
+
When the engine dispatch of a start with flow controls failed without a definite answer, such as an exhausted database pool or an acknowledgement lost after the engine took the run, `start()` threw `Submission … remains unacknowledged`. The submission stayed durable and recovery retried it, so a caller that reported the failure could see the run happen anyway.
|
|
204
|
+
|
|
205
|
+
`start()` now returns `status: 'queued'` with `deferral.mode: 'unacknowledged'`, and `handle.id` is the receipt recovery binds to the resulting run. `start(…, { wait: true })` and `run(…)` follow it. A definite rejection still throws.
|
|
206
|
+
|
|
207
|
+
`WorkflowAdmissionOutcome`'s `accept` may now resolve to a `WorkflowQueuedAdmission` as well as an execution ID. Custom admission adapters only produce that outcome; the framework's facade is what calls `accept`, so adapter code keeps compiling. `isUnacknowledgedSubmission` is exported from `@voltro/workflow`.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
42
211
|
## [0.79.1] — 2026-09-24
|
|
43
212
|
|
|
44
213
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-sso-saml",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.0",
|
|
4
4
|
"description": "Enterprise SAML 2.0 SSO — SP-initiated login, ACS assertion consumer, and SP metadata. Signature verification via @node-saml/node-saml (optional, lazy); the framework integration (routes, session minting, attribute mapping) is built in.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@effect/platform": "^0.97.2",
|
|
37
|
-
"@voltro/database": "0.
|
|
38
|
-
"@voltro/protocol": "0.
|
|
37
|
+
"@voltro/database": "0.81.0",
|
|
38
|
+
"@voltro/protocol": "0.81.0"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
41
|
"@node-saml/node-saml": "^5.1.0"
|