@voltro/cli 0.23.0 → 0.25.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +537 -0
  2. package/THIRD-PARTY-NOTICES.md +2 -2
  3. package/bin/voltro.mjs +39 -4
  4. package/dist/{apiBuild-JQtIhZPy.js → apiBuild-BqhCSytw.js} +46 -24
  5. package/dist/apiBuild-N1R4V792.js +2 -0
  6. package/dist/bin.js +3 -3
  7. package/dist/{commands-Bsu9Buln.js → commands-7EmYJ9Xg.js} +2796 -2478
  8. package/dist/dbCommand-CIrdFLp9.js +2 -0
  9. package/dist/{dbCommand-GI7-BVZt.js → dbCommand-FUU12FvD.js} +285 -246
  10. package/dist/{dev-DRxF_qOB.js → dev-BvHT7WZa.js} +1 -1
  11. package/dist/{dev-jM07kq_D.js → dev-MacSQ1Ll.js} +2042 -1631
  12. package/dist/fileConventions-3bffWssN.js +30 -0
  13. package/dist/frameworkTableAssembly-BsnCKzQ6.js +2 -0
  14. package/dist/{frameworkTableAssembly-sg41s14F.js → frameworkTableAssembly-Cw5zJz6n.js} +2 -2
  15. package/dist/index.js +1 -1
  16. package/dist/inspect-C9gjHwBk.js +2 -0
  17. package/dist/{inspect-_ldwsAwH.js → inspect-DuLUrZp9.js} +14 -1
  18. package/dist/{inspectMetrics-dcC7gnxu.js → inspectMetrics-EQwH7BI4.js} +15 -15
  19. package/dist/manifestBuild-BVwS1Z_6.js +2 -0
  20. package/dist/{manifestBuild-Bs1Uw22_.js → manifestBuild-Dneq4_Jx.js} +1 -1
  21. package/dist/serveCommand-5ZFiNO1R.js +1241 -0
  22. package/dist/serveEntry.js +2 -2
  23. package/dist/{start-C_4VUDT8.js → start-C-ZWSDpg.js} +2 -2
  24. package/dist/startEntry.js +2 -2
  25. package/package.json +17 -17
  26. package/templates/AGENTS.core.md +12 -5
  27. package/templates/AGENTS.md +14 -7
  28. package/templates/agent-docs/_index.md +2 -2
  29. package/templates/agent-docs/_manifest.json +3 -3
  30. package/templates/agent-docs/authentication.md +21 -0
  31. package/templates/agent-docs/cli.md +32 -0
  32. package/templates/agent-docs/data.md +491 -167
  33. package/templates/agent-docs/database/migrations.md +132 -63
  34. package/templates/agent-docs/database/schema.md +80 -0
  35. package/templates/agent-docs/database/seedsdialects.md +12 -0
  36. package/templates/agent-docs/deployment.md +75 -1
  37. package/templates/agent-docs/plugins.md +1 -1
  38. package/templates/agent-docs/reference.md +2 -2
  39. package/templates/agent-docs/security.md +18 -0
  40. package/templates/agent-docs/templates/apibackends.md +5 -5
  41. package/templates/agent-docs/templates/overview.md +1 -1
  42. package/templates/agent-docs/whats-new.md +263 -169
  43. package/templates/agent-docs/workflows.md +74 -20
  44. package/templates/apps/api-ai/package.json +8 -7
  45. package/templates/apps/api-auth/package.json +9 -8
  46. package/templates/apps/api-backend/package.json +8 -7
  47. package/templates/apps/api-backend-deactivation/package.json +8 -7
  48. package/templates/apps/api-backend-mail/package.json +9 -8
  49. package/templates/apps/api-backend-mariadb/package.json +10 -9
  50. package/templates/apps/api-backend-storage/package.json +9 -8
  51. package/templates/apps/api-data-advanced/package.json +9 -8
  52. package/templates/apps/api-durable/README.md +2 -1
  53. package/templates/apps/api-durable/events/orders.event.ts +18 -0
  54. package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +17 -15
  55. package/templates/apps/api-durable/package.json +9 -8
  56. package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +6 -4
  57. package/templates/apps/api-feature-flags/package.json +10 -9
  58. package/templates/apps/api-governance/package.json +9 -8
  59. package/templates/apps/api-kv/package.json +9 -8
  60. package/templates/apps/api-moderation/package.json +9 -8
  61. package/templates/apps/api-observability/package.json +9 -8
  62. package/templates/apps/api-ratelimit/package.json +9 -8
  63. package/templates/apps/api-rbac/package.json +9 -8
  64. package/templates/apps/api-rest/package.json +8 -7
  65. package/templates/apps/api-saas/package.json +12 -11
  66. package/templates/apps/api-search/package.json +9 -8
  67. package/templates/apps/api-versioning/package.json +9 -8
  68. package/templates/apps/api-webhooks/README.md +2 -2
  69. package/templates/apps/api-webhooks/events/orders.event.ts +35 -0
  70. package/templates/apps/api-webhooks/package.json +10 -9
  71. package/templates/apps/api-webhooks/template.json +1 -1
  72. package/templates/apps/changelog/package.json +7 -6
  73. package/templates/apps/edge-functions/package.json +3 -2
  74. package/templates/apps/frontend-admin/package.json +9 -8
  75. package/templates/apps/frontend-app/package.json +9 -8
  76. package/templates/apps/frontend-blank/package.json +8 -7
  77. package/templates/apps/frontend-contact/package.json +8 -7
  78. package/templates/apps/frontend-contact/src/globals.d.ts +6 -0
  79. package/templates/apps/frontend-dashboard/package.json +8 -7
  80. package/templates/apps/frontend-docs/package.json +8 -7
  81. package/templates/apps/frontend-docs/src/globals.d.ts +6 -0
  82. package/templates/apps/frontend-i18n/package.json +7 -6
  83. package/templates/apps/frontend-landing/package.json +8 -7
  84. package/templates/apps/frontend-landing/src/globals.d.ts +6 -0
  85. package/templates/apps/frontend-spa/package.json +8 -7
  86. package/templates/apps/frontend-spa/src/globals.d.ts +6 -0
  87. package/templates/apps/frontend-ssr/package.json +8 -7
  88. package/templates/apps/frontend-ssr/src/globals.d.ts +6 -0
  89. package/templates/apps/frontend-ssr-api/package.json +9 -8
  90. package/templates/apps/frontend-static-blog/package.json +7 -6
  91. package/templates/apps/frontend-static-blog/src/globals.d.ts +6 -0
  92. package/dist/apiBuild-CD-4JLLA.js +0 -2
  93. package/dist/dbCommand-CSguuUQn.js +0 -2
  94. package/dist/fileConventions-CBNPZVOV.js +0 -29
  95. package/dist/frameworkTableAssembly-DRJrAVr1.js +0 -2
  96. package/dist/inspect-BA67TF6v.js +0 -2
  97. package/dist/manifestBuild-i-fRHg_H.js +0 -2
  98. package/dist/serveCommand-Cl8sicD5.js +0 -1162
  99. package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +0 -22
