@voltro/cli 0.12.0 → 0.13.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 +93 -0
- package/dist/{apiBuild-GefjNI5d.js → apiBuild-B6_ehbem.js} +3 -3
- package/dist/apiBuild-Bzpb383i.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-BmNi1RgZ.js → commands-DYOuGlEC.js} +1668 -1371
- package/dist/dbCommand-ADD4mrTI.js +2 -0
- package/dist/{dbCommand-DNrkb58g.js → dbCommand-DIOJQwsq.js} +71 -57
- package/dist/{dev-d0LjACxE.js → dev-Fb9BmavX.js} +1483 -1414
- package/dist/dev-xdijqojc.js +3 -0
- package/dist/{seedRunner-DZmhoqNt.js → frameworkTableAssembly-CYlrafoK.js} +110 -331
- package/dist/frameworkTableAssembly-XvKO7wQt.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-uEfIS1vp.js → inspectMetrics-DOiiII34.js} +302 -293
- package/dist/seedRunner-D6eu-u5U.js +229 -0
- package/dist/{serveCommand-B2-YV9TU.js → serveCommand-CqYxzBZR.js} +368 -367
- package/dist/serveEntry.js +3 -3
- package/dist/{start-DKcQd52a.js → start-CjG_0xhR.js} +1 -1
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/authentication.md +36 -0
- package/templates/agent-docs/cli.md +78 -4
- package/templates/agent-docs/configuration.md +48 -0
- package/templates/agent-docs/database/migrations.md +32 -0
- package/templates/agent-docs/database/transactions.md +31 -0
- package/templates/agent-docs/testing.md +24 -0
- package/templates/agent-docs/whats-new.md +41 -137
- package/templates/apps/api-ai/package.json +7 -7
- 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-deactivation/tests/users.deactivate.test.ts +1 -1
- 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-storage/package.json +8 -8
- 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/package.json +8 -8
- 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/package.json +8 -8
- 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/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/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 +23 -6
- 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/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -8
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-s7swuEU5.js +0 -2
- package/dist/dbCommand-Bt6BCG7g.js +0 -2
- package/dist/dev-YHbHmeXt.js +0 -3
- package/templates/apps/api-rbac/tests/notes.create.test.ts +0 -80
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { X as e } from "./inspectMetrics-
|
|
2
|
-
import {
|
|
1
|
+
import { X as e } from "./inspectMetrics-DOiiII34.js";
|
|
2
|
+
import { c as t } from "./seedRunner-D6eu-u5U.js";
|
|
3
3
|
import { r as n } from "./appModuleLoader-C9r9mxZt.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-CqYxzBZR.js";
|
|
5
5
|
export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as e, A as t, C as n, E as r, G as i, H as a, I as o, J as s, N as c, O as l, Q as u, R as ee, U as d, V as f, W as p, _ as m, a as h, at as g, b as _, c as v, ct as y, et as b, f as te, g as x, h as S, i as C, j as w, k as ne, m as T, nt as re, o as ie, p as E, q as D, r as O, s as k, t as A, tt as j, v as M, w as ae, y as N } from "./inspectMetrics-
|
|
1
|
+
import { $ as e, A as t, C as n, E as r, G as i, H as a, I as o, J as s, N as c, O as l, Q as u, R as ee, U as d, V as f, W as p, _ as m, a as h, at as g, b as _, c as v, ct as y, et as b, f as te, g as x, h as S, i as C, j as w, k as ne, m as T, nt as re, o as ie, p as E, q as D, r as O, s as k, t as A, tt as j, v as M, w as ae, y as N } from "./inspectMetrics-DOiiII34.js";
|
|
2
2
|
import { D as oe, E as se, T as ce, a as P, p as le, w as ue } from "./inspect-Dwx0_tUj.js";
|
|
3
3
|
import { t as de } from "./bootTiming-BdyP9nYw.js";
|
|
4
4
|
import { dirname as fe, extname as F, join as I, resolve as L } from "node:path";
|
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { X as e } from "./inspectMetrics-
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { X as e } from "./inspectMetrics-DOiiII34.js";
|
|
2
|
+
import { t } from "./start-CjG_0xhR.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.13.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.13.0",
|
|
66
|
+
"@voltro/cache": "0.13.0",
|
|
67
|
+
"@voltro/data-transfer": "0.13.0",
|
|
68
|
+
"@voltro/database": "0.13.0",
|
|
69
|
+
"@voltro/env": "0.13.0",
|
|
70
|
+
"@voltro/kv": "0.13.0",
|
|
71
|
+
"@voltro/logger": "0.13.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.13.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.13.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.13.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.13.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.13.0",
|
|
77
|
+
"@voltro/protocol": "0.13.0",
|
|
78
|
+
"@voltro/runtime": "0.13.0",
|
|
79
|
+
"@voltro/serverless": "0.13.0",
|
|
80
|
+
"@voltro/workflow": "0.13.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -452,7 +452,7 @@ each plugin's own README.
|
|
|
452
452
|
|
|
453
453
|
| Topic | Open | Summary |
|
|
454
454
|
|---|---|---|
|
|
455
|
-
| **What's new in 0.
|
|
455
|
+
| **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
456
|
| 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
457
|
| 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
458
|
| 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. |
|
|
@@ -563,6 +563,42 @@ 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) => {
|
|
577
|
+
const role = await roleCache.get(subject.id) // cache it — see below
|
|
578
|
+
return role === 'admin' ? ['employee:admin', 'employee:read'] : ['employee:read']
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
})
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The same authorization is now declarable on the descriptor:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
export const payrollList = defineQuery({
|
|
588
|
+
name: 'payroll.list',
|
|
589
|
+
guards: [requireScope('employee:admin')], // visible in the manifest, checkable in CI
|
|
590
|
+
…
|
|
591
|
+
})
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**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.
|
|
595
|
+
|
|
596
|
+
**It does not run for anonymous callers** — there is no identity to look a role up for.
|
|
597
|
+
|
|
598
|
+
**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.
|
|
599
|
+
|
|
600
|
+
Wired identically under `voltro dev` and `voltro serve`.
|
|
601
|
+
|
|
566
602
|
## Wiring it into the app
|
|
567
603
|
|
|
568
604
|
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:
|
|
@@ -163,7 +163,7 @@ nothing type-checks — the mutation succeeds, the write lands, and the wrong li
|
|
|
163
163
|
fails to update.
|
|
164
164
|
|
|
165
165
|
`voltro dev` records what each procedure ACTUALLY touched, into
|
|
166
|
-
`app.graph.observed.json` (gitignored automatically). When that file is present,
|
|
166
|
+
`app.graph.observed.generated.json` (gitignored automatically). When that file is present,
|
|
167
167
|
`check` diffs it against the declarations:
|
|
168
168
|
|
|
169
169
|
```
|
|
@@ -447,6 +447,25 @@ _voltro dev, codegen, agents-md — what runs during local development and the e
|
|
|
447
447
|
|
|
448
448
|
`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
449
|
|
|
450
|
+
### Running `voltro dev` in a container
|
|
451
|
+
|
|
452
|
+
If your dev pod runs as root with the host workspace bind-mounted, everything
|
|
453
|
+
the framework generates would otherwise land `root:root` inside your own tree —
|
|
454
|
+
and on the host `voltro build` then fails on its own output:
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
EACCES: permission denied, open '…/apps/display/.framework/index.html'
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
`voltro dev` and `voltro build` hand their generated output (`.framework`,
|
|
461
|
+
`.env.local`, every `*.generated.*`) to whoever owns the app root, and warn
|
|
462
|
+
loudly when they cannot. Only generated state — the framework never takes
|
|
463
|
+
ownership of a file you wrote.
|
|
464
|
+
|
|
465
|
+
The cleaner fix is on your side and worth doing anyway: start the container as
|
|
466
|
+
the workspace owner, `docker run --user $(id -u):$(id -g)`. Then nothing needs
|
|
467
|
+
handing over at all.
|
|
468
|
+
|
|
450
469
|
## `voltro dev <appDir>`
|
|
451
470
|
|
|
452
471
|
```bash
|
|
@@ -885,9 +904,31 @@ exactly the apps that already adopted it, and misses the ones that never did.
|
|
|
885
904
|
Those are usually the same apps that secured least elsewhere, which is why the
|
|
886
905
|
absence is worth naming out loud rather than assuming the default did its job.
|
|
887
906
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
907
|
+
The rule follows the IMPORT GRAPH, not the filename. Server-convention files
|
|
908
|
+
(`*.server.ts`, `*.cron.ts`, `*.subscribe.ts`, …) are the starting points, and
|
|
909
|
+
any file reachable from them and from **nothing else** counts as server code
|
|
910
|
+
too. That matters: keyed on filenames alone the rule caught 9 of 39 outbound
|
|
911
|
+
calls on the app that reported it — the other 30 sat in `lib/*.ts` helpers
|
|
912
|
+
(payments, an AI provider, TTS) imported only from server executors. A
|
|
913
|
+
`lib/payments-mollie.ts` is not client code, and no file extension can say so.
|
|
914
|
+
|
|
915
|
+
A helper a page ALSO imports stays unflagged, and that is the property keeping
|
|
916
|
+
this rule useful: `fetch` is unremarkable in a browser component, and flagging it
|
|
917
|
+
there would make the rule noise that gets scrolled past — taking the real
|
|
918
|
+
findings with it. Relative imports and your tsconfig `paths` aliases are both
|
|
919
|
+
followed.
|
|
920
|
+
|
|
921
|
+
The detector also flags an **executor that never names its own descriptor**.
|
|
922
|
+
Pairing is by FILENAME, which is right — and it means a `*.server.ts` can be a
|
|
923
|
+
complete, correct executor with no reference at all to the contract it
|
|
924
|
+
implements. Those are exactly the files where a hand-written input drifts from
|
|
925
|
+
the wire: in one reported codebase, six executors declared `boardPurpose: string`
|
|
926
|
+
where their own descriptor said `Schema.Literal(...)`, discarding the contract at
|
|
927
|
+
the executor boundary. Fix by importing the descriptor and typing the input as
|
|
928
|
+
`ExecutorInput<typeof descriptor>`. Only a SIBLING import clears the finding —
|
|
929
|
+
an executor importing nothing but `@voltro/*` and `node:*` has still not named
|
|
930
|
+
its contract.
|
|
931
|
+
|
|
891
932
|
|
|
892
933
|
### The `workflows.start` audit
|
|
893
934
|
|
|
@@ -1014,6 +1055,39 @@ matching rule lives inside the CLI, so you cannot re-derive the list with your
|
|
|
1014
1055
|
own grep. `--json` prints the complete scan, nothing elided, with no preflight
|
|
1015
1056
|
output mixed in:
|
|
1016
1057
|
|
|
1058
|
+
### Duplicate package instances
|
|
1059
|
+
|
|
1060
|
+
`voltro doctor` also reports any identity-sensitive package resolved at more than
|
|
1061
|
+
one version — `effect`, `@effect/*`, `@voltro/*`, react/react-dom:
|
|
1062
|
+
|
|
1063
|
+
```
|
|
1064
|
+
• 1 package(s) resolved at more than one version:
|
|
1065
|
+
effect — 3.18.4, 3.21.0
|
|
1066
|
+
node_modules/effect
|
|
1067
|
+
../../node_modules/effect
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
This is worth its own check because of how it PRESENTS. Effect's types are
|
|
1071
|
+
nominal, so a `Schema` built by one copy is not the type the other expects, and
|
|
1072
|
+
the errors land in the GENERATED `rpcGroup.generated.ts` — a file you cannot edit
|
|
1073
|
+
and did not write:
|
|
1074
|
+
|
|
1075
|
+
```
|
|
1076
|
+
Property '[TypeId]' is missing in type … Schema<any, any, unknown>
|
|
1077
|
+
Type 'typeof Never' is not assignable to type 'All'
|
|
1078
|
+
Argument of type 'Rpc<…, Stream<…>, …>' is not assignable to 'Any'
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
Read cold, that says "the framework emits bad types". It says nothing about the
|
|
1082
|
+
dependency tree, which is where the problem is. And the RUNTIME usually stays
|
|
1083
|
+
green — two instances only diverge where identity matters — so the app boots,
|
|
1084
|
+
serves and passes its tests while `tsc` is red.
|
|
1085
|
+
|
|
1086
|
+
Fix it in the install, not the code: align the version across the workspace (a
|
|
1087
|
+
root `pnpm.overrides` / `resolutions` entry for `effect` is the blunt
|
|
1088
|
+
instrument), then reinstall. Do NOT add `@ts-nocheck` to the generated file — it
|
|
1089
|
+
is exactly where a genuine mistake in your own descriptors surfaces.
|
|
1090
|
+
|
|
1017
1091
|
```bash
|
|
1018
1092
|
voltro doctor . --json # the complete scan: every file path, machine-readable
|
|
1019
1093
|
```
|
|
@@ -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).
|
|
@@ -2600,6 +2600,38 @@ export const users = table('users', {
|
|
|
2600
2600
|
|
|
2601
2601
|
The planner folds the diff into one `ALTER TABLE users RENAME COLUMN firstName TO givenName`, classified `safe`. After the rename is applied in every env, the marker can be removed (covered in [rename-and-drop.md](./rename-and-drop.md)).
|
|
2602
2602
|
|
|
2603
|
+
## "the migration did not converge" (apply refuses to record a fingerprint)
|
|
2604
|
+
|
|
2605
|
+
```
|
|
2606
|
+
applyPlan: the migration did not converge. 31 operation(s) were executed without
|
|
2607
|
+
error, but re-planning against the live schema still finds 31:
|
|
2608
|
+
- alter-column-default todos.attachments
|
|
2609
|
+
…
|
|
2610
|
+
No fingerprint was recorded — recording one would make the next boot report
|
|
2611
|
+
"schema up to date" for a schema that was never applied.
|
|
2612
|
+
```
|
|
2613
|
+
|
|
2614
|
+
Every statement ran and the database accepted every one of them, and none of them
|
|
2615
|
+
changed anything. That is possible because DDL that changes nothing succeeds
|
|
2616
|
+
exactly as quietly as DDL that works — `ALTER COLUMN x TYPE text` on a column
|
|
2617
|
+
that is already `text` is a valid, successful no-op.
|
|
2618
|
+
|
|
2619
|
+
This message exists because the alternative is worse. Before the convergence
|
|
2620
|
+
check, such a plan reported `applied 31 op(s)`, recorded a fingerprint, and every
|
|
2621
|
+
later boot short-circuited on "schema up to date" — for a schema that had never
|
|
2622
|
+
been applied. One app ran that way for two releases. The apply now proves it
|
|
2623
|
+
worked before it records anything: the same planner, run against the database as
|
|
2624
|
+
it now is, must have nothing left to do.
|
|
2625
|
+
|
|
2626
|
+
**It is a framework bug, not a mistake in your schema.** The named operations
|
|
2627
|
+
emit DDL that does not take effect. Report the operation kinds plus the column
|
|
2628
|
+
types involved. In the meantime the schema is unchanged and safe — nothing was
|
|
2629
|
+
half-applied, and no fingerprint was written, so `voltro db plan` still shows you
|
|
2630
|
+
the truth.
|
|
2631
|
+
|
|
2632
|
+
If you need to move forward before a fix lands, apply the equivalent DDL by hand
|
|
2633
|
+
and re-run `voltro db plan` to confirm it converges.
|
|
2634
|
+
|
|
2603
2635
|
## "Schema fingerprint mismatch" (prod refuse)
|
|
2604
2636
|
|
|
2605
2637
|
Full error:
|
|
@@ -235,6 +235,37 @@ The `where` predicate is a regular [Predicate](/docs/database/query-builder#pred
|
|
|
235
235
|
AST — same shape `.where()` uses. Sub-queries via `inSubquery` /
|
|
236
236
|
`exists` are supported.
|
|
237
237
|
|
|
238
|
+
### Typed: `updateManyRow`
|
|
239
|
+
|
|
240
|
+
`updateMany` takes a string table name and an untyped patch, so a misspelled
|
|
241
|
+
column or a wrongly-typed value is only found by the database — or not at all,
|
|
242
|
+
if the column happens to exist. `updateManyRow` takes the TABLE OBJECT instead
|
|
243
|
+
and checks the patch against the row type:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { updateManyRow, eq } from '@voltro/database'
|
|
247
|
+
|
|
248
|
+
await updateManyRow(ctx.store, posts, { hidden: true }, {
|
|
249
|
+
where: eq('userId', bannedId),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
await updateManyRow(ctx.store, posts, { hiddne: true }, { where: … })
|
|
253
|
+
// ^^^^^^^ compile error: not a column
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
It is worth using rather than the string form, and the evidence is concrete: one
|
|
257
|
+
app migrating 29 `store.upsert` call sites to the typed `upsertRow` got 15 `tsc`
|
|
258
|
+
errors across 8 distinct defects that no test had caught — including seven
|
|
259
|
+
per-user mutations with no authentication check at all, which wrote
|
|
260
|
+
`ctx.request.subject.id` (typed `string | null`) into a NOT NULL column, so an
|
|
261
|
+
anonymous caller reached the database and got a raw statement failure instead of
|
|
262
|
+
a typed refusal.
|
|
263
|
+
|
|
264
|
+
The subtlest one is the most persuasive: a value spread from a plain object
|
|
265
|
+
literal widens to `string`, and a column's `.oneOf()` union rejects it even
|
|
266
|
+
though the value IS one of the members. Neither a reviewer nor a test would
|
|
267
|
+
plausibly find that; only the row type asks the question. (`as const` fixes it.)
|
|
268
|
+
|
|
238
269
|
### Reactivity
|
|
239
270
|
|
|
240
271
|
`updateMany` emits ONE ChangeEvent per affected row, so reactive
|
|
@@ -66,6 +66,30 @@ voltro e2e # boot api + web siblings, run tests, tear down
|
|
|
66
66
|
|
|
67
67
|
_makeTestContext — the in-memory request ctx for handler and tool tests. The real mixin-wrapped store, mockStore seeding, subject/tenant re-scoping, and the deterministic clock / email / LLM mocks._
|
|
68
68
|
|
|
69
|
+
## tsconfig `paths` aliases
|
|
70
|
+
|
|
71
|
+
`voltro test` derives Vite's `resolve.alias` from your app's tsconfig
|
|
72
|
+
`compilerOptions.paths`, so an app that maps `@/* → ./src/*` can test modules
|
|
73
|
+
that import through it without any extra config:
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
// tsconfig.json
|
|
77
|
+
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { greeting } from '@/locales/en' // resolves under `voltro test`
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Before this, the first person to write a test for each aliased file discovered
|
|
85
|
+
`Cannot find package '@/locales/en' imported from src/lib/i18n.ts` — one file at
|
|
86
|
+
a time — and worked around it with a local `vitest.config.ts` restating what
|
|
87
|
+
tsconfig already said. The dev and build pipelines resolve these already (your
|
|
88
|
+
app runs), so the test runner disagreeing with them was a gap, not a policy.
|
|
89
|
+
|
|
90
|
+
A project-local `vitest.config.ts` still merges on top, so an app that already
|
|
91
|
+
wrote the workaround keeps working.
|
|
92
|
+
|
|
69
93
|
## `makeTestContext`
|
|
70
94
|
|
|
71
95
|
`makeTestContext(options?)` returns an **`AppContext`** — the exact `ctx` a mutation / query / action / tool executor receives at runtime — with the deterministic test doubles added on top. Because it *is* an `AppContext`, you pass it straight into a handler: `await myHandler(input, makeTestContext({ … }))`. The acting subject is at `ctx.request.subject`; `ctx.store` is the **real** mixin-wrapped store backed by an in-memory data store (so tenant auto-scoping, soft-delete filtering, audit auto-fill, and the fluent `select` / `update` / `delete` builders behave exactly as in production); `ctx.cache` is a real in-memory cache whose TTLs honour `ctx.clock`.
|