@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,20 +1,33 @@
|
|
|
1
|
-
// The live incident feed — the heart of the status page.
|
|
2
|
-
// anonymous visitor's browser can subscribe: `useSubscription('app',
|
|
1
|
+
// The live incident feed — the heart of the status page. Deliberately OPEN, so
|
|
2
|
+
// an anonymous visitor's browser can subscribe: `useSubscription('app',
|
|
3
3
|
// 'incidents.live')`. Every insert/update to `incidents` lands as a delta, so a
|
|
4
4
|
// newly-declared incident (or a resolve) shows up on every open status page
|
|
5
5
|
// within seconds — no polling.
|
|
6
|
+
//
|
|
7
|
+
// This template is the clearest place in the library to see that `openAccess:`
|
|
8
|
+
// and `guards:` are two spellings of the SAME obligation, not a strict option
|
|
9
|
+
// and a lax one. The reads here are open on purpose; the four mutations beside
|
|
10
|
+
// them carry `guards: [{ scope: 'status:write' }]` and are satisfiable through
|
|
11
|
+
// the real role map in `authz.ts`. Both are decisions. What the framework
|
|
12
|
+
// refuses to boot on is neither.
|
|
6
13
|
import { defineQuery } from '@voltro/protocol'
|
|
7
14
|
import { Schema } from 'effect'
|
|
8
15
|
|
|
9
16
|
export const incidentsLive = defineQuery({
|
|
10
17
|
name: 'incidents.live',
|
|
18
|
+
openAccess:
|
|
19
|
+
'the public incident feed a status page exists to publish — title, impact, status and '
|
|
20
|
+
+ 'timestamps of THIS operator\'s own incidents, nothing caller-derived and no personal '
|
|
21
|
+
+ 'data. A status page that requires a login is useless during the outage it reports.',
|
|
11
22
|
input: Schema.Struct({}),
|
|
12
|
-
output: Schema.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
output: Schema.Array(
|
|
24
|
+
Schema.Struct({
|
|
25
|
+
id: Schema.String,
|
|
26
|
+
title: Schema.String,
|
|
27
|
+
impact: Schema.String,
|
|
28
|
+
status: Schema.String,
|
|
29
|
+
startedAt: Schema.Date,
|
|
30
|
+
resolvedAt: Schema.NullOr(Schema.Date),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
20
33
|
})
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
// Live feed of incident timeline entries
|
|
1
|
+
// Live feed of incident timeline entries — public by design. The page joins
|
|
2
2
|
// these to incidents by `incidentId` client-side to render each incident's
|
|
3
|
-
// update history.
|
|
3
|
+
// update history. `incidents.update`, which writes them, is
|
|
4
|
+
// `status:write`-guarded.
|
|
4
5
|
import { defineQuery } from '@voltro/protocol'
|
|
5
6
|
import { Schema } from 'effect'
|
|
6
7
|
|
|
7
8
|
export const updatesList = defineQuery({
|
|
8
9
|
name: 'updates.list',
|
|
10
|
+
openAccess:
|
|
11
|
+
'the public incident timeline a status page exists to publish — the operator\'s own '
|
|
12
|
+
+ 'update posts, nothing caller-derived and no personal data. Posting one is '
|
|
13
|
+
+ '`status:write`-guarded.',
|
|
9
14
|
input: Schema.Struct({}),
|
|
10
|
-
output: Schema.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
output: Schema.Array(
|
|
16
|
+
Schema.Struct({
|
|
17
|
+
id: Schema.String,
|
|
18
|
+
incidentId: Schema.String,
|
|
19
|
+
body: Schema.String,
|
|
20
|
+
status: Schema.String,
|
|
21
|
+
createdAt: Schema.Date,
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
17
24
|
})
|
|
@@ -54,10 +54,18 @@ describe('incidents.create — operator-gated', () => {
|
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
describe('incidents.live — reads are PUBLIC', () => {
|
|
57
|
-
it('an anonymous visitor may subscribe
|
|
57
|
+
it('an anonymous visitor may subscribe — the query declares `openAccess`', async () => {
|
|
58
58
|
const ctx = ctxFor(subjectFor(null, null, 'anonymous'))
|
|
59
59
|
await expect(invoke(incidentsLive, incidentsLiveHandler, {}, ctx)).resolves.toBeDefined()
|
|
60
60
|
})
|
|
61
|
+
|
|
62
|
+
it('…which is a DECISION, not an omission', () => {
|
|
63
|
+
// Before `openAccess` existed, "nobody decided" and "we decided it is open"
|
|
64
|
+
// were the same descriptor and a reviewer could not tell them apart. Assert
|
|
65
|
+
// the marker is present, or this suite would keep passing on a query whose
|
|
66
|
+
// guard someone deleted by accident.
|
|
67
|
+
expect(incidentsLive.openAccess).toBeTruthy()
|
|
68
|
+
})
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
describe('incidents.create — descriptor', () => {
|
|
@@ -6,6 +6,18 @@ import { Schema } from 'effect'
|
|
|
6
6
|
|
|
7
7
|
export const documentAsOf = defineAction({
|
|
8
8
|
name: 'documents.asOf',
|
|
9
|
+
// Open, with the same load-bearing detail as `documents.history`: the
|
|
10
|
+
// executor passes `ctx.request.subject.tenantId` into `rowAsOf` explicitly,
|
|
11
|
+
// because a version table is not auto-scoped by the `tenant()` mixin. That
|
|
12
|
+
// argument is what keeps a caller-supplied id from reading another tenant's
|
|
13
|
+
// past values; do not drop it.
|
|
14
|
+
//
|
|
15
|
+
// No auth strategy and no rbac ship in this template, so a scope guard would
|
|
16
|
+
// deny every caller rather than the wrong ones.
|
|
17
|
+
openAccess:
|
|
18
|
+
'returns one past value of a document in the CALLER\'S OWN tenant — `rowAsOf` is passed '
|
|
19
|
+
+ '`subject.tenantId` explicitly, so an id from another tenant returns null. Time travel '
|
|
20
|
+
+ 'reaches only where the live read already could.',
|
|
9
21
|
input: Schema.Struct({ id: Schema.String, at: Schema.Number }),
|
|
10
22
|
output: Schema.NullOr(Schema.Struct({
|
|
11
23
|
title: Schema.String,
|
|
@@ -6,6 +6,19 @@ import { Schema } from 'effect'
|
|
|
6
6
|
|
|
7
7
|
export const documentHistory = defineAction({
|
|
8
8
|
name: 'documents.history',
|
|
9
|
+
// Open — and this one needs its bound named, because a version table is a
|
|
10
|
+
// second copy of your data that the `tenant()` mixin does NOT scope for you.
|
|
11
|
+
// The executor passes `ctx.request.subject.tenantId` to `rowHistory`
|
|
12
|
+
// explicitly, so a caller-supplied id from another tenant yields nothing.
|
|
13
|
+
// Read that as the load-bearing line it is: drop the argument and this
|
|
14
|
+
// becomes a cross-tenant read of every past value.
|
|
15
|
+
//
|
|
16
|
+
// No auth strategy and no rbac ship in this template, so a
|
|
17
|
+
// `guards: [{ scope: 'documents:history' }]` would deny every caller.
|
|
18
|
+
openAccess:
|
|
19
|
+
'lists past versions of a document in the CALLER\'S OWN tenant — `rowHistory` is passed '
|
|
20
|
+
+ '`subject.tenantId` explicitly, so an id from another tenant returns nothing (a version '
|
|
21
|
+
+ 'table is not auto-scoped by `tenant()`). No auth strategy ships here to guard against.',
|
|
9
22
|
input: Schema.Struct({ id: Schema.String }),
|
|
10
23
|
output: Schema.Array(Schema.Struct({
|
|
11
24
|
version: Schema.Number,
|
|
@@ -8,6 +8,15 @@ import { Schema } from 'effect'
|
|
|
8
8
|
export const createDocument = defineMutation({
|
|
9
9
|
name: 'documents.create',
|
|
10
10
|
target: { table: 'documents', op: 'insert' },
|
|
11
|
+
// Open: writes only the title + content the caller sent, into the request's
|
|
12
|
+
// tenant (`tenant()` auto-stamps `tenantId`), and touches no existing row.
|
|
13
|
+
// No auth strategy and no rbac ship in this template, so every caller is an
|
|
14
|
+
// anonymous Subject holding no scopes and a `guards: [{ scope }]` would deny
|
|
15
|
+
// all of them rather than the wrong ones.
|
|
16
|
+
openAccess:
|
|
17
|
+
'inserts a document built only from caller-supplied fields into the request\'s tenant; '
|
|
18
|
+
+ 'reads nothing and modifies no existing row. No auth strategy ships here, so there is no '
|
|
19
|
+
+ 'identity a scope guard could name.',
|
|
11
20
|
input: Schema.Struct({
|
|
12
21
|
title: Schema.NonEmptyString,
|
|
13
22
|
content: Schema.String,
|
|
@@ -8,6 +8,18 @@ import { Schema } from 'effect'
|
|
|
8
8
|
export const updateDocument = defineMutation({
|
|
9
9
|
name: 'documents.update',
|
|
10
10
|
target: { table: 'documents', op: 'update' },
|
|
11
|
+
// Open: `tenant()` scopes the update, so a caller-supplied `id` from another
|
|
12
|
+
// tenant matches no row. Nothing is destroyed either — the versioning plugin
|
|
13
|
+
// snapshots the previous value, so every edit is recoverable through
|
|
14
|
+
// `documents.history` / `documents.asOf`.
|
|
15
|
+
//
|
|
16
|
+
// History is an audit trail, not an access control: it tells you WHAT
|
|
17
|
+
// changed, never whether the change was allowed. No auth strategy or rbac
|
|
18
|
+
// ships here, so a `guards: [{ scope }]` would deny every caller.
|
|
19
|
+
openAccess:
|
|
20
|
+
'edits a document of the request\'s tenant (an id outside it matches no row); the previous '
|
|
21
|
+
+ 'value is snapshotted, so nothing is destroyed. Version history records changes — it '
|
|
22
|
+
+ 'does not authorize them.',
|
|
11
23
|
input: Schema.Struct({
|
|
12
24
|
id: Schema.String,
|
|
13
25
|
content: 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-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-versioning": "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-versioning": "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,22 @@ import { Schema } from 'effect'
|
|
|
4
4
|
export const fulfillOrder = defineMutation({
|
|
5
5
|
name: 'orders.fulfill',
|
|
6
6
|
target: { table: 'orders', op: 'insert' },
|
|
7
|
+
// Open — and unlike a plain insert, this one FANS OUT: on commit it emits
|
|
8
|
+
// `order.completed` to every subscribed target, so an unguarded call makes
|
|
9
|
+
// this server POST to third parties. Two things bound that today and both are
|
|
10
|
+
// checkable: the emit is post-commit (a rollback ships nothing), and the
|
|
11
|
+
// target list is empty until someone calls `ctx.webhooks.subscribe`, which is
|
|
12
|
+
// not reachable from this procedure.
|
|
13
|
+
//
|
|
14
|
+
// No auth strategy and no rbac ship in this template, so a
|
|
15
|
+
// `guards: [{ scope: 'orders:write' }]` would deny every caller instead of
|
|
16
|
+
// the wrong ones. Add an identity, then that guard — before real subscribers
|
|
17
|
+
// exist. (The INCOMING `orders` webhook is a different door with its own
|
|
18
|
+
// control: an HMAC signature over `<unix>.<rawBody>`, not this decision.)
|
|
19
|
+
openAccess:
|
|
20
|
+
'inserts an order from caller-supplied fields into the request\'s tenant, then emits '
|
|
21
|
+
+ '`order.completed` post-commit. The emit reaches only targets someone registered via '
|
|
22
|
+
+ '`ctx.webhooks.subscribe` — none by default — so guard this before subscribers exist.',
|
|
7
23
|
input: Schema.Struct({ sku: Schema.NonEmptyString, totalCents: Schema.Number }),
|
|
8
24
|
output: Schema.Struct({
|
|
9
25
|
id: Schema.String,
|
|
@@ -13,19 +13,20 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-webhooks": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
23
|
-
"@voltro/sql-postgres": "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-webhooks": "0.35.0",
|
|
21
|
+
"@voltro/protocol": "0.35.0",
|
|
22
|
+
"@voltro/runtime": "0.35.0",
|
|
23
|
+
"@voltro/sql-postgres": "0.35.0",
|
|
24
24
|
"effect": "^3.22.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.35.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
30
|
"vitest": "^4.1.10"
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -4,6 +4,16 @@ import { Schema } from 'effect'
|
|
|
4
4
|
export const listOrders = defineQuery({
|
|
5
5
|
name: 'orders.list',
|
|
6
6
|
source: 'orders',
|
|
7
|
+
// Open: `tenant()` confines every delivery to the tenantId on the request, so
|
|
8
|
+
// one tenant's orders never reach another's subscription, and the rows carry
|
|
9
|
+
// only sku / amount / status — no customer identity. With no auth strategy
|
|
10
|
+
// configured that tenantId is the caller's own `x-tenant` header, and a
|
|
11
|
+
// `guards: [{ scope }]` would be unsatisfiable: an anonymous Subject holds no
|
|
12
|
+
// scopes, so the guard would deny 100% of traffic.
|
|
13
|
+
openAccess:
|
|
14
|
+
'lists the request tenant\'s own orders (sku, amount, status — no customer identity); '
|
|
15
|
+
+ '`tenant()` scopes every delivery. No auth strategy ships here, so the tenant comes from '
|
|
16
|
+
+ 'the caller\'s `x-tenant` header — add a strategy, then a `guards:`.',
|
|
7
17
|
input: Schema.Struct({}),
|
|
8
18
|
output: Schema.Array(Schema.Struct({
|
|
9
19
|
id: Schema.String,
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/changelog": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/changelog": "0.35.0",
|
|
16
|
+
"@voltro/cli": "0.35.0",
|
|
17
|
+
"@voltro/client": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/i18n": "0.35.0",
|
|
20
|
+
"@voltro/web": "0.35.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"@types/react": "^19.0.0",
|
|
26
26
|
"@types/react-dom": "^19.0.0",
|
|
27
27
|
"tsx": "^4.22.4",
|
|
28
|
+
"jsdom": "^30.0.0",
|
|
28
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
31
|
"vitest": "^4.1.10"
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -12,21 +12,23 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.35.0",
|
|
16
|
+
"@voltro/cli": "0.35.0",
|
|
17
|
+
"@voltro/client": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/i18n": "0.35.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.35.0",
|
|
21
|
+
"@voltro/web": "0.35.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.35.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"jsdom": "^30.0.0",
|
|
29
30
|
"typescript": "^6.0.3",
|
|
31
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
30
32
|
"vitest": "^4.1.10"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
// The scopes the admin
|
|
2
|
-
// derives a CONVENTIONAL scope per entity action — `<table>:create`,
|
|
3
|
-
// `<table>:write`, `<table>:delete` (see deriveEntityAdmins) — and hides any
|
|
4
|
-
// action the current subject's scopes don't satisfy.
|
|
1
|
+
// The scopes the admin judges its write affordances against.
|
|
5
2
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// READ THIS BEFORE SWAPPING IT OUT — the shape changed, and the old shape had a
|
|
4
|
+
// trap. The admin no longer invents a scope per entity (`<table>:create` and
|
|
5
|
+
// friends). It reads each procedure's OWN declared access off the capability
|
|
6
|
+
// map (`spec.create.guards` — the descriptor's `guards:` / `openAccess:`) and
|
|
7
|
+
// decides with `useAccessDecision`. So these scopes are matched against what
|
|
8
|
+
// your api really declares; a name that appears in neither place gates nothing.
|
|
9
|
+
//
|
|
10
|
+
// That is the whole fix. A UI gating on an invented convention hides every write
|
|
11
|
+
// from every caller of every app that named its scopes differently — a total
|
|
12
|
+
// outage of the affordance that reads like a working permission check, and that
|
|
13
|
+
// a demo seeding `admin:full` can never surface.
|
|
14
|
+
//
|
|
15
|
+
// DEMO: we feed `admin:full` (the blanket-bypass scope) so the scaffold shows
|
|
16
|
+
// every action on first boot. In a real app, REPLACE this with the current
|
|
17
|
+
// subject's scopes from your session — e.g. a reactive query:
|
|
9
18
|
//
|
|
10
19
|
// const { data } = useSubscription<{ scopes: string[] }>('app', 'auth.session')
|
|
11
20
|
// <PermissionProvider scopes={data?.scopes ?? []}>…</PermissionProvider>
|
|
12
21
|
//
|
|
13
|
-
//
|
|
22
|
+
// useAccessDecision reads these reactively, so affordances update the instant
|
|
14
23
|
// the subject's roles change. (This is UX gating, not enforcement — the api's
|
|
15
|
-
// own
|
|
24
|
+
// own guards remain the real authorization boundary. A control shown while the
|
|
25
|
+
// decision is `unknown` is deliberate: a per-row guard is the server's to
|
|
26
|
+
// answer, and it answers with a typed ScopeError.)
|
|
16
27
|
export const DEMO_SCOPES: ReadonlyArray<string> = ['admin:full']
|
|
@@ -18,7 +18,7 @@ export default defineLocale<typeof en>()({
|
|
|
18
18
|
'lang.label': 'Sprache',
|
|
19
19
|
|
|
20
20
|
'landing.heading': 'Das Backoffice, für dich erledigt.',
|
|
21
|
-
'landing.body': 'Dieser Admin liest zur Laufzeit die <strong>Capability-Map</strong> deiner API und rendert eine live, reaktive <code>DataTable</code> + <code>AutoForm</code> für jede Entität, die sie freigibt —
|
|
21
|
+
'landing.body': 'Dieser Admin liest zur Laufzeit die <strong>Capability-Map</strong> deiner API und rendert eine live, reaktive <code>DataTable</code> + <code>AutoForm</code> für jede Entität, die sie freigibt — gegated auf den Zugriff, den jede Prozedur selbst deklariert, mit einer Herkunfts-Lade pro Zeile und universellem Undo/Redo. Richte ihn auf eine beliebige Voltro-API und er passt sich an; der Seiten-Code unter <code>src/pages/admin/</code> gehört dir zum Bearbeiten.',
|
|
22
22
|
'landing.cta': 'Beim Admin anmelden →',
|
|
23
23
|
|
|
24
24
|
'login.heading': 'Anmelden',
|
|
@@ -38,6 +38,7 @@ export default defineLocale<typeof en>()({
|
|
|
38
38
|
'overview.reactiveSuffix': ' · reaktiv',
|
|
39
39
|
'overview.readonly': 'schreibgeschützt',
|
|
40
40
|
'overview.empty': 'Diese API stellt keine Benutzer-Entitäten bereit.',
|
|
41
|
+
'overview.serverOnly': '{count, plural, one {# reine Server-Spalte, ausgeblendet} other {# reine Server-Spalten, ausgeblendet}}',
|
|
41
42
|
|
|
42
43
|
'entity.create': 'Erstellen',
|
|
43
44
|
'entity.addLabel': '{table} hinzufügen',
|
|
@@ -48,6 +49,15 @@ export default defineLocale<typeof en>()({
|
|
|
48
49
|
'entity.delete': 'Löschen',
|
|
49
50
|
'entity.unknown': 'Keine Entität <code>{entity}</code> in der Capability-Map dieser API.',
|
|
50
51
|
|
|
52
|
+
'entity.requires': 'erfordert {scopes}',
|
|
53
|
+
'entity.openAccess': 'offener Zugriff — {reason}',
|
|
54
|
+
'entity.accessUndeclared': 'keine Zugriffsentscheidung deklariert',
|
|
55
|
+
'entity.accessUnknown': 'Diese API prüft die Berechtigung pro Zeile, das kann nur sie beantworten. Das Bedienelement wird angezeigt; die API entscheidet beim Absenden.',
|
|
56
|
+
|
|
57
|
+
'entity.serverOnly': '{count, plural, one {# reine Server-Spalte verlässt} other {# reine Server-Spalten verlassen}} die API nie und {count, plural, one {kann} other {können}} nicht übermittelt werden: {names}',
|
|
58
|
+
'entity.sensitive': 'Personenbezogene Daten — in Exporten maskiert: {names}',
|
|
59
|
+
'entity.notEditable': 'Kein einzelner Primärschlüssel, daher lassen sich Zeilen nicht zum Bearbeiten oder Löschen adressieren. Nur Liste.',
|
|
60
|
+
|
|
51
61
|
'prov.title': 'Herkunft · <code>{table}</code> #{id}',
|
|
52
62
|
'prov.close': 'schließen',
|
|
53
63
|
'prov.loading': 'lädt…',
|
|
@@ -24,7 +24,7 @@ export default defineCatalog({
|
|
|
24
24
|
|
|
25
25
|
// ---- Public landing ((marketing)/index.tsx) ----
|
|
26
26
|
'landing.heading': 'The back-office, done for you.',
|
|
27
|
-
'landing.body': 'This admin reads your api’s <strong>capability map</strong> at runtime and renders a live, reactive <code>DataTable</code> + <code>AutoForm</code> for every entity it exposes —
|
|
27
|
+
'landing.body': 'This admin reads your api’s <strong>capability map</strong> at runtime and renders a live, reactive <code>DataTable</code> + <code>AutoForm</code> for every entity it exposes — gated on the access each procedure itself declares, with a per-row provenance drawer and universal undo/redo. Point it at any Voltro api and it adapts; the page code under <code>src/pages/admin/</code> is yours to edit.',
|
|
28
28
|
'landing.cta': 'Sign in to the admin →',
|
|
29
29
|
|
|
30
30
|
// ---- Public sign-in ((marketing)/login.tsx) ----
|
|
@@ -47,6 +47,7 @@ export default defineCatalog({
|
|
|
47
47
|
'overview.reactiveSuffix': ' · reactive',
|
|
48
48
|
'overview.readonly': 'read-only',
|
|
49
49
|
'overview.empty': 'No user entities exposed by this api.',
|
|
50
|
+
'overview.serverOnly': '{count, plural, one {# server-only column, hidden} other {# server-only columns, hidden}}',
|
|
50
51
|
|
|
51
52
|
// ---- Per-entity CRUD (admin/[entity].tsx) ----
|
|
52
53
|
'entity.create': 'Create',
|
|
@@ -58,6 +59,17 @@ export default defineCatalog({
|
|
|
58
59
|
'entity.delete': 'Delete',
|
|
59
60
|
'entity.unknown': 'No entity <code>{entity}</code> in this api’s capability map.',
|
|
60
61
|
|
|
62
|
+
// ---- Declared access, shown rather than guessed (admin/[entity].tsx) ----
|
|
63
|
+
'entity.requires': 'requires {scopes}',
|
|
64
|
+
'entity.openAccess': 'open access — {reason}',
|
|
65
|
+
'entity.accessUndeclared': 'no access decision declared',
|
|
66
|
+
'entity.accessUnknown': 'This api checks permission per row, so only it can answer. The control is shown; the api decides when you submit.',
|
|
67
|
+
|
|
68
|
+
// ---- Exposure axes, straight from the schema (admin/[entity].tsx) ----
|
|
69
|
+
'entity.serverOnly': '{count, plural, one {# server-only column} other {# server-only columns}} never leave the api and cannot be submitted: {names}',
|
|
70
|
+
'entity.sensitive': 'Personal data — masked in exports: {names}',
|
|
71
|
+
'entity.notEditable': 'No single primary key, so rows can’t be addressed for edit or delete. List only.',
|
|
72
|
+
|
|
61
73
|
// ---- Provenance drawer (admin/[entity].tsx) ----
|
|
62
74
|
'prov.title': 'Provenance · <code>{table}</code> #{id}',
|
|
63
75
|
'prov.close': 'close',
|
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
// Per-entity CRUD — URL `/admin/<entity>`. Everything here is DISCOVERED:
|
|
2
2
|
// useCapabilityManifest → deriveEntityAdmins gives this entity's list query +
|
|
3
|
-
// create/update/delete
|
|
4
|
-
//
|
|
5
|
-
// - <DataTable api="app" query={
|
|
6
|
-
// - <AutoForm api="app" mutation={
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// create/update/delete actions + the columns. We bind the shipped schema-driven
|
|
4
|
+
// components to those tags:
|
|
5
|
+
// - <DataTable api="app" query={spec.list.tag}/> — live, reactive rows
|
|
6
|
+
// - <AutoForm api="app" mutation={spec.create.tag}/> — fields from the input schema
|
|
7
|
+
//
|
|
8
|
+
// WRITES ARE GATED ON THE ACCESS THE API DECLARES, not on a scope string this
|
|
9
|
+
// page invented. `spec.create.guards` is the procedure's own `guards:` /
|
|
10
|
+
// `openAccess:`, straight off the capability map, so this gate and the server's
|
|
11
|
+
// check read the same data and cannot drift.
|
|
12
|
+
//
|
|
13
|
+
// The decision is THREE-valued, and `unknown` is SHOWN, not hidden: a guard with
|
|
14
|
+
// a `resource` extractor is answered per ROW by the server, and a browser
|
|
15
|
+
// holding only global scopes cannot pre-compute it. Hiding those would empty the
|
|
16
|
+
// admin for exactly the multi-tenant apps whose subjects carry no global scopes.
|
|
17
|
+
// The server stays the authorization boundary and answers a typed ScopeError.
|
|
18
|
+
//
|
|
19
|
+
// Each row has a provenance ("why is this here?") drawer (08) + delete; the
|
|
20
|
+
// layout's undo bar reverts the last change (00/09). Edit this file freely —
|
|
21
|
+
// it's your template code, not a sealed runtime feature.
|
|
11
22
|
import { useState, type ReactNode } from 'react'
|
|
12
23
|
import type { PageMeta } from '@voltro/web'
|
|
13
24
|
import { useParams, AutoForm, DataTable } from '@voltro/web'
|
|
14
25
|
import {
|
|
15
26
|
useCapabilityManifest,
|
|
16
27
|
deriveEntityAdmins,
|
|
17
|
-
|
|
28
|
+
useAccessDecision,
|
|
29
|
+
requiredScopes,
|
|
30
|
+
openAccessReason,
|
|
18
31
|
useMutation,
|
|
19
32
|
useProvenance,
|
|
33
|
+
type EntityAction,
|
|
20
34
|
type EntityAdminSpec,
|
|
21
35
|
} from '@voltro/client'
|
|
22
36
|
import { T, useTFn } from '@voltro/i18n'
|
|
@@ -55,23 +69,36 @@ const ProvenanceDrawer = ({ table, id, onClose }: { readonly table: string; read
|
|
|
55
69
|
)
|
|
56
70
|
}
|
|
57
71
|
|
|
72
|
+
// What the api requires for this action, in words — so an operator who hits a
|
|
73
|
+
// ScopeError can read the scope they are missing instead of guessing at it.
|
|
74
|
+
const AccessNote = ({ action }: { readonly action: EntityAction }): ReactNode => {
|
|
75
|
+
const reason = openAccessReason(action.guards)
|
|
76
|
+
const scopes = requiredScopes(action.guards)
|
|
77
|
+
if (reason !== undefined) return <span className="meta"><T id="entity.openAccess" values={{ reason }} /></span>
|
|
78
|
+
if (scopes.length > 0) return <span className="meta"><T id="entity.requires" values={{ scopes: scopes.join(', ') }} /></span>
|
|
79
|
+
return <span className="meta"><T id="entity.accessUndeclared" /></span>
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
// The bound CRUD surface for one entity. A child component so its hooks
|
|
59
|
-
// (
|
|
83
|
+
// (useAccessDecision/useMutation) get the real spec, never undefined.
|
|
60
84
|
const EntityAdmin = ({ spec }: { readonly spec: EntityAdminSpec }): ReactNode => {
|
|
61
85
|
const [sel, setSel] = useState<string | null>(null)
|
|
62
86
|
const t = useTFn()
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
// Always-called; the tag is only used when
|
|
66
|
-
const del = useMutation<
|
|
87
|
+
const create = useAccessDecision(spec.create.guards)
|
|
88
|
+
const remove = useAccessDecision(spec.delete.guards)
|
|
89
|
+
// Always-called; the tag is only used when spec.delete.tag exists (gated below).
|
|
90
|
+
const del = useMutation<Record<string, string>>('app', spec.delete.tag ?? '__voltro.noop')
|
|
91
|
+
// The column the api keys rows on — NOT a hard-coded `id`. A table keyed on
|
|
92
|
+
// anything else would otherwise get an empty id sent on every delete.
|
|
93
|
+
const pk = spec.pkColumn ?? 'id'
|
|
67
94
|
|
|
68
95
|
const rowActions = (row: Row): ReactNode => {
|
|
69
|
-
const id = String(row[
|
|
96
|
+
const id = String(row[pk] ?? '')
|
|
70
97
|
return (
|
|
71
98
|
<span className="row-actions">
|
|
72
99
|
<button type="button" className="btn ghost" onClick={() => setSel(id)}><T id="entity.why" /></button>
|
|
73
|
-
{spec.
|
|
74
|
-
<button type="button" className="btn danger" onClick={() => void del.mutate({ id })}><T id="entity.delete" /></button>
|
|
100
|
+
{spec.delete.tag && spec.editable && remove !== 'denied' && id ? (
|
|
101
|
+
<button type="button" className="btn danger" onClick={() => void del.mutate({ [pk]: id })}><T id="entity.delete" /></button>
|
|
75
102
|
) : null}
|
|
76
103
|
</span>
|
|
77
104
|
)
|
|
@@ -81,17 +108,33 @@ const EntityAdmin = ({ spec }: { readonly spec: EntityAdminSpec }): ReactNode =>
|
|
|
81
108
|
<section>
|
|
82
109
|
<h1>{spec.table}</h1>
|
|
83
110
|
|
|
84
|
-
{spec.
|
|
111
|
+
{/* `.serverOnly()` columns are absent from `spec.columns` by construction —
|
|
112
|
+
they never cross the wire, and the runtime REFUSES a mutation input that
|
|
113
|
+
sets one. Saying so beats a silently shorter column list. */}
|
|
114
|
+
{spec.serverOnlyColumns.length > 0 ? (
|
|
115
|
+
<p className="meta">
|
|
116
|
+
<T id="entity.serverOnly" values={{ count: spec.serverOnlyColumns.length, names: spec.serverOnlyColumns.join(', ') }} />
|
|
117
|
+
</p>
|
|
118
|
+
) : null}
|
|
119
|
+
{spec.sensitiveColumns.length > 0 ? (
|
|
120
|
+
<p className="meta">
|
|
121
|
+
<T id="entity.sensitive" values={{ names: spec.sensitiveColumns.join(', ') }} />
|
|
122
|
+
</p>
|
|
123
|
+
) : null}
|
|
124
|
+
{!spec.editable ? <p className="meta"><T id="entity.notEditable" /></p> : null}
|
|
125
|
+
|
|
126
|
+
{spec.create.tag && create !== 'denied' ? (
|
|
85
127
|
<div className="section">
|
|
86
|
-
<h2><T id="entity.create" /></h2>
|
|
87
|
-
<
|
|
128
|
+
<h2><T id="entity.create" /> <AccessNote action={spec.create} /></h2>
|
|
129
|
+
{create === 'unknown' ? <p className="muted"><T id="entity.accessUnknown" /></p> : null}
|
|
130
|
+
<AutoForm api="app" mutation={spec.create.tag} submitLabel={t('entity.addLabel', { table: spec.table })} />
|
|
88
131
|
</div>
|
|
89
132
|
) : null}
|
|
90
133
|
|
|
91
134
|
<div className="section">
|
|
92
135
|
<h2><T id="entity.rows" /></h2>
|
|
93
|
-
{spec.
|
|
94
|
-
<DataTable api="app" query={spec.
|
|
136
|
+
{spec.list.tag ? (
|
|
137
|
+
<DataTable api="app" query={spec.list.tag} rowActions={rowActions} emptyText={t('entity.empty', { table: spec.table })} />
|
|
95
138
|
) : (
|
|
96
139
|
<p className="muted"><T id="entity.noList" /></p>
|
|
97
140
|
)}
|