@@ -1,5 +1,5 @@
1
- import { Z as e } from "./inspectMetrics-dcC7gnxu.js";
1
+ import { Z as e } from "./inspectMetrics-EQwH7BI4.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-Cl8sicD5.js";
4
+ import { t as r } from "./serveCommand-5ZFiNO1R.js";
5
5
  export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
@@ -1,5 +1,5 @@
1
- import { $ as e, D as t, F as n, G as r, H as i, J as a, K as o, M as s, T as c, U as l, W as u, Y as d, _ as ee, a as te, b as f, c as p, dt as m, et as h, f as g, ft as _, g as ne, h as v, i as re, lt as y, m as b, mt as x, nt as ie, o as S, ot as C, p as w, pt as ae, r as T, rt as E, s as D, t as O, tt as k, v as A, w as j, x as M, y as oe, z as se } from "./inspectMetrics-dcC7gnxu.js";
2
- import { D as ce, E as le, T as ue, a as de, p as N, w as P } from "./inspect-_ldwsAwH.js";
1
+ import { $ as e, D as t, F as n, G as r, H as i, J as a, K as o, M as s, T as c, U as l, W as u, Y as d, _ as ee, a as te, b as f, c as p, dt as m, et as h, f as g, ft as _, g as ne, h as v, i as re, lt as y, m as b, mt as x, nt as ie, o as S, ot as C, p as w, pt as ae, r as T, rt as E, s as D, t as O, tt as k, v as A, w as j, x as M, y as oe, z as se } from "./inspectMetrics-EQwH7BI4.js";
2
+ import { D as ce, E as le, T as ue, a as de, p as N, w as P } from "./inspect-DuLUrZp9.js";
3
3
  import { t as fe } from "./bootTiming-BdyP9nYw.js";
