@voltro/cli 0.33.0 → 0.34.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 +1801 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-BICVGnEC.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CE7Ca9gm.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Dv8r9tGO.js +231 -0
- package/dist/checkCommand-YZDmkAGd.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-CNXKlcvD.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-DC6w2tNZ.js +30 -0
- package/dist/codemodRunner-DRRqXR74.js +5243 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-gutsz-Ac.js +808 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-3YG8p-UA.js +25 -0
- package/dist/dataCommand-qL0r7fPO.js +535 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
- package/dist/dbCommand-CpdKLeQq.js +2 -0
- package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
- package/dist/dev-CoG-ZPx8.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-BvqGBwNG.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
- package/dist/dormancyCommand-Dszo57d6.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
- package/dist/envCommand-DPgdV1Bq.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-DHpkgjgH.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
- package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
- package/dist/generateCommand-oibemh97.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-BJw9nLUR.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
- package/dist/inspect-C_T_WGvl.js +2 -0
- package/dist/inspectCmd-Bppy-GGw.js +224 -0
- package/dist/inspectFetch-Cm8_wVvp.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-CCca3KRZ.js +260 -0
- package/dist/manifestBuild-ChsKAhmn.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-CBwOt_iV.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CRxalXTs.js +91 -0
- package/dist/scheduleCmd--jksTrf6.js +69 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
- package/dist/seedRunner-TFHHiToI.js +329 -0
- package/dist/serveCommand-B_isw7q4.js +1647 -0
- package/dist/serveCommand-DOvbgRnQ.js +2 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-9LiUOfES.js +1087 -0
- package/dist/start-B-9Nsp-S.js +3 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-CkEZQrtt.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-Bkptutss.js +585 -0
- package/dist/updateCommand-us1_hdIC.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
- package/dist/webDev-Cg-fFiyd2.js +2 -0
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-CID96Rga.js +267 -0
- package/dist/workflowsCmd-D1VTmLMY.js +608 -0
- package/package.json +179 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +22 -5
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +265 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +794 -50
- package/templates/agent-docs/data.md +550 -11
- package/templates/agent-docs/database/migrations.md +174 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +69 -3
- package/templates/agent-docs/introduction.md +87 -16
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +95 -20
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +161 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +1 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +157 -6
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +150 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +10 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +1700 -104
- package/templates/agent-docs/workflows.md +130 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -350,7 +350,9 @@ export const ImportCustomers = workflow({
|
|
|
350
350
|
})
|
|
351
351
|
```
|
|
352
352
|
|
|
353
|
-
`payload` is the start input. `success` is the resolved output. `error` is the typed failure channel. `idempotencyKey` deduplicates concurrent or repeated starts with the same logical input. `messages` is optional codegen metadata; it emits `WorkflowSignals`, `WorkflowUpdates`,
|
|
353
|
+
`payload` is the start input. `success` is the resolved output. `error` is the typed failure channel. `idempotencyKey` deduplicates concurrent or repeated starts with the same logical input. `messages` is optional codegen metadata; it emits `WorkflowSignals`, `WorkflowUpdates`, and `WorkflowMessages` type maps, while runtime validation still happens at `awaitSignal(...)` / `awaitUpdate(...)`.
|
|
354
|
+
|
|
355
|
+
There are exactly two message channels: `signals` (fire-and-forget) and `updates` (synchronous, with a result). A `queries` channel was declarable until 0.34.0 and never had a send path — nothing could invoke one — so it is gone. To read a run's state, write an ordinary `*.query.ts` over `_voltro_workflow_runs` / `_voltro_workflow_run_steps`; to ask a running workflow something and get an answer, use `updates`.
|
|
354
356
|
|
|
355
357
|
## Step boundaries
|
|
356
358
|
|
|
@@ -493,7 +495,15 @@ The dashboard shows child runs, their parent execution id, and the selected poli
|
|
|
493
495
|
|
|
494
496
|
## Tenant and subject
|
|
495
497
|
|
|
496
|
-
Workflow starts persist the starter
|
|
498
|
+
Workflow starts persist the starter's trace id, source, parent execution id, parent-close policy — and their **identity**, never their authority — in `_voltro_workflow_start_contexts`. Whichever runner first executes the workflow loads that context before building the executor `AppContext`.
|
|
499
|
+
|
|
500
|
+
**The guarantee: identity is persisted, authority is re-resolved at resume.**
|
|
501
|
+
|
|
502
|
+
- **Identity** (type, id, `tenantId`, `metadata`) is written and read through the same stripping function the session cookie mints through. It has to survive: the tenant scope reads `tenantId`, the run row is attributed to `id`, and a plugin service resolving a per-user credential reads `metadata`. A workflow started by tenant A still acts on tenant A's rows in three days' time.
|
|
503
|
+
- **Authority** comes from your [`auth.resolveScopes`](/docs/authentication/strategies) on every execution attempt, with `ctx.origin === 'workflow'`. Wire no resolver and a resumed run has no scopes — fail-closed, and the same default a cookie-authenticated request has.
|
|
504
|
+
- **A run with no recorded caller** — a bootstrap, or one whose row aged out — runs as `SYSTEM_SUBJECT` and is not put through your resolver.
|
|
505
|
+
|
|
506
|
+
The column used to hold the whole `Subject`, scopes included. A role removed on Monday was still asserted by Thursday's resume, out of a row nothing re-validated, on a path with no request, no cookie and no expiry. Rows written by an older build are stripped on **read**, so a resumed run cannot re-assert authority that was persisted before this changed.
|
|
497
507
|
|
|
498
508
|
Still include tenant/user ids that the business process must enforce in `payload`, validate them in the first step, and scope store reads/writes deliberately. Payload data is replay-safe and makes authorization decisions auditable across retries and deploys.
|
|
499
509
|
|
|
@@ -1572,7 +1582,7 @@ The drainer runs on **one replica per tick** through the same claim arbiter the
|
|
|
1572
1582
|
<!-- source: en/workflows/versioning.md -->
|
|
1573
1583
|
## Versioning
|
|
1574
1584
|
|
|
1575
|
-
_Workflow definition versions, compatibility metadata,
|
|
1585
|
+
_Workflow definition versions, compatibility metadata, in-body patch markers, and the replay nondeterminism tripwire._
|
|
1576
1586
|
|
|
1577
1587
|
Long-running workflows can outlive a deploy. Voltro does not run old JavaScript forever; a resumed run executes the current code. Make that explicit by versioning the workflow definition.
|
|
1578
1588
|
|
|
@@ -1595,27 +1605,71 @@ Voltro stores `workflowVersion` and `workflowPatches` on every `_voltro_workflow
|
|
|
1595
1605
|
|
|
1596
1606
|
## Compatibility
|
|
1597
1607
|
|
|
1598
|
-
`compatibleWith`
|
|
1608
|
+
`compatibleWith` documents which run versions the current code can still resume. It is enforced: a resuming run whose stored `workflowVersion` is not listed is **terminally failed** with `WorkflowVersionIncompatible`.
|
|
1609
|
+
|
|
1610
|
+
That makes it a blunt instrument, and the bluntness is the point to understand before you reach for it. Bump `version` and leave the old one out, and every in-flight run on the old version dies. Do not bump, and those runs replay against the changed body with no protection at all. Neither is what you usually want — which is what `patches` is for.
|
|
1599
1611
|
|
|
1600
|
-
## Patch
|
|
1612
|
+
## Patch markers
|
|
1601
1613
|
|
|
1602
|
-
|
|
1614
|
+
A patch marker lets the **body itself** branch, so runs that started before a change finish on the old path while new runs take the new one. This is the middle option between "kill the in-flight runs" and "hope the replay works out".
|
|
1603
1615
|
|
|
1604
1616
|
```ts
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1617
|
+
import { patch, step, workflow } from '@voltro/workflow'
|
|
1618
|
+
|
|
1619
|
+
export const Charge = workflow({
|
|
1620
|
+
name: 'billing.charge',
|
|
1621
|
+
payload: { orderId: Schema.String },
|
|
1622
|
+
idempotencyKey: ({ orderId }) => `billing.charge:${orderId}`,
|
|
1623
|
+
patches: ['split-tax-calculation'],
|
|
1624
|
+
})
|
|
1625
|
+
|
|
1626
|
+
export default () => (payload) =>
|
|
1627
|
+
Effect.gen(function* () {
|
|
1628
|
+
if (yield* patch('split-tax-calculation')) {
|
|
1629
|
+
const net = yield* step({ name: 'net-total', execute: computeNet(payload) })
|
|
1630
|
+
const tax = yield* step({ name: 'tax', execute: computeTax(payload) })
|
|
1631
|
+
return net + tax
|
|
1632
|
+
}
|
|
1633
|
+
return yield* step({ name: 'total', execute: computeTotal(payload) })
|
|
1634
|
+
})
|
|
1609
1635
|
```
|
|
1610
1636
|
|
|
1611
|
-
|
|
1637
|
+
The answer is pinned to the **run**, not to the deployed code. `patches` is stamped onto `_voltro_workflow_runs.workflowPatches` when the run starts and read back from that row on every resume, so:
|
|
1638
|
+
|
|
1639
|
+
- a run started **before** you added the marker answers `false` for the rest of its life, however many times it replays;
|
|
1640
|
+
- a run started **after** answers `true`, and keeps answering `true` even if you later change the declaration.
|
|
1641
|
+
|
|
1642
|
+
That is what makes the branch deterministic across a redeploy. Outside a recorded workflow body — a unit test, a bare `step()` call — `patch()` is `false`, which is the pre-patch path.
|
|
1643
|
+
|
|
1644
|
+
### Retiring a patch
|
|
1645
|
+
|
|
1646
|
+
Once no run predating the marker can still be in flight, delete the old branch and remove the entry from `patches`. Runs that stamped it keep the marker on their row for the audit trail; `patch()` simply stops being called.
|
|
1647
|
+
|
|
1648
|
+
## The replay nondeterminism tripwire
|
|
1649
|
+
|
|
1650
|
+
Journal entries are keyed by step **name**, with no shape check. Edit a workflow body while runs are in flight and the engine replays the cached result for every name that still matches and freshly executes every name that does not. Nothing errors. A renamed step re-runs a side effect the run already performed; a removed step silently skips work the journal says was done.
|
|
1651
|
+
|
|
1652
|
+
Voltro watches for that. A run re-entering its body compares the steps it **reaches** against the steps it recorded on earlier attempts, and writes a `nondeterminism-suspected` event when they disagree:
|
|
1653
|
+
|
|
1654
|
+
| Finding | Means |
|
|
1655
|
+
|---|---|
|
|
1656
|
+
| `unreached-step` | A step this run ran on an earlier attempt that the current code never reaches — renamed, removed, or moved behind a branch. Its journaled result is orphaned. |
|
|
1657
|
+
| `extra-step-occurrence` | A recorded step reached more times than it was ever recorded — typically a loop bound that changed under a live run. |
|
|
1658
|
+
|
|
1659
|
+
The comparison is set membership plus a per-name count, never a total order: concurrent steps interleave differently on every attempt, so an order check would report correct code as broken.
|
|
1660
|
+
|
|
1661
|
+
**It is an event, never a failure.** The run keeps going and reaches its normal outcome. The checks sit on a best-effort recorder, so a lost step-row write is enough to make one fire — a false positive that killed a run would be worse than the divergence it suspects. Treat the event as "open this run and look", not as an outage.
|
|
1662
|
+
|
|
1663
|
+
The tripwire covers every path that replays an existing journal, including `voltro workflows redrive`. It is off on a first body entry (there is nothing to compare) and on a run with more than `VOLTRO_WORKFLOW_REPLAY_SHAPE_LIMIT` recorded steps (default 2000), where a truncated history would manufacture its own false positives.
|
|
1612
1664
|
|
|
1613
1665
|
## Rules
|
|
1614
1666
|
|
|
1615
|
-
- Bump `version`
|
|
1667
|
+
- Bump `version` only when old runs genuinely cannot continue — it kills them.
|
|
1668
|
+
- Reach for `patches` first for a body change that in-flight runs should not see.
|
|
1616
1669
|
- Keep old payload decoders inside the workflow body only while their version remains compatible.
|
|
1617
1670
|
- Prefer additive payload changes with defaults over breaking changes.
|
|
1618
1671
|
- Use the dashboard version chip during deploys to find runs that started on an older contract.
|
|
1672
|
+
- Treat a `nondeterminism-suspected` event as a deploy that needed a patch marker and did not get one.
|
|
1619
1673
|
|
|
1620
1674
|
|
|
1621
1675
|
|
|
@@ -1633,9 +1687,9 @@ Voltro records workflow runs, step attempts, and lifecycle events into framework
|
|
|
1633
1687
|
| Table | Contents |
|
|
1634
1688
|
|---|---|
|
|
1635
1689
|
| `_voltro_workflow_start_contexts` | One row per started execution id: starter subject, trace id, source, parent execution id, parent-close policy, and creation time. Used for cross-runner context handoff. |
|
|
1636
|
-
| `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy
|
|
1690
|
+
| `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy, plus the crash-loop bookkeeping `runnerEnteredAt` + `reclaimCount`. |
|
|
1637
1691
|
| `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
|
|
1638
|
-
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-redriven`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
|
|
1692
|
+
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-redriven`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`, `nondeterminism-suspected`, `run-crashlooped`, `run-stalled`. |
|
|
1639
1693
|
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.publish(...)`: event id, name, payload, source, subject, trace id, occurred time. |
|
|
1640
1694
|
| `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
|
|
1641
1695
|
|
|
@@ -1651,6 +1705,68 @@ Triage a dead-letter run one of three ways:
|
|
|
1651
1705
|
- **Re-drive** it — `voltro workflows redrive <id>` re-drives the run from its durable journal: completed steps replay, only the failed step(s) re-execute. Use this instead of `retry` for a long pipeline where redoing steps 1…N‑1 is expensive or unsafe. It is the after-the-fact counterpart to [`suspendOnFailure` + `resume`](/docs/workflows/retries) and works under `voltro serve` too. Refuses a non-failed / already-discarded run; declines cleanly when there is no durable journal (the memory store). Records a `run-redriven` event.
|
|
1652
1706
|
- **Discard** it — `voltro workflows discard <id>` acknowledges the failure so it drops off the `--dead-letter` view. It is an **ack, not a re-classification**: the run stays `status: 'failed'` (the outcome + audit trail survive) and gains a `discardedAt` timestamp. `--status failed` still lists it, marked `discarded`; only `--dead-letter` hides it. Discarding a non-failed run is refused, and discarding is idempotent.
|
|
1653
1707
|
|
|
1708
|
+
## Stuck runs and crash loops
|
|
1709
|
+
|
|
1710
|
+
Three failure modes are detected rather than left for whoever opens the dashboard.
|
|
1711
|
+
|
|
1712
|
+
**A run that stops moving.** A staleness sweep reports live runs (`running` or `suspended`) that have made no progress for longer than the threshold — default 30 minutes — writing a `run-stalled` event carrying `idleMs`, the reason (`awaiting-signal`, `suspended`, `no-progress`) and the last progress instant. It changes no run state; it is a signal, not an intervention. The classic catch is a run parked on a signal nobody ever sends.
|
|
1713
|
+
|
|
1714
|
+
Two things it deliberately stays quiet about, because otherwise the signal is worthless:
|
|
1715
|
+
|
|
1716
|
+
- A run inside a durable `sleep` / `sleepUntil` whose wake instant is still in the future. That run is waiting by design, and a seven-day timer is not a stall.
|
|
1717
|
+
- A run already reported since its last progress. A stall is reported once and again only after the run moves and stalls afresh.
|
|
1718
|
+
|
|
1719
|
+
Configure it in `app.config.ts` — every field optional:
|
|
1720
|
+
|
|
1721
|
+
```ts
|
|
1722
|
+
export default defineApiApp({
|
|
1723
|
+
workflows: {
|
|
1724
|
+
staleness: {
|
|
1725
|
+
// Set this above your slowest single STEP, not above your longest RUN:
|
|
1726
|
+
// a run waiting on a durable timer is already excluded. Default 30 min.
|
|
1727
|
+
stallAfterMs: 30 * 60_000,
|
|
1728
|
+
runPage: 200, // live runs examined per tick, oldest first
|
|
1729
|
+
onStalled: async (run) => {
|
|
1730
|
+
await page(`${run.tag} ${run.runId} idle ${run.idleMs}ms (${run.reason})`)
|
|
1731
|
+
},
|
|
1732
|
+
},
|
|
1733
|
+
},
|
|
1734
|
+
// How often the sweep looks. `VOLTRO_STALENESS_SWEEP_MS` overrides it.
|
|
1735
|
+
scheduling: { stalenessSweepMs: 5 * 60_000 },
|
|
1736
|
+
})
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
`onStalled` must not throw — a rejection is collected and reported, so one bad
|
|
1740
|
+
pager integration cannot stop detection for every other workflow.
|
|
1741
|
+
|
|
1742
|
+
The sweep runs on a **coordinated** schedule, so one firing per interval
|
|
1743
|
+
fleet-wide rather than one per replica. Unlike every other framework background
|
|
1744
|
+
task it **never stops ticking when idle**: those disarm because an arrival wakes
|
|
1745
|
+
them, and a run going stale writes nothing there is to wake on. That makes the
|
|
1746
|
+
cadence an unconditional cost, which is why it defaults to five minutes rather
|
|
1747
|
+
than one second — on a thirty-minute threshold that is 12 coordination rows an
|
|
1748
|
+
hour instead of 3 600.
|
|
1749
|
+
|
|
1750
|
+
`voltro doctor` runs the same detection **once**, for the moment you are standing
|
|
1751
|
+
in front of a deployment asking whether anything is wedged:
|
|
1752
|
+
|
|
1753
|
+
```
|
|
1754
|
+
✗ stuck runs: 2 run(s) have made no progress
|
|
1755
|
+
invoices.settle wr_01J… idle 4h awaiting-signal
|
|
1756
|
+
report.nightly wr_01J… idle 2h no-progress
|
|
1757
|
+
Nothing was changed — this is a SUSPICION, not a verdict. `awaiting-signal`
|
|
1758
|
+
usually means the sender never came.
|
|
1759
|
+
```
|
|
1760
|
+
|
|
1761
|
+
It is the one doctor rule that reads your DATABASE rather than your source, so
|
|
1762
|
+
run it where the app's DB env vars are set; anywhere else it prints a named skip
|
|
1763
|
+
rather than a clean tick. It records nothing and calls no `onStalled`, so running
|
|
1764
|
+
it neither pages anyone nor suppresses the background sweep's next real report.
|
|
1765
|
+
|
|
1766
|
+
**A run that keeps killing its runner.** A step that crashes the process — OOM, a native crash — cannot be caught as an error: the shard lease expires, a surviving replica claims it, and executes the same payload. Without a ceiling that rotates around the fleet forever. Voltro counts consecutive runner deaths on the run row (`runnerEnteredAt` is set on every body entry and cleared on every clean exit; `reclaimCount` counts entries that found the previous marker still set). At `VOLTRO_WORKFLOW_MAX_RECLAIMS` (default 3) the run is parked as `suspended` with `errorTag: 'WorkflowCrashLooped'` and a `run-crashlooped` event, and the body is not entered again. The counter is consecutive — any clean re-entry resets it — so a long-lived healthy run is never parked for a crash it had months ago, and an operator resume gives it a fresh budget.
|
|
1767
|
+
|
|
1768
|
+
**A run replaying against edited code.** See [Versioning](/docs/workflows/versioning) for the `nondeterminism-suspected` event.
|
|
1769
|
+
|
|
1654
1770
|
## Dashboard
|
|
1655
1771
|
|
|
1656
1772
|
When `voltro dev` is running, the Workflows panel lists recent runs, their status, start source, timing, payload, output/error, step attempts, and events. The run detail view is the fastest way to answer:
|
|
@@ -25,6 +25,17 @@ export type SummaryResult = Schema.Schema.Type<typeof SummaryResult>
|
|
|
25
25
|
|
|
26
26
|
export const summarize = defineAction({
|
|
27
27
|
name: 'support.summarize',
|
|
28
|
+
// Summarises the `text` the caller passed in this same call — it reads no
|
|
29
|
+
// table, no other caller's data, and never returns the provider key. No
|
|
30
|
+
// identity ships with this template (no auth strategy, no rbac), so a scope
|
|
31
|
+
// guard here would deny every caller.
|
|
32
|
+
//
|
|
33
|
+
// What this does NOT say is "harmless": it spends provider tokens per call,
|
|
34
|
+
// so before it faces the internet put `@voltro/plugin-ratelimit` in front of
|
|
35
|
+
// it and swap this line for a `guards:` once your app has a caller to name.
|
|
36
|
+
openAccess:
|
|
37
|
+
'summarises caller-supplied text; reads no table and no other caller\'s data. '
|
|
38
|
+
+ 'Metered by the AI provider, so rate-limit and guard it before it is public.',
|
|
28
39
|
input: Schema.Struct({
|
|
29
40
|
// The raw text to summarise (a support thread, a doc, a transcript).
|
|
30
41
|
text: Schema.String,
|
|
@@ -12,17 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/ai": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/protocol": "0.34.0",
|
|
20
|
+
"@voltro/runtime": "0.34.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.34.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
26
27
|
"vitest": "^4.1.10"
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -8,6 +8,19 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const me = defineAction({
|
|
10
10
|
name: 'session.me',
|
|
11
|
+
// Open ON PURPOSE, and it is the one procedure in an authenticated app that
|
|
12
|
+
// has to be: it answers "who am I", and the caller who most needs an answer
|
|
13
|
+
// is the one who is nobody yet. A guard here would return `ScopeError` to an
|
|
14
|
+
// anonymous browser instead of `{ type: 'anonymous' }`, and the sign-in
|
|
15
|
+
// redirect has nothing to branch on.
|
|
16
|
+
//
|
|
17
|
+
// Safe because the answer is a projection of the caller's OWN request: the
|
|
18
|
+
// Subject `voltroPasswordStrategy` resolved from the cookie they sent. It
|
|
19
|
+
// reads no table and can say nothing about anybody else.
|
|
20
|
+
openAccess:
|
|
21
|
+
'echoes the caller\'s own resolved Subject (type/id/tenantId) — reads no table and '
|
|
22
|
+
+ 'reveals nothing the caller did not present. Anonymous callers must reach it, or '
|
|
23
|
+
+ 'nothing can tell them to sign in.',
|
|
11
24
|
input: Schema.Struct({}),
|
|
12
25
|
output: Schema.Struct({
|
|
13
26
|
// 'anonymous' | 'user' | 'apiKey' | 'serviceAccount'
|
|
@@ -13,18 +13,19 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-auth": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.34.0",
|
|
20
|
+
"@voltro/protocol": "0.34.0",
|
|
21
|
+
"@voltro/runtime": "0.34.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.34.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.34.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
28
29
|
"vitest": "^4.1.10"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
|
|
|
9
9
|
|
|
10
10
|
export const createNote = defineMutation({
|
|
11
11
|
name: 'notes.create',
|
|
12
|
+
// Open, deliberately — see `queries/notes.query.ts` for the long form. In
|
|
13
|
+
// short: `assertOwnTenant` still rejects a `tenantId` that does not match the
|
|
14
|
+
// resolved subject, but with no auth strategy configured that subject is
|
|
15
|
+
// anonymous and holds no scopes, so any `guards: [{ scope }]` would deny
|
|
16
|
+
// every caller instead of some of them.
|
|
17
|
+
openAccess:
|
|
18
|
+
'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
|
|
19
|
+
+ '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
|
|
20
|
+
+ 'there is no identity a scope guard could name.',
|
|
12
21
|
target: {
|
|
13
22
|
table: 'notes',
|
|
14
23
|
op: 'insert',
|
|
@@ -3,27 +3,31 @@
|
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./rpcGroup": "./rpcGroup.generated.ts"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"dev": "voltro dev .",
|
|
8
11
|
"lint": "voltro doctor .",
|
|
9
12
|
"migrate": "voltro migrate",
|
|
10
13
|
"test": "voltro test",
|
|
11
|
-
"typecheck": "tsc --noEmit"
|
|
14
|
+
"typecheck": "voltro codegen . && tsc --noEmit"
|
|
12
15
|
},
|
|
13
16
|
"dependencies": {
|
|
14
17
|
"@effect/platform": "^0.97.0",
|
|
15
18
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
19
|
+
"@voltro/cli": "0.34.0",
|
|
20
|
+
"@voltro/database": "0.34.0",
|
|
21
|
+
"@voltro/env": "0.34.0",
|
|
22
|
+
"@voltro/plugin-multitenancy": "0.34.0",
|
|
23
|
+
"@voltro/protocol": "0.34.0",
|
|
24
|
+
"@voltro/runtime": "0.34.0",
|
|
22
25
|
"effect": "^3.22.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
28
|
+
"@voltro/testing": "0.34.0",
|
|
26
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
27
31
|
"vitest": "^4.1.10"
|
|
28
32
|
}
|
|
29
33
|
}
|
|
@@ -8,13 +8,35 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const listNotes = defineQuery({
|
|
10
10
|
name: 'notes.list',
|
|
11
|
+
// Every wire-exposed procedure must declare an access decision — `guards:` or
|
|
12
|
+
// `openAccess:` — or the app refuses to boot (`security.defaultDeny`). This
|
|
13
|
+
// one is open, and the reason says what that buys and what it does not:
|
|
14
|
+
//
|
|
15
|
+
// `tenant()` confines the result to the tenantId on the request and re-checks
|
|
16
|
+
// it on every delivery, so one tenant's rows never reach another's
|
|
17
|
+
// subscription. But this template configures NO auth strategy, so that
|
|
18
|
+
// tenantId comes from the caller's own `x-tenant` header — it shapes the
|
|
19
|
+
// result, it does not authorize the caller. A `guards: [{ scope }]` here
|
|
20
|
+
// would be unsatisfiable: with no auth strategy and no rbac, every caller
|
|
21
|
+
// resolves to an anonymous Subject that holds no scopes, so the guard would
|
|
22
|
+
// deny 100% of traffic. That is not strict security, it is an outage.
|
|
23
|
+
//
|
|
24
|
+
// So: add an auth strategy (see the `api-auth` template) FIRST, then swap
|
|
25
|
+
// this line for a `guards:` in the same change — at that point `tenant()`
|
|
26
|
+
// becomes real isolation because the tenant comes from a verified session.
|
|
27
|
+
openAccess:
|
|
28
|
+
'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
|
|
29
|
+
+ 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
|
|
30
|
+
+ 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
|
|
11
31
|
input: Schema.Struct({}),
|
|
12
|
-
output: Schema.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
output: Schema.Array(
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
id: Schema.String,
|
|
35
|
+
title: Schema.String,
|
|
36
|
+
body: Schema.String,
|
|
37
|
+
done: Schema.Boolean,
|
|
38
|
+
tenantId: Schema.String,
|
|
39
|
+
createdAt: Schema.Date,
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
20
42
|
})
|
|
@@ -8,6 +8,20 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const getUser = defineAction({
|
|
10
10
|
name: 'users.get',
|
|
11
|
+
// Reads one `users` row by id and returns its email — so read the reason
|
|
12
|
+
// literally, not as a shrug. `users` carries `deactivation()` and NOT
|
|
13
|
+
// `tenant()`, so nothing scopes this read to a caller. It is open because the
|
|
14
|
+
// whole table is this demo's own output: the template ships no seed and
|
|
15
|
+
// `users.create` below is the only writer, both on the same unauthenticated
|
|
16
|
+
// surface. There is no third party's address in here to leak.
|
|
17
|
+
//
|
|
18
|
+
// The moment you point this at real accounts that stops being true. Wire an
|
|
19
|
+
// auth strategy (see `api-auth`) and replace this line with
|
|
20
|
+
// `guards: [{ scope: 'users:read' }]` in the same change.
|
|
21
|
+
openAccess:
|
|
22
|
+
'reads one row of this demo\'s own `users` table by id; the table has no seed and no '
|
|
23
|
+
+ 'writer but `users.create` on this same open surface, so it holds no third party\'s data. '
|
|
24
|
+
+ 'Guard it before real accounts land here.',
|
|
11
25
|
input: Schema.Struct({ id: Schema.String }),
|
|
12
26
|
output: Schema.NullOr(Schema.Struct({
|
|
13
27
|
id: Schema.String,
|
|
@@ -4,6 +4,14 @@ import { Schema } from 'effect'
|
|
|
4
4
|
export const createUser = defineMutation({
|
|
5
5
|
name: 'users.create',
|
|
6
6
|
target: { table: 'users', op: 'insert' },
|
|
7
|
+
// Writes exactly the email + name the caller passed, into this template's own
|
|
8
|
+
// `users` demo table, and echoes them back. It reads nothing and can reach no
|
|
9
|
+
// existing row (`email` is `.unique()`, so a duplicate fails rather than
|
|
10
|
+
// overwriting). No identity ships with this template, so a scope guard would
|
|
11
|
+
// deny every caller.
|
|
12
|
+
openAccess:
|
|
13
|
+
'inserts only the fields the caller supplied into the demo `users` table and echoes them '
|
|
14
|
+
+ 'back; reads nothing and cannot modify an existing row (`email` is unique).',
|
|
7
15
|
input: Schema.Struct({
|
|
8
16
|
email: Schema.NonEmptyString,
|
|
9
17
|
name: Schema.String,
|
|
@@ -9,6 +9,16 @@ import { Schema } from 'effect'
|
|
|
9
9
|
export const deactivateUser = defineMutation({
|
|
10
10
|
name: 'users.deactivate',
|
|
11
11
|
target: { table: 'users', op: 'update' },
|
|
12
|
+
// Stamps `deactivatedAt` on a caller-named row. Open only because the table
|
|
13
|
+
// is this demo's own output (no seed, one writer, no tenant scope) — the
|
|
14
|
+
// decision itself is "anyone may lock anyone out", which is precisely the
|
|
15
|
+
// shape you must NOT ship. This is the first procedure in this template to
|
|
16
|
+
// grow a `guards: [{ scope: 'users:admin' }]` once an auth strategy gives you
|
|
17
|
+
// a caller to name.
|
|
18
|
+
openAccess:
|
|
19
|
+
'flips `deactivatedAt` on a caller-named row of the demo `users` table — reversible, and '
|
|
20
|
+
+ 'the table holds only rows this same open surface created. An account-locking action: '
|
|
21
|
+
+ 'guard it in the same change that adds authentication.',
|
|
12
22
|
input: Schema.Struct({ id: Schema.String }),
|
|
13
23
|
output: Schema.Struct({
|
|
14
24
|
id: Schema.String,
|
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-deactivation": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.34.0",
|
|
20
|
+
"@voltro/protocol": "0.34.0",
|
|
21
|
+
"@voltro/runtime": "0.34.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.34.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
27
28
|
"vitest": "^4.1.10"
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -10,6 +10,23 @@ import { defineAction } from '@voltro/protocol'
|
|
|
10
10
|
|
|
11
11
|
export const sendWelcome = defineAction({
|
|
12
12
|
name: 'mail.sendWelcome',
|
|
13
|
+
// Read this one carefully before copying it. Sending THIS APP'S branded mail
|
|
14
|
+
// to a caller-supplied address is one of the few things a tenant boundary
|
|
15
|
+
// does not bound — the recipient is an arbitrary third party, not a row.
|
|
16
|
+
//
|
|
17
|
+
// It is open here for exactly two reasons, both checkable in this repo:
|
|
18
|
+
// `provider: 'console'` in app.config.ts means nothing leaves the process
|
|
19
|
+
// (the send is logged and captured in the dev outbox), and no auth strategy
|
|
20
|
+
// ships in this template, so a `guards: [{ scope: 'mail:send' }]` would deny
|
|
21
|
+
// every caller rather than the wrong ones.
|
|
22
|
+
//
|
|
23
|
+
// The moment you swap in `resend` / `postmark` / `sendgrid` / `smtp`, this
|
|
24
|
+
// becomes an open relay for your own domain's reputation. Add an auth
|
|
25
|
+
// strategy and that `mail:send` guard in the SAME change.
|
|
26
|
+
openAccess:
|
|
27
|
+
'sends the `welcome` template to a caller-supplied address via the `console` provider — '
|
|
28
|
+
+ 'nothing leaves the process, it is logged and captured in the dev outbox. Switching to a '
|
|
29
|
+
+ 'real provider makes this an open relay: add an auth strategy and a `mail:send` guard first.',
|
|
13
30
|
input: Schema.Struct({
|
|
14
31
|
email: Schema.String,
|
|
15
32
|
name: Schema.String,
|
|
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
|
|
|
9
9
|
|
|
10
10
|
export const createNote = defineMutation({
|
|
11
11
|
name: 'notes.create',
|
|
12
|
+
// Open, deliberately — see `queries/notes.query.ts` for the long form. In
|
|
13
|
+
// short: `assertOwnTenant` still rejects a `tenantId` that does not match the
|
|
14
|
+
// resolved subject, but with no auth strategy configured that subject is
|
|
15
|
+
// anonymous and holds no scopes, so any `guards: [{ scope }]` would deny
|
|
16
|
+
// every caller instead of some of them.
|
|
17
|
+
openAccess:
|
|
18
|
+
'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
|
|
19
|
+
+ '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
|
|
20
|
+
+ 'there is no identity a scope guard could name.',
|
|
12
21
|
target: {
|
|
13
22
|
table: 'notes',
|
|
14
23
|
op: 'insert',
|
|
@@ -13,19 +13,20 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@react-email/components": "^1.0.12",
|
|
15
15
|
"@react-email/render": "^1.4.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-mail": "0.
|
|
20
|
-
"@voltro/plugin-multitenancy": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/plugin-mail": "0.34.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.34.0",
|
|
21
|
+
"@voltro/protocol": "0.34.0",
|
|
22
|
+
"@voltro/runtime": "0.34.0",
|
|
23
23
|
"effect": "^3.22.0",
|
|
24
24
|
"react": "^19.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.34.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
30
|
"vitest": "^4.1.10"
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -8,13 +8,35 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const listNotes = defineQuery({
|
|
10
10
|
name: 'notes.list',
|
|
11
|
+
// Every wire-exposed procedure must declare an access decision — `guards:` or
|
|
12
|
+
// `openAccess:` — or the app refuses to boot (`security.defaultDeny`). This
|
|
13
|
+
// one is open, and the reason says what that buys and what it does not:
|
|
14
|
+
//
|
|
15
|
+
// `tenant()` confines the result to the tenantId on the request and re-checks
|
|
16
|
+
// it on every delivery, so one tenant's rows never reach another's
|
|
17
|
+
// subscription. But this template configures NO auth strategy, so that
|
|
18
|
+
// tenantId comes from the caller's own `x-tenant` header — it shapes the
|
|
19
|
+
// result, it does not authorize the caller. A `guards: [{ scope }]` here
|
|
20
|
+
// would be unsatisfiable: with no auth strategy and no rbac, every caller
|
|
21
|
+
// resolves to an anonymous Subject that holds no scopes, so the guard would
|
|
22
|
+
// deny 100% of traffic. That is not strict security, it is an outage.
|
|
23
|
+
//
|
|
24
|
+
// So: add an auth strategy (see the `api-auth` template) FIRST, then swap
|
|
25
|
+
// this line for a `guards:` in the same change — at that point `tenant()`
|
|
26
|
+
// becomes real isolation because the tenant comes from a verified session.
|
|
27
|
+
openAccess:
|
|
28
|
+
'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
|
|
29
|
+
+ 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
|
|
30
|
+
+ 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
|
|
11
31
|
input: Schema.Struct({}),
|
|
12
|
-
output: Schema.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
output: Schema.Array(
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
id: Schema.String,
|
|
35
|
+
title: Schema.String,
|
|
36
|
+
body: Schema.String,
|
|
37
|
+
done: Schema.Boolean,
|
|
38
|
+
tenantId: Schema.String,
|
|
39
|
+
createdAt: Schema.Date,
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
20
42
|
})
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# {{capProjectName}} — MariaDB backend env. Copy to `.env` and adjust.
|
|
2
2
|
|
|
3
|
+
# ── Environment ────────────────────────────────────────────────────────
|
|
4
|
+
#
|
|
5
|
+
# DECLARE IT. Every `voltro db …` / `voltro migrate` invocation resolves an
|
|
6
|
+
# UNSET `NODE_ENV` to `production` — the same way `voltro serve` and `voltro
|
|
7
|
+
# start` do — so a bare `pnpm migrate` with no NODE_ENV refuses rather than
|
|
8
|
+
# applying an un-reviewed diff to what might be a production database. It also
|
|
9
|
+
# decides which `_voltro_*` bookkeeping tables the command declares, and a
|
|
10
|
+
# migration command that resolves it differently from the serving process
|
|
11
|
+
# declares a DIFFERENT schema.
|
|
12
|
+
#
|
|
13
|
+
# `voltro dev` declares `development` for itself and needs nothing from here;
|
|
14
|
+
# this line is what makes the explicit schema commands work locally.
|
|
15
|
+
NODE_ENV=development
|
|
16
|
+
|
|
3
17
|
# ── Database (MariaDB + binlog CDC) ────────────────────────────────────
|
|
4
18
|
DB_DIALECT=mariadb
|
|
5
19
|
DB_URL=mysql://app:app@localhost:3307/{{projectNameSnake}}
|