@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
|
@@ -8,6 +8,18 @@ import { Schema } from 'effect'
|
|
|
8
8
|
export const createProfile = defineMutation({
|
|
9
9
|
name: 'profiles.create',
|
|
10
10
|
target: { table: 'profiles', op: 'insert' },
|
|
11
|
+
// Open: writes exactly the three fields the caller passed and echoes back
|
|
12
|
+
// only `{ id, name, email }` — `ssn` is `.serverOnly()` and is deliberately
|
|
13
|
+
// absent from the output, so what the caller sent in never comes back out.
|
|
14
|
+
// It reads nothing and cannot reach an existing row.
|
|
15
|
+
//
|
|
16
|
+
// No auth strategy and no rbac ship in this template, so every caller is an
|
|
17
|
+
// anonymous Subject holding no scopes; a `guards: [{ scope }]` would deny all
|
|
18
|
+
// of them rather than the wrong ones. Add an identity, then the guard.
|
|
19
|
+
openAccess:
|
|
20
|
+
'inserts only the fields the caller supplied and echoes back `{ id, name, email }` — the '
|
|
21
|
+
+ '`ssn` it takes is `.serverOnly()` and never appears in a response. Reads nothing, '
|
|
22
|
+
+ 'modifies no existing row.',
|
|
11
23
|
input: Schema.Struct({
|
|
12
24
|
name: Schema.NonEmptyString,
|
|
13
25
|
email: Schema.String,
|
|
@@ -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-audit": "0.
|
|
20
|
-
"@voltro/plugin-governance": "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-audit": "0.34.0",
|
|
20
|
+
"@voltro/plugin-governance": "0.34.0",
|
|
21
|
+
"@voltro/protocol": "0.34.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -13,6 +13,21 @@ import { Schema } from 'effect'
|
|
|
13
13
|
|
|
14
14
|
export const syncPull = defineAction({
|
|
15
15
|
name: 'sync.pull',
|
|
16
|
+
// Open: every KV key this touches is namespaced with the request's tenant
|
|
17
|
+
// (`sync:<tenantId>:cursor` / `:seen:*` in the executor) and the rows it
|
|
18
|
+
// writes go into a `tenant()` table, so one caller's sync state is not
|
|
19
|
+
// reachable from another's. The "upstream" is a pure local generator — there
|
|
20
|
+
// is no network call and no credential.
|
|
21
|
+
//
|
|
22
|
+
// No auth strategy and no rbac ship in this template, so a
|
|
23
|
+
// `guards: [{ scope: 'sync:run' }]` would be unsatisfiable: every caller is
|
|
24
|
+
// an anonymous Subject holding no scopes, and the guard would deny all of
|
|
25
|
+
// them. Growing a role map here purely to look guarded would teach a pattern
|
|
26
|
+
// this template is not about.
|
|
27
|
+
openAccess:
|
|
28
|
+
'advances the request tenant\'s own sync watermark; every KV key is namespaced by that '
|
|
29
|
+
+ 'tenant and the rows land in a `tenant()` table. The upstream is a local pure generator '
|
|
30
|
+
+ '— no network call, no credential.',
|
|
16
31
|
input: Schema.Struct({
|
|
17
32
|
// How many upstream events to pull this call (default 5, capped in the executor).
|
|
18
33
|
limit: Schema.optional(Schema.Int.pipe(Schema.greaterThan(0))),
|
|
@@ -11,6 +11,19 @@ import { Schema } from 'effect'
|
|
|
11
11
|
|
|
12
12
|
export const syncReset = defineAction({
|
|
13
13
|
name: 'sync.reset',
|
|
14
|
+
// Open — but note what bounds the blast radius, because it is a deliberate
|
|
15
|
+
// choice in the executor and not a property of "reset": it deletes only keys
|
|
16
|
+
// under `sync:<request tenantId>:`, never `ctx.kv.clear()`, which would drop
|
|
17
|
+
// the app's ENTIRE KV namespace across every tenant in one call. It destroys
|
|
18
|
+
// no rows; a following `sync.pull` rebuilds the watermark.
|
|
19
|
+
//
|
|
20
|
+
// No auth strategy and no rbac ship here, so a scope guard would deny every
|
|
21
|
+
// caller rather than the wrong ones. An operational reset is a good candidate
|
|
22
|
+
// for a `guards: [{ scope: 'sync:admin' }]` the moment you have an identity.
|
|
23
|
+
openAccess:
|
|
24
|
+
'deletes only the request tenant\'s own `sync:<tenant>:` keys — never `ctx.kv.clear()`, '
|
|
25
|
+
+ 'which would wipe every tenant\'s namespace — and destroys no rows; the next `sync.pull` '
|
|
26
|
+
+ 'rebuilds the watermark. An operational reset: guard it once an identity exists.',
|
|
14
27
|
input: Schema.Struct({
|
|
15
28
|
// Also delete the idempotency markers (default: keep them).
|
|
16
29
|
markers: Schema.optional(Schema.Boolean),
|
|
@@ -9,6 +9,13 @@ import { Schema } from 'effect'
|
|
|
9
9
|
|
|
10
10
|
export const syncStatus = defineAction({
|
|
11
11
|
name: 'sync.status',
|
|
12
|
+
// Open: returns two integers about the request tenant's OWN sync state — a
|
|
13
|
+
// watermark and a marker count read from `sync:<tenantId>:` keys. No row
|
|
14
|
+
// contents, no other tenant's keys, no writes. No auth strategy or rbac ships
|
|
15
|
+
// in this template, so a scope guard would deny every caller.
|
|
16
|
+
openAccess:
|
|
17
|
+
'returns two integers describing the request tenant\'s own sync state (watermark + live '
|
|
18
|
+
+ 'marker count) from its own KV namespace; no row contents, no writes.',
|
|
12
19
|
input: Schema.Struct({}),
|
|
13
20
|
output: Schema.Struct({
|
|
14
21
|
cursor: Schema.Int, // the durable watermark
|
|
@@ -12,18 +12,19 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/kv": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.34.0",
|
|
16
|
+
"@voltro/database": "0.34.0",
|
|
17
|
+
"@voltro/env": "0.34.0",
|
|
18
|
+
"@voltro/kv": "0.34.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "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
|
}
|
|
@@ -11,13 +11,24 @@ import { Schema } from 'effect'
|
|
|
11
11
|
|
|
12
12
|
export const listEvents = defineQuery({
|
|
13
13
|
name: 'events.list',
|
|
14
|
+
// Open: `tenant()` confines every delivery to the tenantId on the request, so
|
|
15
|
+
// one caller's synced rows never reach another's subscription. With no auth
|
|
16
|
+
// strategy configured that tenantId is the caller's own `x-tenant` header —
|
|
17
|
+
// it shapes the feed, it does not authorize the caller — and a
|
|
18
|
+
// `guards: [{ scope }]` would be unsatisfiable, denying 100% of traffic.
|
|
19
|
+
openAccess:
|
|
20
|
+
'streams the synced event rows of the request\'s tenant (`tenant()` scopes every '
|
|
21
|
+
+ 'delivery); the rows are this demo\'s own generated events. No auth strategy ships here, '
|
|
22
|
+
+ 'so the tenant comes from the caller\'s `x-tenant` header — add a strategy, then a `guards:`.',
|
|
14
23
|
input: Schema.Struct({}),
|
|
15
|
-
output: Schema.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
output: Schema.Array(
|
|
25
|
+
Schema.Struct({
|
|
26
|
+
id: Schema.String,
|
|
27
|
+
externalId: Schema.String,
|
|
28
|
+
kind: Schema.String,
|
|
29
|
+
sequence: Schema.Number,
|
|
30
|
+
tenantId: Schema.String,
|
|
31
|
+
createdAt: Schema.Date,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
23
34
|
})
|
|
@@ -9,6 +9,17 @@ import { Schema } from 'effect'
|
|
|
9
9
|
export const createComment = defineMutation({
|
|
10
10
|
name: 'comments.create',
|
|
11
11
|
target: { table: 'comments', op: 'insert' },
|
|
12
|
+
// Open, same reasoning as `posts.create` — and here the `flag` action makes
|
|
13
|
+
// the distinction sharper still: it does not reject anything, it writes the
|
|
14
|
+
// row and queues it for review. A rule that never refuses is plainly not an
|
|
15
|
+
// access control, so the access decision has to be its own line.
|
|
16
|
+
//
|
|
17
|
+
// No auth strategy and no rbac ship here, so a `guards: [{ scope }]` would
|
|
18
|
+
// deny every caller instead of the wrong ones.
|
|
19
|
+
openAccess:
|
|
20
|
+
'inserts caller-supplied text into the request\'s tenant; the `flag` moderation rule '
|
|
21
|
+
+ 'queues it for review and rejects nothing, so it is a review workflow rather than an '
|
|
22
|
+
+ 'access control.',
|
|
12
23
|
input: Schema.Struct({
|
|
13
24
|
body: Schema.String,
|
|
14
25
|
}),
|
|
@@ -11,6 +11,19 @@ import { Schema } from 'effect'
|
|
|
11
11
|
export const createPost = defineMutation({
|
|
12
12
|
name: 'posts.create',
|
|
13
13
|
target: { table: 'posts', op: 'insert' },
|
|
14
|
+
// Content moderation is not access control, and this template is the place
|
|
15
|
+
// to be exact about that: the `block` rule inspects WHAT is being written and
|
|
16
|
+
// can reject it; `guards:` / `openAccess:` decide WHO may write at all. The
|
|
17
|
+
// moderation interceptor lets an authorized troll through and stops an
|
|
18
|
+
// unauthorized poet — neither substitutes for the other.
|
|
19
|
+
//
|
|
20
|
+
// Open because no auth strategy and no rbac ship here: every caller is an
|
|
21
|
+
// anonymous Subject with no scopes, so a scope guard would deny all of them.
|
|
22
|
+
// The write carries only caller-supplied text into the request's tenant.
|
|
23
|
+
openAccess:
|
|
24
|
+
'inserts caller-supplied text into the request\'s tenant, after the `block` moderation '
|
|
25
|
+
+ 'rule has vetted it. Moderation judges the CONTENT, not the caller — it is not an '
|
|
26
|
+
+ 'access decision, which is why this line exists beside it.',
|
|
14
27
|
input: Schema.Struct({
|
|
15
28
|
title: Schema.NonEmptyString,
|
|
16
29
|
body: Schema.String,
|
|
@@ -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-moderation": "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-moderation": "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
|
},
|
|
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
|
}
|
|
@@ -4,6 +4,14 @@ import { Schema } from 'effect'
|
|
|
4
4
|
export const createNote = defineMutation({
|
|
5
5
|
name: 'notes.create',
|
|
6
6
|
target: { table: 'notes', op: 'insert' },
|
|
7
|
+
// Open: writes only the title + body the caller sent, into the request's
|
|
8
|
+
// tenant (`tenant()` auto-stamps `tenantId`), and touches no existing row.
|
|
9
|
+
// No auth strategy and no rbac ship here, so every caller is an anonymous
|
|
10
|
+
// Subject holding no scopes and a scope guard would deny all of them.
|
|
11
|
+
openAccess:
|
|
12
|
+
'inserts a note built only from caller-supplied fields into the request\'s tenant; reads '
|
|
13
|
+
+ 'nothing and modifies no existing row. No auth strategy ships here, so there is no '
|
|
14
|
+
+ 'identity a scope guard could name.',
|
|
7
15
|
input: Schema.Struct({ title: Schema.NonEmptyString, body: Schema.String }),
|
|
8
16
|
output: Schema.Struct({
|
|
9
17
|
id: Schema.String,
|
|
@@ -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/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/plugin-prometheus": "0.
|
|
20
|
-
"@voltro/plugin-sentry": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.34.0",
|
|
19
|
+
"@voltro/plugin-prometheus": "0.34.0",
|
|
20
|
+
"@voltro/plugin-sentry": "0.34.0",
|
|
21
|
+
"@voltro/protocol": "0.34.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -4,6 +4,19 @@ import { Schema } from 'effect'
|
|
|
4
4
|
export const listNotes = defineQuery({
|
|
5
5
|
name: 'notes.list',
|
|
6
6
|
source: 'notes',
|
|
7
|
+
// Open: `tenant()` confines every delivery to the tenantId on the request, so
|
|
8
|
+
// one tenant's notes never reach another's subscription. With no auth
|
|
9
|
+
// strategy configured that tenantId comes from the caller's own `x-tenant`
|
|
10
|
+
// header — result shaping, not access control — and a `guards: [{ scope }]`
|
|
11
|
+
// would be unsatisfiable, denying 100% of traffic rather than some of it.
|
|
12
|
+
//
|
|
13
|
+
// Worth stating in the OBSERVABILITY template specifically: a denied call is
|
|
14
|
+
// still a span and still a metric. Traces show you what happened, not who was
|
|
15
|
+
// allowed to make it happen.
|
|
16
|
+
openAccess:
|
|
17
|
+
'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
|
|
18
|
+
+ 'strategy ships here, so the tenant comes from the caller\'s `x-tenant` header — add a '
|
|
19
|
+
+ 'strategy, then a `guards:`. Tracing records calls; it does not authorize them.',
|
|
7
20
|
input: Schema.Struct({}),
|
|
8
21
|
output: Schema.Array(Schema.Struct({
|
|
9
22
|
id: Schema.String,
|
|
@@ -10,6 +10,20 @@ import { Schema } from 'effect'
|
|
|
10
10
|
|
|
11
11
|
export const createNote = defineMutation({
|
|
12
12
|
name: 'notes.create',
|
|
13
|
+
// A rate limit is not an access decision, and this template is where that is
|
|
14
|
+
// easiest to get wrong. The interceptor answers HOW OFTEN a caller may do
|
|
15
|
+
// this; `guards:` / `openAccess:` answer WHETHER THEY MAY AT ALL. A limit
|
|
16
|
+
// slows an attacker down and lets them through 99 times an hour, so it can
|
|
17
|
+
// never stand in for the second answer — the two live side by side.
|
|
18
|
+
//
|
|
19
|
+
// Open here because no auth strategy and no rbac ship in this template: every
|
|
20
|
+
// caller resolves to an anonymous Subject holding no scopes, so a
|
|
21
|
+
// `guards: [{ scope }]` would deny all of them. `assertOwnTenant` still
|
|
22
|
+
// rejects a `tenantId` that does not match the resolved subject.
|
|
23
|
+
openAccess:
|
|
24
|
+
'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
|
|
25
|
+
+ '(`assertOwnTenant` rejects a mismatch). The rate limit bounds HOW OFTEN, never WHETHER '
|
|
26
|
+
+ '— it is not an access decision, which is why this line exists beside it.',
|
|
13
27
|
target: {
|
|
14
28
|
table: 'notes',
|
|
15
29
|
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/plugin-ratelimit": "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-multitenancy": "0.34.0",
|
|
20
|
+
"@voltro/plugin-ratelimit": "0.34.0",
|
|
21
|
+
"@voltro/protocol": "0.34.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -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/plugin-rbac": "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-multitenancy": "0.34.0",
|
|
20
|
+
"@voltro/plugin-rbac": "0.34.0",
|
|
21
|
+
"@voltro/protocol": "0.34.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -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/plugin-openapi": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
21
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/plugin-openapi": "0.34.0",
|
|
19
|
+
"@voltro/protocol": "0.34.0",
|
|
20
|
+
"@voltro/runtime": "0.34.0",
|
|
21
|
+
"@voltro/sql-postgres": "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
|
}
|
|
@@ -9,6 +9,19 @@ import { Schema } from 'effect'
|
|
|
9
9
|
export const createProject = defineMutation({
|
|
10
10
|
name: 'projects.create',
|
|
11
11
|
target: { table: 'projects', op: 'insert' },
|
|
12
|
+
// An entitlement check is not an access decision. `requireEntitlement`
|
|
13
|
+
// answers HOW MANY the tenant has paid for; `guards:` / `openAccess:` answer
|
|
14
|
+
// WHO MAY CALL IT. A quota gate refuses the fourth project and cheerfully
|
|
15
|
+
// allows the first three — from anyone.
|
|
16
|
+
//
|
|
17
|
+
// Open because this template ships no auth strategy and no rbac (it is the
|
|
18
|
+
// billing bundle; `api-saas-starter` is the same domain WITH auth, and it
|
|
19
|
+
// guards these procedures). With no identity, every caller is an anonymous
|
|
20
|
+
// Subject holding no scopes and a `guards: [{ scope }]` would deny all of them.
|
|
21
|
+
openAccess:
|
|
22
|
+
'inserts a project named by the caller into the request\'s tenant, after the billing '
|
|
23
|
+
+ 'entitlement gate. A quota bounds HOW MANY, never WHO — see `api-saas-starter` for the '
|
|
24
|
+
+ 'same procedure with an auth strategy and a `projects:write` guard.',
|
|
12
25
|
input: Schema.Struct({ name: Schema.NonEmptyString }),
|
|
13
26
|
output: Schema.Struct({
|
|
14
27
|
id: Schema.String,
|
|
@@ -13,21 +13,22 @@
|
|
|
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/plugin-analytics-postgres": "0.
|
|
19
|
-
"@voltro/plugin-billing": "0.
|
|
20
|
-
"@voltro/plugin-multitenancy": "0.
|
|
21
|
-
"@voltro/plugin-notifications": "0.
|
|
22
|
-
"@voltro/plugin-presence": "0.
|
|
23
|
-
"@voltro/protocol": "0.
|
|
24
|
-
"@voltro/runtime": "0.
|
|
25
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/database": "0.34.0",
|
|
18
|
+
"@voltro/plugin-analytics-postgres": "0.34.0",
|
|
19
|
+
"@voltro/plugin-billing": "0.34.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.34.0",
|
|
21
|
+
"@voltro/plugin-notifications": "0.34.0",
|
|
22
|
+
"@voltro/plugin-presence": "0.34.0",
|
|
23
|
+
"@voltro/protocol": "0.34.0",
|
|
24
|
+
"@voltro/runtime": "0.34.0",
|
|
25
|
+
"@voltro/sql-postgres": "0.34.0",
|
|
26
26
|
"effect": "^3.22.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@voltro/testing": "0.
|
|
29
|
+
"@voltro/testing": "0.34.0",
|
|
30
30
|
"typescript": "^6.0.3",
|
|
31
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
31
32
|
"vitest": "^4.1.10"
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -8,6 +8,17 @@ import { Schema } from 'effect'
|
|
|
8
8
|
export const listProjects = defineQuery({
|
|
9
9
|
name: 'projects.list',
|
|
10
10
|
source: 'projects',
|
|
11
|
+
// Open: `tenant()` confines every delivery to the tenantId on the request, so
|
|
12
|
+
// one tenant's projects never reach another's subscription. This template
|
|
13
|
+
// bundles billing + notifications + presence and deliberately ships NO auth
|
|
14
|
+
// strategy — pair it with `api-auth` / `api-saas-starter` for the identity
|
|
15
|
+
// half. Until then the tenantId is the caller's own `x-tenant` header, and a
|
|
16
|
+
// `guards: [{ scope }]` would be unsatisfiable: an anonymous Subject holds no
|
|
17
|
+
// scopes, so the guard would deny 100% of traffic.
|
|
18
|
+
openAccess:
|
|
19
|
+
'lists the request tenant\'s own projects (`tenant()` scopes every delivery). This '
|
|
20
|
+
+ 'template ships no auth strategy — see `api-saas-starter`, which does and guards these '
|
|
21
|
+
+ 'same procedures — so the tenant comes from the caller\'s `x-tenant` header.',
|
|
11
22
|
input: Schema.Struct({}),
|
|
12
23
|
output: Schema.Array(Schema.Struct({
|
|
13
24
|
id: Schema.String,
|
|
@@ -9,6 +9,19 @@ import { Schema } from 'effect'
|
|
|
9
9
|
|
|
10
10
|
export const me = defineAction({
|
|
11
11
|
name: 'session.me',
|
|
12
|
+
// The ONE open procedure in this app, and it has to be. The four domain
|
|
13
|
+
// procedures beside it carry guards an anonymous caller cannot satisfy —
|
|
14
|
+
// which is correct — but the dashboard's SSR gate has to be able to ask
|
|
15
|
+
// "who am I" while it is still nobody, or it has nothing to branch on and the
|
|
16
|
+
// redirect to /login never happens.
|
|
17
|
+
//
|
|
18
|
+
// Safe because the answer is a projection of the caller's own request: the
|
|
19
|
+
// Subject `voltroPasswordStrategy` resolved from the cookie they sent. It
|
|
20
|
+
// reads no table and can say nothing about anybody else.
|
|
21
|
+
openAccess:
|
|
22
|
+
'echoes the caller\'s own resolved Subject (type/id/tenantId) — reads no table and reveals '
|
|
23
|
+
+ 'nothing the caller did not present. Anonymous callers must reach it, or the SSR gate '
|
|
24
|
+
+ 'cannot redirect them to sign in.',
|
|
12
25
|
input: Schema.Struct({}),
|
|
13
26
|
output: Schema.Struct({
|
|
14
27
|
// 'anonymous' | 'user' | 'apiKey' | 'serviceAccount'
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { defineEnv, envVar } from '@voltro/env'
|
|
23
23
|
import { authRoutesPlugin, memoryUserStore, voltroPasswordStrategy } from '@voltro/plugin-auth'
|
|
24
24
|
import { billingPlugin } from '@voltro/plugin-billing'
|
|
25
|
+
import { resolveScopes } from './authz'
|
|
25
26
|
|
|
26
27
|
export const env = defineEnv({
|
|
27
28
|
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
@@ -83,5 +84,23 @@ export default {
|
|
|
83
84
|
// Subject. Reads the SAME VOLTRO_SESSION_SECRET the routes sign with, so
|
|
84
85
|
// the signing and verifying sides cannot drift.
|
|
85
86
|
strategies: [voltroPasswordStrategy()],
|
|
87
|
+
// …and turns that identity into AUTHORITY. A session cookie carries no
|
|
88
|
+
// `scopes` by design (identity is settled at sign-in; authority is re-read
|
|
89
|
+
// per request), so without this the guards on the four domain procedures
|
|
90
|
+
// would be unsatisfiable — the dashboard would boot and answer `ScopeError`
|
|
91
|
+
// to every caller, which is an outage, not security.
|
|
92
|
+
//
|
|
93
|
+
// It runs ONLY on a subject a strategy matched, never for anonymous. That
|
|
94
|
+
// asymmetry IS what the guards enforce: signed in → member scopes; signed
|
|
95
|
+
// out → nothing. See `authz.ts` for the vocabulary, and for why a quota and
|
|
96
|
+
// a tenant filter are not substitutes for it.
|
|
97
|
+
resolveScopes,
|
|
86
98
|
},
|
|
99
|
+
// Point `voltro doctor` / `voltro check` at the scope vocabulary so the
|
|
100
|
+
// unknown-scope rule is LIVE here. This app does scope-based authorization
|
|
101
|
+
// without `@voltro/plugin-rbac`, and the plugin is what normally publishes
|
|
102
|
+
// that set — without this pointer the rule stays dormant and a guard naming a
|
|
103
|
+
// scope nothing grants (a typo, a rename) becomes a permanently uncallable
|
|
104
|
+
// procedure that nothing reports.
|
|
105
|
+
doctor: { scopeVocabulary: './authz.ts#MEMBER_SCOPES' },
|
|
87
106
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
// ── Three gates, three different questions ─────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// This template stacks all three, and conflating any two of them is the
|
|
12
|
+
// classic SaaS authorization bug:
|
|
13
|
+
//
|
|
14
|
+
// tenant() WHICH ROWS — the runtime AND-merges
|
|
15
|
+
// `tenantId = subject.tenantId` into every read.
|
|
16
|
+
// requireEntitlement() HOW MANY — the plan's metered quota. It refuses the
|
|
17
|
+
// fourth project and allows the first three, to anyone.
|
|
18
|
+
// guards: [{ scope }] WHO MAY CALL IT AT ALL — this file.
|
|
19
|
+
//
|
|
20
|
+
// A quota is not an access control and neither is a tenant filter. The gate
|
|
21
|
+
// that answers "may this caller do this" is the third one, and every
|
|
22
|
+
// wire-exposed procedure here declares it (or an `openAccess:` reason — there
|
|
23
|
+
// is exactly one, `session.me`).
|
|
24
|
+
|
|
25
|
+
import type { Subject } from '@voltro/protocol'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What a signed-in member of a tenant may do.
|
|
29
|
+
*
|
|
30
|
+
* **One role, on purpose.** This template has exactly one kind of account: a
|
|
31
|
+
* member of the tenant they signed up into. Inventing `owner` / `billing-admin`
|
|
32
|
+
* / `viewer` here would be a role map written to look secure rather than to
|
|
33
|
+
* describe this app — and a scope every caller already holds is a guard that
|
|
34
|
+
* reads as protection and enforces nothing.
|
|
35
|
+
*
|
|
36
|
+
* The line these guards DO draw is real: **signed in or not.** With no cookie a
|
|
37
|
+
* caller cannot list a tenant's projects, cannot read the invite roster (which
|
|
38
|
+
* carries teammates' email addresses), and cannot spend a seat.
|
|
39
|
+
*
|
|
40
|
+
* Split it by BLAST RADIUS the day your product has two kinds of account.
|
|
41
|
+
* `team:invite` is already separate from `projects:write` for that reason:
|
|
42
|
+
* inviting a person costs a seat and puts an outsider inside the tenant, which
|
|
43
|
+
* is a bigger blast than creating a project.
|
|
44
|
+
*/
|
|
45
|
+
export const MEMBER_SCOPES = [
|
|
46
|
+
'projects:read',
|
|
47
|
+
'projects:write',
|
|
48
|
+
'team:read',
|
|
49
|
+
'team:invite',
|
|
50
|
+
] as const
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where a caller's authority comes from.
|
|
54
|
+
*
|
|
55
|
+
* `auth.resolveScopes` runs ONLY on a subject a strategy MATCHED — never for
|
|
56
|
+
* anonymous, where there is no identity to look anything up for. That is
|
|
57
|
+
* exactly what makes these guards satisfiable AND meaningful:
|
|
58
|
+
*
|
|
59
|
+
* • no session cookie → `voltroPasswordStrategy` skips → anonymous Subject,
|
|
60
|
+
* no scopes, every guard denies.
|
|
61
|
+
* • valid cookie → matched `user` Subject → this runs → member scopes.
|
|
62
|
+
*
|
|
63
|
+
* A session cookie deliberately carries no `scopes` of its own (identity is
|
|
64
|
+
* settled at sign-in; authority is re-read per request), so this function is
|
|
65
|
+
* the only thing that can grant them — which is why removing a member's access
|
|
66
|
+
* takes effect on their EXISTING session rather than when the cookie expires.
|
|
67
|
+
*
|
|
68
|
+
* PRODUCTION replaces the body with a lookup: `subject.metadata.roles` (set by
|
|
69
|
+
* your strategy) or a read against your own tables through the `store` the
|
|
70
|
+
* framework hands the resolver. Failure posture: return
|
|
71
|
+
* `{ kind: 'unavailable', reason }` if the lookup itself fails — the request
|
|
72
|
+
* then fails CLOSED with a named reason, instead of an empty array being
|
|
73
|
+
* applied as though it were a policy decision.
|
|
74
|
+
*/
|
|
75
|
+
export const resolveScopes = (_subject: Subject): ReadonlyArray<string> => [...MEMBER_SCOPES]
|
|
@@ -8,6 +8,16 @@ import { Schema } from 'effect'
|
|
|
8
8
|
export const createInvite = defineMutation({
|
|
9
9
|
name: 'invites.create',
|
|
10
10
|
target: { table: 'invites', op: 'insert' },
|
|
11
|
+
// Members only, and on its OWN scope rather than `projects:write` — the
|
|
12
|
+
// vocabulary is split by blast radius, not by convenience. Inviting a person
|
|
13
|
+
// spends a seat (real money) and puts an outsider inside the tenant; creating
|
|
14
|
+
// a project does neither. When this app grows an `owner` role, `team:invite`
|
|
15
|
+
// is the scope that moves to it and `projects:write` is the one that stays.
|
|
16
|
+
//
|
|
17
|
+
// The guard runs before `requireEntitlement`, so an anonymous call cannot
|
|
18
|
+
// spend the tenant's seat quota. Satisfiable as shipped: a matched session
|
|
19
|
+
// gets `team:invite` from `auth.resolveScopes` (authz.ts).
|
|
20
|
+
guards: [{ scope: 'team:invite' }],
|
|
11
21
|
input: Schema.Struct({ email: Schema.NonEmptyString }),
|
|
12
22
|
output: Schema.Struct({
|
|
13
23
|
id: Schema.String,
|