4
4
  import { dirname as F, extname as I, join as L, resolve as R } from "node:path";
5
5
  import { fileURLToPath as z, pathToFileURL as B } from "node:url";
@@ -1,3 +1,3 @@
1
- import { Z as e } from "./inspectMetrics-dcC7gnxu.js";
2
- import { t } from "./start-C_4VUDT8.js";
1
+ import { Z as e } from "./inspectMetrics-EQwH7BI4.js";
2
+ import { t } from "./start-C-ZWSDpg.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.23.0",
3
+ "version": "0.25.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.108.0",
63
63
  "@effect/sql": "^0.52.0",
64
64
  "@effect/workflow": "^0.19.0",
65
- "@voltro/ai": "0.23.0",
66
- "@voltro/cache": "0.23.0",
67
- "@voltro/data-transfer": "0.23.0",
68
- "@voltro/database": "0.23.0",
69
- "@voltro/env": "0.23.0",
70
- "@voltro/kv": "0.23.0",
71
- "@voltro/logger": "0.23.0",
72
- "@voltro/plugin-auth": "0.23.0",
73
- "@voltro/plugin-broadcast": "0.23.0",
74
- "@voltro/plugin-mail": "0.23.0",
75
- "@voltro/plugin-storage": "0.23.0",
76
- "@voltro/plugin-webhooks": "0.23.0",
77
- "@voltro/protocol": "0.23.0",
78
- "@voltro/runtime": "0.23.0",
79
- "@voltro/serverless": "0.23.0",
80
- "@voltro/workflow": "0.23.0",
65
+ "@voltro/ai": "0.25.0",
66
+ "@voltro/cache": "0.25.0",
67
+ "@voltro/data-transfer": "0.25.0",
68
+ "@voltro/database": "0.25.0",
69
+ "@voltro/env": "0.25.0",
70
+ "@voltro/kv": "0.25.0",
71
+ "@voltro/logger": "0.25.0",
72
+ "@voltro/plugin-auth": "0.25.0",
73
+ "@voltro/plugin-broadcast": "0.25.0",
74
+ "@voltro/plugin-mail": "0.25.0",
75
+ "@voltro/plugin-storage": "0.25.0",
76
+ "@voltro/plugin-webhooks": "0.25.0",
77
+ "@voltro/protocol": "0.25.0",
78
+ "@voltro/runtime": "0.25.0",
79
+ "@voltro/serverless": "0.25.0",
80
+ "@voltro/workflow": "0.25.0",
81
81
  "chokidar": "^5.0.0",
82
82
  "ioredis": "^5.11.1",
83
83
  "tinyglobby": "^0.2.17",
@@ -39,18 +39,24 @@ Workflows split the same way (`*.workflow.tsx` + `*.workflow.server.tsx`).
39
39
 
40
40
  Ask in order; stop at the first "yes":
41
41
 
