@voltro/cli 0.33.0 → 0.35.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 +1968 -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-B8aoJvuw.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
- package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
- package/dist/appGraph-zuMGKVYX.js +2 -0
- 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-CD8K4XOr.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Ct9xkTrS.js +232 -0
- package/dist/checkCommand-DKpDLlqu.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-D5wsCmA_.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-C4YoQIc2.js +30 -0
- package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-BE8E7zF3.js +816 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-D7SgZGaN.js +25 -0
- package/dist/dataCommand-BhYwDgg-.js +537 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/dbCommand-DS4b97Is.js +2 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
- package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
- package/dist/dev-D2BikO7a.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-CM4Ch9C7.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
- package/dist/dormancyCommand-QewYug_s.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
- package/dist/envCommand-CCGPRQY1.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-CHsLCtDf.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
- package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
- package/dist/generateCommand-DbgcUpGw.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-DwOgK1t6.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
- package/dist/inspect-P4pxoMaV.js +2 -0
- package/dist/inspectCmd-EHFZ9yYu.js +224 -0
- package/dist/inspectFetch-EMuhTG_9.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-D36xK7Zu.js +260 -0
- package/dist/manifestBuild-COkJoyAr.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-BV7I-ZHZ.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/probeCommand-CZfaaUOZ.js +122 -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-CH3eUiMw.js +91 -0
- package/dist/scheduleCmd-DQRu6BZC.js +149 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
- package/dist/seedRunner-ZmLSqNe2.js +333 -0
- package/dist/serveCommand-CbDHU6l-.js +2 -0
- package/dist/serveCommand-iwlUBNS1.js +1766 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-BgN62boB.js +3 -0
- package/dist/start-T4VesWiM.js +1087 -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-DStmCJPi.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-BlMZhWgO.js +2 -0
- package/dist/updateCommand-x0pI_x-B.js +585 -0
- package/dist/webDev-BcykISYQ2.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
- package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
- package/package.json +209 -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 +23 -6
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +313 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +853 -50
- package/templates/agent-docs/data.md +608 -12
- package/templates/agent-docs/database/migrations.md +223 -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 +284 -25
- package/templates/agent-docs/introduction.md +88 -17
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +188 -42
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +247 -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 +32 -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 +162 -8
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/storage.md +11 -0
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +152 -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 +27 -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 +98 -136
- package/templates/agent-docs/workflows.md +231 -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/appGraph-CvQCte0z.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-BwIrO5nv.js +0 -638
- 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
|
@@ -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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-mail": "0.35.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "0.35.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.35.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}}
|
|
@@ -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
|
"@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-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-storage": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
23
|
-
"@voltro/sql-mysql": "0.
|
|
16
|
+
"@voltro/cli": "0.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
20
|
+
"@voltro/plugin-storage": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "0.35.0",
|
|
23
|
+
"@voltro/sql-mysql": "0.35.0",
|
|
24
24
|
"effect": "^3.22.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.35.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}} — SQLite 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 (embedded SQLite) ─────────────────────────────────────────
|
|
4
18
|
DB_DIALECT=sqlite
|
|
5
19
|
# A file path keeps data across restarts (the `.data/` dir is gitignored):
|
|
@@ -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,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-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-sqlite": "0.
|
|
16
|
+
"@voltro/cli": "0.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
20
|
+
"@voltro/protocol": "0.35.0",
|
|
21
|
+
"@voltro/runtime": "0.35.0",
|
|
22
|
+
"@voltro/sql-sqlite": "0.35.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.35.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
28
29
|
"vitest": "^4.1.10"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -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
|
})
|
|
@@ -10,6 +10,19 @@ import { defineAction } from '@voltro/protocol'
|
|
|
10
10
|
|
|
11
11
|
export const uploadAvatar = defineAction({
|
|
12
12
|
name: 'storage.uploadAvatar',
|
|
13
|
+
// Writes bytes the caller sent to a PUBLIC object and returns its URL. It
|
|
14
|
+
// reads nothing and can overwrite nothing — the key is minted server-side per
|
|
15
|
+
// upload — but an unguarded upload endpoint is free hosting for whatever the
|
|
16
|
+
// internet feels like storing. `provider: 'memory'` in app.config.ts keeps
|
|
17
|
+
// that inside this process; the plugin's `maxBytes` / content-type policy is
|
|
18
|
+
// what bounds it once you point at S3/R2.
|
|
19
|
+
//
|
|
20
|
+
// No auth strategy ships in this template, so a scope guard would deny every
|
|
21
|
+
// caller. Add one, then `guards: [{ scope: 'files:write' }]`.
|
|
22
|
+
openAccess:
|
|
23
|
+
'stores caller-supplied bytes as a new PUBLIC object and returns its URL — reads nothing, '
|
|
24
|
+
+ 'overwrites nothing (the key is minted server-side). The `memory` provider keeps it '
|
|
25
|
+
+ 'in-process; guard it before pointing at a real bucket.',
|
|
13
26
|
input: Schema.Struct({
|
|
14
27
|
bytesBase64: Schema.String,
|
|
15
28
|
contentType: Schema.String,
|
|
@@ -9,6 +9,18 @@ import { defineAction } from '@voltro/protocol'
|
|
|
9
9
|
|
|
10
10
|
export const uploadDocument = defineAction({
|
|
11
11
|
name: 'storage.uploadDocument',
|
|
12
|
+
// The WRITE is open; the READ is not, and the split is the point. This stores
|
|
13
|
+
// caller-supplied bytes as a PRIVATE object and returns only its id — the
|
|
14
|
+
// bytes come back exclusively through the plugin's access-checked
|
|
15
|
+
// `storage.mintUrl` route, which applies the object's policy (owner + tenant
|
|
16
|
+
// + granted roles) and is not this procedure's decision to make.
|
|
17
|
+
//
|
|
18
|
+
// No auth strategy ships in this template, so a `guards:` here would deny
|
|
19
|
+
// every caller. Add one, then `guards: [{ scope: 'files:write' }]`.
|
|
20
|
+
openAccess:
|
|
21
|
+
'stores caller-supplied bytes as a new PRIVATE object and returns only its id; reading it '
|
|
22
|
+
+ 'back goes through the plugin\'s access-checked `storage.mintUrl`, which this does not '
|
|
23
|
+
+ 'bypass. Guard the write too once the app has an identity.',
|
|
12
24
|
input: Schema.Struct({
|
|
13
25
|
bytesBase64: Schema.String,
|
|
14
26
|
contentType: 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',
|
|
@@ -11,18 +11,19 @@
|
|
|
11
11
|
"test": "voltro test"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/plugin-storage": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.35.0",
|
|
15
|
+
"@voltro/database": "0.35.0",
|
|
16
|
+
"@voltro/env": "0.35.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
18
|
+
"@voltro/plugin-storage": "0.35.0",
|
|
19
|
+
"@voltro/protocol": "0.35.0",
|
|
20
|
+
"@voltro/runtime": "0.35.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.35.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,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,13 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const getContent = defineAction({
|
|
10
10
|
name: 'content.get',
|
|
11
|
+
// Editors only — it returns the FULL row, every per-type field, for a
|
|
12
|
+
// caller-supplied id. Tenant scoping already makes another tenant's row read
|
|
13
|
+
// as absent; this decides whether an unauthenticated visitor may ask at all.
|
|
14
|
+
// Satisfied by a signed-in session (`auth.resolveScopes` in app.config.ts
|
|
15
|
+
// grants `content:read` to a matched subject); anonymous callers hold no
|
|
16
|
+
// scopes and are refused.
|
|
17
|
+
guards: [{ scope: 'content:read' }],
|
|
11
18
|
input: Schema.Struct({
|
|
12
19
|
type: Schema.NonEmptyString,
|
|
13
20
|
id: Schema.NonEmptyString,
|
|
@@ -23,6 +23,12 @@ const FieldWire = Schema.Struct({
|
|
|
23
23
|
|
|
24
24
|
export const contentTypes = defineAction({
|
|
25
25
|
name: 'content.types',
|
|
26
|
+
// Editors only. It carries no row data at all — but it IS a map of your
|
|
27
|
+
// content model (every type, every field, every constraint), which is
|
|
28
|
+
// reconnaissance you have no reason to hand an anonymous caller. Guarded with
|
|
29
|
+
// the same `content:read` the editor already needs, so this costs a signed-in
|
|
30
|
+
// editor nothing and tells a stranger nothing.
|
|
31
|
+
guards: [{ scope: 'content:read' }],
|
|
26
32
|
input: Schema.Struct({}),
|
|
27
33
|
output: Schema.Struct({
|
|
28
34
|
types: Schema.Array(
|
|
@@ -7,6 +7,19 @@ import { Schema } from 'effect'
|
|
|
7
7
|
|
|
8
8
|
export const me = defineAction({
|
|
9
9
|
name: 'session.me',
|
|
10
|
+
// The ONE open procedure in this app, and it has to be. Every `content.*`
|
|
11
|
+
// procedure beside it carries a `content:*` guard that an anonymous caller
|
|
12
|
+
// cannot satisfy — which is correct — but the SSR auth gate has to be able to
|
|
13
|
+
// ask "who am I" while it is still nobody, or it has nothing to branch on and
|
|
14
|
+
// the redirect to /login never happens.
|
|
15
|
+
//
|
|
16
|
+
// Safe because the answer is a projection of the caller's own request: the
|
|
17
|
+
// Subject `voltroPasswordStrategy` resolved from the cookie they sent. It
|
|
18
|
+
// reads no table and can say nothing about anybody else.
|
|
19
|
+
openAccess:
|
|
20
|
+
'echoes the caller\'s own resolved Subject (type/id/tenantId) — reads no table and reveals '
|
|
21
|
+
+ 'nothing the caller did not present. Anonymous callers must reach it, or the SSR gate '
|
|
22
|
+
+ 'cannot redirect them to sign in.',
|
|
10
23
|
input: Schema.Struct({}),
|
|
11
24
|
output: Schema.Struct({
|
|
12
25
|
type: Schema.String,
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// (`store: 'memory'`, in-process users); the README shows the production swaps.
|
|
20
20
|
import { defineEnv, envVar } from '@voltro/env'
|
|
21
21
|
import { authRoutesPlugin, memoryUserStore, voltroPasswordStrategy } from '@voltro/plugin-auth'
|
|
22
|
+
import { resolveScopes } from './authz'
|
|
22
23
|
|
|
23
24
|
export const env = defineEnv({
|
|
24
25
|
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
@@ -57,5 +58,23 @@ export default {
|
|
|
57
58
|
// Resolves the session cookie into the request Subject the handlers read
|
|
58
59
|
// (`ctx.request.subject`) and the store spine scopes writes by.
|
|
59
60
|
strategies: [voltroPasswordStrategy()],
|
|
61
|
+
// …and turns that identity into AUTHORITY. A session cookie carries no
|
|
62
|
+
// `scopes` by design (identity is settled at sign-in; authority is re-read
|
|
63
|
+
// per request), so without this the `content:*` guards on every procedure
|
|
64
|
+
// below would be unsatisfiable — the editor would boot and answer
|
|
65
|
+
// `ScopeError` to everyone, which is an outage, not security.
|
|
66
|
+
//
|
|
67
|
+
// It runs ONLY on a subject a strategy matched, never for anonymous. That
|
|
68
|
+
// asymmetry is what the guards actually enforce here: signed in → editor
|
|
69
|
+
// scopes; signed out → nothing. See `authz.ts` for the vocabulary and for
|
|
70
|
+
// what to replace this with when roles live in your own tables.
|
|
71
|
+
resolveScopes,
|
|
60
72
|
},
|
|
73
|
+
// Point `voltro doctor` / `voltro check` at the scope vocabulary so the
|
|
74
|
+
// unknown-scope rule is LIVE here. This app does scope-based authorization
|
|
75
|
+
// without `@voltro/plugin-rbac`, and the plugin is what normally publishes
|
|
76
|
+
// that set — without this pointer the rule stays dormant and a guard naming a
|
|
77
|
+
// scope nothing grants (a typo, a rename) becomes a permanently uncallable
|
|
78
|
+
// procedure that nothing reports.
|
|
79
|
+
doctor: { scopeVocabulary: './authz.ts#EDITOR_SCOPES' },
|
|
61
80
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The app's authorization vocabulary, in ONE place.
|
|
2
|
+
//
|
|
3
|
+
// Split out of `app.config.ts` for the same reason `api-rbac` splits its role
|
|
4
|
+
// map: the tests can build the real resolver from the real scopes. A test that
|
|
5
|
+
// redeclares the vocabulary it is testing proves only that its own copy is
|
|
6
|
+
// self-consistent — the copy drifts, the test stays green, and the drift is
|
|
7
|
+
// exactly the thing worth catching.
|
|
8
|
+
//
|
|
9
|
+
// ── Every procedure in this app declares an access decision ────────────────
|
|
10
|
+
//
|
|
11
|
+
// `guards: [{ scope }]` naming a scope from this file, or an `openAccess:`
|
|
12
|
+
// reason. The framework refuses to boot on a procedure that declares neither
|
|
13
|
+
// (`security.defaultDeny`, on by default). There is exactly one `openAccess`
|
|
14
|
+
// here — `session.me`, which an anonymous browser MUST be able to call, or
|
|
15
|
+
// nothing can tell it to sign in.
|
|
16
|
+
|
|
17
|
+
import type { Subject } from '@voltro/protocol'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a signed-in editor may do.
|
|
21
|
+
*
|
|
22
|
+
* **One role, on purpose, and read the reason before you copy it.** This
|
|
23
|
+
* template has exactly one kind of account: an editor. Inventing `author` /
|
|
24
|
+
* `reviewer` / `publisher` here would be a role map written to look secure
|
|
25
|
+
* rather than to describe this app, and a scope every caller already holds is
|
|
26
|
+
* a guard that reads as protection and enforces nothing.
|
|
27
|
+
*
|
|
28
|
+
* The line these guards DO draw is real, and it is the line the template is
|
|
29
|
+
* about: **signed in or not.** `content.saveDraft` / `publish` / `unpublish`
|
|
30
|
+
* are refused to an anonymous caller, and the README's claim that only
|
|
31
|
+
* signed-in editors reach the write surface stops being a comment and becomes
|
|
32
|
+
* something the dispatch spine enforces before the executor runs.
|
|
33
|
+
*
|
|
34
|
+
* Split it the day your product has two kinds of account — by BLAST RADIUS,
|
|
35
|
+
* not by job title: publishing to the live site is a bigger blast than saving
|
|
36
|
+
* a draft, so `content:publish` is the natural second scope.
|
|
37
|
+
*/
|
|
38
|
+
export const EDITOR_SCOPES = ['content:read', 'content:write'] as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Where a caller's authority comes from.
|
|
42
|
+
*
|
|
43
|
+
* `auth.resolveScopes` runs ONLY on a subject a strategy MATCHED — never for
|
|
44
|
+
* anonymous, where there is no identity to look anything up for. That is
|
|
45
|
+
* precisely what makes these guards satisfiable AND meaningful here:
|
|
46
|
+
*
|
|
47
|
+
* • no session cookie → `voltroPasswordStrategy` skips → anonymous Subject,
|
|
48
|
+
* no scopes, every `content:*` guard denies.
|
|
49
|
+
* • valid cookie → matched `user` Subject → this runs → editor scopes.
|
|
50
|
+
*
|
|
51
|
+
* A session cookie deliberately carries no `scopes` of its own (identity is
|
|
52
|
+
* settled at sign-in; authority is re-read per request), so this function is
|
|
53
|
+
* the only thing that can grant them — which is why removing an editor's
|
|
54
|
+
* access takes effect on their EXISTING session rather than when it expires.
|
|
55
|
+
*
|
|
56
|
+
* PRODUCTION replaces the body with a lookup: `subject.metadata.roles` (set by
|
|
57
|
+
* your strategy) or a read against your own tables via the `store` the
|
|
58
|
+
* framework hands the resolver. Failure posture: return
|
|
59
|
+
* `{ kind: 'unavailable', reason }` if the lookup itself fails — the request
|
|
60
|
+
* then fails CLOSED with a named reason instead of an empty array being
|
|
61
|
+
* applied as a policy decision.
|
|
62
|
+
*/
|
|
63
|
+
export const resolveScopes = (_subject: Subject): ReadonlyArray<string> => [...EDITOR_SCOPES]
|
|
@@ -7,6 +7,16 @@ import { Schema } from 'effect'
|
|
|
7
7
|
|
|
8
8
|
export const publishContent = defineMutation({
|
|
9
9
|
name: 'content.publish',
|
|
10
|
+
// Editors only — this is the procedure that makes content LIVE, so it is the
|
|
11
|
+
// one whose guard matters most. Enforced before the executor and before the
|
|
12
|
+
// transaction opens; satisfied by a signed-in session (`auth.resolveScopes`
|
|
13
|
+
// grants `content:write`), refused to an anonymous caller.
|
|
14
|
+
//
|
|
15
|
+
// When your product grows a review step, this is where the vocabulary splits:
|
|
16
|
+
// give publishing its own `content:publish` scope in `authz.ts` and leave
|
|
17
|
+
// `content:write` on saveDraft. Split by BLAST RADIUS — going live is a
|
|
18
|
+
// bigger blast than saving a draft.
|
|
19
|
+
guards: [{ scope: 'content:write' }],
|
|
10
20
|
input: Schema.Struct({ type: Schema.NonEmptyString, id: Schema.NonEmptyString }),
|
|
11
21
|
output: Schema.Union(
|
|
12
22
|
Schema.Struct({ ok: Schema.Literal(true), id: Schema.String, status: Schema.String }),
|