@voltro/cli 0.18.0 → 0.19.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 +158 -3
- package/bin/voltro.mjs +11 -0
- package/dist/apiBuild-9NXH53Sd.js +2 -0
- package/dist/{apiBuild-CPDTJHkH.js → apiBuild-CY5pEwwq.js} +1 -1
- package/dist/bin.js +1 -1
- package/dist/{commands-laJDMj2m.js → commands-BvvoQL0v.js} +1410 -1203
- package/dist/{dev-BOFxC21E.js → dev-B6rfgKfo.js} +1424 -1392
- package/dist/{dev-DOK0w6ZW.js → dev-C_E1XxNF.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/serveCommand-vPF5ucXC.js +1136 -0
- package/dist/serveEntry.js +1 -1
- package/package.json +17 -17
- package/templates/AGENTS.core.md +25 -9
- package/templates/AGENTS.md +26 -10
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/authentication.md +9 -0
- package/templates/agent-docs/database/misc.md +26 -3
- package/templates/agent-docs/database/querying.md +2 -0
- package/templates/agent-docs/whats-new.md +82 -161
- 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-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-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-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 -9
- 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-D1UBJ4TM.js +0 -2
- package/dist/serveCommand-CxcxHc9Y.js +0 -1129
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Q as e } from "./inspectMetrics-D1DmLeJs.js";
|
|
2
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-vPF5ucXC.js";
|
|
5
5
|
export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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.19.0",
|
|
66
|
+
"@voltro/cache": "0.19.0",
|
|
67
|
+
"@voltro/data-transfer": "0.19.0",
|
|
68
|
+
"@voltro/database": "0.19.0",
|
|
69
|
+
"@voltro/env": "0.19.0",
|
|
70
|
+
"@voltro/kv": "0.19.0",
|
|
71
|
+
"@voltro/logger": "0.19.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.19.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.19.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.19.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.19.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.19.0",
|
|
77
|
+
"@voltro/protocol": "0.19.0",
|
|
78
|
+
"@voltro/runtime": "0.19.0",
|
|
79
|
+
"@voltro/serverless": "0.19.0",
|
|
80
|
+
"@voltro/workflow": "0.19.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
|
@@ -103,30 +103,41 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
103
103
|
in-handler for checks that need LOADED data. A hand-written `requireScope(...)`
|
|
104
104
|
at the top of every executor — or a hand-kept map from rpc tag to policy rule,
|
|
105
105
|
which is fail-open by omission — is what `guards:` exists to delete.
|
|
106
|
-
6. **
|
|
106
|
+
6. **Which ROWS may the caller see** (not: may they make the call)? →
|
|
107
|
+
**`setRowFilter`** — a predicate derived from the Subject, AND-merged into
|
|
108
|
+
EVERY read of that table, including every subscription delivery. `guards:`
|
|
109
|
+
and this answer different questions and the pair is easy to conflate:
|
|
110
|
+
- `guards:` → *may I call this procedure?* → a typed `ScopeError`.
|
|
111
|
+
- `setRowFilter` → *which rows may I see?* → the rows are simply absent.
|
|
112
|
+
A `WHERE ownerId = me` in the list handler covers the query and **NOT** the
|
|
113
|
+
stream, which is the failure this deletes. Register it from a
|
|
114
|
+
`*.startup.tsx`; its `load` must read through an UNFILTERED store (the boot
|
|
115
|
+
store / the `database` handle) — reading through the filtered one applies the
|
|
116
|
+
filter to itself and blows the stack. Depth → **`authentication`**.
|
|
117
|
+
7. **Storing a token, secret, or credential in a column?** → **`.encrypted()`**
|
|
107
118
|
on the column. Boot fails loudly if no cipher is configured, so an
|
|
108
119
|
`.encrypted()` column can never silently persist plaintext.
|
|
109
|
-
|
|
120
|
+
8. **Writing an Effect-form handler?** → `const store = yield* EffectStore` —
|
|
110
121
|
its failures land on the typed error channel. `Effect.promise(() =>
|
|
111
122
|
ctx.store.query(…))` throws that channel away and turns a store failure into
|
|
112
123
|
a defect.
|
|
113
|
-
|
|
124
|
+
9. **Cursor pagination?** → **`paginateBy(descriptor, column, cursor, limit,
|
|
114
125
|
direction?)`** (or **`paginateById`**, its `id`-column shorthand). Not a
|
|
115
126
|
hand-rolled limit+1 / slice / `hasMore` triple. `direction` flips the
|
|
116
127
|
COMPARISON as well as the sort — a `desc` feed pages with `<`.
|
|
117
|
-
|
|
128
|
+
10. **Assembling data whose SHAPE depends on the data** (a tree walk where each
|
|
118
129
|
level's ids come from the level above)? → **`ctx.load` / `ctx.loadMany`** —
|
|
119
130
|
same-tick reads of one table coalesce into one `WHERE id IN (...)`, so the
|
|
120
131
|
walk costs one query per LEVEL. Use `relations()` + `.with()` whenever the
|
|
121
132
|
shape IS static; this is the fallback, not the default.
|
|
122
|
-
|
|
133
|
+
11. **A mutation must cause an EXTERNAL side effect** (webhook, Jira sync,
|
|
123
134
|
payment)? → **`ctx.outbox.enqueue(effect, payload)`** + a
|
|
124
135
|
**`defineOutboxHandler`** in a `*.outbox.ts`. The enqueue writes through the
|
|
125
136
|
mutation's TRANSACTION, so the intent commits with the write or not at all;
|
|
126
137
|
delivery happens after commit, with backoff and a dead-letter. Do NOT call
|
|
127
138
|
the remote from the mutation (not transactional), and do not hand-build a
|
|
128
139
|
deliveries table + drain cron — that IS this primitive.
|
|
129
|
-
|
|
140
|
+
12. **Calling a third party ON BEHALF OF A USER** (their Jira token, their Slack
|
|
130
141
|
grant)? → **`defineConnection({ id, kind: 'oauth2' | 'pat' })`** in a
|
|
131
142
|
`*.connection.ts`, then **`ctx.connections.get(id)`**. One declaration yields
|
|
132
143
|
the encrypted per-user token store, the OAuth authorize/callback pair,
|
|
@@ -140,6 +151,7 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
140
151
|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
|
|
141
152
|
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
|
|
142
153
|
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
|
|
154
|
+
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
|
|
143
155
|
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
|
|
144
156
|
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
|
|
145
157
|
| a token/secret column written as plain text | `.encrypted()` |
|
|
@@ -456,7 +468,7 @@ separate markers, and using one to answer another's question is the mistake:
|
|
|
456
468
|
|
|
457
469
|
| Marker | Answers | Enforced by |
|
|
458
470
|
|---|---|---|
|
|
459
|
-
| `.serverOnly()` | may this value leave the server AT ALL? |
|
|
471
|
+
| `.serverOnly()` | may this value leave the server AT ALL? | **`voltro serve` fails to boot; `voltro doctor` exits non-zero.** `voltro dev` only warns |
|
|
460
472
|
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
|
|
461
473
|
| `.encrypted()` | is it encrypted AT REST? | the store's codec |
|
|
462
474
|
|
|
@@ -470,8 +482,12 @@ export const users = table('users', {
|
|
|
470
482
|
```
|
|
471
483
|
|
|
472
484
|
- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
|
|
473
|
-
query that declares such a column in its OUTPUT does not
|
|
474
|
-
|
|
485
|
+
query that declares such a column in its OUTPUT does not reach production:
|
|
486
|
+
`voltro serve` refuses to boot and `voltro doctor` exits non-zero, so the
|
|
487
|
+
failure happens at deploy time rather than in a bundle. `voltro dev` only
|
|
488
|
+
WARNS — mid-edit a refused boot is worse than the bug — so do not read a green
|
|
489
|
+
dev boot as a clean audit. `VOLTRO_SERVER_ONLY=strict` makes dev fail too;
|
|
490
|
+
put `voltro doctor` in CI if you want one gate that covers both.
|
|
475
491
|
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
|
|
476
492
|
encrypted; the runtime decrypts them for a handler, so an encrypted column
|
|
477
493
|
flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
|
package/templates/AGENTS.md
CHANGED
|
@@ -103,30 +103,41 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
103
103
|
in-handler for checks that need LOADED data. A hand-written `requireScope(...)`
|
|
104
104
|
at the top of every executor — or a hand-kept map from rpc tag to policy rule,
|
|
105
105
|
which is fail-open by omission — is what `guards:` exists to delete.
|
|
106
|
-
6. **
|
|
106
|
+
6. **Which ROWS may the caller see** (not: may they make the call)? →
|
|
107
|
+
**`setRowFilter`** — a predicate derived from the Subject, AND-merged into
|
|
108
|
+
EVERY read of that table, including every subscription delivery. `guards:`
|
|
109
|
+
and this answer different questions and the pair is easy to conflate:
|
|
110
|
+
- `guards:` → *may I call this procedure?* → a typed `ScopeError`.
|
|
111
|
+
- `setRowFilter` → *which rows may I see?* → the rows are simply absent.
|
|
112
|
+
A `WHERE ownerId = me` in the list handler covers the query and **NOT** the
|
|
113
|
+
stream, which is the failure this deletes. Register it from a
|
|
114
|
+
`*.startup.tsx`; its `load` must read through an UNFILTERED store (the boot
|
|
115
|
+
store / the `database` handle) — reading through the filtered one applies the
|
|
116
|
+
filter to itself and blows the stack. Depth → **`authentication`**.
|
|
117
|
+
7. **Storing a token, secret, or credential in a column?** → **`.encrypted()`**
|
|
107
118
|
on the column. Boot fails loudly if no cipher is configured, so an
|
|
108
119
|
`.encrypted()` column can never silently persist plaintext.
|
|
109
|
-
|
|
120
|
+
8. **Writing an Effect-form handler?** → `const store = yield* EffectStore` —
|
|
110
121
|
its failures land on the typed error channel. `Effect.promise(() =>
|
|
111
122
|
ctx.store.query(…))` throws that channel away and turns a store failure into
|
|
112
123
|
a defect.
|
|
113
|
-
|
|
124
|
+
9. **Cursor pagination?** → **`paginateBy(descriptor, column, cursor, limit,
|
|
114
125
|
direction?)`** (or **`paginateById`**, its `id`-column shorthand). Not a
|
|
115
126
|
hand-rolled limit+1 / slice / `hasMore` triple. `direction` flips the
|
|
116
127
|
COMPARISON as well as the sort — a `desc` feed pages with `<`.
|
|
117
|
-
|
|
128
|
+
10. **Assembling data whose SHAPE depends on the data** (a tree walk where each
|
|
118
129
|
level's ids come from the level above)? → **`ctx.load` / `ctx.loadMany`** —
|
|
119
130
|
same-tick reads of one table coalesce into one `WHERE id IN (...)`, so the
|
|
120
131
|
walk costs one query per LEVEL. Use `relations()` + `.with()` whenever the
|
|
121
132
|
shape IS static; this is the fallback, not the default.
|
|
122
|
-
|
|
133
|
+
11. **A mutation must cause an EXTERNAL side effect** (webhook, Jira sync,
|
|
123
134
|
payment)? → **`ctx.outbox.enqueue(effect, payload)`** + a
|
|
124
135
|
**`defineOutboxHandler`** in a `*.outbox.ts`. The enqueue writes through the
|
|
125
136
|
mutation's TRANSACTION, so the intent commits with the write or not at all;
|
|
126
137
|
delivery happens after commit, with backoff and a dead-letter. Do NOT call
|
|
127
138
|
the remote from the mutation (not transactional), and do not hand-build a
|
|
128
139
|
deliveries table + drain cron — that IS this primitive.
|
|
129
|
-
|
|
140
|
+
12. **Calling a third party ON BEHALF OF A USER** (their Jira token, their Slack
|
|
130
141
|
grant)? → **`defineConnection({ id, kind: 'oauth2' | 'pat' })`** in a
|
|
131
142
|
`*.connection.ts`, then **`ctx.connections.get(id)`**. One declaration yields
|
|
132
143
|
the encrypted per-user token store, the OAuth authorize/callback pair,
|
|
@@ -140,6 +151,7 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
140
151
|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
|
|
141
152
|
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
|
|
142
153
|
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
|
|
154
|
+
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
|
|
143
155
|
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
|
|
144
156
|
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
|
|
145
157
|
| a token/secret column written as plain text | `.encrypted()` |
|
|
@@ -456,7 +468,7 @@ separate markers, and using one to answer another's question is the mistake:
|
|
|
456
468
|
|
|
457
469
|
| Marker | Answers | Enforced by |
|
|
458
470
|
|---|---|---|
|
|
459
|
-
| `.serverOnly()` | may this value leave the server AT ALL? |
|
|
471
|
+
| `.serverOnly()` | may this value leave the server AT ALL? | **`voltro serve` fails to boot; `voltro doctor` exits non-zero.** `voltro dev` only warns |
|
|
460
472
|
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
|
|
461
473
|
| `.encrypted()` | is it encrypted AT REST? | the store's codec |
|
|
462
474
|
|
|
@@ -470,8 +482,12 @@ export const users = table('users', {
|
|
|
470
482
|
```
|
|
471
483
|
|
|
472
484
|
- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
|
|
473
|
-
query that declares such a column in its OUTPUT does not
|
|
474
|
-
|
|
485
|
+
query that declares such a column in its OUTPUT does not reach production:
|
|
486
|
+
`voltro serve` refuses to boot and `voltro doctor` exits non-zero, so the
|
|
487
|
+
failure happens at deploy time rather than in a bundle. `voltro dev` only
|
|
488
|
+
WARNS — mid-edit a refused boot is worse than the bug — so do not read a green
|
|
489
|
+
dev boot as a clean audit. `VOLTRO_SERVER_ONLY=strict` makes dev fail too;
|
|
490
|
+
put `voltro doctor` in CI if you want one gate that covers both.
|
|
475
491
|
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
|
|
476
492
|
encrypted; the runtime decrypts them for a handler, so an encrypted column
|
|
477
493
|
flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
|
|
@@ -579,7 +595,7 @@ each plugin's own README.
|
|
|
579
595
|
|
|
580
596
|
| Topic | Open | Summary |
|
|
581
597
|
|---|---|---|
|
|
582
|
-
| **What's new in 0.
|
|
598
|
+
| **What's new in 0.19.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. |
|
|
583
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. |
|
|
584
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. |
|
|
585
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.19.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. |
|
|
@@ -590,6 +590,15 @@ column read raw hands back the string `enc:v1:…`, which compares, concatenates
|
|
|
590
590
|
renders and logs perfectly well, and simply never matches the token you compare
|
|
591
591
|
it to.
|
|
592
592
|
|
|
593
|
+
**The soft-delete row is the one to read twice if you are porting raw SQL onto
|
|
594
|
+
this store.** A read here behaves the way your SQL did and returns tombstones —
|
|
595
|
+
the store does *not* start appending `deletedAt IS NULL` behind you. So a lookup
|
|
596
|
+
that must see a soft-deleted row (a login that revives a returning user, say)
|
|
597
|
+
needs no opt-out and no `.withDeleted()`; that opt-out belongs to `ctx.store`,
|
|
598
|
+
which *does* apply the filter. Assuming the filter is present is the more
|
|
599
|
+
expensive mistake of the two: it turns a working login into a "not found →
|
|
600
|
+
insert → unique violation on email", and nothing about the code says so.
|
|
601
|
+
|
|
593
602
|
This is also what changes when you move a read **off** hand-written SQL and onto
|
|
594
603
|
the store. Raw SQL sees ciphertext and you decrypt it yourself — `decryptField`
|
|
595
604
|
from `@voltro/runtime` is the escape hatch for exactly that. Through either
|
|
@@ -248,9 +248,32 @@ Enforcement: the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
|
|
|
248
248
|
columns from every returned row **automatically** — you declare the exposure
|
|
249
249
|
policy once at the schema and can't forget it on a handler. For a hand-written
|
|
250
250
|
query, omit the column from the `output` schema (and don't put it in the returned
|
|
251
|
-
object)
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
object).
|
|
252
|
+
|
|
253
|
+
A hand-written output is the case `crud.*` cannot cover, so an audit checks it:
|
|
254
|
+
a wire-reachable query whose `source` table carries a `.serverOnly()` column that
|
|
255
|
+
its `output` **declares**. What that costs is different per command, on purpose:
|
|
256
|
+
|
|
257
|
+
| Command | On a leak |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `voltro serve` | **the boot fails** |
|
|
260
|
+
| `voltro doctor` | **exits non-zero** — put it in CI |
|
|
261
|
+
| `voltro dev` | warns, naming the query and column |
|
|
262
|
+
|
|
263
|
+
Dev only warns because a refused boot between two keystrokes is worse than the
|
|
264
|
+
bug; production is the opposite, so that is where the gate is. **Do not read a
|
|
265
|
+
green `voltro dev` boot as a clean audit** — the warning sits in the boot log
|
|
266
|
+
among everything else. `voltro doctor` is the check to automate.
|
|
267
|
+
|
|
268
|
+
`VOLTRO_SERVER_ONLY` moves the line in both directions: `strict` makes `voltro
|
|
269
|
+
dev` fail too, `warn` downgrades `voltro serve` to a warning, `off` silences it
|
|
270
|
+
entirely. The downgrades are documented rather than hidden because the
|
|
271
|
+
alternative to a stated escape hatch is deleting the marker — and a check whose
|
|
272
|
+
only way out is to disable it gets disabled.
|
|
273
|
+
|
|
274
|
+
(`voltro check` does **not** run this audit. It has a live-api mode that has no
|
|
275
|
+
access to your table definitions, and a rule that fires in one of its two modes
|
|
276
|
+
would be worse than one that fires in neither.)
|
|
254
277
|
|
|
255
278
|
Distinct from `.encrypted()` on purpose: encryption at rest says nothing about
|
|
256
279
|
who may receive the plaintext — a private note you decrypt *for its owner* is a
|
|
@@ -114,6 +114,8 @@ ctx.store.select('notes')
|
|
|
114
114
|
|
|
115
115
|
There is no `'like'`. It used to be here, and it was a lie: it mapped to `contains`, so `.where('path', 'like', '/api/%')` matched only rows literally containing the characters `/api/%` and the wildcard you wrote did nothing. An operator named after SQL's must honour your wildcards or not exist.
|
|
116
116
|
|
|
117
|
+
> **Writing an in-memory store for tests? Do not reimplement these.** `evaluatePredicate` is exported from `@voltro/database` and is the same function the memory store runs, so a test double built on it agrees with a real database by construction. Hand-rolling the switch is where a stub quietly diverges — a `case 'contains'` written as `String(a).includes(b)` is case-SENSITIVE where the real one folds case, and coerces a number where the real one returns `false`. That stub passes queries a live dialect fails, which is the worst direction for a test double to be wrong in.
|
|
118
|
+
|
|
117
119
|
These are the only operators the ergonomic `.where(col, op, value)` form accepts. For `IS NULL` / `NOT IN` / `IS NOT NULL`, pass a predicate built with the `@voltro/database` helpers:
|
|
118
120
|
|
|
119
121
|
```ts
|