42
- 1. **Needs a LIVE feed that auto-updates on writes?** → **query** (`queries/`).
43
- The only streaming primitive. Declares `source: 'table'` so writes invalidate it.
44
- 2. **A single atomic DB write (one commit boundary)?** **mutation** (`mutations/`).
42
+ 1. **Something HAPPENED, with no row behind it?** → **event** (`*.event.ts`).
43
+ A game started, a door opened, a terminal confirmed. It has a time and no
44
+ value afterwards, where a row has a value and no time. Ephemeral, fan-out to
45
+ every listening client, at-most-once. **If you are about to write a table so
46
+ that a subscriber fires, this is what you want.**
47
+ 2. **Needs a LIVE feed that auto-updates on writes?** → **query** (`queries/`).
48
+ The reactive read primitive. Declares `source: 'table'` so writes invalidate it.
49
+ 3. **A single atomic DB write (one commit boundary)?** → **mutation** (`mutations/`).
45
50
  Runs in a transaction; a throw rolls back; ChangeEvents drain only on commit.
46
51
  Declares `target: { table, op }` → drives client auto-optimistic.
47
- 3. **External I/O — HTTP, file, AI, payment, email, signed URL?** → **action**
52
+ 4. **External I/O — HTTP, file, AI, payment, email, signed URL?** → **action**
48
53
  (`actions/`). NOT transactional; no rollback of side effects.
49
- 4. **Multi-step work that must survive a crash/deploy, retry-from-where-it-died?**
54
+ 5. **Multi-step work that must survive a crash/deploy, retry-from-where-it-died?**
50
55
  → **workflow** (`*.workflow.tsx`). Durable, suspend/resume.
51
56
 
52
57
  | Use case | Primitive |
53
58
  |---|---|
59
+ | A game starts / a door opens / a printer finishes — many screens react | Event |
54
60
  | List todos / messages on a page | Query |
55
61
  | Toggle a todo, send a message, edit a name | Mutation |
56
62
  | Generate a thumbnail / send a welcome email / call an LLM ad-hoc | Action |
@@ -248,6 +254,7 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
248
254
  | `*.mutation.ts` + `*.mutation.server.ts` | transactional unary write |
249
255
  | `*.action.ts` + `*.action.server.ts` | non-transactional external I/O |
250
256
  | `*.stream.ts` + `*.stream.server.ts` | server→client stream (e.g. AI tokens) |
257
+ | `*.event.ts` | ephemeral fan-out event (`defineEvent`; no `.server` half) |
251
258
  | `*.workflow.tsx` + `*.workflow.server.tsx` | durable multi-step work |
252
259
  | `*.trigger.tsx` | domain event → workflow |
253
260
  | `*.cron.tsx` | scheduled job (single file) |
@@ -39,18 +39,24 @@ Workflows split the same way (`*.workflow.tsx` + `*.workflow.server.tsx`).
39
39
 
40
40
  Ask in order; stop at the first "yes":
41
41
 
42
- 1. **Needs a LIVE feed that auto-updates on writes?** → **query** (`queries/`).
43
- The only streaming primitive. Declares `source: 'table'` so writes invalidate it.
44
- 2. **A single atomic DB write (one commit boundary)?** **mutation** (`mutations/`).
42
+ 1. **Something HAPPENED, with no row behind it?** → **event** (`*.event.ts`).
43
+ A game started, a door opened, a terminal confirmed. It has a time and no
44
+ value afterwards, where a row has a value and no time. Ephemeral, fan-out to
45
+ every listening client, at-most-once. **If you are about to write a table so
46
+ that a subscriber fires, this is what you want.**
47
+ 2. **Needs a LIVE feed that auto-updates on writes?** → **query** (`queries/`).
48
+ The reactive read primitive. Declares `source: 'table'` so writes invalidate it.
49
+ 3. **A single atomic DB write (one commit boundary)?** → **mutation** (`mutations/`).
45
50
  Runs in a transaction; a throw rolls back; ChangeEvents drain only on commit.
46
51
  Declares `target: { table, op }` → drives client auto-optimistic.
