@voltro/ui-shadcn 0.67.0 → 0.69.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 +437 -0
- package/THIRD-PARTY-NOTICES.md +1 -113
- package/dist/index.d.ts +36 -3
- package/dist/index.js +200 -140
- package/dist/signal.css +105 -2
- package/dist/tokens.css +9 -0
- package/package.json +2 -2
- package/src/compositions/signalWorkspace.tsx +63 -0
- package/src/index.ts +1 -0
- package/src/primitives/checkbox.tsx +2 -2
- package/src/primitives/dialog.tsx +2 -2
- package/src/signal.css +105 -2
- package/src/tokens.css +9 -0
- package/src/widgets.tsx +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,443 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.69.0] — 2026-09-10
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/runtime** — A typed store error now reaches the caller as itself instead of being re-wrapped as `StoreOperationFailed`.
|
|
47
|
+
|
|
48
|
+
The Effect store's catch boundary passed two of the six `StoreError` variants through by name and wrapped everything else. The wrapper builds `cause` with `wireSafeCause`, which reads DRIVER fields — and a typed error carries none, so it landed on its own class name. A consumer's postgres answered `23503` with `ai_generation_requests_createdBy_fkey`; the store classified it into a `ConstraintViolation` carrying that name, and the client received `StoreOperationFailed { cause: "ConstraintViolation" }`. The constraint name was extracted correctly and then discarded by re-wrapping — the half that turns a half-hour search into a one-line diagnosis, and the half `cause` promises in its own doc comment.
|
|
49
|
+
|
|
50
|
+
`ConstraintViolation`, `TableValidationFailed` and `ServerOnlyColumnWrite` now arrive typed, with their constraint, issues and columns intact. The rule is over the SET — every `StoreError` variant — because naming variants individually is what produced this.
|
|
51
|
+
|
|
52
|
+
**Check your descriptors' `error:` unions.** A handler that declared only `StoreOperationFailed` to catch constraint failures will now see a `ConstraintViolation` it does not declare, which reaches the client as an untagged internal error instead. Declare the variant you actually want to catch, or `StoreError` for all of them.
|
|
53
|
+
|
|
54
|
+
**`voltro update` carries you across this** — codemod `0.69.0/06_typed-store-errors-are-not-rewrapped`. 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.69.0).
|
|
55
|
+
- **@voltro/runtime, @voltro/voltro** — Issuing an API key now plants its `actors` row, and `ApiKeyStore` gained a required `ensureActor`.
|
|
56
|
+
|
|
57
|
+
`actors.kind` is declared by the framework as `'user' | 'serviceAccount' | 'apiKey' | 'system'` and `audit()` makes every `createdBy` a foreign key onto that table — so an API key is explicitly an audit subject. `issue()` wrote only `_voltro_api_keys`, so the row never existed: measured in a consumer's database, `actors with kind='apiKey'` came back 0, and the first audit-stamped insert by a key-authenticated caller died on the foreign key. `apiKeys:` and `audit()` are both on by default and were not combinable.
|
|
58
|
+
|
|
59
|
+
The row is planted BEFORE the key row, so a failure leaves no usable key rather than one that authenticates and then dies on its first audited write. Its id equals the key id, which is what `apiKeyStrategy` presents as `subject.id` — the `actors.id == subject.id` convention the core table documents. Revoking leaves the row: it is the author of everything that key wrote.
|
|
60
|
+
|
|
61
|
+
`@voltro/voltro` is named too because it re-exports the runtime surface, so the break reaches an app importing from the aggregate exactly as it reaches one importing from `@voltro/runtime`. (`memoryApiKeyStore`'s return type also widened to expose the planted rows for tests — an intersection, still assignable to `ApiKeyStore`, so that half breaks nobody.)
|
|
62
|
+
|
|
63
|
+
`ensureActor` is REQUIRED rather than optional. An optional hook would be the same defect with a nicer name — the mistake was omission, and an optional field is omissible. Both framework stores implement it; only an app with its own `ApiKeyStore` has anything to do.
|
|
64
|
+
|
|
65
|
+
**`voltro update` carries you across this** — codemod `0.69.0/05_api-key-store-plants-its-actor`. 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.69.0).
|
|
66
|
+
- **@voltro/ai, @voltro/plugin-billing, @voltro/plugin-comments** — `ai`, `@ai-sdk/provider`, `@ai-sdk/gateway` and `stripe` are peer dependencies now — and `@voltro/plugin-comments` stops shipping its own `effect`.
|
|
67
|
+
|
|
68
|
+
`@voltro/ai` declared `ai: ^7.0.92` under `dependencies`. An app that configures a provider directly declares `ai` itself, and one that pinned it below our floor got a SECOND physical copy — with it a second `@ai-sdk/provider-utils`. Those types are branded with a symbol, so two structurally identical copies are not assignable to each other:
|
|
69
|
+
|
|
70
|
+
Argument of type '…provider-utils@5.0.39/…ToolSet | undefined' is not assignable to parameter of type '…provider-utils@5.0.33/…ToolSet | undefined'
|
|
71
|
+
|
|
72
|
+
Nothing fails at install and nothing fails at runtime. It surfaces as a typecheck error, only at the seams where an SDK type crosses the package boundary — which for `@voltro/ai` is thirteen types in its own api golden, `ToolSet` among them. `effect` has been a peer from the start for exactly this argument; it had never been applied to the SDKs whose types cross the same line.
|
|
73
|
+
|
|
74
|
+
**The criterion is narrower than "the type crosses the boundary", and the difference is what keeps this from becoming churn.** Counted over the repo, 21 packages have a third-party dependency whose types appear in their api golden. A second copy needs a second DECLARER, so the question is whether the APP is expected to declare the package too. It is for `effect` (every handler imports it), for `ai` / `@ai-sdk/*` (you declare them to configure a provider) and for `stripe` (`normalizeStripeEvent` takes an event built by the app's own SDK instance). It is not for `@effect/sql`, the dialect drivers, `ioredis`, `@opentelemetry/*` or `@radix-ui/*` — one range is declared in one place and one copy resolves.
|
|
75
|
+
|
|
76
|
+
`scripts/check-boundary-peers.mjs` (CI + `pnpm check:boundary-peers`) asserts that rule and PRINTS the judged-invisible set on every run, because a check that has quietly narrowed reads exactly like a clean one. It ships a `--selftest` that re-introduces `ai` as a dependency and requires the rule to fire.
|
|
77
|
+
|
|
78
|
+
`@voltro/plugin-comments` was the one package declaring `effect` under `dependencies` while every other peers it — the two-copies rule the repo already holds absolutely, with a single unapplied exception.
|
|
79
|
+
|
|
80
|
+
Most apps do nothing: the installer resolves a peer it can satisfy. An install that now warns about an unmet peer is the change working — it is saying that your pin and ours do not intersect, which is what silently produced the second copy before.
|
|
81
|
+
|
|
82
|
+
**`voltro update` carries you across this** — codemod `0.69.0/03_sdk-peers-are-declared-by-the-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.69.0).
|
|
83
|
+
- **@voltro/plugin-storage** — The self-service upload routes refuse a caller with no identity, unless the app declares `storagePlugin({ anonymousUploads: true })`.
|
|
84
|
+
|
|
85
|
+
Those routes are `openAccess`, and each justified itself in source with "the ticket binds the calling subject + tenant, caps size, and the finalize path runs limits + scan". Every clause after the first depends on there BEING a calling subject, and `openAccess` does not establish one — an unauthenticated request resolves to an anonymous subject carrying `id: null`. So a visitor could mint a ticket with `sub: null, tenant: null`, and the object it finalized belonged to no owner and sat in no tenant: unreachable by every later owner check, tenant filter and quota, with a subject binding that compares `null` against `null` and therefore passes for every subsequent caller.
|
|
86
|
+
|
|
87
|
+
The framework already refuses exactly that comparison one level up — `defineMutation` rejects `requiresApproval` beside `openAccess` because "two anonymous callers both compare as `null`". Same shape, one layer down.
|
|
88
|
+
|
|
89
|
+
Whether a caller must be identified is not a wire question and the descriptor cannot answer it: only the app knows whether it wants a public intake. So the WIRE stays open — `openAccess` is unchanged and still means "anyone who can reach this may call it" — and the identity requirement lives in the plugin, where the app's own answer is in scope. `anonymousUploads` defaults to false; an app that turns it on is choosing ownerless objects deliberately, and should pair it with `limits` and a `scan`.
|
|
90
|
+
|
|
91
|
+
It is fixed over the SET rather than at the route that shows it best. Five handlers took the subject straight off the request — the ticket mint, the presigned mint, the resumable begin, the multipart begin and the direct upload — and `ctx.request.subject.id` reads as locally correct at every one of them; a sixth is one paste away. There is one resolver now, `ingestUrl` is routed through it too so write attribution has a single spelling in the file, and a test asserts over the source that no `sub:` or `ownerId:` takes its value from the request.
|
|
92
|
+
|
|
93
|
+
The finalize and multipart-completion comparisons said "the token is bound to the minting subject; a different caller can't finalize it", which is false for a ticket that was minted anonymously. That is one named function now, and it says what the binding actually is in that case: possession of the signed, short-lived token, and nothing else.
|
|
94
|
+
|
|
95
|
+
**`voltro update` carries you across this** — codemod `0.69.0/02_anonymous-uploads-must-be-declared`. 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.69.0).
|
|
96
|
+
- **@voltro/database** — `releaseMigrationLock` is gone; `acquireMigrationLock` returns the release.
|
|
97
|
+
|
|
98
|
+
Because the MySQL migration lock was being released while the migration was still running.
|
|
99
|
+
|
|
100
|
+
Every dialect but sqlite implements it as a SESSION-scoped object — `GET_LOCK`, `pg_advisory_lock`, `sp_getapplock @LockOwner = 'Session'` — and all three die with the connection that took them. Taken through the pooled client, that stated an intent the plumbing did not keep. Measured on MySQL 8, with a second connection watching both the lock and the process list:
|
|
101
|
+
|
|
102
|
+
direkt nach acquire gehalten von conn 131 +500ms lock=FREI conn 131 lebt=false
|
|
103
|
+
|
|
104
|
+
The holder was gone inside half a second — closed, not returned to the pool — so the server dropped the lock and two `voltro db apply` runs against one server could both proceed past the thing that exists to stop exactly that. Postgres survived the same window, which is a driver's idle policy rather than a property: `@effect/sql-pg` keeps its connections and `@effect/sql-mysql2` does not. The lock now RESERVES its connection for as long as it is held, on every session-scoped dialect. sqlite is exempt and must stay exempt — its lock is a no-op and reserving its single connection would starve the work.
|
|
105
|
+
|
|
106
|
+
`acquireMigrationLock` returns the release instead of there being a free `releaseMigrationLock(sql, scope)`. A free release cannot know which connection took the lock, and on all three dialects a release on the wrong connection is a no-op that reports success — which is what it had been doing.
|
|
107
|
+
|
|
108
|
+
**Fixing that alone would have wedged every migration after a failed one**, and that is the more important half. `applyPlan` and the file-based runner released the lock from a `try/finally` inside `Effect.gen`, and a `finally` there does NOT run when a yielded effect FAILS: Effect short-circuits without resuming the generator. Measured — a deliberately failing operation reached neither the finalizer nor the release. The hole is as old as those functions and was invisible for as long as the release did nothing and the lock dissolved on its own. Two defects cancelling reads exactly like neither existing. Every acquisition is bracketed with `Effect.acquireUseRelease` now, which also covers defects and interruption, and a guard asserts it over the SET of call sites rather than the one that was found — three had the same shape, and the shape looks correct.
|
|
109
|
+
|
|
110
|
+
The fakes gained the `reserve` a `SqlClient` always owed, from one shared definition rather than six copies. It suspends: a real connection method BUILDS an effect rather than running the statement, and a fake that records on call instead of on run reports `RELEASE_LOCK` before the DDL it follows — a fake inventing an ordering violation, which reads exactly like a real one.
|
|
111
|
+
|
|
112
|
+
**`voltro update` carries you across this** — codemod `0.69.0/01_migration-lock-returns-its-release`. 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.69.0).
|
|
113
|
+
- **@voltro/protocol** — `workflowToRpc` preserves the literal workflow name and the payload schema, so a workflow no longer widens the generated client.
|
|
114
|
+
|
|
115
|
+
It was typed `Rpc.Rpc<string, Schema.Schema.Any, …>`. A by-name lookup in an rpc group matches members by tag and every tag is assignable to `string`, so that one member joined the lookup for every OTHER procedure: a single workflow erased typed access to the entire group.
|
|
116
|
+
|
|
117
|
+
Filed BREAKING under this file's own "more precise is still BREAKING" rule — nothing was removed, the client simply gained the type information it should always have had, and that is strictly more ways to fail `tsc`. Apps without a workflow are unaffected.
|
|
118
|
+
|
|
119
|
+
Migration: run `voltro codegen` (or boot `voltro dev`), then typecheck. The new errors are real disagreements — a payload that never matched its schema and failed at runtime instead, a now-redundant cast, or a cast that refuses because the two shapes genuinely differ. Do not silence any of them with `as unknown as`; that restores exactly the blindness this removes.
|
|
120
|
+
|
|
121
|
+
**`voltro update` carries you across this** — codemod `0.69.0/04_workflow-rpc-types-are-precise`. 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.69.0).
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- **@voltro/database, @voltro/runtime** — `upsertRowOutcome(store, table, row, options)` — the upsert, plus whether it INSERTED or UPDATED.
|
|
126
|
+
|
|
127
|
+
`upsert` returns the row and not what happened to it, so an app that has to report "created" reads first and then upserts. Measured on a real ingest: one extra query per row, purely to set a flag — and a window, because another writer between the read and the write makes the answer wrong.
|
|
128
|
+
|
|
129
|
+
It costs nothing to answer. The store wrapper mints the row's `id` before the statement, and on the conflict path every dialect hands back the EXISTING row's id — a fact `upsertBinding`'s header already relied on for the encrypted-column binding. Two ids that differ ARE the answer. No dialect implements anything: the outcome is decided from what the wrapper already had in hand.
|
|
130
|
+
|
|
131
|
+
**`'inserted'` is never a stand-in for "could not tell",** and that is the half worth reading. The free comparison needs an id the wrapper minted — a caller-supplied id is unchanged on both paths, and a database-assigned one does not exist yet — so in those two cases the destination is READ first and the answer stays exact. Only a call that asked for the outcome pays for it; plain `upsert` is byte-for-byte the call it was.
|
|
132
|
+
|
|
133
|
+
That required separating two facts `upsertDestinationId` had collapsed. It returns the OFFERED id both when nothing conflicts and when the conflicting row's id happens to BE the offered one — identical answers for binding a ciphertext, opposite answers for "what did this do". `upsertConflictId` reports existence; the destination is derived from it.
|
|
134
|
+
|
|
135
|
+
The helper is `async` so its refusal (a hand-written `DataStore` without the method) arrives as a rejection rather than a synchronous throw: a function whose type says `Promise` and which sometimes throws before returning one splits every caller's error handling in two.
|
|
136
|
+
|
|
137
|
+
**No `upsertMany`.** The batch form was asked for alongside this, and the reported batches are ~30 rows — at that size the round trips are not the cost, the missing `created` flag was, and it is now free. A conflict-write batch still belongs in ONE `store.transactional` with a capped input, which is what the `bulk-write-in-loop` advice says.
|
|
138
|
+
- **@voltro/cli** — `VOLTRO_DASHBOARD=on` — the counterpart `off` never had.
|
|
139
|
+
|
|
140
|
+
The launcher skips the dashboard when stdout is not a TTY, which is a good stand-in for "nobody is watching": CI, a smoke run, a piped script. It stops being one the moment a task runner is in the way. `turbo run dev` — how a multi-app project is normally brought up — captures each task's output to prefix it (24 731 prefixed lines in one gate log), so `isTTY` is false for a developer sitting right in front of it, and the dashboard could not be turned on at all.
|
|
141
|
+
|
|
142
|
+
Measured on the same non-TTY boot, before and after:
|
|
143
|
+
|
|
144
|
+
without dashboard not launched: stdout is not a TTY (CI/smoke) with auto-launching dashboard at http://localhost:5179
|
|
145
|
+
|
|
146
|
+
It overrides only the guess. The recursion guard, the launch lock and the port-in-use check are measurements rather than proxies and still apply — so two apps started together still share one dashboard, and the second reports `already-running` instead of racing for the port.
|
|
147
|
+
|
|
148
|
+
The skip reason names the variable, because a guess you cannot overrule is a wall.
|
|
149
|
+
- **@voltro/devtools-ui** — Show automatic data synchronization as a connection status, highlight changed cells and new visible rows, and briefly retain confirmed deleted rows in red. Query navigation stays quiet, active editors retain focus, and reduced motion uses a static tint. Local and Cloud providers carry CDC deletion identities.
|
|
150
|
+
- **@voltro/ui-shadcn, @voltro/devtools-ui** — Cloud and local Devtools now share `SignalWorkspace`, `SignalWorkspaceHeader` and `AppInspectorHeader`: one masthead, background, heading rhythm and app identity. Local navigation and live charts keep their full functionality while using the shared workspace styling and semantic theme colors.
|
|
151
|
+
- **@voltro/database** — `insertManyRows(store, table, rows)` — `insertRow`'s batch, typed on the element.
|
|
152
|
+
|
|
153
|
+
It forwards to `store.insertMany` unchanged, so the chunking, the dialect behaviour and the conflict semantics stay exactly what the string-keyed call already did. What it adds is the type link the string-keyed form cannot have: every element is checked against the table's insert row, so an element missing a required column, misspelling one, or passing a `number` where the column is a timestamp is a compile error at the call site rather than a dialect error somewhere inside the batch.
|
|
154
|
+
|
|
155
|
+
The batch needed its own helper rather than a loop over `insertRow` because of where the mismatches actually come from: a batch is usually MAPPED — from a parsed import, an API page — and the loop body's inferred element type is only as good as whatever produced the array. That is the shape a mapped `Date` versus `number` survives review in.
|
|
156
|
+
|
|
157
|
+
The set-based conflict forms (`insertManyIgnore`, `upsertMany`) remain unbuilt, for the reasons already recorded against them; this is a typing gap, not a conflict-handling one.
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- **@voltro/protocol** — An auth strategy that declares a `claimsBearerPrefix` is now asked BEFORE strategies that declare none, for a token carrying that prefix.
|
|
162
|
+
|
|
163
|
+
Turning on `apiKeys:` made every API key unusable as soon as a JWT strategy sat in the chain. Two reasonable pieces produced a third thing nobody chose: `apiKeys:` appends the framework key strategy LAST, and `jwtBearerStrategy` — so all six catalog strategies (supabase, auth0, clerk, kinde, oidc, workos) — claims EVERY `Authorization: Bearer`. A token it cannot verify comes back `failed`, not `skip`, and a `failed` ends the chain by design, because a credential another IdP rejected could be an attack masquerading as ours. So a valid key was refused by the JWT verifier before the key strategy was ever asked. Nothing logged an error; the key simply never worked.
|
|
164
|
+
|
|
165
|
+
The boot check could not see it: it warns when TWO strategies declare the same prefix, and the JWT strategy declares none while claiming everything.
|
|
166
|
+
|
|
167
|
+
`failed` still ends the chain — that property is untouched. What changed is who is asked first: a declared prefix is a strategy saying "this token shape is mine", which is more specific than claiming everything by declaring nothing. A token matching no declared prefix leaves the chain in exactly the order the app wrote it, so a real JWT is still claimed, and still rejected, by its own verifier.
|
|
168
|
+
- **@voltro/cli** — `voltro build` fails when the SSR bundle it just wrote cannot be imported — and `build: { ssrExternal: [...] }` is the exit that `ssr.noExternal: true` did not have.
|
|
169
|
+
|
|
170
|
+
The web SSR build bundles everything the graph reaches so a production image needs nothing from `node_modules`. A CommonJS dependency pulled into that ES module carries its `__dirname` / `__filename` along, and neither exists there. Measured against a real app: the build is GREEN, the bundle is written, and the artefact dies on its first import with `ReferenceError: __dirname is not defined in ES module scope`. The same dependency externalised: exit 0.
|
|
171
|
+
|
|
172
|
+
Two things were missing, and only fixing both helps. `noExternal: true` was hard-wired with no config evaluation beside it, so an app had no way to keep one package a runtime import — there was no app-side workaround at all. And the build reported success, so the failure arrived at deploy time as a boot crash three layers from its cause.
|
|
173
|
+
|
|
174
|
+
The externalisation is deliberately NOT automatic. Externalising on the framework's own initiative puts a package back among the image's runtime requirements; an image built on the promise that it needs nothing from `node_modules` would then fail to boot for a second reason, with nothing said about either.
|
|
175
|
+
|
|
176
|
+
**The detector reads the artefact, and it was wrong once before it was right.** Run over the 357 built ESM files in this repo it flagged exactly one — a false positive, in `@voltro/cli`'s own bundle, on the line
|
|
177
|
+
|
|
178
|
+
"import { join as __join, dirname as __dirname } from 'node:path'"
|
|
179
|
+
|
|
180
|
+
which is a string the build WRITES into a generated file. That is the same trap this repo already recorded once, for a different rule in the same package: a regex cannot tell a specifier from a specifier-shaped string. String literals are blanked before the scan and an import alias counts as a definition.
|
|
181
|
+
|
|
182
|
+
The correction had a second edge. A parameter arm added for the CommonJS wrapper (`function (…, __dirname)`) read `join(__dirname, 'assets')` as a declaration — a call argument and a parameter are the same characters — and silenced the exact reference the check exists for. It is gone; no shim of that shape appears in any of the 357 files, so it bought nothing and cost the rule. Both directions are asserted, and the false positive is kept as a fixture.
|
|
183
|
+
- **@voltro/devtools-ui, @voltro/ui-shadcn** — Keep the dashboard masthead, tabs and inspector context compact so working tables start higher in the viewport. Group the app return link, identity, address and status in one responsive header shared by local and Cloud inspectors.
|
|
184
|
+
- **@voltro/devtools-ui, @voltro/ui-shadcn** — Use the shared styled Checkbox for dashboard filters, workflow confirmations, inspect access and schema-driven checkbox forms, matching Cloud configuration controls while preserving native label, keyboard and form behavior. Checked and indeterminate fills now retain the primary color in dark mode.
|
|
185
|
+
- **@voltro/cli** — The auto-launched dashboard died 85 ms after it was announced, silently.
|
|
186
|
+
|
|
187
|
+
`bin/voltro.mjs` is the only entry that registers the tsx ESM loader, and the CLI cannot run without it — it imports the app's own `app.config.ts` at runtime. The launcher derived the child's bin from `process.argv[1]` and handled ONE spelling: `<cli>/src/bin.ts` → `<cli>/bin/voltro.mjs`. Everything else fell through to argv[1] itself.
|
|
188
|
+
|
|
189
|
+
But `bin/voltro.mjs` re-execs, so inside the CLI argv[1] is the entry BEHIND it. Measured on a normal boot:
|
|
190
|
+
|
|
191
|
+
argv[1] = …/packages/cli/dist/bin.js bin = …/packages/cli/dist/bin.js
|
|
192
|
+
|
|
193
|
+
That is what every installed user runs, and every maintainer tree that has been built once. The child was handed the bundled entry with no loader and died on its first TypeScript import:
|
|
194
|
+
|
|
195
|
+
ERR_MODULE_NOT_FOUND: Cannot find module '…/packages/logger/src/logger' imported from '…/packages/logger/src/index.ts'
|
|
196
|
+
|
|
197
|
+
Nothing said so. The child is spawned with `stdio: 'ignore'` and nothing watched its exit, so the last word was the launcher's own `dashboard launched at http://localhost:5179` — a URL that answered nothing. The `/src/bin.ts` branch is why this was invisible from inside the framework repo: running unbuilt from source is the one case that worked.
|
|
198
|
+
|
|
199
|
+
The bin is resolved by walking up from the launcher's own module until `bin/voltro.mjs` is found, so `src/` and `dist/` reach the same answer — the point being that it must not depend on which of the two is running. Verified end to end from a `voltro dev` boot: `dashboard launched`, then HTTP 200 with the dashboard's own title.
|
|
200
|
+
- **@voltro/cli** — The recommended dashboard install line contradicted the stability contract.
|
|
201
|
+
|
|
202
|
+
`pnpm add -D @voltro/dashboard` writes a caret range. The contract at the top of `CHANGELOG.md` tells users to pin exact versions and never `^`, and a project that follows it ends up with one dependency spelled differently from all the others — because we told it to. The message and both docs pages now say `--save-exact`.
|
|
203
|
+
- **@voltro/cli** — An interrupted dashboard build made the launcher choose the one mode that cannot run in it — and then said nothing when the child died.
|
|
204
|
+
|
|
205
|
+
The mode was chosen by `existsSync(.framework/dist/index.html)`. A build writes that file EARLY and writes `dist/server/ssrEntry.js` LATE, so a build that dies in between — the prerender step is the one that does — leaves a tree that looks built. Measured on exactly such a tree:
|
|
206
|
+
|
|
207
|
+
serving the Voltro dashboard on :5199 … mode: start fatal: production `voltro start` requires a precompiled SSR bundle at …/.framework/dist/server/ssrEntry.js but it is missing
|
|
208
|
+
|
|
209
|
+
Keyed on the bundle instead, the same tree falls back to `dev`: vite ready in 169 ms, the port answers.
|
|
210
|
+
|
|
211
|
+
There were TWO deciders, spelled differently — the auto-launcher and the standalone `voltro dashboard` — and both were wrong the same way. One `dashboardIsPrebuilt` now, asserted over the set, because a third is one paste away.
|
|
212
|
+
|
|
213
|
+
**And the death was silent.** The launcher spawns with `stdio: 'ignore'`, which throws the child's own diagnosis away, and had no `exit` or `error` handler at all. So the last word on a dead dashboard was the parent's own `auto-launching dashboard at http://localhost:…` — a promise that was already false by the time anyone read it. A non-zero exit is reported now, and a signal stays quiet because that is how Ctrl-C reaches the whole process group.
|
|
214
|
+
- **@voltro/devtools-ui** — Rework the shared data explorer around compact resizable columns, debounced search, explicit filter application, accessible record editing and searchable references. Preserve database defaults when creating rows, distinguish empty text from null, retain invalid drafts and write failures, confirm destructive actions and unsaved changes, and expose an optional host refresh callback for current snapshots.
|
|
215
|
+
- **@voltro/devtools-ui** — Resize the data explorer's table list independently, read complete table names in delayed themed tooltips, and resize columns with visible pointer/keyboard handles. Double-click a divider to restore its default width.
|
|
216
|
+
- **@voltro/cli** — Resolve local dashboard stream credentials for the inspected app in both dev and production, matching snapshot reads, and flush the development stream handshake immediately so live data updates can connect without repeated authorization failures.
|
|
217
|
+
- **@voltro/devtools-ui** — Make inspector tasks directly discoverable through a grouped sidebar, replace flat route cards with a shared expandable route explorer, and keep Data explorer visible for frontend apps through an API data-source picker. Bound the mobile table list and support keyboard activation of editable cells and foreign-key navigation.
|
|
218
|
+
- **@voltro/client** — A `title` annotation on a schema field now becomes the field's label — where the title is one somebody wrote.
|
|
219
|
+
|
|
220
|
+
`schemaToFields` / `schemaToColumns` derived every label from the humanised property name and ignored `title` entirely, so an app that had already annotated its schema (for OpenAPI output, say) had to say the same thing twice through `formField({ label })`, with the two free to drift.
|
|
221
|
+
|
|
222
|
+
`title` could not simply be read, and the reason is worth stating because the obvious rule is wrong. effect writes its OWN titles into that field, and it writes them INLINE on the property node — not only into a `$defs` entry behind a `$ref`. Measured:
|
|
223
|
+
|
|
224
|
+
Schema.NonEmptyString.annotations({ description: 'Der Kundenname' }) -> { title: 'nonEmptyString', description: 'Der Kundenname', … } Schema.String.pipe(Schema.minLength(2)) -> { title: 'minLength(2)', … }
|
|
225
|
+
|
|
226
|
+
So "use the title unless it arrived through a `$ref`" puts `minLength(2)` on a form field. What separates the two is the SHAPE of the name effect generates: a lowerCamel type name, optionally carrying the filter's arguments in parens. A title matching that is declined; everything else is the author's.
|
|
227
|
+
|
|
228
|
+
Two properties keep that honest. The failure direction is safe — a declined title falls back to the humanised name, which is what every field got before, so the rule can never do worse than the old behaviour. And it is not left as a claim about a dependency: the test regenerates the corpus through a live `JSONSchema.make` and fails if any generated title stops matching, so a change in effect goes red in the suite instead of wrong on somebody's form.
|
|
229
|
+
|
|
230
|
+
`formField({ label })` still wins over both — it is the one channel effect cannot write into.
|
|
231
|
+
- **@voltro/cli** — `voltro update` no longer rewrites a package.json that does not consume the framework.
|
|
232
|
+
|
|
233
|
+
Reported after an upgrade changed `services/api/package.json` — an independent Strapi 4 backend with NO voltro dependency — from React 18 to 19. Nothing in the command's output named the file, and the app avoided it on the next round by setting versions by hand and running its own installer, which is a sidestep and not a fix.
|
|
234
|
+
|
|
235
|
+
The peer alignment already had three conditions, and all three are about the DEPENDENCY: only a peer the manifest already declares, only when its floor is below ours, only ranges both sides can parse. Every one of them was satisfied here — React was declared, 18 is below 19, both ranges parse. None of them asked whether the package was ours to edit.
|
|
236
|
+
|
|
237
|
+
It is not a new judgement, it is an unapplied one. `planWorkspaceBump` — the `@voltro/*` half of the same command — has always dropped manifests that declare no `@voltro/*` dep. The peer alignment was added later and did not inherit it. It does now: a manifest is in scope when it declares `voltro` or any `@voltro/*` in its dependencies, devDependencies or peerDependencies, and one package being out of scope does not take its siblings with it.
|
|
238
|
+
|
|
239
|
+
`consumesVoltro` is deliberately the whole test. A peer requirement comes FROM a `@voltro/*` package; a workspace member that declares none is not bound by it, however many packages it happens to share a name with.
|
|
240
|
+
- **@voltro/cli** — `input/uncapped-array` is silent on `internal: true`, and `raw-fetch` is silent on a `fetch` an SDK asked for as an option.
|
|
241
|
+
|
|
242
|
+
Both rules were reporting a set their own sentence does not describe, and both were found by an app that read every remaining finding instead of filing them.
|
|
243
|
+
|
|
244
|
+
**`input/uncapped-array` says "the caller chooses how many".** `internal: true` takes a procedure off the wire, so there is no such caller — only the app's own code calling its own procedure. Measured on one codebase: after every wire procedure was bounded, 212 findings remained and ALL 212 were internal. So the rule spent its entire remaining output on the case its premise excludes, which is how a rule stops being read. `imperative-scope` has skipped `internal: true` for the same reason; this applies the existing decision rather than making a new one. A procedure on the wire is reported exactly as before, which is the half that keeps the exemption from being a hole.
|
|
245
|
+
|
|
246
|
+
**`raw-fetch` recommends `yield* HttpClient`.** Several provider factories take a `fetch`-shaped function as an option (`createGoogleGenerativeAI({ fetch })`). Inside it the URL is the SDK's, never a caller's, so the SSRF half has nothing to bite on — and the remedy is not available either: the seam requires a plain callback, so `yield* HttpClient` names an API that cannot be used at that call site. A rule whose only remedy does not apply teaches the reader to skim the line, which is the one place a genuinely caller-supplied URL would have been named.
|
|
247
|
+
|
|
248
|
+
The seam is SUBTRACTED, not exempted: only calls inside the function that is the value of a `fetch:` property are discounted, so a file that supplies such a seam AND calls `fetch` on its own account is still reported for the second one. Without that, adding one option object would switch the rule off for a file.
|
|
249
|
+
|
|
250
|
+
### Internal (no consumer-facing effect)
|
|
251
|
+
|
|
252
|
+
- **@voltro/database** — The migration lock is released by its bracket and nowhere else.
|
|
253
|
+
|
|
254
|
+
Correcting unreleased work in this same release, so no shipped behaviour changes: `applyPlan` gained an `Effect.acquireUseRelease` bracket and KEPT the `try { … } finally { yield* held.release }` it replaced. The overlap was documented as free — "releasing twice is harmless: the second `RELEASE_LOCK` finds nothing held on that connection".
|
|
255
|
+
|
|
256
|
+
That holds for `RELEASE_LOCK` and for `pg_advisory_unlock`. It is false for the third dialect: `sp_releaseapplock` RAISES on a lock the session does not hold, so every mssql apply died on the second release with `Cannot release the application lock … because it is not currently held`. A claim about three dialects, verified on two.
|
|
257
|
+
|
|
258
|
+
Removing it is also the better release POINT — the `finally` ran before the convergence proof, handing the lock back while the work it protects was still running. The bracket holds it to the end and covers failure, defect and interruption, which a `finally` inside `Effect.gen` does not.
|
|
259
|
+
|
|
260
|
+
`migrationLockIsBracketed.test.ts` asserted that an acquisition IS bracketed; it now also asserts there is exactly ONE release. Those are different claims and only the first was being made, which is how this rode through.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## [0.68.0] — 2026-09-09
|
|
265
|
+
|
|
266
|
+
### ⚠ BREAKING
|
|
267
|
+
|
|
268
|
+
- **@voltro/devtools-ui, @voltro/cli, @voltro/plugin-queue** — The shared inspect pages now show reverse-ETL dead letters and replica handoff health, flag kill-switch history, and governance subject-graph limitations. Migration: provide `CdcOutPage.useDeadLetter`, `FlagsPage.useAudit`, and `GovernancePage.useSubjectGraph` as typed data-source hooks; preserve the complete CDC outbox/handoff response and handle `MetricRow.kind: 'plugin'`. Metrics accept both API invocation windows and web registry snapshots. Both dashboards share task navigation and add plugin inventory, data expectations, compute budgets, experiments, invariant checks, and agent-tool inventory. Production now exposes the same standing registries as development with process-scoped observation metadata. Schedules add bounded backfill with explicit confirmation and preserve HTTP 409 outcomes; hosts must supply `SchedulesPage.useBackfill`. Workflow run details support resuming from a recorded step, and the Flow view validates queued payloads and confirms intent retry/discard. Optional hooks are `WorkflowsPage.useResumeFromStep`, `useIntentValidation`, and `useOperateIntent`; both shipped hosts wire them. The shared dev/serve inspect router now reaches step resume, redrive, discard and synchronous update instead of returning 404.
|
|
269
|
+
|
|
270
|
+
The Data explorer row detail now offers last-write audit evidence through optional `DataViewerPage.useProvenance`, with separate missing-row, unattributed and unavailable states. Shared workflow controls fit narrow viewports and use theme-aware status text.
|
|
271
|
+
|
|
272
|
+
App metrics now include the full process metric registry with label/type filters and distribution details via `/_voltro/inspect/metrics/registry`. Hosts can supply `MetricsPage.useRegistry`; both dashboards do. Nonfinite histogram and summary readings remain explicit in JSON. Scope notices retain process identity even for a complete single-process answer, and row detail uses a keyboard-accessible native dialog with readable values for read-only columns.
|
|
273
|
+
|
|
274
|
+
Both dashboards add the transactional outbox queue, retained delivery attempts and a confirmed one-attempt resend with a required reason. Custom capability bags must provide `canResendOutbox`; privileged history reads use `canViewAuditLog`. The shared dev/serve routes enforce read and write credentials and omit payload/response bodies.
|
|
275
|
+
|
|
276
|
+
Mail preview now requires `Capabilities.canPreviewMail`; custom hosts must derive it from operator access and the app's inspect write credential. Mail views retain process/declaration scope, show failed reads explicitly and discard a preview that completes after its template or props changed.
|
|
277
|
+
|
|
278
|
+
Storage access writes now require `Capabilities.canManageStorage`. Derive it from operator authorization and the app's inspect write credential; read-only users can inspect grants but cannot create or revoke them. Plugin panels choose effective instances and clear previous selections' read/action state.
|
|
279
|
+
|
|
280
|
+
Queue inspection replaces `connected` with `producerSelected`: whether the selected instance owns the app-wide outbox/CDC producer slot. It is not a broker-health probe. Custom `QueueData` producers must provide `producerSelected` and remove `connected`.
|
|
281
|
+
|
|
282
|
+
Both dashboards compare remote SQL schemas through the real planner, with immediate results and explicit fetch failures. Dev and production wire the same comparison service; results are bounded to 20 per process. Custom hosts must provide `canCompareMigrationEnvironments` and return `CrossEnvDiffSnapshot` from `MigrationsPage.useCompareCrossEnv` and `InspectManifestApi.compareCrossEnv`. Cloud admins may compare with both inspect credentials; schema mutations remain owner-restricted.
|
|
283
|
+
|
|
284
|
+
Plugin snapshots include `httpRoutes` (method and path only). Custom inventory producers must provide this array. Pass `PluginsPage.appUrl` to link literal GET routes under the inspected app, including configured OpenAPI and Swagger mounts. Links never contain inspect credentials.
|
|
285
|
+
|
|
286
|
+
Data row pages preserve empty-page counts, upstream observation scope and connection state. `DataRowsPage.syncState` optionally reports `live` or `reconnecting`; disconnected views refresh snapshots every 15 seconds and reconcile after reconnect. Cloud commits page rows and status atomically and invalidates credential or organization changes. Local streams send the per-app bearer as an Authorization header on every handshake; the SSE proxy no longer accepts a shared stream cookie.
|
|
287
|
+
|
|
288
|
+
Cloud inspect schemas retain upstream scope, origin, completeness and capture time, including array-shaped web metrics. `DataSource.observation` optionally carries this envelope alongside a payload; `MetricsPage` supports it. The five standing-inspection pages display the sampled process and scope rather than only their collection timestamp.
|
|
289
|
+
|
|
290
|
+
Cloud RPCs preserve observation envelopes, optional log scopes and event delivery declarations. Cluster inspection maps actual scheduler claim IDs and owners. Local reads serialize refreshes and clear app/credential-stale snapshots; fleet responses require assembly evidence and retain departed replicas. Web metric arrays use the same JSON-safe nonfinite readings as the registry; custom hosts must accept `MetricReading` in metric values and histogram boundaries. Cloud and local operating surfaces use compact headings, responsive log/cluster rows and unobstructed mobile controls.
|
|
291
|
+
|
|
292
|
+
The inspector combines snapshot refresh and access guidance in a compact toolbar; expand the read-only notice to configure credentials. Log filters retain associated visible labels after entry. Cache hit rate stays unmeasured until the first request, while hit/miss counters remain zero. Cluster warnings reuse the shared callout icon.
|
|
293
|
+
|
|
294
|
+
**`voltro update` carries you across this** — codemod `0.68.0/01_dashboard-inspection-evidence`. 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.68.0).
|
|
295
|
+
- **@voltro/runtime, @voltro/voltro** — Outbox resends record an independent request timestamp, so manual attempts without an actor ID remain manual. Re-arming uses a conditional store update to refuse concurrent worker claims or competing requests. Migration: custom `makeOutboxFacade` store adapters must provide `updateMany` instead of `update`; the framework DataStore already does. Boot or `db apply` reconciles the nullable `resendRequestedAt` queue column.
|
|
296
|
+
|
|
297
|
+
**`voltro update` carries you across this** — codemod `0.68.0/01_dashboard-inspection-evidence`. 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.68.0).
|
|
298
|
+
|
|
299
|
+
### Added
|
|
300
|
+
|
|
301
|
+
- **@voltro/ui-shadcn** — `Dialog` accepts a typed native dialog ref for `showModal()` and `close()`. Its props retain all `DialogHTMLAttributes<HTMLDialogElement>` and add React's native element ref; no existing attribute is removed or narrowed.
|
|
302
|
+
- Inspect declared rate-limit rules, RPC and HTTP decisions, SQL binding and fail-open observations in both dashboards, with explicit plugin-instance selection and process scope. Custom rate-limit stores can report unchecked requests using `RateLimitDecision.failOpenReason` (`unbound` or `store-error`).
|
|
303
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/client, @voltro/web, @voltro/cli** — `voltro dev` tells you when the api client a page loaded is not the one the server binds, naming the procedures that disagree.
|
|
304
|
+
|
|
305
|
+
Every other freshness mechanism here predicts staleness from source bytes: hash what the browser's client is built from, re-bundle when it moves. Those have twice hashed a set smaller than what the browser actually loads, and the symptom each time was silence — a client decoding against an older schema drops the fields it does not know, and an unknown field is not a decode error, it is simply absent. This asks the question from the other end. The server publishes per-rpc-tag digests of the schemas it bound, the client computes the same over the group it loaded, and a disagreement is one console warning plus a `runtime.surface-mismatch` diagnostic. A gap in an import walk, an optimizer that did not re-run, a service worker and a stale cache all look the same from there: different schemas.
|
|
306
|
+
|
|
307
|
+
The digest is a STRUCTURAL walk of the schema AST, and the obvious implementation would have been useless. `String(schema.ast)` on a `Schema.TaggedError` renders the declared name, so two fields added to a typed error leave it identical — a check built on it would have passed cleanly through the incident it exists to catch. Tests pin that a field added to a tagged error, a tagged class, a nested struct, a union, a tuple and a record each move the digest, so an Effect version that breaks the walk fails loudly rather than going quiet.
|
|
308
|
+
|
|
309
|
+
Two limits stated rather than hidden. A recursive schema (`Schema.suspend`) renders opaque: following it would not terminate, so a field added inside one is invisible, though everything around it still counts. And a procedure only the CLIENT has is never reported — a browser group legitimately omits server-only routes, and a check that cries wolf about the normal case is one people switch off.
|
|
310
|
+
|
|
311
|
+
Dev only, by an asymmetry that is deliberate: `voltro serve` ships a built bundle where this staleness cannot arise from a dep optimizer, and a rolling deploy would make a transient mismatch normal.
|
|
312
|
+
|
|
313
|
+
`apiSurface: compatible` — the only replaced golden lines in `@voltro/client` and `@voltro/web` are one member added to the `ClientDiagnosticKind` union (`runtime.surface-mismatch`). Every existing member is unchanged, the framework is the only producer, and a reader that does not know the new kind sees one more event it can ignore. Only a switch exhaustive over the vocabulary needs an arm. `@voltro/web` is listed because it re-exports that type and its golden carries the same line.
|
|
314
|
+
- **@voltro/ui-shadcn** — Shared Signal opener classes unify responsive headlines, sublines and CTA rows across the Framework landing, Cloud landing and Docs index, including secondary-link and primary-button interactions.
|
|
315
|
+
- **@voltro/testing** — `ctx.asSubject(subject)` and `ctx.asTenant(tenantId)` return a re-scoped test context instead of passing one to a block — for a test with two actors alive at once.
|
|
316
|
+
|
|
317
|
+
`withSubject` nests, which reads well for "write as A, then assert B cannot see it" and badly for a conflict: two editors on one row, a lock contest, an undo the other person's write must refuse. Those want two named contexts and interleaved calls, and a block per alternation buries the sequence under test. Both forms call the same builder, so a context held side by side cannot differ from one handed to a callback: one store, one event bus, one cache, one clock.
|
|
318
|
+
|
|
319
|
+
The `store` option's documentation now says the seed is COPIED. Passing one seed to two `makeTestContext` calls reads like sharing a store and does not share one — each context builds its own from the rows, which is what keeps two tests in a file independent.
|
|
320
|
+
|
|
321
|
+
A test pins the trap that made this worth adding: spreading the context to replace `request.subject` moves the field you read and nothing that decides a write. `ctx.store` was wrapped for the original subject, so tenant scoping, row filters and the `audit()` stamp keep resolving to the first identity, and a write made that way lands with the wrong `updatedBy` — letting a test assert that one person touched a row while the row records another.
|
|
322
|
+
|
|
323
|
+
### Fixed
|
|
324
|
+
|
|
325
|
+
- **@voltro/cli** — Server bundles no longer wrap every closure in an `Object.defineProperty`.
|
|
326
|
+
|
|
327
|
+
They were built with `minify: true, keepNames: true`. esbuild implements `keepNames` by emitting `__name(fn, "original")`, and `__name` is an `Object.defineProperty` on the function object. For a function DEFINITION that runs once; for a function EXPRESSION created inside a hot path it runs on every instance, and defining a property that way moves the instance's own properties out of its in-object slots into a separate `PropertyArray`.
|
|
328
|
+
|
|
329
|
+
Measured on the memory fixture with 200 open subscriptions, `heapUsed` after a forced GC:
|
|
330
|
+
|
|
331
|
+
minify + keepNames 168.0 KB/subscription, idle 49319 KB identifiers unmangled, no keepNames 135.4 KB/subscription, idle 44767 KB
|
|
332
|
+
|
|
333
|
+
A heap-snapshot diff of the same pair carries the fingerprint of the mechanism in its object counts — +28 200 `closure` nodes against +25 982 `array (object properties)` nodes, for the same 200 subscriptions.
|
|
334
|
+
|
|
335
|
+
The reason `keepNames` was there is unchanged and still correct: Effect's tags, error `name`s and the boot-refusal marker are read as STRINGS across the bundle boundary, and a mangled class name turns a precise refusal into an anonymous one. What was wrong was the estimate written beside it — "it costs a few percent of the saving" — which measured BYTES while the bill was paid in HEAP, once per live subscription, for as long as the subscription lives.
|
|
336
|
+
|
|
337
|
+
So the names are kept by NOT mangling identifiers, which is what created the need for a wrapper in the first place. Whitespace and syntax minification still apply, so the parse-time property these bundles exist for survives: the serve bundle grows 6184 KB → 8388 KB on disk while the cold-boot `modules` phase moves 139 ms → 137 ms, inside the noise. The idle-heap saving alone exceeds the bytes added, before a single subscription exists.
|
|
338
|
+
|
|
339
|
+
It applies to the api SERVE bundle and nothing else, because the bill scales with how many closures a process holds ALIVE. The web start bundle and the middleware bundle serve per-request closures that are collected, not live subscriptions, so the swap bought them nothing — and applying it there overran the start-bundle byte budget by 45 %, which is how the scope was found. They keep `keepNames`, as does the browser bundle, where download bytes over a user's connection dominate.
|
|
340
|
+
|
|
341
|
+
`serverBundleMinify.test.ts` asserts that each exemption is still real — an exemption list that describes nothing reads like a considered decision — and that `apiBuild.ts` being exempted for its precompiled-entry bag does not quietly exempt the serve bundle in the same file.
|
|
342
|
+
|
|
343
|
+
`serveBundleBytes` is re-pinned 6.58 → 9.42 MB, and it is the only artefact budget that moves. What the budget protects is unchanged: the cold-boot `modules` phase measured 139 → 137 ms, inside the noise, on the very number minification was bought for. Bytes are the proxy there; parse time is the property.
|
|
344
|
+
- **@voltro/cli** — `voltro build` on a web app now generates the rpc groups of the apis it imports.
|
|
345
|
+
|
|
346
|
+
`rpcGroup.generated.ts` is gitignored, so a clean clone has none — and only `voltro dev` and `voltro codegen` ever wrote it. The api branch of `voltro build` already self-healed its own, with the right reason recorded beside it: "a production build from a clean CI checkout is the exact case that never ran `voltro dev`". A WEB app imports that file from the API's package, where nothing was healing it, and the note two hundred lines up said so — "unlike the web case this file usually EXISTS, which is worse: a missing import fails the build" — while leaving the worse case alone.
|
|
347
|
+
|
|
348
|
+
What a user got instead was the bundler's:
|
|
349
|
+
|
|
350
|
+
Rolldown failed to resolve import "<pkg>/rpcGroup" from "<app>/.framework/main.tsx" If you do want to externalize this module explicitly add it to `build.rolldownOptions.external`
|
|
351
|
+
|
|
352
|
+
An internal path they did not write, and advice that is exactly wrong.
|
|
353
|
+
|
|
354
|
+
Only WORKSPACE apis: an external api is a URL with no local package, and `resolveApis` deliberately admits those.
|
|
355
|
+
|
|
356
|
+
Resolving the api's directory needs care, and the first attempt got it wrong in the quietest possible way. `${pkg}/package.json` is NOT resolvable when the package declares an `exports` map that omits it — which every voltro api does, because its map exists to expose `./rpcGroup`. That attempt caught the throw and continued, so the whole helper was a no-op that looked like it worked. It now resolves the subpath the bundler itself will use, and falls back to the workspace link (verifying the manifest's `name`) for the case this exists for: the file is not there yet, so every resolution into that package fails.
|
|
357
|
+
- **@voltro/cli** — `voltro dev` re-bundles the browser's api client when a shared file a descriptor IMPORTS changes, not only when a descriptor file itself does.
|
|
358
|
+
|
|
359
|
+
The dev server pre-bundles the generated rpc group's whole import graph, and Vite never re-optimizes a pre-bundled dependency on a content change — so the framework fingerprints that graph itself. It fingerprinted the descriptor files alone. A descriptor declaring `error: [SomeTypedError]` whose class lives in a shared `lib/` file therefore left the fingerprint untouched when the class gained a field, and the browser kept decoding the failure against the older class, dropping the new fields with nothing reported anywhere: an unknown field decodes as absent, not as an error, so there is no exception, no warning, and no request in the api log to find.
|
|
360
|
+
|
|
361
|
+
Three mechanisms carried that blind spot — the stamp codegen embeds in the generated file, the across-boot force, and the in-session watcher — so the fix is one shared definition rather than a third patch. `valueImportGraph.ts` now owns what an import is and where a specifier leads; `browserSafetyGuard` reads the same resolver instead of its own copy. The in-session watcher watches the closure it hashes, since hashing the right set and watching a smaller one leaves the same gap.
|
|
362
|
+
|
|
363
|
+
Type-only imports stay out (they are erased), executor halves stay out (the browser never loads them, and including them would re-bundle on every handler edit), and `@voltro/*` stays out — in an install those are built JavaScript that stops the walk anyway.
|
|
364
|
+
|
|
365
|
+
`descriptorSourceFingerprint` is deliberately unchanged. It answers whether the generated FILE is stale, which the descriptor set settles: an edit inside a shared `lib/` file changes no import line and no rpc tag, so the group it would regenerate is byte-identical.
|
|
366
|
+
- **@voltro/client** — `useConnectionStatus` no longer causes a React hydration mismatch on an SSR page.
|
|
367
|
+
|
|
368
|
+
It passed `getSnapshot` as `useSyncExternalStore`'s SERVER reader as well as its client one, so the server rendered whatever the live recovery coordinator held at that instant while the browser's hydration render read a coordinator that had already moved. React discards the server markup for that subtree and re-renders it on the client — the SSR benefit gone — and reports it only as a console error in a browser.
|
|
369
|
+
|
|
370
|
+
Both readers now agree on a frozen boot snapshot (`INITIAL_RECOVERY_SNAPSHOT`, exported from `@voltro/client`), and React swaps to the live value on the first commit after hydration, so nothing is lost but the mismatch. `navigator.onLine` went through the same door for the same reason.
|
|
371
|
+
- **@voltro/cli** — Three things `voltro dev` did silently.
|
|
372
|
+
|
|
373
|
+
**The dashboard auto-launch skipped without a word.** The launch was `if (resolveDashboardAppPath(root)) { … }` with no `else`, so a project that has never installed `@voltro/dashboard` — a separate package — watched the documented auto-launch not happen and learned nothing, while the same absence reached through `voltro dashboard` printed the exact install line. The boot prints that same sentence once now, from one constant both callers read, and stays quiet when `VOLTRO_DASHBOARD=off` or `--disable-dashboard` says the developer has already declined.
|
|
374
|
+
|
|
375
|
+
The launcher can also decline for five reasons of its OWN — each carrying a `reason`, all five dropped — so "it is installed and it did not come up" had no answer either, on the console or in the log file. A non-TTY stdout (a process manager, a piped run, CI) skips the launch by design, and by design is not the same as unexplained. The reason is logged at `debug`: three of the five are the developer's own decision or a peer process already doing the work, so a console line would be noise, but the log file answers the question when it is asked.
|
|
376
|
+
|
|
377
|
+
**A web app got no inspect token at all.** The mint ran on the api boot path only, so in a project with both, the api had a token and demanded it while the web app was fail-closed shut: `/_voltro/inspect/{app,routes,metrics}` all refused, its runtime-registry entry carried no token, and the dashboard listed the web app and could read nothing from it — the route list, the render modes and the web process's own metrics being exactly what only that half has. The web boot mints before it registers, so the entry carries the token.
|
|
378
|
+
|
|
379
|
+
It mints the INSPECT pair only. The framework's mintable set is split by what owns it now: a web dev boot has no store, so minting a field-encryption key there would write a real AES key into a `.env.local` that nothing can use. An api boot still mints all three.
|
|
380
|
+
|
|
381
|
+
The messages that described the token as minted "per project" said the wrong thing in four places; it is per APP, and an api and a web app each publish their own on their own registry entry.
|
|
382
|
+
|
|
383
|
+
**Orphaned registry temp files were never swept.** The atomic write is `writeFile` then `rename`, with a `catch` that unlinks its own tmp — and that catch cannot run when the process dies between the two, which is what Ctrl-C on a `voltro dev` does. Measured independently on two machines: 270 each, oldest a month old, 188 of them zero bytes, in the same directory the dashboard reads its app list from. A successful write now sweeps its own pattern, once per process, for files older than an hour — old enough that a live writer's tmp, which exists for microseconds, can never be reached.
|
|
384
|
+
- **@voltro/cli** — `voltro db …` now honours the `store` an app declares.
|
|
385
|
+
|
|
386
|
+
`dev.ts` states the rule for the whole framework — "env `$DB_DIALECT` → config.store → 'memory'" — and the db subcommands implemented `$DB_DIALECT` → `'postgres'`. The middle step was missing and the fallback was the wrong one, so an app declaring `store: 'sqlite'` (or mysql, mssql, turso) had every `voltro db` subcommand build a POSTGRES layer against a database that was not there.
|
|
387
|
+
|
|
388
|
+
What that looked like, in a project with no migration files at all:
|
|
389
|
+
|
|
390
|
+
db apply: a file-based migration FAILED — the schema diff was NOT run failed: SqlError: PgClient: Failed to connect
|
|
391
|
+
|
|
392
|
+
Two wrong statements in one message: the project has no file migrations, and the failure is not about migrations. The command was simply pointed at the wrong engine.
|
|
393
|
+
|
|
394
|
+
`$DB_DIALECT` still wins, because an operator acting on a running deployment outranks what the project declared. A declared `memory` store is deliberately not recorded: it is not a dialect these subcommands can operate on, and recording it would make the resolver refuse with a message naming `DB_DIALECT=memory` to someone who never set that variable.
|
|
395
|
+
|
|
396
|
+
The declared store is read once per command run rather than threaded through the resolver's twenty-one synchronous call sites; `resetDeclaredStore` exists so a suite can prove the value does not leak between runs.
|
|
397
|
+
- **@voltro/runtime** — The fleet view no longer reports a LIVE replica as departed.
|
|
398
|
+
|
|
399
|
+
`mergeFleetObservations` classified any observation row whose replica the local roster did not list as departed — a process that is gone. But a membership view is per process: replicas sharing a database and no broadcast bus each hold a roster of one, so two live replicas reported each other as departed while quoting an age of under two seconds, which is the same answer saying the process was alive moments ago.
|
|
400
|
+
|
|
401
|
+
Departure now needs age as well as roster-absence: a row is departed only when it is unrostered AND older than the freshness window, because a departed process cannot have written inside it. A fresh unrostered row is a responder whose existence the roster does not know about, which is what it always was.
|
|
402
|
+
|
|
403
|
+
The behaviour this replaces is kept: a row from a long-gone process is still old and still unrostered, so it still lands in `departed` rather than inflating `responded` or splitting the framework-version count.
|
|
404
|
+
- **@voltro/cli** — A 403 on the no-JS `/form/*` path now leaves a record saying which of the three refusal reasons fired.
|
|
405
|
+
|
|
406
|
+
`/form/*` and `POST /rpc` are two surfaces of ONE origin decision. The rpc listener has logged `refused cross-site request` under `voltro:security` with the reason and the offending origin since the guard shipped; the form path answered a terse `origin not allowed` to the browser, wrote nothing anywhere, and dropped the `decision.reason` it had just computed. So a refusal on the surface a user reaches WITHOUT JavaScript — the one where there is no console to inspect and no network panel to open — was the one with no diagnosis on either side.
|
|
407
|
+
|
|
408
|
+
The form path now logs the same message under the same scope, so an operator greps one scope for both surfaces. The response body is deliberately unchanged and still identical for every reason: the client learns it was refused, not which allowlist entry to guess at.
|
|
409
|
+
|
|
410
|
+
`host` is logged beside `origin` because it is part of the decision rather than context. Both `sameAuthority` and `bothLoopback` compare against the Host header and both return false when it is absent, so a request whose Origin reads as perfectly same-origin is refused the moment Host does not arrive — a case that, in a record carrying the origin alone, is indistinguishable from a genuine cross-site POST.
|
|
411
|
+
- **@voltro/cli** — The dashboard proxy sent its OWN inspect token to every app it opened.
|
|
412
|
+
|
|
413
|
+
Three sites forwarded `process.env.VOLTRO_INSPECT_TOKEN` — the token of whichever process was doing the forwarding — as the bearer for an arbitrary target: the dashboard proxy on the api boot path, the same proxy on the web boot path, and the in-page overlay's proxy to its api. `inspectFetch.ts` has resolved per registry entry all along, which is why `voltro logs` works from any directory; the three readers that forward on someone else's behalf never learned it.
|
|
414
|
+
|
|
415
|
+
That was invisible for exactly as long as every app on a machine shared one token. It stops being true the moment tokens are minted per app, and then the proxy opens precisely one app and is refused by every other — 401, `missing Authorization header`, against a registry entry carrying a perfectly good token of its own. In a project with an api and a web app, the half that answers is whichever one happens to own the forwarding process.
|
|
416
|
+
|
|
417
|
+
`inspectTokenForTarget` / `inspectTokenForTargetSync` resolve the bearer from the target's ORIGIN against the runtime registry, falling back to this process's token when the target is not registered — an operator-configured remote target has nothing else to offer, and a single-app machine has always used exactly that. A malformed target or an unreadable registry degrades to the same fallback rather than throwing, which matters for the sync form, used on a request path with no error boundary above it.
|
|
418
|
+
|
|
419
|
+
The write credential stays env-only and deliberately unpublished: the registry file is a per-app READ capability, and putting an action-authorising token in it would widen every entry into something worth stealing.
|
|
420
|
+
|
|
421
|
+
Three sites were wrong at once, so the guard is over the SET rather than over the line that was reported — a fourth forwarder is one paste away, and the paste looks locally correct, `process.env.VOLTRO_INSPECT_TOKEN` being, after all, this process's token.
|
|
422
|
+
- **@voltro/cli, @voltro/ui-shadcn** — The local dashboard JSON proxy forwards the separate inspect write credential on action requests in both development and production. Explicit read credentials are never upgraded with the proxy's local write token; runtime dashboard app configuration accepts an optional `writeToken`. The shared `Dialog` now accepts its native React ref so access forms can open with `showModal()` and retain native focus containment.
|
|
423
|
+
- **@voltro/cli** — `voltro e2e` reported a spec whose process was KILLED as passed.
|
|
424
|
+
|
|
425
|
+
Node gives a signal-killed child `code: null`, and the runner coerced that with `code ?? 0` — so an out-of-memory runner, a CI cancel or a harness timeout turned "this spec never finished" into "this spec succeeded", with nothing else in the run saying otherwise. A killed spec is a failure now, and the line names the signal, because `exit 1` for a SIGKILL sends the reader looking for an assertion that never ran.
|
|
426
|
+
|
|
427
|
+
`voltro typecheck` and the deploy command runner already failed closed on the same null; what they could not say is WHY. A typecheck failure with no diagnostics above it reads as a tsc bug rather than as a machine that ran out of memory, so both print the signal now.
|
|
428
|
+
|
|
429
|
+
The same discard sat in nine spawn helpers across the test suite, where it made a killed child fail as `expected null to be 1` — a message describing a behaviour defect that had not happened, on a rule that was working. All nine carry the signal into what the assertion prints, and `spawnedChildSignals.test.ts` keeps the tenth copy from being written: the one site where an exit code alone is provably enough is exempted BY NAME with its reason, and an exemption that stops matching fails the run.
|
|
430
|
+
- **@voltro/plugin-mail** — Mail inspection declares recent-send buffers as process-local and template lists as declarations, so dashboards retain replica identity and completeness instead of presenting them as a complete delivery history.
|
|
431
|
+
- **@voltro/web** — Closing an intercepting-route overlay returns focus to whatever opened it.
|
|
432
|
+
|
|
433
|
+
The overlay is a native `<dialog>` opened with `showModal()`, and the code relied on the platform for focus restoration — reasonably, because that is what the spec says `close()` does. The spec also says it only happens while the dialog is still in a document, and React deletes a subtree by detaching its topmost host node before walking the children to run their effect cleanups. So when the overlay is unmounted as part of a route change, its `close()` can land on an element that is already detached, and the restoration step silently does nothing.
|
|
434
|
+
|
|
435
|
+
Measured in a real chromium against `voltro start`: the trigger was focused (`activeElement=open-2`), `showModal()` moved focus into the dialog (`activeElement=DIALOG`), and after Back the dialog was gone with `document.activeElement` on `BODY`. The trigger itself was untouched — same node, still connected — so this was not a re-render replacing it. For anyone navigating by keyboard, Back dropped them at the top of the document.
|
|
436
|
+
|
|
437
|
+
The overlay now records what was focused before it opened and restores it on close. It FILLS THE GAP rather than fighting the platform: when `close()` restored focus itself this is a no-op on the same element, and a focus the app moved somewhere real while the overlay was open is left alone — only a focus that has fallen to nothing gets put back.
|
|
438
|
+
|
|
439
|
+
`e2e-fixtures/web-intercept` driven through a real browser goes from one failure to none, and both halves are pinned in jsdom, which has no `showModal` at all and is therefore the right harness for the explicit path.
|
|
440
|
+
- **@voltro/cli, @voltro/protocol** — Production now mounts plugin inspect endpoints through the same authenticated dispatcher as development. Plugin JSON GET endpoints can declare process, shared-store or declaration scope to retain origin and completeness at effective and aliased mounts. Plugin writes reject non-loopback browser origins in the shared dispatcher.
|
|
441
|
+
- Keep plugin operation targets, permissions and failures explicit in both dashboards. Confirm retention, subject erasure, search reindex/repair and moderation decisions; bind subject results to the selected subject and reject already-reviewed moderation items with HTTP 409. Report process/declaration/store scope without treating missing data as an empty or healthy state.
|
|
442
|
+
- Resolve dashboard plugin reads and writes against the selected effective instance. Distinguish failed reads from empty lists, show bounded storage and notification samples honestly, and retain declaration/process scope for billing configuration and queue counters.
|
|
443
|
+
- **@voltro/runtime** — The default `Referrer-Policy` made the framework refuse its own no-JS forms.
|
|
444
|
+
|
|
445
|
+
`no-referrer` was the shipped default, and under it a browser sends `Origin: null` on a same-origin state-changing request — the Fetch specification serialises the origin as `null` when the referrer policy is `no-referrer`. The origin guard refuses an opaque origin, correctly and by design. So every no-JS `<form method="post">` in every app answered **403**, on `voltro dev` and `voltro start` alike, for every user. Two security defaults, each defensible alone, cancelling each other: we asked the browser to withhold provenance and then refused the request for lacking it.
|
|
446
|
+
|
|
447
|
+
The same request also arrived without a `Referer`, which `/form/*` reads to choose the PRG redirect target and the 422 re-render's return path.
|
|
448
|
+
|
|
449
|
+
The default is now `strict-origin-when-cross-origin`. It withholds the path cross-origin — the privacy property `no-referrer` was chosen for — while leaving the `Origin` intact, and nulls it only on an https→http downgrade, which is the one case where withholding it is the point. It is also what a browser does with no header at all. `same-origin` would also fix the form path but nulls the `Origin` on a legitimate cross-origin post to an allowlisted api, which is exactly what a split deployment's `VOLTRO_ALLOWED_ORIGINS` exists to permit.
|
|
450
|
+
|
|
451
|
+
Measured in a real headless chromium against a bare `node:http` server, one header changed between rows:
|
|
452
|
+
|
|
453
|
+
(none) → Origin: http://127.0.0.1:45412 no-referrer → Origin: null ← and no Referer strict-origin-when-cross-origin → Origin: http://127.0.0.1:45412 same-origin → Origin: http://127.0.0.1:45412
|
|
454
|
+
|
|
455
|
+
An app that set `referrerPolicy: 'no-referrer'` explicitly keeps it and keeps the 403; `preservesSameOriginHeader` is exported so the incompatibility can be asserted rather than rediscovered.
|
|
456
|
+
- **@voltro/ui-shadcn** — The shared token stylesheet now supplies light/dark success, warning and info colors and their Tailwind utilities, so Callout status treatments render consistently in every consuming app without local token definitions.
|
|
457
|
+
- **@voltro/ui-shadcn** — Signal header actions use the navigation's resting border contrast, 16px icon size and text line height so equally sized controls also align visually.
|
|
458
|
+
- **@voltro/cli** — `voltro --help` stopped advertising a subcommand that was removed on purpose, and `voltro support --help` answers as help.
|
|
459
|
+
|
|
460
|
+
`voltro support sentry <eventId>` went away in 0.67.0 — sentry-cli 3 dropped `sourcemaps explain` — with a codemod carrying users across it. What outlived the removal was the command table's own summary, which went on listing `Subcommands: collect, sentry` while the implementation answered `unknown subcommand 'sentry'` with exit 2. The summary names the removal and where to look instead, so a reader arriving from an older help output or a runbook learns why rather than meeting a refusal.
|
|
461
|
+
|
|
462
|
+
`voltro support --help` also came back as `unknown subcommand '--help'` with exit 2. The command declares `handlesHelp`, so the flag arrives as the first argument and hit the subcommand check — an error for the one request that is never one, on the command somebody reaches for BECAUSE they do not yet know the subcommands. `--help`, `-h` and `help` all print usage and exit 0.
|
|
463
|
+
- **@voltro/cli** — `voltro test` no longer turns a TYPE-ONLY `tsconfig` `paths` mapping into a runtime alias. An app that maps `"react": ["./node_modules/@types/react"]` — the usual trick so `tsc` resolves ambient types in a hoisted monorepo — could not run a single test that imported React: vitest resolved the real `import 'react'` to a package of declarations and every such file died with `"." is not exported under the conditions ["node","development","import"] from @types/react`.
|
|
464
|
+
|
|
465
|
+
`voltro build` and `voltro dev` already skipped these mappings; the test runner and `voltro doctor` used a second alias reader that did not. The rule now has one definition (`isTypeOnlyPathTarget`) that all three import, and the two readers are pinned against each other in a test — the codemod path is deliberately excluded, since it hands `paths` back to a TypeScript compiler, which is who a type-only mapping is written for.
|
|
466
|
+
|
|
467
|
+
### Internal (no consumer-facing effect)
|
|
468
|
+
|
|
469
|
+
- **@voltro/cli** — `scripts/browser-surface-mismatch.mjs` drives the schema cross-check in a real Chromium — a real `fetch` to the derived capabilities URL, a real console, the real `buildApiRuntime`.
|
|
470
|
+
|
|
471
|
+
The unit tests reach everything except the environment the check actually runs in, and this one's whole value is that it SAYS something where the alternative is silence: a version that had quietly stopped firing would look, from outside, exactly like a client that is always fresh. Three cases, and the last two are what keep the first honest — a disagreeing surface warns and names the procedure, an agreeing one is silent, and a server publishing no surface at all is silent too. Falsified by disconnecting the reporter: four cases go red, the two silence cases stay green.
|
|
472
|
+
|
|
473
|
+
It joins `browser-checks.mjs` by existing; the runner discovers `browser-*.mjs` by name.
|
|
474
|
+
- **@voltro/cli** — Synchronize the published dashboard-inspection codemod manifest and the development inspect-door regression with shared routing. Measure Redis delivery only after broker subscription readiness, establish file-watcher readiness in every workspace root, and run these real-process, watcher and Redis suites in the serial integration group.
|
|
475
|
+
- **@voltro/client** — `documentHidden` in the subscription cache reads `document` through `globalThis` with a local structural type, the way `formBinding` already does, instead of `typeof document !== 'undefined' && document.visibilityState`. Same value in every environment. The spelling mattered only to a program that compiles `@voltro/client` without the DOM lib: `@voltro/cli`'s test tsconfig does, and its `lint` reported TS2584 twice on that line while the client's own `lint` — DOM lib present — was green. A cross-package view of a file is its own check, and this one runs only in CI's Lint job.
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
42
479
|
## [0.67.0] — 2026-09-07
|
|
43
480
|
|
|
44
481
|
### ⚠ BREAKING
|