@voltro/cli 0.20.2 → 0.22.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 +398 -0
- package/THIRD-PARTY-NOTICES.md +61 -61
- package/dist/{apiBuild-qZBEu59d.js → apiBuild-2zRxj2hJ.js} +93 -80
- package/dist/apiBuild-sqNmWmY5.js +2 -0
- package/dist/bin.js +41 -20
- package/dist/{commands-kYbfVoPO.js → commands-DGaeTn2f.js} +3055 -2640
- package/dist/dbCommand-CO3eSAZR.js +2 -0
- package/dist/{dbCommand-BPp9R0Wc.js → dbCommand-DVASmZj2.js} +305 -249
- package/dist/dev-BEYBSGJ7.js +3 -0
- package/dist/{dev-OHLsAq4K.js → dev-DeHVUxQx.js} +1732 -1686
- package/dist/index.js +1 -1
- package/dist/inspect-BA67TF6v.js +2 -0
- package/dist/inspect-_ldwsAwH.js +945 -0
- package/dist/{inspectMetrics-DvPNXmGA.js → inspectMetrics-4AVPn6IS.js} +584 -571
- package/dist/{manifestBuild-P9yuCY2d.js → manifestBuild-Bs1Uw22_.js} +1 -1
- package/dist/manifestBuild-i-fRHg_H.js +2 -0
- package/dist/serveCommand-DEVTtnLy.js +1161 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-CXQ7WL1W.js → start-BjGUIIvj.js} +389 -389
- package/dist/startEntry.js +2 -2
- package/package.json +27 -27
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +181 -5
- package/templates/agent-docs/data.md +58 -0
- package/templates/agent-docs/database/migrations.md +175 -31
- package/templates/agent-docs/database/schema.md +7 -0
- package/templates/agent-docs/database/seedsdialects.md +3 -3
- package/templates/agent-docs/plugins.md +1 -1
- package/templates/agent-docs/schema-driven-ui.md +2 -2
- package/templates/agent-docs/templates/apibackends.md +1 -1
- package/templates/agent-docs/whats-new.md +236 -9
- package/templates/apps/api-ai/package.json +12 -12
- package/templates/apps/api-auth/package.json +13 -13
- package/templates/apps/api-backend/package.json +12 -12
- package/templates/apps/api-backend-deactivation/package.json +12 -12
- package/templates/apps/api-backend-mail/package.json +11 -11
- package/templates/apps/api-backend-mariadb/package.json +14 -14
- package/templates/apps/api-backend-storage/package.json +11 -11
- package/templates/apps/api-data-advanced/package.json +13 -13
- package/templates/apps/api-durable/package.json +13 -13
- package/templates/apps/api-feature-flags/package.json +14 -14
- package/templates/apps/api-governance/package.json +13 -13
- package/templates/apps/api-kv/package.json +13 -13
- package/templates/apps/api-moderation/package.json +13 -13
- package/templates/apps/api-observability/package.json +13 -13
- package/templates/apps/api-ratelimit/package.json +13 -13
- package/templates/apps/api-rbac/package.json +13 -13
- package/templates/apps/api-rest/package.json +12 -12
- package/templates/apps/api-saas/package.json +16 -16
- package/templates/apps/api-search/package.json +13 -13
- package/templates/apps/api-versioning/package.json +13 -13
- package/templates/apps/api-webhooks/package.json +14 -14
- package/templates/apps/changelog/package.json +8 -8
- package/templates/apps/edge-functions/package.json +5 -5
- package/templates/apps/frontend-admin/package.json +10 -10
- package/templates/apps/frontend-app/package.json +10 -10
- package/templates/apps/frontend-blank/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +11 -11
- package/templates/apps/frontend-dashboard/package.json +9 -9
- package/templates/apps/frontend-docs/package.json +9 -9
- package/templates/apps/frontend-i18n/package.json +8 -8
- package/templates/apps/frontend-landing/package.json +9 -9
- package/templates/apps/frontend-spa/package.json +9 -9
- package/templates/apps/frontend-ssr/package.json +9 -9
- package/templates/apps/frontend-ssr-api/package.json +10 -10
- package/templates/apps/frontend-static-blog/package.json +8 -8
- package/dist/apiBuild-C9aHz4Yh.js +0 -2
- package/dist/dbCommand-DtUe-0dM.js +0 -2
- package/dist/dev-2AvdzDg2.js +0 -3
- package/dist/inspect-DcZ04OME.js +0 -2
- package/dist/inspect-Dwx0_tUj.js +0 -921
- package/dist/manifestBuild-D1MzJAiQ.js +0 -2
- package/dist/serveCommand-BKqTKWTX.js +0 -1136
- /package/templates/patches/{@effect__cluster@0.59.0.patch → @effect__cluster@0.60.0.patch} +0 -0
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { X as e } from "./inspectMetrics-4AVPn6IS.js";
|
|
2
|
+
import { t } from "./start-BjGUIIvj.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -59,42 +59,42 @@
|
|
|
59
59
|
"node": ">=24.0.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@effect/platform-node": "^0.
|
|
63
|
-
"@effect/sql": "^0.
|
|
64
|
-
"@effect/workflow": "^0.
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
62
|
+
"@effect/platform-node": "^0.108.0",
|
|
63
|
+
"@effect/sql": "^0.52.0",
|
|
64
|
+
"@effect/workflow": "^0.19.0",
|
|
65
|
+
"@voltro/ai": "0.22.0",
|
|
66
|
+
"@voltro/cache": "0.22.0",
|
|
67
|
+
"@voltro/data-transfer": "0.22.0",
|
|
68
|
+
"@voltro/database": "0.22.0",
|
|
69
|
+
"@voltro/env": "0.22.0",
|
|
70
|
+
"@voltro/kv": "0.22.0",
|
|
71
|
+
"@voltro/logger": "0.22.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.22.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.22.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.22.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.22.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.22.0",
|
|
77
|
+
"@voltro/protocol": "0.22.0",
|
|
78
|
+
"@voltro/runtime": "0.22.0",
|
|
79
|
+
"@voltro/serverless": "0.22.0",
|
|
80
|
+
"@voltro/workflow": "0.22.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
|
84
84
|
"ts-morph": "^28.0.0"
|
|
85
85
|
},
|
|
86
86
|
"optionalDependencies": {
|
|
87
|
-
"@tailwindcss/vite": "^4.3.
|
|
87
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
88
88
|
"@vercel/nft": "^1.10.2",
|
|
89
|
-
"@vitejs/plugin-react": "^6.0.
|
|
89
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
90
90
|
"esbuild": "^0.28.0",
|
|
91
|
-
"tsx": "^4.23.
|
|
92
|
-
"vite": "^8.1.
|
|
91
|
+
"tsx": "^4.23.1",
|
|
92
|
+
"vite": "^8.1.5"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@effect/platform": "^0.
|
|
96
|
-
"@effect/rpc": "^0.
|
|
97
|
-
"effect": "^3.
|
|
95
|
+
"@effect/platform": "^0.97.0",
|
|
96
|
+
"@effect/rpc": "^0.76.0",
|
|
97
|
+
"effect": "^3.22.0"
|
|
98
98
|
},
|
|
99
99
|
"publishConfig": {
|
|
100
100
|
"access": "public"
|
package/templates/AGENTS.md
CHANGED
|
@@ -595,7 +595,7 @@ each plugin's own README.
|
|
|
595
595
|
|
|
596
596
|
| Topic | Open | Summary |
|
|
597
597
|
|---|---|---|
|
|
598
|
-
| **What's new in 0.
|
|
598
|
+
| **What's new in 0.22.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. |
|
|
599
599
|
| 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. |
|
|
600
600
|
| 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. |
|
|
601
601
|
| 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.22.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. |
|
|
@@ -495,10 +495,11 @@ Three checks run at boot and print one line each when they have something to say
|
|
|
495
495
|
- **`apiKeys: true` with no `apikeys:issue:*` scope declared.** The capability is
|
|
496
496
|
on and reachable by nobody; every issue request fails its guard.
|
|
497
497
|
|
|
498
|
-
`voltro doctor` adds a fourth, over your source:
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
498
|
+
`voltro doctor` adds a fourth, over your source: its **authz scan** asks of every
|
|
499
|
+
executor — queries included — whether it references an access check at all, and
|
|
500
|
+
lists the ones that reference none. It learns your own `require*` / `assert*`
|
|
501
|
+
guard names, so it does not report the call sites of guards you already wrote.
|
|
502
|
+
See [the authz scan](./build-and-start.md).
|
|
502
503
|
|
|
503
504
|
## `voltro dev <appDir>`
|
|
504
505
|
|
|
@@ -544,6 +545,45 @@ compile. Already-compiled (warm) pages are never throttled, so a hot app stays
|
|
|
544
545
|
fully concurrent. Tune the cap with `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY` (below) —
|
|
545
546
|
drop it on a low-memory box, raise it on a big machine.
|
|
546
547
|
|
|
548
|
+
### A failed server render fails the request
|
|
549
|
+
|
|
550
|
+
If the server render throws, `voltro dev` answers **500** with the error and its
|
|
551
|
+
stack, marks the response `x-voltro-rendered-by: ssr-dev-failed`, and logs it at
|
|
552
|
+
`error`. It does **not** fall back to a client-rendered shell.
|
|
553
|
+
|
|
554
|
+
That is deliberate, and it is the same outcome `voltro start` produces in
|
|
555
|
+
production. A fallback would hand you a page that renders in the browser and a
|
|
556
|
+
500 in production from the identical code — and because an empty `<div
|
|
557
|
+
id="root">` is what a client-only app looks like, the usual conclusion is "the
|
|
558
|
+
framework does not server-render", not "my page threw". The failure is loud so
|
|
559
|
+
the cause is the thing you see.
|
|
560
|
+
|
|
561
|
+
The practical consequence: anything that only misbehaves under
|
|
562
|
+
`renderToPipeableStream` — a component that suspends with no `<Suspense>`
|
|
563
|
+
boundary above it, a loader that throws, a hydration-unsafe value — surfaces in
|
|
564
|
+
`voltro dev` at the moment you hit the route.
|
|
565
|
+
|
|
566
|
+
**Suspending is fine here, and does not need a boundary you add.** A component
|
|
567
|
+
that suspends during a streamed server render — a lazily-loaded translation
|
|
568
|
+
catalog, a `React.lazy` component, `react-i18next` with `useSuspense: true` —
|
|
569
|
+
renders normally: `renderToPipeableStream` treats the root as an implicit
|
|
570
|
+
boundary, so a suspend delays the shell flush instead of failing. Measured, not
|
|
571
|
+
assumed; a regression test pins it.
|
|
572
|
+
|
|
573
|
+
Do **not** add a blanket `<Suspense>` at the root to "fix" a suspend. It makes
|
|
574
|
+
things worse in a way that is hard to see: React downgrades an errored boundary
|
|
575
|
+
to client rendering, so a page that THROWS starts answering 200 with
|
|
576
|
+
`<template data-msg="Switched to client rendering">` instead of failing. You lose
|
|
577
|
+
the hard failure above and gain nothing — the suspend already worked. Mount
|
|
578
|
+
boundaries where you want a *fallback* (`loading.tsx` per route, `<Await>` for
|
|
579
|
+
deferred loader values), not to make suspending legal.
|
|
580
|
+
|
|
581
|
+
Two places where a suspend genuinely is not supported, both by React rather than
|
|
582
|
+
by choice: `renderToString`, which backs the static prerender
|
|
583
|
+
(`renderMode: 'static'`), emits the fallback instead of waiting — so a suspending
|
|
584
|
+
component in a prerendered page needs its own boundary or a resolved value; and
|
|
585
|
+
the client render after hydration, which follows React's own rules.
|
|
586
|
+
|
|
547
587
|
|
|
548
588
|
Under `VOLTRO_LOG_LEVEL=debug` each cold compile logs its own duration, so a slow
|
|
549
589
|
first paint can be attributed to a specific module:
|
|
@@ -737,6 +777,32 @@ The restart is a full re-exec — there is no in-process hot-reload of a
|
|
|
737
777
|
handler body; editing a query's executor respawns the child (debounced
|
|
738
778
|
80ms, so a burst of saves collapses into one restart).
|
|
739
779
|
|
|
780
|
+
### How the old process is stopped
|
|
781
|
+
|
|
782
|
+
SIGTERM first. The child runs its teardown — plugin `onDeactivate`, the
|
|
783
|
+
CDC detach, the scheduler and workflow runtime, the connection pool, and
|
|
784
|
+
every `ctx.onShutdown(cb)` a `*.startup.ts` registered — and then exits.
|
|
785
|
+
That is normally tens of milliseconds and you never see it.
|
|
786
|
+
|
|
787
|
+
It gets **1.5 seconds**, then SIGKILL, and the escalation says so:
|
|
788
|
+
|
|
789
|
+
```text
|
|
790
|
+
child ignored SIGTERM — escalated to SIGKILL pid=41207
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Read that as "something in this app's shutdown does not complete" — a
|
|
794
|
+
pool draining against a database that is already gone, a plugin
|
|
795
|
+
`onDeactivate` waiting on a dead socket. The restart still happens; it
|
|
796
|
+
just costs the full grace window every time, and whatever teardown had
|
|
797
|
+
not finished was cut off. Worth fixing at the source rather than living
|
|
798
|
+
with, because the same hang is a slow — then failed — shutdown in
|
|
799
|
+
production.
|
|
800
|
+
|
|
801
|
+
Neither timeout is optional: a stop that can wait forever is a dev
|
|
802
|
+
server that stops restarting entirely, with the old process still
|
|
803
|
+
holding the port and your browser's websocket still attached to code you
|
|
804
|
+
edited minutes ago.
|
|
805
|
+
|
|
740
806
|
## When the dev server stops
|
|
741
807
|
|
|
742
808
|
A restart replaces the child; the supervisor keeps watching. When the dev
|
|
@@ -976,6 +1042,65 @@ Docker step) and prints the exact remedy: add a `voltro build .` step before
|
|
|
976
1042
|
`voltro serve .`. Drop it into your image build right after `voltro build` to
|
|
977
1043
|
guarantee the artefact is present before the image ships.
|
|
978
1044
|
|
|
1045
|
+
### The authz scan
|
|
1046
|
+
|
|
1047
|
+
`voltro doctor` answers one mechanical question over every executor: **does it
|
|
1048
|
+
reference an access check at all?**
|
|
1049
|
+
|
|
1050
|
+
```
|
|
1051
|
+
authz scan · 586 executor(s)
|
|
1052
|
+
✗ no access check 21
|
|
1053
|
+
⚠ inline ownership check, no named guard 24 (informational)
|
|
1054
|
+
✓ guards: on the descriptor 0
|
|
1055
|
+
✓ calls a guard from the vocabulary 320
|
|
1056
|
+
– accepted as recorded debt 221 (voltro-authz-allowlist.txt)
|
|
1057
|
+
|
|
1058
|
+
✗ teams.deleteSubTeam — deletes teams with nothing constraining WHICH row
|
|
1059
|
+
api/teams/deleteSubTeam.mutation.server.ts
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
It scans **queries and streams too**, not only writes: an executor that takes an
|
|
1063
|
+
id and returns the row is the same hole as one that writes it. The exploitable
|
|
1064
|
+
shape is "acts on a row the client named, without comparing anything on that row
|
|
1065
|
+
to the caller", and a read has it.
|
|
1066
|
+
|
|
1067
|
+
**It learns your guard names.** An exported `require*` / `assert*` from your own
|
|
1068
|
+
source counts as a guard, so `requireTeamAccess()` is recognised without any
|
|
1069
|
+
configuration. Without that the scan would report every call site of your own
|
|
1070
|
+
guards, which is the failure mode that makes a check ignorable.
|
|
1071
|
+
|
|
1072
|
+
**An inline ownership check is informational.** `row.userId !== subject.id → new
|
|
1073
|
+
AccessDeniedError({})` is correct code — it is listed so you can see where the
|
|
1074
|
+
rule lives in a handler rather than on a descriptor, and it never fails the run.
|
|
1075
|
+
|
|
1076
|
+
Findings are ordered by blast radius: a `delete` outranks an `insert`, and a
|
|
1077
|
+
target table that is `tenant()`-scoped or referenced by other tables outranks one
|
|
1078
|
+
that is neither.
|
|
1079
|
+
|
|
1080
|
+
#### The ratchet — how to adopt this on an existing app
|
|
1081
|
+
|
|
1082
|
+
A first run on a large app reports hundreds of handlers, and nobody triages
|
|
1083
|
+
hundreds of findings. So record them once and fail only on what comes after:
|
|
1084
|
+
|
|
1085
|
+
```bash
|
|
1086
|
+
voltro doctor --write-authz-allowlist # writes voltro-authz-allowlist.txt
|
|
1087
|
+
voltro doctor # exits 1 on anything NEW
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
The file is **debt, not approval** — every line is a handler nobody has confirmed
|
|
1091
|
+
is safe. It is keyed by rpc tag rather than path, so moving a file can neither
|
|
1092
|
+
re-open a hole nor hide one, and it is consulted **last**: an executor that gains
|
|
1093
|
+
a real guard is reported as guarded whether or not its line is still there. The
|
|
1094
|
+
list can only shrink unless someone adds to it deliberately.
|
|
1095
|
+
|
|
1096
|
+
#### Before you hand-roll another check
|
|
1097
|
+
|
|
1098
|
+
If your checks are imperative because a scope cannot express "may this subject
|
|
1099
|
+
act on THIS row", that is what `guards: [{ action, resourceType, resource }]` is
|
|
1100
|
+
for — and an app whose relationships live in its own tables (a `teamMembers` row,
|
|
1101
|
+
say) registers its own tuple source instead of copying data into a framework
|
|
1102
|
+
table. See [Authorization](../authentication/authorization.md).
|
|
1103
|
+
|
|
979
1104
|
### The predicate-column check
|
|
980
1105
|
|
|
981
1106
|
`eq` / `isNull` / `inSet` are free functions, so the column name arrives as a bare
|
|
@@ -1470,6 +1595,30 @@ has never run is not "failing".
|
|
|
1470
1595
|
Pair it with `voltro logs --level error`: a failing handler now logs at **error**
|
|
1471
1596
|
level, so the two surfaces agree.
|
|
1472
1597
|
|
|
1598
|
+
## `voltro schedule run <name>` — fire one job, now
|
|
1599
|
+
|
|
1600
|
+
```sh
|
|
1601
|
+
voltro schedule run nightly-reconcile
|
|
1602
|
+
voltro schedule run nightly-reconcile --process billing --format json
|
|
1603
|
+
voltro schedule run nightly-reconcile --url https://api.example.com # a deployed app
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
For the normal case: a nightly job that corrects business data, and you want to
|
|
1607
|
+
run it once and watch. It reports the run id, and `voltro inspect schedules`
|
|
1608
|
+
shows the outcome.
|
|
1609
|
+
|
|
1610
|
+
A run id of `null` is not a failure and is reported as its own outcome: the run
|
|
1611
|
+
was **coordinated away** — another replica holds the lock, or the previous run is
|
|
1612
|
+
still going and this schedule's `onOverlap` is `'skip'`. Printing "ok" there
|
|
1613
|
+
would claim work that never started.
|
|
1614
|
+
|
|
1615
|
+
`--trigger external` records the run as externally triggered instead of manual,
|
|
1616
|
+
for schedules that are normally fired by an outside scheduler.
|
|
1617
|
+
|
|
1618
|
+
This works against `voltro serve` as well as `voltro dev`. It did not before —
|
|
1619
|
+
production mounted no inspect surface at all, which also meant the post-deploy
|
|
1620
|
+
gate below could only ever be run against a dev server.
|
|
1621
|
+
|
|
1473
1622
|
## Targeting a deployed app
|
|
1474
1623
|
|
|
1475
1624
|
Every command in this family resolves its target from the local runtime registry — the apps running on *this* machine. Pass `--url` to point one at a **deployed** app instead:
|
|
@@ -1489,7 +1638,34 @@ voltro check --url https://api.example.com
|
|
|
1489
1638
|
|
|
1490
1639
|
## The inspect HTTP surface
|
|
1491
1640
|
|
|
1492
|
-
Every `voltro dev` / `voltro start` instance exposes
|
|
1641
|
+
Every `voltro dev` / `voltro start` instance exposes an introspection surface
|
|
1642
|
+
under `/_voltro/inspect/*`.
|
|
1643
|
+
|
|
1644
|
+
**It is not read-only.** The core endpoints are reads, but installed plugins
|
|
1645
|
+
mount their own — and some are POSTs that DO things: `plugin-governance` mounts
|
|
1646
|
+
`/erase` (an irreversible GDPR right-to-be-forgotten deletion) and `/export` (a
|
|
1647
|
+
full personal-data dump); `plugin-storage` mounts `/share` and `/revoke`.
|
|
1648
|
+
|
|
1649
|
+
**So a mutating method needs a second credential.** `VOLTRO_INSPECT_WRITE_TOKEN`,
|
|
1650
|
+
sent as the `x-voltro-inspect-write` header ON TOP of the bearer — an additional
|
|
1651
|
+
factor, not an alternative: the read token still has to be correct. GET / HEAD /
|
|
1652
|
+
OPTIONS are unaffected. Unset, those endpoints are refused.
|
|
1653
|
+
|
|
1654
|
+
```sh
|
|
1655
|
+
curl -H "authorization: Bearer $VOLTRO_INSPECT_TOKEN" \
|
|
1656
|
+
-H "x-voltro-inspect-write: $VOLTRO_INSPECT_WRITE_TOKEN" \
|
|
1657
|
+
-X POST http://localhost:4000/_voltro/inspect/plugins/governance/erase
|
|
1658
|
+
```
|
|
1659
|
+
|
|
1660
|
+
`voltro dev` mints it per project like the read token, and the dashboard proxy
|
|
1661
|
+
injects it for loopback targets, so the dev loop is unchanged. **Nothing mints it
|
|
1662
|
+
for `serve` / `start`** — in production a destructive endpoint should take a
|
|
1663
|
+
deliberate act to enable. Use a DIFFERENT value from the read token; reusing it
|
|
1664
|
+
gives the split no meaning.
|
|
1665
|
+
|
|
1666
|
+
A plugin mounting a non-GET inspect endpoint must also declare the
|
|
1667
|
+
`inspect:write` permission, and the boot audit refuses it otherwise. That governs
|
|
1668
|
+
what a PLUGIN may mount; the write credential governs who may call it. The [Voltro Dashboard](/docs/observability/dashboard) consumes it to render the route sitemap, RPC list, subscription panel, workflow runs, and metrics. You can also hit the endpoints directly with `curl` (the `voltro inspect` subcommands above are the thin wrapper over exactly these).
|
|
1493
1669
|
|
|
1494
1670
|
```bash
|
|
1495
1671
|
PORT=4000 # from the app's app.config.ts
|
|
@@ -328,6 +328,32 @@ defineMutation({ name: 'notes.create', target: { table: 'notes', op: 'insert' },
|
|
|
328
328
|
|
|
329
329
|
With that pairing, `useMutation('app', 'notes.create')` can stage an optimistic row in active `notes.list` caches without client-side cache plumbing.
|
|
330
330
|
|
|
331
|
+
### A guard that reads a second table belongs in `source`
|
|
332
|
+
|
|
333
|
+
`source` is the reactive trigger set: the query re-runs when a listed table
|
|
334
|
+
changes, and only then. So a guard that loads a row from ANOTHER table to decide
|
|
335
|
+
access has made that table part of what the result depends on:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
export const teamBoard = defineQuery({
|
|
339
|
+
name: 'boards.forTeam',
|
|
340
|
+
input: Schema.Struct({ teamId: Schema.String }),
|
|
341
|
+
output: BoardRows,
|
|
342
|
+
// `boards` alone is wrong here — `requireTeamAccess` reads `teamMembers`.
|
|
343
|
+
source: ['boards', 'teamMembers'],
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Leave `teamMembers` out and the subscription does not re-run when membership
|
|
348
|
+
changes. **That is an authorization staleness, not a cosmetic one:** revoke
|
|
349
|
+
someone's membership and their open subscription keeps serving rows they may no
|
|
350
|
+
longer see, until something else happens to invalidate it.
|
|
351
|
+
|
|
352
|
+
Nothing warns about this at runtime — a query that silently stops reacting looks
|
|
353
|
+
exactly like one with nothing to report. Reported by a team whose own invariant
|
|
354
|
+
caught it after five computed queries under-declared their `source`; the fix was
|
|
355
|
+
array sources.
|
|
356
|
+
|
|
331
357
|
## `output` is the serializer — `timestampMs`
|
|
332
358
|
|
|
333
359
|
A descriptor's `output` is not documentation of the shape. It **is** the
|
|
@@ -951,6 +977,38 @@ const message = matchError(err, {
|
|
|
951
977
|
}, () => 'Something went wrong')
|
|
952
978
|
```
|
|
953
979
|
|
|
980
|
+
## `internal: true` — off the wire entirely
|
|
981
|
+
|
|
982
|
+
Every discovered `*.mutation.ts` / `*.query.ts` / `*.action.ts` / `*.stream.ts` is
|
|
983
|
+
callable over the WebSocket by any authenticated browser session. `publicApi` and
|
|
984
|
+
`exposeAsTool` opt IN to wider surfaces; `internal: true` opts OUT of the default
|
|
985
|
+
one:
|
|
986
|
+
|
|
987
|
+
```ts
|
|
988
|
+
export const createFromAction = defineMutation({
|
|
989
|
+
name: 'auditLog.createFromAction',
|
|
990
|
+
input: Schema.Struct({ actorId: Schema.String, eventType: Schema.String }),
|
|
991
|
+
output: Schema.Void,
|
|
992
|
+
internal: true,
|
|
993
|
+
})
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
It is not emitted into `rpcGroup.generated.ts`, and neither `voltro dev` nor
|
|
997
|
+
`voltro serve` registers a route — the tag is unroutable over `/rpc` and the
|
|
998
|
+
WebSocket. Server code calls it by importing its executor directly.
|
|
999
|
+
|
|
1000
|
+
**A naming convention is not a boundary.** One app had grown 18 procedures named
|
|
1001
|
+
`*Internal`, meaning "only other server code calls this"; all 18 were in the
|
|
1002
|
+
client group, and one of them accepted `actorId` / `actorEmail` / `actorType`
|
|
1003
|
+
from the caller and wrote an audit row. No guard, zero callers, reachable by
|
|
1004
|
+
anyone logged in. If the only thing keeping a procedure off the wire is that
|
|
1005
|
+
nobody wrote a client call for it, it is on the wire — the same reasoning as
|
|
1006
|
+
`.serverOnly()` on a column, one level up.
|
|
1007
|
+
|
|
1008
|
+
**It is not a substitute for a guard.** An internal procedure still runs with
|
|
1009
|
+
whatever authority its caller has. This removes the wire surface, not the need to
|
|
1010
|
+
check who is asking; `voltro doctor`'s authz scan still covers it.
|
|
1011
|
+
|
|
954
1012
|
## When Not To Use A Mutation
|
|
955
1013
|
|
|
956
1014
|
- **External I/O.** Use an action or workflow.
|