@voltro/cli 0.56.0 → 0.57.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 +194 -0
- package/dist/apiBuild-B83Cb2Rv.js +2 -0
- package/dist/{apiBuild-Bdaetr37.js → apiBuild-DDr2aNFd.js} +2 -2
- package/dist/bin.js +1 -1
- package/dist/{build-CI36wL4R.js → build-QKP6Bm0J.js} +191 -187
- package/dist/{checkCommand-D0QV_zM_.js → checkCommand-BISqx1OJ.js} +1 -1
- package/dist/{checkCommand-CAwFXrxA.js → checkCommand-DUtMWjcR.js} +4 -4
- package/dist/{codegenCommand-CidbQzbv.js → codegenCommand-DnVuDxwT.js} +10 -10
- package/dist/{commands-CWjfThXv.js → commands-CRbxgxv0.js} +22 -22
- package/dist/{dashboardCommand-BekcY5Ls.js → dashboardCommand-Bf_-Ne3P.js} +5 -5
- package/dist/{dataCommand-2pccgbIy.js → dataCommand-BoBJJ-Gb.js} +2 -2
- package/dist/{dbCommand-DpK_vQET.js → dbCommand-CMAIz-Bf.js} +3 -3
- package/dist/dbCommand-DHi_RuDl.js +2 -0
- package/dist/dev-Bl9HqtV7.js +3 -0
- package/dist/{dev-Dw263KPu.js → dev-DOEJXicj.js} +2109 -2092
- package/dist/{doctorCommand-aR_bFmIi.js → doctorCommand-BrWu67JZ.js} +331 -189
- package/dist/doctorCommand-DrQv9SL3.js +2 -0
- package/dist/{dormancyCommand-eXTQMbHU.js → dormancyCommand-xn2y-pJm.js} +1 -1
- package/dist/{embeddingsCommand-CTmiQvwa.js → embeddingsCommand-Cn5MbRDM.js} +1 -1
- package/dist/emptyResultHeadline-Csa5fZOF.js +18 -0
- package/dist/{envCommand-BDUgV7EM.js → envCommand-UJmJIbs9.js} +8 -8
- package/dist/{evolveCommand-YV8qW1LU.js → evolveCommand-Db30twUy.js} +1 -1
- package/dist/frameworkTableAssembly-B96WCNJA.js +2 -0
- package/dist/{frameworkTableAssembly-DNOFXfEQ.js → frameworkTableAssembly-vfkzuzEo.js} +104 -85
- package/dist/index.js +1 -1
- package/dist/inspect-B0hL41s0.js +2 -0
- package/dist/{inspect-CNYvNXPU.js → inspect-DZnan87F.js} +68 -52
- package/dist/{logsCmd-BU8uCdys.js → logsCmd-C5y9HyrG.js} +58 -53
- package/dist/{manifestBuild-CEkjfpwc.js → manifestBuild-DnbFKF6w.js} +1 -1
- package/dist/manifestBuild-UXrnUcXP.js +2 -0
- package/dist/{migrate-SICulyz1.js → migrate-DtC3lu7H.js} +4 -4
- package/dist/{probeCommand-6HxEkNDG.js → probeCommand-C5fuN6Z2.js} +1 -1
- package/dist/{runtimeTrace-DgYMc09E.js → runtimeTrace-BQL_lfz6.js} +1 -1
- package/dist/{sdkgen-PY-umd6O.js → sdkgen-CMUPrDjH.js} +1 -1
- package/dist/{serveCommand-DIJ3ma76.js → serveCommand-B_w-Mpb_.js} +1013 -954
- package/dist/serveCommand-Dtb48ffg.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/{start-B9NGB8gn.js → start-YaUehtDV.js} +564 -571
- package/dist/{start-BFQQkL1i.js → start-s25GAIgn.js} +1 -1
- package/dist/startEntry.js +1 -1
- package/dist/{tracesCmd-BWYDqMy6.js → tracesCmd-C1qKa94g.js} +68 -63
- package/dist/{webDev-C2dRz9s5.js → webDev-B6ZMX42w.js} +1 -1
- package/dist/{webDev-C53hJdcL.js → webDev-BgdkyjP6.js} +881 -774
- package/dist/{webhooksCommand-uuPu8qQX.js → webhooksCommand-CYXTNvXq.js} +2 -2
- package/package.json +19 -19
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/ai.md +1 -1
- package/templates/agent-docs/authentication.md +34 -19
- package/templates/agent-docs/cli.md +48 -0
- package/templates/agent-docs/configuration.md +29 -4
- package/templates/agent-docs/database/scaling.md +10 -1
- package/templates/agent-docs/deployment.md +22 -0
- package/templates/agent-docs/plugins/audit.md +21 -5
- package/templates/agent-docs/plugins/billing.md +2 -0
- package/templates/agent-docs/reference.md +1 -0
- package/templates/agent-docs/scheduling.md +23 -0
- package/templates/agent-docs/schema-driven-ui.md +81 -0
- package/templates/agent-docs/whats-new.md +100 -403
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +7 -7
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +7 -7
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/package.json +7 -7
- package/templates/apps/frontend-blank/package.json +6 -6
- package/templates/apps/frontend-cms/package.json +8 -8
- package/templates/apps/frontend-collab/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +6 -6
- package/templates/apps/frontend-docs/package.json +8 -8
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +7 -7
- package/templates/apps/frontend-saas/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +6 -6
- package/templates/apps/frontend-ssr/package.json +6 -6
- package/templates/apps/frontend-ssr-api/package.json +7 -7
- package/templates/apps/frontend-static-blog/package.json +8 -8
- package/templates/apps/frontend-status/package.json +7 -7
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-Vw1figjO.js +0 -2
- package/dist/dbCommand-DrycGWWt.js +0 -2
- package/dist/dev-B9Gz0k85.js +0 -3
- package/dist/doctorCommand-BMWs6aVm.js +0 -2
- package/dist/frameworkTableAssembly-CGNC0qr7.js +0 -2
- package/dist/inspect-S2rWy1Ys.js +0 -2
- package/dist/manifestBuild-DIa_s6u0.js +0 -2
- package/dist/serveCommand-BITS8Hpj.js +0 -2
|
@@ -222,8 +222,8 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
|
|
|
222
222
|
...t === void 0 ? {} : { payload: t }
|
|
223
223
|
};
|
|
224
224
|
}, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
|
|
225
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
226
|
-
return r((await n(await t(e))).events.map((e) => ({
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-Bl9HqtV7.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
|
|
226
|
+
return r((await n(await t(e), { root: e })).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: e.descriptor
|
|
229
229
|
}))).map((e) => x(e.descriptor));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -884,24 +884,24 @@
|
|
|
884
884
|
"@effect/platform-node": "^0.108.0",
|
|
885
885
|
"@effect/sql": "^0.52.0",
|
|
886
886
|
"@effect/workflow": "^0.19.0",
|
|
887
|
-
"@voltro/ai": "0.
|
|
888
|
-
"@voltro/cache": "0.
|
|
889
|
-
"@voltro/client": "0.
|
|
890
|
-
"@voltro/content": "0.
|
|
891
|
-
"@voltro/data-transfer": "0.
|
|
892
|
-
"@voltro/database": "0.
|
|
893
|
-
"@voltro/env": "0.
|
|
894
|
-
"@voltro/kv": "0.
|
|
895
|
-
"@voltro/logger": "0.
|
|
896
|
-
"@voltro/plugin-auth": "0.
|
|
897
|
-
"@voltro/plugin-broadcast": "0.
|
|
898
|
-
"@voltro/plugin-mail": "0.
|
|
899
|
-
"@voltro/plugin-storage": "0.
|
|
900
|
-
"@voltro/plugin-webhooks": "0.
|
|
901
|
-
"@voltro/protocol": "0.
|
|
902
|
-
"@voltro/runtime": "0.
|
|
903
|
-
"@voltro/serverless": "0.
|
|
904
|
-
"@voltro/workflow": "0.
|
|
887
|
+
"@voltro/ai": "0.57.0",
|
|
888
|
+
"@voltro/cache": "0.57.0",
|
|
889
|
+
"@voltro/client": "0.57.0",
|
|
890
|
+
"@voltro/content": "0.57.0",
|
|
891
|
+
"@voltro/data-transfer": "0.57.0",
|
|
892
|
+
"@voltro/database": "0.57.0",
|
|
893
|
+
"@voltro/env": "0.57.0",
|
|
894
|
+
"@voltro/kv": "0.57.0",
|
|
895
|
+
"@voltro/logger": "0.57.0",
|
|
896
|
+
"@voltro/plugin-auth": "0.57.0",
|
|
897
|
+
"@voltro/plugin-broadcast": "0.57.0",
|
|
898
|
+
"@voltro/plugin-mail": "0.57.0",
|
|
899
|
+
"@voltro/plugin-storage": "0.57.0",
|
|
900
|
+
"@voltro/plugin-webhooks": "0.57.0",
|
|
901
|
+
"@voltro/protocol": "0.57.0",
|
|
902
|
+
"@voltro/runtime": "0.57.0",
|
|
903
|
+
"@voltro/serverless": "0.57.0",
|
|
904
|
+
"@voltro/workflow": "0.57.0",
|
|
905
905
|
"chokidar": "^5.0.0",
|
|
906
906
|
"ioredis": "^5.11.1",
|
|
907
907
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -749,7 +749,7 @@ each plugin's own README.
|
|
|
749
749
|
|
|
750
750
|
| Topic | Open | Summary |
|
|
751
751
|
|---|---|---|
|
|
752
|
-
| **What's new in 0.
|
|
752
|
+
| **What's new in 0.57.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
753
753
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
754
754
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
755
755
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.57.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"group": null,
|
|
261
261
|
"description": "Project the typed descriptor graph into UI — forms, tables, pickers, and reactive components, all bound to a descriptor with near-zero glue.",
|
|
262
262
|
"path": "agent-docs/schema-driven-ui.md",
|
|
263
|
-
"files":
|
|
263
|
+
"files": 24
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
"id": "security",
|
|
@@ -1326,7 +1326,7 @@ defaults to treating an `AgentEvent`-shaped `{ _tag: 'done' | 'error' }` as the
|
|
|
1326
1326
|
end of the run. Override `isTerminal` when your element type signals completion
|
|
1327
1327
|
some other way, or the hook will keep trying to resume a finished stream.
|
|
1328
1328
|
|
|
1329
|
-
For multi-node deployments use `dataStoreResumableStreamStore(ctx.store)` — it persists to the framework's own database (`
|
|
1329
|
+
For multi-node deployments use `dataStoreResumableStreamStore(ctx.store)` — it persists to the framework's own database (`_voltro_stream_events` + `_voltro_stream_state`, created for you: the table assembly adds both for any app that depends on `@voltro/ai`, so `voltro dev` / `voltro db apply` / `voltro migrate` all plan them) and elects exactly ONE producer per `streamId` via an atomic claim, so only one node runs the model while every node's consumers tail the shared log. Sweep finished streams with `gcResumableStreams(store, { olderThan })`.
|
|
1330
1330
|
|
|
1331
1331
|
For the fastest path, `redisResumableStreamStore(redis, { ttlSeconds })` backs the log with a Redis LIST (`RPUSH`/`LRANGE`) plus a `SET … NX` producer claim — TTL evicts finished/abandoned streams without a sweep. `@voltro/ai` takes no Redis dependency; you inject a tiny `ResumableRedis` client (five methods: `setNx` / `rpush` / `lrange` / `set` / `exists`) adapting ioredis / node-redis. All three backends satisfy the same `ResumableStreamStore` interface, so they swap without touching the producer/consumer code.
|
|
1332
1332
|
|
|
@@ -2691,31 +2691,46 @@ comment, and this one is load-bearing: the resume grant is issued on its
|
|
|
2691
2691
|
strength. Omit `tables:` entirely and nothing is verified and nothing resumes —
|
|
2692
2692
|
the conservative default.
|
|
2693
2693
|
|
|
2694
|
-
## Eager loads are
|
|
2694
|
+
## Eager loads are narrowed too
|
|
2695
2695
|
|
|
2696
2696
|
A relation pulled in with `.with(...)` is resolved **below** the seam that
|
|
2697
|
-
|
|
2698
|
-
store recurses through its own raw read; the SQL stores
|
|
2699
|
-
one join). So a
|
|
2700
|
-
unfiltered
|
|
2697
|
+
AND-merges the filter onto a read's base table: the stores expand the eager tree
|
|
2698
|
+
themselves (the memory store recurses through its own raw read; the SQL stores
|
|
2699
|
+
fold the relation into one join or JSON aggregate). So for a while a filtered
|
|
2700
|
+
table reached through a relation came back unfiltered, and then — once that was
|
|
2701
|
+
found — the read refused rather than serve those rows.
|
|
2701
2702
|
|
|
2702
|
-
|
|
2703
|
-
|
|
2703
|
+
Neither is the case now. The middleware writes your filter's predicate into each
|
|
2704
|
+
narrowed branch's `where`, which is where a caller could have put it themselves,
|
|
2705
|
+
so every resolver on every dialect applies it without knowing a row filter
|
|
2706
|
+
exists:
|
|
2704
2707
|
|
|
2708
|
+
```ts
|
|
2709
|
+
// what you wrote
|
|
2710
|
+
notes.with({ readers: true })
|
|
2711
|
+
|
|
2712
|
+
// what the store resolves, when your filter narrows `readers`
|
|
2713
|
+
notes.with({ readers: { where: /* your filter's predicate for `readers` */ } })
|
|
2705
2714
|
```
|
|
2706
|
-
row filter: 'readers' is reached through an eager load on 'notes', and an eager
|
|
2707
|
-
relation is resolved BELOW the row filter — its rows would come back
|
|
2708
|
-
unfiltered. Refusing the read rather than serving it.
|
|
2709
|
-
→ read 'readers' as its own query (it is filtered there), or
|
|
2710
|
-
→ drop 'readers' from this .with(...) if the relation does not need
|
|
2711
|
-
row-level narrowing.
|
|
2712
|
-
```
|
|
2713
2715
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2716
|
+
Your own `where` is kept and the filter goes **under** it, so a branch you
|
|
2717
|
+
narrowed stays narrower and nothing a caller writes can widen the filter.
|
|
2718
|
+
Nested `.with(...)` is narrowed at every level. A relation reaching a table your
|
|
2719
|
+
filter does not narrow is untouched.
|
|
2720
|
+
|
|
2721
|
+
**One case still refuses: a filtered `manyToMany` JUNCTION.** A branch `where`
|
|
2722
|
+
is a predicate on the relation's TARGET, and a filter narrowing the junction has
|
|
2723
|
+
nowhere to be expressed — so that read raises rather than joining junction rows
|
|
2724
|
+
the filter would have removed:
|
|
2725
|
+
|
|
2726
|
+
```
|
|
2727
|
+
row filter: the junction table 'noteTags' behind relation 'tags' on 'notes' is
|
|
2728
|
+
row-filtered, and a many-to-many junction is read below the seam that could
|
|
2729
|
+
narrow it — its rows would be joined unfiltered. Refusing the read rather than
|
|
2730
|
+
serving it.
|
|
2731
|
+
→ read 'noteTags' as its own query (it is filtered there), or
|
|
2732
|
+
→ drop 'tags' from this .with(...).
|
|
2733
|
+
```
|
|
2719
2734
|
|
|
2720
2735
|
## Row filters vs. guards
|
|
2721
2736
|
|
|
@@ -1856,6 +1856,8 @@ It covers both halves of the stack:
|
|
|
1856
1856
|
| server | a `token` / `secret` / `password` column with no encryption | `.encrypted()` |
|
|
1857
1857
|
| server | a notify / webhook helper called at a mutation's tail | `defineSubscriber` / `defineReaction` |
|
|
1858
1858
|
| server | a `*.subscribe.ts` handler that writes or publishes, with no `once:` | `once: true` on `defineSubscriber` |
|
|
1859
|
+
| server | an executor builds a field its descriptor's `output` does not declare | add it to `output` — the struct IS the serializer |
|
|
1860
|
+
| server | a mutation writes a NULLABLE column through an input field that cannot be `null` | `Schema.optional(Schema.NullOr(...))` |
|
|
1859
1861
|
| server | `hasMore` + `limit + 1` | `paginateById` |
|
|
1860
1862
|
| server | `.getTime()` / `.toISOString()` mapping a row on the way out | `timestampMs` / `timestampMsOrNull` from `@voltro/database/wire` in the descriptor's `output` struct |
|
|
1861
1863
|
| client | per-field `useState` + a submit flag | `useFormBinding` |
|
|
@@ -1900,6 +1902,25 @@ decision was made, not which way. It reads the handler through the AST, so
|
|
|
1900
1902
|
like an inline arrow; a handler IMPORTED from another module is not judged at
|
|
1901
1903
|
all, since its body is not in the file being read.
|
|
1902
1904
|
|
|
1905
|
+
**Two rules read a DECLARATION against a DECLARATION, and are therefore exact.**
|
|
1906
|
+
`executor-builds-an-undeclared-field` compares the object literal an executor
|
|
1907
|
+
returns with its descriptor's `output` struct: since the struct IS the
|
|
1908
|
+
serializer, a key it does not declare is stripped on the way out, so every
|
|
1909
|
+
reader downstream gets `undefined` and renders a blank with nothing logged
|
|
1910
|
+
anywhere. `nullable-column-a-mutation-cannot-clear` compares a mutation's target
|
|
1911
|
+
table's nullable columns with its input schema — a nullable column written
|
|
1912
|
+
through a field that cannot be `null` can be set once and never emptied.
|
|
1913
|
+
|
|
1914
|
+
Both REFUSE rather than guess, and the refusals are the interesting part. A
|
|
1915
|
+
spread on either side of the output comparison (`{ ...row, extra }`, or a
|
|
1916
|
+
`...Base` inside the declared struct) means the key set is not knowable from the
|
|
1917
|
+
source; reporting the visible half would name the field the author can already
|
|
1918
|
+
see and miss the ones they cannot. An output that is a named schema rather than
|
|
1919
|
+
a literal struct is unjudgeable, not empty — the second reading would make every
|
|
1920
|
+
field a finding. And a table declaration the scan cannot find produces no
|
|
1921
|
+
finding at all, because "this table has no nullable columns" and "I could not
|
|
1922
|
+
look" lead to opposite conclusions.
|
|
1923
|
+
|
|
1903
1924
|
It stays quiet on a reader on purpose. `once:` on a cache-warming subscriber
|
|
1904
1925
|
would silence it on every replica but one, which is worse than the repetition it
|
|
1905
1926
|
removes — only the handler's author knows which of the two they wrote.
|
|
@@ -2368,6 +2389,33 @@ voltro check --url https://api.example.com
|
|
|
2368
2389
|
|
|
2369
2390
|
`--token` (or `VOLTRO_INSPECT_TOKEN`) supplies the bearer; `VOLTRO_INSPECT_URL` sets a default target so you can drop the flag. Works for `inspect`, `logs`, `traces`, `workflows`, `cluster` and `check`.
|
|
2370
2391
|
|
|
2392
|
+
### `logs` and `traces` need a ring on the target — off by default
|
|
2393
|
+
|
|
2394
|
+
A deployed app runs `voltro serve`, and `voltro serve` keeps **no** in-process log ring or span buffer unless you ask for one. Without it both commands answer:
|
|
2395
|
+
|
|
2396
|
+
```
|
|
2397
|
+
voltro logs: NOTHING WAS SEARCHED — the target could not be read: HTTP 404
|
|
2398
|
+
endpoint not enabled on this instance — This instance keeps no in-process logs ring…
|
|
2399
|
+
```
|
|
2400
|
+
|
|
2401
|
+
That headline is the point: *"no records matched the given filters"* would have sent you off widening `--tail` forever. Turn a ring on per app:
|
|
2402
|
+
|
|
2403
|
+
```ts
|
|
2404
|
+
// app.config.ts
|
|
2405
|
+
export default defineAppConfig({
|
|
2406
|
+
inspect: {
|
|
2407
|
+
logs: true, // default size
|
|
2408
|
+
traces: { size: 2_000 }, // or name it
|
|
2409
|
+
},
|
|
2410
|
+
})
|
|
2411
|
+
```
|
|
2412
|
+
|
|
2413
|
+
or per deployment, without a rebuild: `VOLTRO_INSPECT_LOGS=2000`, `VOLTRO_INSPECT_TRACES=on`, `=off` to override a config that declared one.
|
|
2414
|
+
|
|
2415
|
+
**Why off by default.** A ring is memory on every replica, forever, for data most deployments already collect from stdout through their platform's pipeline. That pipeline stays the main road; the ring answers the different question — *what has THIS instance been doing in the last few minutes* — and it answers it for a hosted app, where reading a pod's stdout is not an option.
|
|
2416
|
+
|
|
2417
|
+
**What a ring does not do.** It holds a bounded number of records on ONE replica. Against a fleet you are reading 1/N, and it is not retention: a restart empties it. `voltro dev` keeps both rings unconditionally — this switch is about what production does.
|
|
2418
|
+
|
|
2371
2419
|
## `voltro probe access` — is a declared guard actually enforced?
|
|
2372
2420
|
|
|
2373
2421
|
`voltro check` reads an app's manifest and reports a procedure with **no access
|
|
@@ -202,10 +202,35 @@ is accepted but only reachable from server-side render code, and the boot
|
|
|
202
202
|
## Plugins declare their env
|
|
203
203
|
|
|
204
204
|
A plugin that reads environment variables declares them via `declaredEnv` on its
|
|
205
|
-
`VoltroPlugin
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
`VoltroPlugin`. The plugin still reads its own values; declaring them makes its
|
|
206
|
+
env needs visible in the manifest, `.env.example`, and the dashboard. The
|
|
207
|
+
first-party plugins already declare theirs, so the manifest is complete out of
|
|
208
|
+
the box.
|
|
209
|
+
|
|
210
|
+
**`required: true` is a boot CONTRACT, not documentation.** Every required entry
|
|
211
|
+
is resolved before any plugin activates, and one that does not resolve **aborts
|
|
212
|
+
the boot** with the plugin named:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
plugin environment validation failed (1 variable):
|
|
216
|
+
• STRIPE_SECRET_KEY — required by @voltro/plugin-billing, and nothing set it
|
|
217
|
+
Stripe API secret key.
|
|
218
|
+
→ set the variable, or pass the value as a plugin option if the plugin takes one, or
|
|
219
|
+
declare it yourself in app.config.ts's `env` (an app declaration overrides a plugin's).
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
That is the point of failing at the boot: a deployment variable which silently
|
|
223
|
+
stops being set — a rotated secret, a typo in a values file, a CI variable nobody
|
|
224
|
+
created — is routine, and the alternative is finding out from the first customer.
|
|
225
|
+
|
|
226
|
+
A `secret: true` variable resolves through the configured **secrets backend**,
|
|
227
|
+
not just `process.env`. A backend that cannot answer fails the boot too, reported
|
|
228
|
+
as *unreadable* rather than as an absent variable — those are different facts and
|
|
229
|
+
need different next steps.
|
|
230
|
+
|
|
231
|
+
Your own `defineEnv` declaration of the same variable **overrides the plugin's**:
|
|
232
|
+
declare it there to give it a default, or to mark it optional because you pass
|
|
233
|
+
the value as a plugin option instead.
|
|
209
234
|
|
|
210
235
|
## Anti-pattern
|
|
211
236
|
|
|
@@ -368,10 +368,19 @@ A changelog table that every replica polls (`SELECT … WHERE seq > :last`) woul
|
|
|
368
368
|
```
|
|
369
369
|
|
|
370
370
|
```
|
|
371
|
-
[voltro:dev] reactivity: cross-instance fan-out is OFF for dialect '
|
|
371
|
+
[voltro:dev] reactivity: cross-instance fan-out is OFF for dialect 'cockroach'. A write on one replica
|
|
372
372
|
will NOT reach clients on other replicas. Add @voltro/plugin-broadcast (Redis / NATS) to close the gap…
|
|
373
373
|
```
|
|
374
374
|
|
|
375
|
+
**The line reports what is RUNNING, not what the dialect could run.** It is derived from the store's resolved change scope, so a dialect that *can* carry changes natively but has capture switched off — `CDC=0`, or an app where every table is `.nonReactive()` — is reported as the gap it is, naming the cause rather than the dialect:
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
[voltro:dev] reactivity: cross-instance fan-out is OFF. 'mariadb' can carry changes natively
|
|
379
|
+
(binlog CDC) but change capture is not running — CDC=0, or every table is .nonReactive()…
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Read that line, not the dialect, when you need to know whether the N² amplification below applies to you: without a native transport there is no second delivery to suppress.
|
|
383
|
+
|
|
375
384
|
When BOTH a native path and the broadcast plugin are wired (e.g. postgres + broadcast), both stay active — and they carry **different things**. The native path carries table changes to every replica. The bus carries reactivity *channels* (`publishReactivity`), which are not database writes and so have no native transport at all.
|
|
376
385
|
|
|
377
386
|
```
|
|
@@ -907,6 +907,28 @@ readinessProbe:
|
|
|
907
907
|
failureThreshold: 3
|
|
908
908
|
```
|
|
909
909
|
|
|
910
|
+
### Configuring them — `health` in `app.config.ts`
|
|
911
|
+
|
|
912
|
+
The paths and the answers are declarable, for **both** app types:
|
|
913
|
+
|
|
914
|
+
```ts
|
|
915
|
+
export default {
|
|
916
|
+
type: 'web' as const,
|
|
917
|
+
health: {
|
|
918
|
+
// Default `/internal`; move it when your app owns a route there.
|
|
919
|
+
path: '/api/health',
|
|
920
|
+
liveness: () => true,
|
|
921
|
+
readiness: async () => catalogLoaded(),
|
|
922
|
+
},
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
**`readiness` is the one worth writing**, because "ready" genuinely differs. For an api it is the dependency ping above. For a screen on a wall it is the opposite — keep showing the last rendered frame while the api wobbles, and count as ready precisely then. Only the app knows.
|
|
927
|
+
|
|
928
|
+
**`liveness` should stay cheap and dependency-free.** A liveness probe that consults a database restarts pods when the database is slow, which is the one thing that makes an outage worse.
|
|
929
|
+
|
|
930
|
+
The probes are answered **before routing** on every boot path, so no route and no guard can claim them. That is not a detail: on a web app `voltro dev` used to have no probe surface, and `/internal/*` fell into the page router — an SPA shell answered `200` with HTML, a page whose loader redirects answered `303`, an auth guard answered `303` to `/login`. A kubelet reads all three as PASS, which is the one property a health probe must not have. Worse, one of those pages had a loader that calls the api, so the **web** pod's readiness hung on the **api**'s reachability, once per probe interval.
|
|
931
|
+
|
|
910
932
|
> **Run `voltro serve` in serving pods — not `voltro dev`.** `voltro dev` is the
|
|
911
933
|
> local-iteration supervisor: file-watch, respawn, codegen, and a boot-time
|
|
912
934
|
> auto-migrate that introspects the whole schema. It does **not** expose the
|
|
@@ -356,7 +356,7 @@ What it discloses, and what it never does:
|
|
|
356
356
|
|
|
357
357
|
| | |
|
|
358
358
|
| --- | --- |
|
|
359
|
-
| a string | its LENGTH — `string(113)`. Never a prefix, never a hash |
|
|
359
|
+
| a string | its LENGTH — `string(113)`. Never a prefix, never a hash. Below a floor, a bucket: `string(<16)` |
|
|
360
360
|
| a number, a boolean, a date | its TYPE only. A number can BE the secret |
|
|
361
361
|
| a declared field name | survives — `jiraToken` |
|
|
362
362
|
| a key that is not an identifier | described, not reproduced — `<key:string(36)>` |
|
|
@@ -369,10 +369,26 @@ Two of those are worth a sentence each.
|
|
|
369
369
|
it looks like a declared field: a plain short identifier. A legitimate key that
|
|
370
370
|
is not one (`content-type`) loses its name and keeps its shape.
|
|
371
371
|
|
|
372
|
-
**A string's length is a real disclosure, and a small one
|
|
373
|
-
credential it carries nothing — every token of a given kind is the
|
|
374
|
-
For a human-chosen password it is a weak hint.
|
|
375
|
-
|
|
372
|
+
**A string's length is a real disclosure, and a small one — above a floor.** For
|
|
373
|
+
a fixed-format credential it carries nothing — every token of a given kind is the
|
|
374
|
+
same length. For a human-chosen password it is a weak hint.
|
|
375
|
+
|
|
376
|
+
Below a floor it stops being small: a six-digit TOTP reported as `string(6)`, or
|
|
377
|
+
a four-digit PIN as `string(4)`, tells a reader with log access exactly what
|
|
378
|
+
shape to try. Nothing this mode is FOR lives under 16 characters — the case that
|
|
379
|
+
motivated it is 113 against an expected 44 — so a string shorter than that
|
|
380
|
+
reports `string(<16)`: shorter than the floor, not how much shorter.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
auditPlugin({ redactOutcome: 'shape', redactionLengthFloor: 0 })
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`0` turns the floor off, for a deployment whose audited payloads are ids and
|
|
387
|
+
tokens and every character of length is worth having; raise it for one holding
|
|
388
|
+
short human-entered secrets. An EMPTY string stays exact (`string(0)`) — "the
|
|
389
|
+
field arrived empty" is a real diagnosis and an empty string is not a secret. The
|
|
390
|
+
floor applies to described keys too, for the reason above them: a key that is not
|
|
391
|
+
a declared field is a key carrying data.
|
|
376
392
|
|
|
377
393
|
`redactSubject` spells its variant `'metadata-shape'`, because its default
|
|
378
394
|
(`'metadata'`) already names the field it acts on.
|
|
@@ -40,6 +40,8 @@ export default {
|
|
|
40
40
|
|
|
41
41
|
`plans` is the single source of tier→limit truth — it lives in code, not the DB, so limits change by redeploy, not migration. Entitlement values are `number | 'unlimited'`. With no provider configured (and no `STRIPE_SECRET_KEY`), the plugin uses an in-memory **mock provider** — zero-config for dev and tests.
|
|
42
42
|
|
|
43
|
+
> **Under `NODE_ENV=production`, a mock chosen by ABSENCE warns.** `STRIPE_SECRET_KEY` is a deployment variable, and one that silently stops being set — a rotated secret, a typo in a values file, a CI variable nobody created — is routine. Without the line, an app that had been charging customers keeps answering every billing call successfully, reaches nobody, and leaves nothing to find afterwards. Set the key, or write `provider: 'mock'` so the mock is a decision on the page rather than an absence. An explicitly declared mock stays silent.
|
|
44
|
+
|
|
43
45
|
What it provides:
|
|
44
46
|
|
|
45
47
|
- The `_voltro_billing_*` tables (customers, subscriptions, invoices, usage, flush_claims, dunning_notices) via `extendSchema.tables`.
|
|
@@ -89,6 +89,7 @@ these before hand-rolling a form, a table, or a picker** — full guide in
|
|
|
89
89
|
| [`usePreview`](/docs/ui/client-utilities/use-preview) | Mutation dry-run — run the real handler in a rolled-back transaction. |
|
|
90
90
|
| [`useProvenance`](/docs/ui/client-utilities/use-provenance) | "Why is this value here?" — lineage lookup for a field. |
|
|
91
91
|
| [`useOnRpcError`](/docs/ui/client-utilities/use-on-rpc-error) / `reportClientError` | Subscribe to the rpc error bus; report a client error to the server. |
|
|
92
|
+
| [`useSubscriptionHealth`](/docs/ui/client-utilities/use-subscription-health) | Which of an api's calls are currently REFUSED — a refused subscription never retries and reads as `loading`. |
|
|
92
93
|
| [`useTracking`](/docs/ui/client-utilities/use-tracking) | Fire mount/unmount + interaction tracking events. |
|
|
93
94
|
| [`useCapabilityManifest`](/docs/ui/client-utilities/use-capability-manifest) | The api's capability manifest (procedures + tables + schemas), fetched once. |
|
|
94
95
|
| [`useRefreshSubscriptions`](/docs/ui/client-utilities/use-refresh-subscriptions) | Force-refresh live subscriptions (e.g. after an out-of-band change). |
|
|
@@ -539,6 +539,29 @@ defineSchedule({
|
|
|
539
539
|
|
|
540
540
|
> The watchdog stops *waiting* and records the timeout; it cannot truly abort a Promise's in-flight side effects (JavaScript has no thread-kill). Make long handlers cooperative — check a deadline, or do the heavy lifting in a [workflow](/docs/workflows/overview) with its own step-level durability.
|
|
541
541
|
|
|
542
|
+
## Across replicas — the heartbeat
|
|
543
|
+
|
|
544
|
+
`onOverlap: 'skip'` is a **cluster-wide** rule, not a per-process one: a firing is suppressed when an occurrence of the same schedule is running on *any* replica. That check reads `_voltro_schedule_runs`, so it needs a way to tell a run that is still working from one whose process died mid-run and left its row on `running` forever.
|
|
545
|
+
|
|
546
|
+
That is `heartbeatAt`: a run in flight bumps it every **30 seconds**, and a row silent for three beats is read as dead rather than as a live occurrence. Without it the only evidence a row carried was `firedAt`, and a window generous enough never to cut off a long run is also long enough to suppress a half-hourly cron for hours after one restart.
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
// app.config.ts
|
|
550
|
+
export default defineAppConfig({
|
|
551
|
+
scheduling: {
|
|
552
|
+
scheduleHeartbeatMs: 30_000, // default; VOLTRO_SCHEDULE_HEARTBEAT_MS overrides it
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Raise it in a deployment with many long-running schedules where the writes matter more than the detection latency; a run shorter than one interval writes no beat at all and costs nothing either way. It is **not a lease** — a stale beat only stops a corpse from holding the schedule shut; nothing takes ownership of the run.
|
|
558
|
+
|
|
559
|
+
The skip is logged with what it measured, so the two cases are legible from the log alone:
|
|
560
|
+
|
|
561
|
+
```
|
|
562
|
+
schedule: overlap skip name=reindex scope=cluster heldBy=pod-7f4c lastSeenMs=1840
|
|
563
|
+
```
|
|
564
|
+
|
|
542
565
|
## Backfill — `backfill`
|
|
543
566
|
|
|
544
567
|
When the process was down across one or more firing instants, what should happen on boot? Computed from the last `_voltro_schedule_runs` row for the schedule.
|
|
@@ -370,6 +370,36 @@ widget })` rides an annotation, `description` becomes help text, and
|
|
|
370
370
|
underneath is an implementation detail — no engine type appears in the public
|
|
371
371
|
API, and production builds stub its devtools channel automatically.
|
|
372
372
|
|
|
373
|
+
### When the FORM decides which mutation it is
|
|
374
|
+
|
|
375
|
+
Some forms only learn their target from what the user does: a calendar entry
|
|
376
|
+
becomes a recurring **series** the moment "repeats" is ticked, and the series
|
|
377
|
+
mutation takes eleven more fields than the single one.
|
|
378
|
+
|
|
379
|
+
Pass a function of the current values instead of a tag:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
const form = useFormBinding<CalendarInput>(
|
|
383
|
+
'app',
|
|
384
|
+
(v) => (v.repeats ? 'calendarRecurringEvent.create' : 'calendarEntries.create'),
|
|
385
|
+
{ defaults: { title: '', repeats: false } },
|
|
386
|
+
)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The schema in force follows the tag, so `form.fields` grows and shrinks with it
|
|
390
|
+
and validation always matches what will actually be submitted. **The values do
|
|
391
|
+
not reset** — the engine is constructed once and never rebuilt, so everything
|
|
392
|
+
typed before the switch survives it. That is the whole reason this exists:
|
|
393
|
+
deriving the tag outside the binding is not available (the values belong to the
|
|
394
|
+
binding and do not exist before it), and re-mounting with a different tag throws
|
|
395
|
+
the user's input away.
|
|
396
|
+
|
|
397
|
+
Two details worth knowing. On the first render there are no values yet, so the
|
|
398
|
+
function is called with your `defaults`. And the accessibility ids are pinned to
|
|
399
|
+
the first tag and stay there — they are DOM ids, and letting them move on the
|
|
400
|
+
keystroke that flips the branch would remount every field, taking the focus and
|
|
401
|
+
the caret with it.
|
|
402
|
+
|
|
373
403
|
### Reference fields, uploads, and testing the form
|
|
374
404
|
|
|
375
405
|
**Reference fields.** Mark a schema field as a table reference and it renders
|
|
@@ -1597,6 +1627,57 @@ identity* changed underneath them.
|
|
|
1597
1627
|
|
|
1598
1628
|
|
|
1599
1629
|
|
|
1630
|
+
---
|
|
1631
|
+
|
|
1632
|
+
<!-- source: en/ui/client-utilities/use-subscription-health.md -->
|
|
1633
|
+
## useSubscriptionHealth
|
|
1634
|
+
|
|
1635
|
+
_Which of an api's calls are currently refused — the state a screen cannot derive from its own hook._
|
|
1636
|
+
|
|
1637
|
+
A refused subscription is **terminal for its transport**. It does not retry, so
|
|
1638
|
+
there is no second error event to react to — and `SubscriptionFailed` presents
|
|
1639
|
+
`data: undefined`, which is the value most reading layers derive `loading` from.
|
|
1640
|
+
A component that branches on `data === undefined` therefore shows a spinner from
|
|
1641
|
+
then on, indefinitely. The state union models this correctly (`failed: true`,
|
|
1642
|
+
`loading: false`); what loses it is the wrapper hook in between, and a wrapper
|
|
1643
|
+
that passes `{ data, loading }` through is the natural shape to write.
|
|
1644
|
+
|
|
1645
|
+
`useSubscriptionHealth` answers the question no single hook can: **is anything on
|
|
1646
|
+
this api currently refused, and what.**
|
|
1647
|
+
|
|
1648
|
+
```tsx
|
|
1649
|
+
import { useSubscriptionHealth } from '@voltro/client'
|
|
1650
|
+
|
|
1651
|
+
const SessionBar = () => {
|
|
1652
|
+
const { healthy, failed } = useSubscriptionHealth('app')
|
|
1653
|
+
if (healthy) return null
|
|
1654
|
+
return <Banner>Not loading: {failed.map((f) => f.tag).join(', ')}</Banner>
|
|
1655
|
+
}
|
|
1656
|
+
```
|
|
1657
|
+
|
|
1658
|
+
A tag enters `failed` when a call for it fails and leaves when a call for the
|
|
1659
|
+
**same tag** succeeds. It is keyed by tag rather than counted, because the
|
|
1660
|
+
question a screen asks is "is this working now" and two failures of one call are
|
|
1661
|
+
one broken thing. `healthy` is the positive form of the same fact, so a component
|
|
1662
|
+
reads `if (!healthy)` instead of `if (failed.length > 0)`.
|
|
1663
|
+
|
|
1664
|
+
Each entry is `{ tag, source, error, at }` — `at` is the timestamp of the
|
|
1665
|
+
failure, so a bar can say *since 12:04* rather than only *broken*.
|
|
1666
|
+
|
|
1667
|
+
## What it does not do
|
|
1668
|
+
|
|
1669
|
+
It is not a retry mechanism and not an error boundary. Recovery is the
|
|
1670
|
+
transport's job: an `Unauthenticated` failure makes the framework re-resolve the
|
|
1671
|
+
credential and rebuild the connection, and a rebuilt client re-subscribes on its
|
|
1672
|
+
own — a failed entry is never carried across the swap, so it is re-attempted
|
|
1673
|
+
rather than restored as broken. Nothing in your app wires that.
|
|
1674
|
+
|
|
1675
|
+
The register is scoped to one api's runtime and is reset when that runtime is
|
|
1676
|
+
rebuilt, deliberately: carrying a failure across a transport swap would report a
|
|
1677
|
+
call as broken that has not been tried since.
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
|
|
1600
1681
|
---
|
|
1601
1682
|
|
|
1602
1683
|
<!-- source: en/ui/client-utilities/use-tracking.md -->
|