47
- 3. **External I/O — HTTP, file, AI, payment, email, signed URL?** → **action**
52
+ 4. **External I/O — HTTP, file, AI, payment, email, signed URL?** → **action**
48
53
  (`actions/`). NOT transactional; no rollback of side effects.
49
- 4. **Multi-step work that must survive a crash/deploy, retry-from-where-it-died?**
54
+ 5. **Multi-step work that must survive a crash/deploy, retry-from-where-it-died?**
50
55
  → **workflow** (`*.workflow.tsx`). Durable, suspend/resume.
51
56
 
52
57
  | Use case | Primitive |
53
58
  |---|---|
59
+ | A game starts / a door opens / a printer finishes — many screens react | Event |
54
60
  | List todos / messages on a page | Query |
55
61
  | Toggle a todo, send a message, edit a name | Mutation |
56
62
  | Generate a thumbnail / send a welcome email / call an LLM ad-hoc | Action |
@@ -248,6 +254,7 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
248
254
  | `*.mutation.ts` + `*.mutation.server.ts` | transactional unary write |
249
255
  | `*.action.ts` + `*.action.server.ts` | non-transactional external I/O |
250
256
  | `*.stream.ts` + `*.stream.server.ts` | server→client stream (e.g. AI tokens) |
257
+ | `*.event.ts` | ephemeral fan-out event (`defineEvent`; no `.server` half) |
251
258
  | `*.workflow.tsx` + `*.workflow.server.tsx` | durable multi-step work |
252
259
  | `*.trigger.tsx` | domain event → workflow |
253
260
  | `*.cron.tsx` | scheduled job (single file) |
@@ -595,7 +602,7 @@ each plugin's own README.
595
602
 
596
603
  | Topic | Open | Summary |
597
604
  |---|---|---|
598
- | **What's new in 0.23.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. |
605
+ | **What's new in 0.25.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
599
606
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
600
607
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
601
608
  | 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. |
@@ -677,7 +684,7 @@ each plugin's own README.
677
684
  | storage | `node_modules/@voltro/plugin-storage/README.md` | File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / R2 / GCS / MinIO / filesystem / memory providers, presigned URLs, a dashboard browser. |
678
685
  | tinybird | `node_modules/@voltro/plugin-tinybird/README.md` | Hosted-ClickHouse AnalyticsSink over Tinybird's Events API + Pipes — pay-as-you-go OLAP without operating a cluster. |
679
686
  | versioning | `node_modules/@voltro/plugin-versioning/README.md` | Full row history + time-travel. audit() records who/when; versioning records what-changed-to-what — a value snapshot of every row on every write, with as-of queries. |
680
- | webhooks | `node_modules/@voltro/plugin-webhooks/README.md` | First-class outgoing + incoming webhooks — defineOutgoingEvent / defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency. |
687
+ | webhooks | `node_modules/@voltro/plugin-webhooks/README.md` | First-class outgoing + incoming webhooks — declared events with a webhook: block, defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency. |
681
688
 
682
689
 
683
690
  ---
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.23.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. |
12
+ | **What's new in 0.25.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. |
@@ -91,4 +91,4 @@ each plugin's own README.
91
91
  | storage | `node_modules/@voltro/plugin-storage/README.md` | File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / R2 / GCS / MinIO / filesystem / memory providers, presigned URLs, a dashboard browser. |
92
92
  | tinybird | `node_modules/@voltro/plugin-tinybird/README.md` | Hosted-ClickHouse AnalyticsSink over Tinybird's Events API + Pipes — pay-as-you-go OLAP without operating a cluster. |
93
93
  | versioning | `node_modules/@voltro/plugin-versioning/README.md` | Full row history + time-travel. audit() records who/when; versioning records what-changed-to-what — a value snapshot of every row on every write, with as-of queries. |
