@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
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
// Executor for `profiles.get`.
|
|
2
|
+
//
|
|
3
|
+
// `p['ssn']` is PLAINTEXT here — the store middleware decrypted the
|
|
4
|
+
// `.encrypted()` column on the way out, and the handler never sees the
|
|
5
|
+
// `enc:v1:…` ciphertext. That is the round-trip this template exists to show,
|
|
6
|
+
// and the last-four projection below is what proves it happened.
|
|
7
|
+
//
|
|
8
|
+
// The projection is not decoration: the column is `.serverOnly()`, so the full
|
|
9
|
+
// value must not reach a client. Deriving the suffix HERE — rather than sending
|
|
10
|
+
// the whole string and trimming it in the browser — is the difference between a
|
|
11
|
+
// value that never left the server and one that did.
|
|
1
12
|
import type { AppContext } from '@voltro/runtime'
|
|
2
13
|
import { eq } from '@voltro/database'
|
|
3
14
|
import { database } from '../database/schema'
|
|
4
15
|
|
|
16
|
+
/** Last four characters, or '' when there is nothing to show. Never throws on a
|
|
17
|
+
* short or empty value — a masked field is not a place to fail a read. */
|
|
18
|
+
const last4 = (value: string): string => (value.length >= 4 ? value.slice(-4) : '')
|
|
19
|
+
|
|
5
20
|
const execute = async (input: { id: string }, ctx: AppContext) => {
|
|
6
21
|
const rows = await ctx.store.query(database.profiles.where(eq('id', input.id)).descriptor)
|
|
7
22
|
const p = rows[0]
|
|
@@ -10,7 +25,8 @@ const execute = async (input: { id: string }, ctx: AppContext) => {
|
|
|
10
25
|
id: p['id'] as string,
|
|
11
26
|
name: p['name'] as string,
|
|
12
27
|
email: p['email'] as string,
|
|
13
|
-
|
|
28
|
+
// Decrypted by the store middleware, then narrowed to what may leave.
|
|
29
|
+
ssnLast4: last4(p['ssn'] as string),
|
|
14
30
|
}
|
|
15
31
|
}
|
|
16
32
|
|
|
@@ -1,17 +1,42 @@
|
|
|
1
|
-
// Reads a profile back
|
|
2
|
-
//
|
|
3
|
-
// `enc:v1:…` ciphertext
|
|
1
|
+
// Reads a profile back. The store middleware DECRYPTS the `.encrypted()` column
|
|
2
|
+
// on read, so the handler holds `ssn` as plaintext and never touches the
|
|
3
|
+
// `enc:v1:…` ciphertext — that is the round-trip this template exists to show.
|
|
4
|
+
//
|
|
5
|
+
// What crosses the WIRE is a SEPARATE decision, and the schema makes it: `ssn`
|
|
6
|
+
// is also `.serverOnly()`, so the full value may not leave the server. The
|
|
7
|
+
// executor proves the decryption worked by returning the last four digits it
|
|
8
|
+
// derived from the plaintext — which is what a support UI would actually
|
|
9
|
+
// render. Encryption at rest is not an exposure marker; `.serverOnly()` is.
|
|
4
10
|
|
|
5
11
|
import { defineAction } from '@voltro/protocol'
|
|
6
12
|
import { Schema } from 'effect'
|
|
7
13
|
|
|
8
14
|
export const getProfile = defineAction({
|
|
9
15
|
name: 'profiles.get',
|
|
16
|
+
// Open, and the reason is a claim you can check against the two files beside
|
|
17
|
+
// this one: the full `ssn` is `.serverOnly()` in `database/schema.ts` and is
|
|
18
|
+
// not named in the output below, so the sensitive value cannot cross the wire
|
|
19
|
+
// at all. What is left — name, email, four digits — is the row this demo's
|
|
20
|
+
// own `profiles.create` put there; the template ships no seed and no other
|
|
21
|
+
// writer.
|
|
22
|
+
//
|
|
23
|
+
// A `guards: [{ scope: 'profiles:read' }]` is what a real deployment wants
|
|
24
|
+
// here, and it is unsatisfiable in THIS app: no auth strategy and no rbac are
|
|
25
|
+
// configured, so every caller resolves to an anonymous Subject holding no
|
|
26
|
+
// scopes and the guard would deny 100% of traffic — an outage, not security.
|
|
27
|
+
// Add an identity (see `api-auth` / `api-rbac`), then the guard.
|
|
28
|
+
openAccess:
|
|
29
|
+
'returns name, email and the last four digits derived server-side from the decrypted '
|
|
30
|
+
+ '`ssn`; the full value is `.serverOnly()` and is absent from this output, so it cannot '
|
|
31
|
+
+ 'cross the wire. Rows come only from this demo\'s own `profiles.create` — no seed, no '
|
|
32
|
+
+ 'other writer.',
|
|
10
33
|
input: Schema.Struct({ id: Schema.String }),
|
|
11
34
|
output: Schema.NullOr(Schema.Struct({
|
|
12
35
|
id: Schema.String,
|
|
13
36
|
name: Schema.String,
|
|
14
37
|
email: Schema.String,
|
|
15
|
-
|
|
38
|
+
// Proof the cipher decrypted the column, without publishing it. Empty when
|
|
39
|
+
// the stored value is shorter than four characters.
|
|
40
|
+
ssnLast4: Schema.String,
|
|
16
41
|
})),
|
|
17
42
|
})
|
|
@@ -28,10 +28,22 @@ export const profiles = table('profiles', {
|
|
|
28
28
|
id: id({ prefix: 'prof' }),
|
|
29
29
|
name: text(),
|
|
30
30
|
email: text(),
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
31
|
+
// TWO markers, because they answer two different questions — say both when
|
|
32
|
+
// you mean both:
|
|
33
|
+
//
|
|
34
|
+
// .encrypted() → AES-256-GCM AT REST. The handler always sees plaintext;
|
|
35
|
+
// the stored value is an opaque `enc:v1:…` string. Encrypt
|
|
36
|
+
// only what you read back WHOLE (you can't filter/sort on
|
|
37
|
+
// it in SQL — it's ciphertext on disk).
|
|
38
|
+
// .serverOnly() → may this value leave the server AT ALL. It may not.
|
|
39
|
+
//
|
|
40
|
+
// Reading `.encrypted()` as "safe to expose" is a category error and a
|
|
41
|
+
// tempting one: the runtime DECRYPTS for the handler, so an encrypted column
|
|
42
|
+
// flows to a client exactly like any other unless it is also `.serverOnly()`.
|
|
43
|
+
// `crud.*` strips serverOnly columns for you; a hand-written procedure must
|
|
44
|
+
// simply not name it in its output — see `actions/profiles.get.action.ts`,
|
|
45
|
+
// which returns the last four digits it derived server-side instead.
|
|
46
|
+
ssn: text().encrypted().serverOnly(),
|
|
35
47
|
})
|
|
36
48
|
.with(audit()) // createdAt drives the retention sweep
|
|
37
49
|
|
|
@@ -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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-audit": "0.35.0",
|
|
20
|
+
"@voltro/plugin-governance": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -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.35.0",
|
|
16
|
+
"@voltro/database": "0.35.0",
|
|
17
|
+
"@voltro/env": "0.35.0",
|
|
18
|
+
"@voltro/kv": "0.35.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
20
|
+
"@voltro/protocol": "0.35.0",
|
|
21
|
+
"@voltro/runtime": "0.35.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.35.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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-moderation": "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
|
},
|
|
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
|
}
|
|
@@ -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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
19
|
+
"@voltro/plugin-prometheus": "0.35.0",
|
|
20
|
+
"@voltro/plugin-sentry": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
20
|
+
"@voltro/plugin-ratelimit": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
20
|
+
"@voltro/plugin-rbac": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "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
|
}
|
|
@@ -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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/plugin-openapi": "0.35.0",
|
|
19
|
+
"@voltro/protocol": "0.35.0",
|
|
20
|
+
"@voltro/runtime": "0.35.0",
|
|
21
|
+
"@voltro/sql-postgres": "0.35.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.35.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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/plugin-analytics-postgres": "0.35.0",
|
|
19
|
+
"@voltro/plugin-billing": "0.35.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
21
|
+
"@voltro/plugin-notifications": "0.35.0",
|
|
22
|
+
"@voltro/plugin-presence": "0.35.0",
|
|
23
|
+
"@voltro/protocol": "0.35.0",
|
|
24
|
+
"@voltro/runtime": "0.35.0",
|
|
25
|
+
"@voltro/sql-postgres": "0.35.0",
|
|
26
26
|
"effect": "^3.22.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@voltro/testing": "0.
|
|
29
|
+
"@voltro/testing": "0.35.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'
|