@voltro/cli 0.12.0 → 0.14.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 +378 -0
- package/dist/{apiBuild-GefjNI5d.js → apiBuild-370ITNRk.js} +3 -3
- package/dist/apiBuild-DatWP5hh.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-BmNi1RgZ.js → commands-D-teL_TM.js} +3756 -2539
- package/dist/dbCommand-B3CeMU3u.js +2 -0
- package/dist/{dbCommand-DNrkb58g.js → dbCommand-Dmi-vHfA.js} +72 -57
- package/dist/{dev-d0LjACxE.js → dev-CKyW_LAv.js} +1511 -1331
- package/dist/dev-CPKbckaN.js +3 -0
- package/dist/fileConventions-COg0ZytD.js +9 -0
- package/dist/frameworkTableAssembly-DCLhpHsF.js +601 -0
- package/dist/frameworkTableAssembly-DYzb2elg.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-uEfIS1vp.js → inspectMetrics-CyVMeofr.js} +962 -943
- package/dist/seedRunner-D6eu-u5U.js +229 -0
- package/dist/serveCommand-CU3Sw4ZM.js +1127 -0
- package/dist/serveEntry.js +3 -3
- package/dist/{start-DKcQd52a.js → start-D8ATvTzo.js} +246 -246
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +77 -3
- package/templates/AGENTS.md +78 -4
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +2 -2
- package/templates/agent-docs/authentication.md +39 -0
- package/templates/agent-docs/cli.md +114 -6
- package/templates/agent-docs/configuration.md +48 -0
- package/templates/agent-docs/data.md +193 -1
- package/templates/agent-docs/database/hosting.md +2 -2
- package/templates/agent-docs/database/migrations.md +88 -3
- package/templates/agent-docs/database/overview.md +11 -0
- package/templates/agent-docs/database/seedsdialects.md +53 -1
- package/templates/agent-docs/database/transactions.md +31 -0
- package/templates/agent-docs/internationalization.md +3 -3
- package/templates/agent-docs/introduction.md +133 -2
- package/templates/agent-docs/plugins.md +2 -1
- package/templates/agent-docs/reference.md +4 -4
- package/templates/agent-docs/routing.md +48 -43
- package/templates/agent-docs/schema-driven-ui.md +12 -2
- package/templates/agent-docs/templates/apibackends.md +1 -8
- package/templates/agent-docs/templates/appshells.md +24 -24
- package/templates/agent-docs/testing.md +40 -1
- package/templates/agent-docs/whats-new.md +51 -131
- package/templates/apps/api-ai/database/schema.ts +0 -2
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/database/schema.ts +0 -3
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/database/schema.ts +0 -1
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +1 -1
- package/templates/apps/api-backend-mail/database/schema.ts +0 -3
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/database/schema.ts +0 -3
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/database/schema.ts +0 -3
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/database/authors.entity.ts +0 -2
- package/templates/apps/api-data-advanced/database/books.entity.ts +0 -1
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-data-advanced/tests/queries.test.ts +6 -1
- package/templates/apps/api-durable/database/schema.ts +0 -3
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/database/schema.ts +0 -1
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/app.config.ts +0 -1
- package/templates/apps/api-governance/database/schema.ts +0 -1
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/database/schema.ts +0 -3
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-kv/tests/sync.test.ts +2 -2
- package/templates/apps/api-moderation/database/schema.ts +0 -2
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/database/schema.ts +0 -1
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/database/schema.ts +0 -1
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/README.md +53 -14
- package/templates/apps/api-rbac/app.config.ts +24 -26
- package/templates/apps/api-rbac/authz.ts +76 -0
- package/templates/apps/api-rbac/database/schema.ts +22 -7
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +7 -5
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +15 -8
- package/templates/apps/api-rbac/mutations/notes.delete.mutation.server.ts +54 -0
- package/templates/apps/api-rbac/mutations/notes.delete.mutation.ts +43 -0
- package/templates/apps/api-rbac/mutations/teams.rename.mutation.server.ts +19 -0
- package/templates/apps/api-rbac/mutations/teams.rename.mutation.ts +37 -0
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rbac/queries/notes.list.query.server.ts +12 -0
- package/templates/apps/api-rbac/queries/notes.list.query.ts +26 -0
- package/templates/apps/api-rbac/template.json +2 -2
- package/templates/apps/api-rbac/tests/authz.test.ts +233 -0
- package/templates/apps/api-rest/database/schema.ts +0 -3
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/database/schema.ts +0 -1
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/database/schema.ts +0 -1
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/database/schema.ts +0 -1
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/database/schema.ts +0 -1
- package/templates/apps/api-webhooks/package.json +9 -8
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/changelog/src/pages/[locale]/{[slug].tsx → [slug].page.tsx} +2 -2
- package/templates/apps/changelog/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/changelog/src/pages/{[slug].test.tsx → [slug].page.test.tsx} +1 -1
- package/templates/apps/changelog/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/pages/(marketing)/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-admin/src/pages/(marketing)/{login.test.tsx → login.page.test.tsx} +1 -1
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +1 -1
- package/templates/apps/frontend-admin/src/pages/admin/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-app/src/pages/{schema-ui.test.tsx → schema-ui.page.test.tsx} +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-contact/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-contact/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.test.tsx → login.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.test.tsx → settings.page.test.tsx} +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/{[...slug].tsx → [...slug].page.tsx} +3 -3
- package/templates/apps/frontend-docs/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/frontend-docs/src/pages/docs/{[...slug].test.tsx → [...slug].page.test.tsx} +1 -1
- package/templates/apps/frontend-docs/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-i18n/src/pages/[locale]/{about.tsx → about.page.tsx} +2 -2
- package/templates/apps/frontend-i18n/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/frontend-i18n/src/pages/{about.test.tsx → about.page.test.tsx} +1 -1
- package/templates/apps/frontend-i18n/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-landing/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +1 -1
- package/templates/apps/frontend-landing/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/pages/{feed-swr.test.tsx → feed-swr.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr/src/pages/{feed.test.tsx → feed.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/{[slug].tsx → [slug].page.tsx} +2 -2
- package/templates/apps/frontend-static-blog/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +2 -2
- package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].test.tsx → [slug].page.test.tsx} +1 -1
- package/templates/apps/frontend-static-blog/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
- package/dist/apiBuild-s7swuEU5.js +0 -2
- package/dist/dbCommand-Bt6BCG7g.js +0 -2
- package/dist/dev-YHbHmeXt.js +0 -3
- package/dist/seedRunner-DZmhoqNt.js +0 -821
- package/dist/serveCommand-B2-YV9TU.js +0 -1112
- package/templates/apps/api-rbac/tests/notes.create.test.ts +0 -80
- /package/templates/apps/changelog/src/pages/{[slug].tsx → [slug].page.tsx} +0 -0
- /package/templates/apps/changelog/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/(marketing)/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/(marketing)/{login.tsx → login.page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/admin/{[entity].tsx → [entity].page.tsx} +0 -0
- /package/templates/apps/frontend-admin/src/pages/admin/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-app/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-app/src/pages/{schema-ui.tsx → schema-ui.page.tsx} +0 -0
- /package/templates/apps/frontend-blank/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-contact/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.tsx → login.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.tsx → settings.page.tsx} +0 -0
- /package/templates/apps/frontend-docs/src/pages/docs/{[...slug].tsx → [...slug].page.tsx} +0 -0
- /package/templates/apps/frontend-docs/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-i18n/src/pages/{about.tsx → about.page.tsx} +0 -0
- /package/templates/apps/frontend-i18n/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-landing/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-spa/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr/src/pages/{feed-swr.tsx → feed-swr.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr/src/pages/{feed.tsx → feed.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-ssr-api/src/pages/{index.tsx → index.page.tsx} +0 -0
- /package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].tsx → [slug].page.tsx} +0 -0
- /package/templates/apps/frontend-static-blog/src/pages/{index.tsx → index.page.tsx} +0 -0
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { $ as e } from "./inspectMetrics-CyVMeofr.js";
|
|
2
|
+
import { t } from "./start-D8ATvTzo.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.14.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",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.107.0",
|
|
63
63
|
"@effect/sql": "^0.51.1",
|
|
64
64
|
"@effect/workflow": "^0.18.2",
|
|
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.
|
|
65
|
+
"@voltro/ai": "0.14.0",
|
|
66
|
+
"@voltro/cache": "0.14.0",
|
|
67
|
+
"@voltro/data-transfer": "0.14.0",
|
|
68
|
+
"@voltro/database": "0.14.0",
|
|
69
|
+
"@voltro/env": "0.14.0",
|
|
70
|
+
"@voltro/kv": "0.14.0",
|
|
71
|
+
"@voltro/logger": "0.14.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.14.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.14.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.14.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.14.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.14.0",
|
|
77
|
+
"@voltro/protocol": "0.14.0",
|
|
78
|
+
"@voltro/runtime": "0.14.0",
|
|
79
|
+
"@voltro/serverless": "0.14.0",
|
|
80
|
+
"@voltro/workflow": "0.14.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -219,7 +219,7 @@ a `web/` (frontend).
|
|
|
219
219
|
│ │ └── rpcGroup.generated.ts # codegen — do NOT edit
|
|
220
220
|
│ └── web/
|
|
221
221
|
│ ├── app.config.ts # { type:'web', apis, theme, locales, … }
|
|
222
|
-
│ └── src/pages/**/*.tsx # file-based routing
|
|
222
|
+
│ └── src/pages/**/*.page.tsx # file-based routing (only *.page.tsx)
|
|
223
223
|
├── packages/ # shared libs
|
|
224
224
|
└── AGENTS.md / CLAUDE.md # this file
|
|
225
225
|
```
|
|
@@ -249,7 +249,77 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
|
|
|
249
249
|
| `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
|
|
250
250
|
| `*.tool.tsx` | tool an agent can call |
|
|
251
251
|
| `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
|
|
252
|
-
| `src/pages
|
|
252
|
+
| `*.page.tsx` (under `src/pages/`) | web page (file-based route) + `*.page.test.tsx` |
|
|
253
|
+
| `*.component.tsx` | exactly ONE component (+ types) |
|
|
254
|
+
| `*.component.ui.tsx` | presentational: one component, READS only — never writes |
|
|
255
|
+
| `*.hook.ts` | exactly ONE `use*` hook (+ types) |
|
|
256
|
+
| `*.types.ts` | types only — zero runtime exports |
|
|
257
|
+
| `*.internal.ts` | only its own directory subtree may import it |
|
|
258
|
+
| `*.fixture.ts` | test material — no production path may reach it |
|
|
259
|
+
| `*.tracking.ts` | the ONLY place analytics may be called from |
|
|
260
|
+
| `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
|
|
261
|
+
| `*.client.ts` | declares itself + its transitive imports browser-safe |
|
|
262
|
+
|
|
263
|
+
**Only `*.page.tsx` is a route.** Everything else under `src/pages/` — components,
|
|
264
|
+
hooks, tests — is ordinary code with no URL, so put it next to the page that uses
|
|
265
|
+
it rather than in a distant `components/` directory. The suffix decides *whether*
|
|
266
|
+
a file is a route; the path under `src/pages/` decides *which URL*. A `*.page.tsx`
|
|
267
|
+
outside `src/pages/` never routes. A page MUST default-export its component —
|
|
268
|
+
without it `voltro dev` refuses to boot and names the file.
|
|
269
|
+
|
|
270
|
+
**Every suffix above is a CONTRACT, not a label.** Something else depends on each
|
|
271
|
+
promise, and `voltro doctor` enforces all of them:
|
|
272
|
+
|
|
273
|
+
- `*.component.ui.tsx` may read (`useT`, `useCan`, `usePermissions`) but must
|
|
274
|
+
never import a write hook (`useMutation`, `useAction`, `useUpload`, …). That is
|
|
275
|
+
what lets a caller render it ten thousand times in a list, reuse it across
|
|
276
|
+
features, and prerender it without reading its source. Lift the write into the
|
|
277
|
+
owning `*.component.tsx` and pass a handler down.
|
|
278
|
+
- `*.internal.ts` is the promise that refactoring inside that directory breaks
|
|
279
|
+
nobody. An import from another subtree revokes it.
|
|
280
|
+
- `*.types.ts` having no runtime export is what makes importing it free and makes
|
|
281
|
+
it impossible for it to sit in a runtime import cycle.
|
|
282
|
+
- `*.store.ts` holds ONE `defineStore`. Reads go through a selector
|
|
283
|
+
(`wizard.use((s) => s.step)`) — there is no whole-state read, because a
|
|
284
|
+
component holding all of it re-renders on every field. Scope a per-entity
|
|
285
|
+
instance with a KEY (`{ key: orderId }`), never a Provider. Seed it for SSR
|
|
286
|
+
from a loader with `seedStore(...)`; it rides the hydration payload the router
|
|
287
|
+
already writes. An action that writes SEVERAL fields wraps them in
|
|
288
|
+
`store.batch('label', () => …)` — one notification, one devtools entry, one
|
|
289
|
+
undo step, and a rollback if it throws; `await` first, then batch (an async
|
|
290
|
+
callback is an error). `{ persist: { key } }` survives a reload.
|
|
291
|
+
NEVER put server data in a store: a subscription is already
|
|
292
|
+
live, and a copy is not — the page then shows the stale one.
|
|
293
|
+
- `*.tracking.ts` is where every `defineTracking(...)` spec lives — the event
|
|
294
|
+
catalogue, so "what do we send to third parties" is a file listing rather than
|
|
295
|
+
archaeology. A component wires one up with `useTracking(spec, props, sink)`; it
|
|
296
|
+
NAMES a spec, it never declares one. (`useTracking` is a hook, so it is not
|
|
297
|
+
confined — only the declaration is.)
|
|
298
|
+
- Do NOT rename a file to `*.component.ui.tsx` just because it happens not to
|
|
299
|
+
write today. The suffix is a promise about what it MAY do.
|
|
300
|
+
|
|
301
|
+
**These rules apply to code YOU write — never to vendored code.** A shadcn
|
|
302
|
+
component (`npx shadcn add`) follows shadcn's conventions and is overwritten by
|
|
303
|
+
the next `add`; renaming it breaks their convention and is undone next run. A
|
|
304
|
+
directory is exempt when the app's `components.json` names it (`aliases.ui`) or
|
|
305
|
+
it carries a `.voltro-vendored` file whose first line says where the code came
|
|
306
|
+
from. Never add that marker to a directory of your own code — the point of the
|
|
307
|
+
rules is that everything we author follows them.
|
|
308
|
+
|
|
309
|
+
**Write the test in the same step as the code — always, for every primitive.**
|
|
310
|
+
Not "afterwards", not in a cleanup pass: the filename is derivable
|
|
311
|
+
(`users/[id].page.tsx` → `users/[id].page.test.tsx`,
|
|
312
|
+
`Card.component.tsx` → `Card.component.test.tsx`,
|
|
313
|
+
`order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
|
|
314
|
+
nothing to decide. A change is not finished until its test exists and passes.
|
|
315
|
+
|
|
316
|
+
Two traps worth naming, because a green run can hide both:
|
|
317
|
+
|
|
318
|
+
- **`voltro test` transpiles without type-checking.** A type error in a TEST file
|
|
319
|
+
passes the suite and fails the package's `typecheck`. Run both.
|
|
320
|
+
- **Never fake a service to make a test pass.** A test that only re-asserts its
|
|
321
|
+
own mock's return value is worse than no test — it reports coverage it does not
|
|
322
|
+
have. Assert the descriptor shape instead and cover the real path elsewhere.
|
|
253
323
|
|
|
254
324
|
## app.config.ts
|
|
255
325
|
|
|
@@ -344,9 +414,13 @@ declare it). Mixins compose via `.with(...)` and auto-stamp bookkeeping columns:
|
|
|
344
414
|
```ts
|
|
345
415
|
export const todos = table('todos', { id: id(), title: text(), done: boolean().default(false) })
|
|
346
416
|
.with(softDelete(), tenant()) // tenant() transitively requires audit()
|
|
347
|
-
.reactive() // opt into the matcher engine (subscriptions)
|
|
348
417
|
```
|
|
349
418
|
|
|
419
|
+
- Every table is REACTIVE by default — nothing to opt into. `.nonReactive()`
|
|
420
|
+
turns reactivity OFF for a table entirely: no subscriber fires, locally or
|
|
421
|
+
across instances (the write still happens). Use it for a hot table nobody
|
|
422
|
+
subscribes to; never for one a query reads — that subscription never fires,
|
|
423
|
+
and `voltro dev` warns.
|
|
350
424
|
- `id()` defaults to a branded **TypeID** (`todo_…`). Insert auto-injects it.
|
|
351
425
|
- Indexes are **table-level** (`.index('name', [...])`) — there is NO
|
|
352
426
|
column-level `.index()`. Declare `.index([...])` AFTER `.with(...)` to address
|
package/templates/AGENTS.md
CHANGED
|
@@ -219,7 +219,7 @@ a `web/` (frontend).
|
|
|
219
219
|
│ │ └── rpcGroup.generated.ts # codegen — do NOT edit
|
|
220
220
|
│ └── web/
|
|
221
221
|
│ ├── app.config.ts # { type:'web', apis, theme, locales, … }
|
|
222
|
-
│ └── src/pages/**/*.tsx # file-based routing
|
|
222
|
+
│ └── src/pages/**/*.page.tsx # file-based routing (only *.page.tsx)
|
|
223
223
|
├── packages/ # shared libs
|
|
224
224
|
└── AGENTS.md / CLAUDE.md # this file
|
|
225
225
|
```
|
|
@@ -249,7 +249,77 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
|
|
|
249
249
|
| `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
|
|
250
250
|
| `*.tool.tsx` | tool an agent can call |
|
|
251
251
|
| `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
|
|
252
|
-
| `src/pages
|
|
252
|
+
| `*.page.tsx` (under `src/pages/`) | web page (file-based route) + `*.page.test.tsx` |
|
|
253
|
+
| `*.component.tsx` | exactly ONE component (+ types) |
|
|
254
|
+
| `*.component.ui.tsx` | presentational: one component, READS only — never writes |
|
|
255
|
+
| `*.hook.ts` | exactly ONE `use*` hook (+ types) |
|
|
256
|
+
| `*.types.ts` | types only — zero runtime exports |
|
|
257
|
+
| `*.internal.ts` | only its own directory subtree may import it |
|
|
258
|
+
| `*.fixture.ts` | test material — no production path may reach it |
|
|
259
|
+
| `*.tracking.ts` | the ONLY place analytics may be called from |
|
|
260
|
+
| `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
|
|
261
|
+
| `*.client.ts` | declares itself + its transitive imports browser-safe |
|
|
262
|
+
|
|
263
|
+
**Only `*.page.tsx` is a route.** Everything else under `src/pages/` — components,
|
|
264
|
+
hooks, tests — is ordinary code with no URL, so put it next to the page that uses
|
|
265
|
+
it rather than in a distant `components/` directory. The suffix decides *whether*
|
|
266
|
+
a file is a route; the path under `src/pages/` decides *which URL*. A `*.page.tsx`
|
|
267
|
+
outside `src/pages/` never routes. A page MUST default-export its component —
|
|
268
|
+
without it `voltro dev` refuses to boot and names the file.
|
|
269
|
+
|
|
270
|
+
**Every suffix above is a CONTRACT, not a label.** Something else depends on each
|
|
271
|
+
promise, and `voltro doctor` enforces all of them:
|
|
272
|
+
|
|
273
|
+
- `*.component.ui.tsx` may read (`useT`, `useCan`, `usePermissions`) but must
|
|
274
|
+
never import a write hook (`useMutation`, `useAction`, `useUpload`, …). That is
|
|
275
|
+
what lets a caller render it ten thousand times in a list, reuse it across
|
|
276
|
+
features, and prerender it without reading its source. Lift the write into the
|
|
277
|
+
owning `*.component.tsx` and pass a handler down.
|
|
278
|
+
- `*.internal.ts` is the promise that refactoring inside that directory breaks
|
|
279
|
+
nobody. An import from another subtree revokes it.
|
|
280
|
+
- `*.types.ts` having no runtime export is what makes importing it free and makes
|
|
281
|
+
it impossible for it to sit in a runtime import cycle.
|
|
282
|
+
- `*.store.ts` holds ONE `defineStore`. Reads go through a selector
|
|
283
|
+
(`wizard.use((s) => s.step)`) — there is no whole-state read, because a
|
|
284
|
+
component holding all of it re-renders on every field. Scope a per-entity
|
|
285
|
+
instance with a KEY (`{ key: orderId }`), never a Provider. Seed it for SSR
|
|
286
|
+
from a loader with `seedStore(...)`; it rides the hydration payload the router
|
|
287
|
+
already writes. An action that writes SEVERAL fields wraps them in
|
|
288
|
+
`store.batch('label', () => …)` — one notification, one devtools entry, one
|
|
289
|
+
undo step, and a rollback if it throws; `await` first, then batch (an async
|
|
290
|
+
callback is an error). `{ persist: { key } }` survives a reload.
|
|
291
|
+
NEVER put server data in a store: a subscription is already
|
|
292
|
+
live, and a copy is not — the page then shows the stale one.
|
|
293
|
+
- `*.tracking.ts` is where every `defineTracking(...)` spec lives — the event
|
|
294
|
+
catalogue, so "what do we send to third parties" is a file listing rather than
|
|
295
|
+
archaeology. A component wires one up with `useTracking(spec, props, sink)`; it
|
|
296
|
+
NAMES a spec, it never declares one. (`useTracking` is a hook, so it is not
|
|
297
|
+
confined — only the declaration is.)
|
|
298
|
+
- Do NOT rename a file to `*.component.ui.tsx` just because it happens not to
|
|
299
|
+
write today. The suffix is a promise about what it MAY do.
|
|
300
|
+
|
|
301
|
+
**These rules apply to code YOU write — never to vendored code.** A shadcn
|
|
302
|
+
component (`npx shadcn add`) follows shadcn's conventions and is overwritten by
|
|
303
|
+
the next `add`; renaming it breaks their convention and is undone next run. A
|
|
304
|
+
directory is exempt when the app's `components.json` names it (`aliases.ui`) or
|
|
305
|
+
it carries a `.voltro-vendored` file whose first line says where the code came
|
|
306
|
+
from. Never add that marker to a directory of your own code — the point of the
|
|
307
|
+
rules is that everything we author follows them.
|
|
308
|
+
|
|
309
|
+
**Write the test in the same step as the code — always, for every primitive.**
|
|
310
|
+
Not "afterwards", not in a cleanup pass: the filename is derivable
|
|
311
|
+
(`users/[id].page.tsx` → `users/[id].page.test.tsx`,
|
|
312
|
+
`Card.component.tsx` → `Card.component.test.tsx`,
|
|
313
|
+
`order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
|
|
314
|
+
nothing to decide. A change is not finished until its test exists and passes.
|
|
315
|
+
|
|
316
|
+
Two traps worth naming, because a green run can hide both:
|
|
317
|
+
|
|
318
|
+
- **`voltro test` transpiles without type-checking.** A type error in a TEST file
|
|
319
|
+
passes the suite and fails the package's `typecheck`. Run both.
|
|
320
|
+
- **Never fake a service to make a test pass.** A test that only re-asserts its
|
|
321
|
+
own mock's return value is worse than no test — it reports coverage it does not
|
|
322
|
+
have. Assert the descriptor shape instead and cover the real path elsewhere.
|
|
253
323
|
|
|
254
324
|
## app.config.ts
|
|
255
325
|
|
|
@@ -344,9 +414,13 @@ declare it). Mixins compose via `.with(...)` and auto-stamp bookkeeping columns:
|
|
|
344
414
|
```ts
|
|
345
415
|
export const todos = table('todos', { id: id(), title: text(), done: boolean().default(false) })
|
|
346
416
|
.with(softDelete(), tenant()) // tenant() transitively requires audit()
|
|
347
|
-
.reactive() // opt into the matcher engine (subscriptions)
|
|
348
417
|
```
|
|
349
418
|
|
|
419
|
+
- Every table is REACTIVE by default — nothing to opt into. `.nonReactive()`
|
|
420
|
+
turns reactivity OFF for a table entirely: no subscriber fires, locally or
|
|
421
|
+
across instances (the write still happens). Use it for a hot table nobody
|
|
422
|
+
subscribes to; never for one a query reads — that subscription never fires,
|
|
423
|
+
and `voltro dev` warns.
|
|
350
424
|
- `id()` defaults to a branded **TypeID** (`todo_…`). Insert auto-injects it.
|
|
351
425
|
- Indexes are **table-level** (`.index('name', [...])`) — there is NO
|
|
352
426
|
column-level `.index()`. Declare `.index([...])` AFTER `.with(...)` to address
|
|
@@ -452,7 +526,7 @@ each plugin's own README.
|
|
|
452
526
|
|
|
453
527
|
| Topic | Open | Summary |
|
|
454
528
|
|---|---|---|
|
|
455
|
-
| **What's new in 0.
|
|
529
|
+
| **What's new in 0.13.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. |
|
|
456
530
|
| 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. |
|
|
457
531
|
| 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. |
|
|
458
532
|
| 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.13.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. |
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"group": null,
|
|
54
54
|
"description": "How Voltro's reactive data layer works — queries, mutations, actions, streams, all over one WebSocket with typed errors and tracked dependencies.",
|
|
55
55
|
"path": "agent-docs/data.md",
|
|
56
|
-
"files":
|
|
56
|
+
"files": 17
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"id": "database/advancedqueries",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"group": null,
|
|
180
180
|
"description": "Scaffold a Voltro project and boot it locally in under a minute.",
|
|
181
181
|
"path": "agent-docs/introduction.md",
|
|
182
|
-
"files":
|
|
182
|
+
"files": 6
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"id": "multi-tenancy",
|
|
@@ -563,6 +563,45 @@ const resolve = composeAuthStrategies(
|
|
|
563
563
|
|
|
564
564
|
Order matters: put the cheapest / most-common strategy first. When no strategy matches, the resolver returns an [anonymous Subject](/docs/authentication/subject) scoped to the `x-tenant` header (or a custom `fallback` you supply).
|
|
565
565
|
|
|
566
|
+
## Roles from your database — `auth.resolveScopes`
|
|
567
|
+
|
|
568
|
+
If your authorization is a database ROLE rather than a scope on the token, the framework cannot see it. `voltro check`'s `rbac/unguarded-mutation` reports every such write as unguarded — correctly, because nothing about the decision is declared — and the declarative alternative is unusable for you: subjects that come from an external IdP carry no scopes, so `requireScope('employee:admin')` would lock out every real user. One app measured 1566 findings it had no way to act on.
|
|
569
|
+
|
|
570
|
+
`resolveScopes` closes that. It runs after a strategy matches and adds scopes to the resolved Subject from whatever source you like:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
// app.config.ts
|
|
574
|
+
export default defineApiConfig({
|
|
575
|
+
auth: {
|
|
576
|
+
resolveScopes: async (subject, { store }) => {
|
|
577
|
+
if (store === undefined) return [] // still booting — claim nothing
|
|
578
|
+
const role = await roleCache.get(subject.id, store) // cache it — see below
|
|
579
|
+
return role === 'admin' ? ['employee:admin', 'employee:read'] : ['employee:read']
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
})
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The same authorization is now declarable on the descriptor:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
export const payrollList = defineQuery({
|
|
589
|
+
name: 'payroll.list',
|
|
590
|
+
guards: [requireScope('employee:admin')], // visible in the manifest, checkable in CI
|
|
591
|
+
…
|
|
592
|
+
})
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**You get the app's DataStore.** A role lives in the database, and without it the only way to reach one was a second connection path beside the framework's — to the same database the request store opens a moment later. It is the BOOT store, not a request-scoped one: strategies resolve before a request store exists, so it is `undefined` while the store is still being built. Return `[]` then rather than guessing.
|
|
596
|
+
|
|
597
|
+
**Scopes only — never a Subject.** The hook cannot change `id` or `tenantId`: identity belongs to the auth strategy, and a hook that could rewrite it would be a forgery surface. The returned scopes are UNIONED with whatever the strategy already set, so a resolver can grant authority but never revoke it.
|
|
598
|
+
|
|
599
|
+
**It does not run for anonymous callers** — there is no identity to look a role up for.
|
|
600
|
+
|
|
601
|
+
**Cache it yourself.** It is on the request path. A `Map` keyed by subject id with a short TTL is usually enough. The framework deliberately does not cache for you, because only you know how quickly a role change has to take effect.
|
|
602
|
+
|
|
603
|
+
Wired identically under `voltro dev` and `voltro serve`.
|
|
604
|
+
|
|
566
605
|
## Wiring it into the app
|
|
567
606
|
|
|
568
607
|
The composed resolver becomes the runtime's `AuthMiddleware` — the per-request middleware that populates `SubjectService` so every handler can `yield* SubjectService` (or read `ctx.subject`). On a single-strategy password app you never touch this; the plugin wires `voltroPasswordStrategy` for you. You only assemble the chain explicitly when you add a second strategy:
|
|
@@ -154,6 +154,20 @@ requiring a scope no role grants, which makes that procedure permanently and
|
|
|
154
154
|
silently uncallable. (With a custom `resolvePermissions` the vocabulary isn't
|
|
155
155
|
exhaustive, so the rule stays quiet rather than flagging correct code.)
|
|
156
156
|
|
|
157
|
+
`rbac/unenforced-scope` is the same registry read in the other direction: a
|
|
158
|
+
scope a role GRANTS that no handler ever guards on. That direction has no
|
|
159
|
+
artefact to inspect — you cannot grep for an authorization check that was never
|
|
160
|
+
written, which is exactly why it survives review. One app modelled
|
|
161
|
+
`api-keys:write` in its role catalogue, complete and reviewed, and no handler
|
|
162
|
+
checked it: any member could mint a shared credential, and nothing failed.
|
|
163
|
+
Tests pass when an authorization check is missing.
|
|
164
|
+
|
|
165
|
+
It is a **warning**, because three innocent explanations exist: a plugin route
|
|
166
|
+
enforces it internally (the graph cannot see inside a plugin), a REST route
|
|
167
|
+
carries its own guards, or it is a UI-affordance scope that `useCan` reads to
|
|
168
|
+
hide a button and no server check backs on purpose. All three are fine. Not
|
|
169
|
+
knowing which is not.
|
|
170
|
+
|
|
157
171
|
#### Declared vs OBSERVED — reconciled against reality
|
|
158
172
|
|
|
159
173
|
A query's `source` and a mutation's `targets` are not documentation: the
|
|
@@ -163,7 +177,7 @@ nothing type-checks — the mutation succeeds, the write lands, and the wrong li
|
|
|
163
177
|
fails to update.
|
|
164
178
|
|
|
165
179
|
`voltro dev` records what each procedure ACTUALLY touched, into
|
|
166
|
-
`app.graph.observed.json` (gitignored automatically). When that file is present,
|
|
180
|
+
`app.graph.observed.generated.json` (gitignored automatically). When that file is present,
|
|
167
181
|
`check` diffs it against the declarations:
|
|
168
182
|
|
|
169
183
|
```
|
|
@@ -447,6 +461,45 @@ _voltro dev, codegen, agents-md — what runs during local development and the e
|
|
|
447
461
|
|
|
448
462
|
`voltro dev` is the day-to-day command. It runs different machinery for api vs web apps but the contract is the same: edit a file, the right thing happens.
|
|
449
463
|
|
|
464
|
+
### Running `voltro dev` in a container
|
|
465
|
+
|
|
466
|
+
If your dev pod runs as root with the host workspace bind-mounted, everything
|
|
467
|
+
the framework generates would otherwise land `root:root` inside your own tree —
|
|
468
|
+
and on the host `voltro build` then fails on its own output:
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
EACCES: permission denied, open '…/apps/display/.framework/index.html'
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
`voltro dev` and `voltro build` hand their generated output (`.framework`,
|
|
475
|
+
`.env.local`, every `*.generated.*`) to whoever owns the app root, and warn
|
|
476
|
+
loudly when they cannot. Only generated state — the framework never takes
|
|
477
|
+
ownership of a file you wrote.
|
|
478
|
+
|
|
479
|
+
The cleaner fix is on your side and worth doing anyway: start the container as
|
|
480
|
+
the workspace owner, `docker run --user $(id -u):$(id -g)`. Then nothing needs
|
|
481
|
+
handing over at all.
|
|
482
|
+
|
|
483
|
+
### What the boot tells you
|
|
484
|
+
|
|
485
|
+
Three checks run at boot and print one line each when they have something to say
|
|
486
|
+
— never fatal, and silent when the answer is fine:
|
|
487
|
+
|
|
488
|
+
- **A reactive table with no change trigger** (postgres). The schema fingerprint
|
|
489
|
+
covers columns, not triggers, so a restored dump or a hand-run `DROP TRIGGER`
|
|
490
|
+
leaves the schema "up to date" and the subscription silently not reaching other
|
|
491
|
+
instances. The reverse is reported too: a `.nonReactive()` table still carrying
|
|
492
|
+
a trigger pays `REPLICA IDENTITY FULL` and a NOTIFY per write for nothing.
|
|
493
|
+
- **A `.nonReactive()` table that a query reads.** That subscription will never
|
|
494
|
+
fire — first snapshot, then silence forever.
|
|
495
|
+
- **`apiKeys: true` with no `apikeys:issue:*` scope declared.** The capability is
|
|
496
|
+
on and reachable by nobody; every issue request fails its guard.
|
|
497
|
+
|
|
498
|
+
`voltro doctor` adds a fourth, over your source: an executor that writes
|
|
499
|
+
`ctx.request.subject.id` while naming no guard, which is how an anonymous caller
|
|
500
|
+
reaches a NOT NULL column and gets a raw statement failure instead of a typed
|
|
501
|
+
refusal.
|
|
502
|
+
|
|
450
503
|
## `voltro dev <appDir>`
|
|
451
504
|
|
|
452
505
|
```bash
|
|
@@ -885,9 +938,31 @@ exactly the apps that already adopted it, and misses the ones that never did.
|
|
|
885
938
|
Those are usually the same apps that secured least elsewhere, which is why the
|
|
886
939
|
absence is worth naming out loud rather than assuming the default did its job.
|
|
887
940
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
941
|
+
The rule follows the IMPORT GRAPH, not the filename. Server-convention files
|
|
942
|
+
(`*.server.ts`, `*.cron.ts`, `*.subscribe.ts`, …) are the starting points, and
|
|
943
|
+
any file reachable from them and from **nothing else** counts as server code
|
|
944
|
+
too. That matters: keyed on filenames alone the rule caught 9 of 39 outbound
|
|
945
|
+
calls on the app that reported it — the other 30 sat in `lib/*.ts` helpers
|
|
946
|
+
(payments, an AI provider, TTS) imported only from server executors. A
|
|
947
|
+
`lib/payments-mollie.ts` is not client code, and no file extension can say so.
|
|
948
|
+
|
|
949
|
+
A helper a page ALSO imports stays unflagged, and that is the property keeping
|
|
950
|
+
this rule useful: `fetch` is unremarkable in a browser component, and flagging it
|
|
951
|
+
there would make the rule noise that gets scrolled past — taking the real
|
|
952
|
+
findings with it. Relative imports and your tsconfig `paths` aliases are both
|
|
953
|
+
followed.
|
|
954
|
+
|
|
955
|
+
The detector also flags an **executor that never names its own descriptor**.
|
|
956
|
+
Pairing is by FILENAME, which is right — and it means a `*.server.ts` can be a
|
|
957
|
+
complete, correct executor with no reference at all to the contract it
|
|
958
|
+
implements. Those are exactly the files where a hand-written input drifts from
|
|
959
|
+
the wire: in one reported codebase, six executors declared `boardPurpose: string`
|
|
960
|
+
where their own descriptor said `Schema.Literal(...)`, discarding the contract at
|
|
961
|
+
the executor boundary. Fix by importing the descriptor and typing the input as
|
|
962
|
+
`ExecutorInput<typeof descriptor>`. Only a SIBLING import clears the finding —
|
|
963
|
+
an executor importing nothing but `@voltro/*` and `node:*` has still not named
|
|
964
|
+
its contract.
|
|
965
|
+
|
|
891
966
|
|
|
892
967
|
### The `workflows.start` audit
|
|
893
968
|
|
|
@@ -1014,6 +1089,39 @@ matching rule lives inside the CLI, so you cannot re-derive the list with your
|
|
|
1014
1089
|
own grep. `--json` prints the complete scan, nothing elided, with no preflight
|
|
1015
1090
|
output mixed in:
|
|
1016
1091
|
|
|
1092
|
+
### Duplicate package instances
|
|
1093
|
+
|
|
1094
|
+
`voltro doctor` also reports any identity-sensitive package resolved at more than
|
|
1095
|
+
one version — `effect`, `@effect/*`, `@voltro/*`, react/react-dom:
|
|
1096
|
+
|
|
1097
|
+
```
|
|
1098
|
+
• 1 package(s) resolved at more than one version:
|
|
1099
|
+
effect — 3.18.4, 3.21.0
|
|
1100
|
+
node_modules/effect
|
|
1101
|
+
../../node_modules/effect
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
This is worth its own check because of how it PRESENTS. Effect's types are
|
|
1105
|
+
nominal, so a `Schema` built by one copy is not the type the other expects, and
|
|
1106
|
+
the errors land in the GENERATED `rpcGroup.generated.ts` — a file you cannot edit
|
|
1107
|
+
and did not write:
|
|
1108
|
+
|
|
1109
|
+
```
|
|
1110
|
+
Property '[TypeId]' is missing in type … Schema<any, any, unknown>
|
|
1111
|
+
Type 'typeof Never' is not assignable to type 'All'
|
|
1112
|
+
Argument of type 'Rpc<…, Stream<…>, …>' is not assignable to 'Any'
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
Read cold, that says "the framework emits bad types". It says nothing about the
|
|
1116
|
+
dependency tree, which is where the problem is. And the RUNTIME usually stays
|
|
1117
|
+
green — two instances only diverge where identity matters — so the app boots,
|
|
1118
|
+
serves and passes its tests while `tsc` is red.
|
|
1119
|
+
|
|
1120
|
+
Fix it in the install, not the code: align the version across the workspace (a
|
|
1121
|
+
root `pnpm.overrides` / `resolutions` entry for `effect` is the blunt
|
|
1122
|
+
instrument), then reinstall. Do NOT add `@ts-nocheck` to the generated file — it
|
|
1123
|
+
is exactly where a genuine mistake in your own descriptors surfaces.
|
|
1124
|
+
|
|
1017
1125
|
```bash
|
|
1018
1126
|
voltro doctor . --json # the complete scan: every file path, machine-readable
|
|
1019
1127
|
```
|
|
@@ -1064,8 +1172,8 @@ A page is listed when ALL of these hold:
|
|
|
1064
1172
|
|
|
1065
1173
|
```text
|
|
1066
1174
|
• renderMode:'spa' candidates (2 pages — loader-free, under a layout, currently ssr/default):
|
|
1067
|
-
src/pages/dashboard/index.tsx (/dashboard) — default renderMode
|
|
1068
|
-
src/pages/admin/settings.tsx (/admin/settings) — renderMode:'ssr'
|
|
1175
|
+
src/pages/dashboard/index.page.tsx (/dashboard) — default renderMode
|
|
1176
|
+
src/pages/admin/settings.page.tsx (/admin/settings) — renderMode:'ssr'
|
|
1069
1177
|
→ renderMode:'spa' skips this page's SSR compile while its layout shell still renders server-side — adopt it if the page BODY does not need SSR (internal/authenticated pages); keep 'ssr' if the page content needs SEO or server first-paint.
|
|
1070
1178
|
```
|
|
1071
1179
|
|
|
@@ -326,6 +326,54 @@ const issued = await svc.issue({ tenantId, name: 'mobile app', scopes: ['read']
|
|
|
326
326
|
// show issued.token ONCE; later: svc.rotate(id), svc.revoke(id), svc.list(tenantId)
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
+
## The second ownership axis — `metadata`
|
|
330
|
+
|
|
331
|
+
`tenantId` and `onBehalfOf` are the two relationships the framework models. If
|
|
332
|
+
your keys also belong to something else — a team, a project, an environment —
|
|
333
|
+
and that binding is what authorizes them, store it in `metadata`:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
const key = await keys.issue({
|
|
337
|
+
tenantId: ctx.request.subject.tenantId,
|
|
338
|
+
name: 'CI deploy',
|
|
339
|
+
createdBy: ctx.request.subject.id, // who minted it
|
|
340
|
+
onBehalfOf: null, // an ORG key: acts as no person
|
|
341
|
+
metadata: { teamId: 'team_7' }, // your axis
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
It comes straight back on resolve, so a guard needs no second query:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
const resolved = await keys.verify(token)
|
|
349
|
+
resolved?.metadata // { teamId: 'team_7' }
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
It survives `rotate` — a rotated key is the same credential with a new secret,
|
|
353
|
+
so dropping it would silently de-authorize every rotated key. And it reaches the
|
|
354
|
+
Subject as `metadata`, alongside the framework's own claims.
|
|
355
|
+
|
|
356
|
+
**It is app data, never identity.** The strategy merges your bag UNDER its own
|
|
357
|
+
claims: `provider`, and the acting `userId`, are written afterwards from
|
|
358
|
+
`onBehalfOf` and always win — including when the answer is "none". A bag that
|
|
359
|
+
could set `userId` would let whoever minted a key choose who the request is.
|
|
360
|
+
|
|
361
|
+
Before this slot existed, an app with a team axis could authenticate through the
|
|
362
|
+
built-in strategy and still not authorize, so `apiKeys: true` was unusable for
|
|
363
|
+
it. The alternatives people reached for were a second table joined on every auth
|
|
364
|
+
check, or `team:<id>` smuggled into `scopes` — where `hasScope` then sees a scope
|
|
365
|
+
that is not a scope.
|
|
366
|
+
|
|
367
|
+
## Two strategies, one prefix
|
|
368
|
+
|
|
369
|
+
If your app already runs its own key strategy on a prefix and you then enable
|
|
370
|
+
`apiKeys: true`, both claim the same shape. The chain is first-match-wins, so the
|
|
371
|
+
first one decides the Subject — and if they resolve to different authority,
|
|
372
|
+
*which strategy answered* decides whether authorization works.
|
|
373
|
+
|
|
374
|
+
`voltro dev` / `voltro serve` warn at boot when this happens. Give them distinct
|
|
375
|
+
prefixes (`apiKeys: { prefix: 'vk_' }`) or drop one.
|
|
376
|
+
|
|
329
377
|
## Security model
|
|
330
378
|
|
|
331
379
|
- **Hash-only storage.** A DB dump never exposes a usable key — only `sha256(token)`. Lose a token → rotate it (`rotate` revokes the old + issues a fresh one with the same scopes).
|