94
- | webhooks | `node_modules/@voltro/plugin-webhooks/README.md` | First-class outgoing + incoming webhooks — defineOutgoingEvent / defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency. |
94
+ | webhooks | `node_modules/@voltro/plugin-webhooks/README.md` | First-class outgoing + incoming webhooks — declared events with a webhook: block, defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency. |
@@ -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": 17
56
+ "files": 18
57
57
  },
58
58
  {
59
59
  "id": "database/advancedqueries",
@@ -134,7 +134,7 @@
134
134
  "group": "database.schema",
135
135
  "description": "Five generation schemes — TypeID (default), ULID, numeric, Snowflake, custom. Decision matrix, auto-injection lifecycle, cursor pagination, branded TypeScript types.",
136
136
  "path": "agent-docs/database/schema.md",
137
- "files": 8
137
+ "files": 9
138
138
  },
139
139
  {
140
140
  "id": "database/seedsdialects",
@@ -608,7 +608,7 @@
608
608
  {
609
609
  "slug": "webhooks",
610
610
  "title": "Webhooks",
611
- "description": "First-class outgoing + incoming webhooks — defineOutgoingEvent / defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency.",
611
+ "description": "First-class outgoing + incoming webhooks — declared events with a webhook: block, defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency.",
612
612
  "pkg": "@voltro/plugin-webhooks",
613
613
  "doc": "plugins/webhooks.md"
614
614
  }
@@ -166,6 +166,27 @@ import { handleSignIn } from '@voltro/plugin-auth'
166
166
 
167
167
  Use `handleSignIn` instead of rolling your own — the timing-oracle gap is the kind of subtle bug that hides for years. Note it returns a `401 HandlerResult`, it does not throw a domain error.
168
168
 
169
+ ## Brute-force lockout
170
+
171
+ `plugin-auth` locks an account after repeated failed credential attempts, so password-spraying and credential-stuffing don't get unlimited guesses. After **5 failed attempts** (a wrong password — or, for MFA users, a wrong second-factor code) within **15 minutes**, sign-in for that email is refused with a `429 account_locked` (carrying a `retryAfterSeconds`) for **15 minutes**. A completed login clears the counter.
172
+
173
+ The counter is keyed by **email**, not user id, and it tracks unknown addresses too: a locked account and an unknown-but-hammered address respond identically, so the lock can't be turned into an existence oracle — the same reasoning as the timing-oracle defence above.
174
+
175
+ It is **on by default** — a security default you get for free. Tune or disable it per app:
176
+
177
+ ```ts
178
+ authRoutesPlugin({
179
+ store,
180
+ lockout: {
181
+ maxAttempts: 5, // failed attempts before locking (default 5)
182
+ windowSeconds: 900, // counting window (default 15 min)
183
+ lockSeconds: 900, // lock duration (default 15 min)
184
+ },
185
+ })
186
+ ```
187
+
188
+ The counter lives in the `loginAttempts` table (contributed via `authTables`), so it appears automatically on your next `voltro db apply` / `voltro dev` boot. For lockout that holds across multiple nodes, back the store with Postgres (`postgresUserStore`) — the in-memory store is single-node.
189
+
169
190
  ## Rehashing on parameter bump
170
191
 
171
192
  When the framework updates the default cost parameters, existing hashes stay valid — `verifyPassword` reads `N`/`r`/`p` from the stored hash string itself (they're encoded inline as `scrypt$<N>$<r>$<p>$…`). Rehash-on-verify ships: `needsRehash(stored)` reports whether a hash is below the current cost, and `verifyPasswordWithRehash(plaintext, stored)` returns `{ valid, rehash? }` — when the password matches an under-cost hash, `rehash` is a freshly-minted replacement. `handleSignIn` wires this through `UserStore.updatePassword`, so a user's stored hash silently strengthens on their next login, no forced reset and no backfill.
@@ -2174,6 +2174,38 @@ voltro data restore ./backups/2026-07-01 # pg_restore / mysql / …
2174
2174
 
2175
2175
  These shell out to the vendor tools resolved from your `DB_DIALECT` + connection env. They produce a dialect-native artifact (`db.dump`, `db.sql`, `db.sqlite`, `db.bacpac`) that is lossless and point-in-time consistent for **same-dialect** restore — the right tool for disaster recovery. Secrets are passed via the tools' environment variables (`PGPASSWORD`, `MYSQL_PWD`), never on the command line, where the tool supports it. The named tool must be installed and on `PATH`.
2176
2176
 
2177
+ ### The provenance stamp — a restore that refuses the wrong DB
2178
+
2179
+ A native dump is opaque: it doesn't say which dialect made it, which schema shape it carries, or when. `backup` writes a sidecar `voltro-backup-stamp.json` next to the artifact recording exactly that — `dialect`, the live schema `fingerprint`, the `@voltro/cli` version, and the timestamp.
2180
+
2181
+ `restore` reads the stamp **before touching the DB** and acts on two failures that are otherwise silent until they corrupt:
2182
+
2183
+ - **Cross-dialect restore → refused.** Restoring a postgres dump while `DB_DIALECT=mysql` is never valid; it stops with an error instead of half-loading. Override with `--force` only if you genuinely know better.
2184
+ - **Schema/code skew → warned.** If the backup's schema fingerprint differs from what the running code declares, restore prints a warning to run `voltro db apply` afterwards — the dump's shape predates (or postdates) this deploy's code. (Production boot already refuses on a fingerprint mismatch; the stamp surfaces it at restore time, before the boot.)
2185
+
2186
+ A backup with no stamp (older, or hand-made) restores with a caution rather than a hard stop.
2187
+
2188
+ ### The restore drill — prove the backup, don't assume it
2189
+
2190
+ ```bash
2191
+ voltro data restore ./backups/2026-07-01 --drill --drill-url postgres://…/scratch
2192
+ # or set DRILL_DB_URL and just: voltro data restore ./backups/2026-07-01 --drill
2193
+ ```
2194
+
2195
+ `--drill` restores the artifact into a **throwaway** database (from `--drill-url` / `DRILL_DB_URL`) and verifies it — **without ever touching the live DB**. It refuses a drill target that resolves to your live connection (a drill that `--clean`s production is the disaster it exists to rehearse against). After the restore it introspects the throwaway DB and compares its schema fingerprint to the backup's stamp:
2196
+
2197
+ - **zero tables restored** → FAIL (the dump is empty or unreadable — this backup would not recover you),
2198
+ - **fingerprint disagrees with the stamp** → FAIL (the restore didn't reproduce what was backed up),
2199
+ - **tables + matching fingerprint** → PASS.
2200
+
2201
+ It exits non-zero on any FAIL, so a scheduled CI job turns a silently-broken backup into a red build. Run it against your latest artifact on a cron — a backup you've never restored is a hypothesis, and this is how you keep it a fact. (The verify is schema-level; a full app boot against the restored DB is a heavier check you can layer on top.)
2202
+
2203
+ ### Point-in-time recovery (PITR) is your database's job, not the framework's
2204
+
2205
+ `backup` is a point-in-time **snapshot**. "Restore to 14:32, just before the bad deploy" (PITR) needs continuous WAL/binlog archiving, which lives at the database/provider layer — pg's `archive_command` + a base backup (pgBackRest / WAL-G), a managed provider's continuous backup (RDS, Cloud SQL, Neon, PlanetScale). The framework deliberately does **not** reimplement it: layer PITR under these native snapshots at the infra layer. A weekly `voltro data backup` + provider PITR together give you both a portable artifact and a fine-grained restore point.
2206
+
2207
+ > **Test your backups.** A backup you've never restored is a hypothesis. Restore your latest artifact into a throwaway database and boot the app against it on a schedule — the stamp's dialect/fingerprint checks turn a silently-broken backup into a loud one, but only an actual restore proves the bytes are good.
2208
+
2177
2209
  ## Masking (prod → dev/stage safely)
2178
2210
 
2179
2211
  Cloning prod into a lower environment must not carry real user data. `voltro data export`