@voltro/cli 0.1.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 +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20236 -0
- package/bin/voltro.mjs +43 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/commands-CfPH2Wf4.js +18061 -0
- package/dist/frameworkInspectState-CX2250XB.js +86 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +5 -0
- package/dist/inspectState.d.ts +116 -0
- package/dist/inspectState.js +2 -0
- package/dist/startup.d.ts +26 -0
- package/dist/startup.js +2 -0
- package/dist/startupRunner-CRhuUl91.js +71 -0
- package/package.json +88 -0
- package/templates/AGENTS.core.md +258 -0
- package/templates/AGENTS.md +351 -0
- package/templates/agent-docs/_index.md +93 -0
- package/templates/agent-docs/_manifest.json +655 -0
- package/templates/agent-docs/ai.md +1845 -0
- package/templates/agent-docs/authentication.md +1788 -0
- package/templates/agent-docs/caching.md +624 -0
- package/templates/agent-docs/cli.md +1650 -0
- package/templates/agent-docs/configuration.md +295 -0
- package/templates/agent-docs/data.md +2172 -0
- package/templates/agent-docs/database/advancedqueries.md +1583 -0
- package/templates/agent-docs/database/columntypes.md +1200 -0
- package/templates/agent-docs/database/hosting.md +881 -0
- package/templates/agent-docs/database/migrations.md +2938 -0
- package/templates/agent-docs/database/misc.md +270 -0
- package/templates/agent-docs/database/overview.md +108 -0
- package/templates/agent-docs/database/querying.md +1622 -0
- package/templates/agent-docs/database/scaling.md +331 -0
- package/templates/agent-docs/database/schema.md +1458 -0
- package/templates/agent-docs/database/seedsdialects.md +1235 -0
- package/templates/agent-docs/database/transactions.md +285 -0
- package/templates/agent-docs/deployment.md +999 -0
- package/templates/agent-docs/internationalization.md +359 -0
- package/templates/agent-docs/introduction.md +438 -0
- package/templates/agent-docs/multi-tenancy.md +610 -0
- package/templates/agent-docs/observability.md +350 -0
- package/templates/agent-docs/plugins.md +1273 -0
- package/templates/agent-docs/reference.md +978 -0
- package/templates/agent-docs/routing.md +1553 -0
- package/templates/agent-docs/scheduling.md +666 -0
- package/templates/agent-docs/schema-driven-ui.md +607 -0
- package/templates/agent-docs/security.md +42 -0
- package/templates/agent-docs/templates/apibackends.md +3214 -0
- package/templates/agent-docs/templates/appshells.md +2062 -0
- package/templates/agent-docs/templates/custom.md +128 -0
- package/templates/agent-docs/templates/overview.md +122 -0
- package/templates/agent-docs/templates/serverless.md +315 -0
- package/templates/agent-docs/testing.md +376 -0
- package/templates/agent-docs/workflows.md +1351 -0
- package/templates/apps/api-ai/README.md +105 -0
- package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
- package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
- package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
- package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
- package/templates/apps/api-ai/app.config.ts +57 -0
- package/templates/apps/api-ai/database/schema.ts +67 -0
- package/templates/apps/api-ai/package.json +27 -0
- package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
- package/templates/apps/api-ai/template.json +6 -0
- package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
- package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
- package/templates/apps/api-ai/tsconfig.json +5 -0
- package/templates/apps/api-auth/.env +17 -0
- package/templates/apps/api-auth/README.md +109 -0
- package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
- package/templates/apps/api-auth/actions/me.action.ts +19 -0
- package/templates/apps/api-auth/app.config.ts +59 -0
- package/templates/apps/api-auth/database/schema.ts +26 -0
- package/templates/apps/api-auth/package.json +28 -0
- package/templates/apps/api-auth/template.json +6 -0
- package/templates/apps/api-auth/tests/me.test.ts +37 -0
- package/templates/apps/api-auth/tsconfig.json +5 -0
- package/templates/apps/api-backend/README.md +25 -0
- package/templates/apps/api-backend/app.config.ts +39 -0
- package/templates/apps/api-backend/database/schema.ts +57 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend/package.json +28 -0
- package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend/template.json +6 -0
- package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend/tsconfig.json +5 -0
- package/templates/apps/api-backend-deactivation/README.md +49 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
- package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
- package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
- package/templates/apps/api-backend-deactivation/package.json +28 -0
- package/templates/apps/api-backend-deactivation/template.json +6 -0
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
- package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
- package/templates/apps/api-backend-mail/README.md +39 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
- package/templates/apps/api-backend-mail/app.config.ts +34 -0
- package/templates/apps/api-backend-mail/database/schema.ts +57 -0
- package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mail/package.json +30 -0
- package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mail/template.json +6 -0
- package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mail/tsconfig.json +5 -0
- package/templates/apps/api-backend-mariadb/.env.example +35 -0
- package/templates/apps/api-backend-mariadb/README.md +31 -0
- package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
- package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mariadb/package.json +30 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mariadb/template.json +6 -0
- package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/README.md +86 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
- package/templates/apps/api-backend-storage/app.config.ts +38 -0
- package/templates/apps/api-backend-storage/database/schema.ts +57 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-storage/package.json +27 -0
- package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-storage/template.json +6 -0
- package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-storage/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/.env +17 -0
- package/templates/apps/api-data-advanced/README.md +88 -0
- package/templates/apps/api-data-advanced/app.config.ts +56 -0
- package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
- package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
- package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
- package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
- package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
- package/templates/apps/api-data-advanced/database/index.ts +23 -0
- package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
- package/templates/apps/api-data-advanced/package.json +28 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
- package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
- package/templates/apps/api-data-advanced/template.json +6 -0
- package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
- package/templates/apps/api-data-advanced/tsconfig.json +5 -0
- package/templates/apps/api-durable/README.md +81 -0
- package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
- package/templates/apps/api-durable/app.config.ts +41 -0
- package/templates/apps/api-durable/database/schema.ts +65 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
- package/templates/apps/api-durable/package.json +28 -0
- package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
- package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
- package/templates/apps/api-durable/template.json +6 -0
- package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
- package/templates/apps/api-durable/tsconfig.json +5 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
- package/templates/apps/api-feature-flags/README.md +63 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
- package/templates/apps/api-feature-flags/app.config.ts +41 -0
- package/templates/apps/api-feature-flags/database/schema.ts +37 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
- package/templates/apps/api-feature-flags/package.json +29 -0
- package/templates/apps/api-feature-flags/template.json +6 -0
- package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
- package/templates/apps/api-feature-flags/tsconfig.json +5 -0
- package/templates/apps/api-governance/.env +4 -0
- package/templates/apps/api-governance/README.md +61 -0
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
- package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
- package/templates/apps/api-governance/app.config.ts +35 -0
- package/templates/apps/api-governance/database/schema.ts +41 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
- package/templates/apps/api-governance/package.json +29 -0
- package/templates/apps/api-governance/template.json +6 -0
- package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
- package/templates/apps/api-governance/tsconfig.json +5 -0
- package/templates/apps/api-kv/README.md +100 -0
- package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
- package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
- package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
- package/templates/apps/api-kv/app.config.ts +48 -0
- package/templates/apps/api-kv/database/schema.ts +62 -0
- package/templates/apps/api-kv/package.json +28 -0
- package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
- package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
- package/templates/apps/api-kv/template.json +6 -0
- package/templates/apps/api-kv/tests/sync.test.ts +103 -0
- package/templates/apps/api-kv/tsconfig.json +5 -0
- package/templates/apps/api-moderation/README.md +46 -0
- package/templates/apps/api-moderation/app.config.ts +33 -0
- package/templates/apps/api-moderation/database/schema.ts +45 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
- package/templates/apps/api-moderation/package.json +29 -0
- package/templates/apps/api-moderation/template.json +6 -0
- package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
- package/templates/apps/api-moderation/tsconfig.json +5 -0
- package/templates/apps/api-observability/README.md +106 -0
- package/templates/apps/api-observability/app.config.ts +32 -0
- package/templates/apps/api-observability/database/schema.ts +38 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
- package/templates/apps/api-observability/package.json +29 -0
- package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
- package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
- package/templates/apps/api-observability/template.json +6 -0
- package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
- package/templates/apps/api-observability/tsconfig.json +5 -0
- package/templates/apps/api-ratelimit/README.md +49 -0
- package/templates/apps/api-ratelimit/app.config.ts +42 -0
- package/templates/apps/api-ratelimit/database/schema.ts +37 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
- package/templates/apps/api-ratelimit/package.json +29 -0
- package/templates/apps/api-ratelimit/template.json +6 -0
- package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
- package/templates/apps/api-ratelimit/tsconfig.json +5 -0
- package/templates/apps/api-rbac/README.md +59 -0
- package/templates/apps/api-rbac/app.config.ts +48 -0
- package/templates/apps/api-rbac/database/schema.ts +37 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
- package/templates/apps/api-rbac/package.json +29 -0
- package/templates/apps/api-rbac/template.json +6 -0
- package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
- package/templates/apps/api-rbac/tsconfig.json +5 -0
- package/templates/apps/api-rest/README.md +85 -0
- package/templates/apps/api-rest/app.config.ts +70 -0
- package/templates/apps/api-rest/database/schema.ts +52 -0
- package/templates/apps/api-rest/lib/product.ts +30 -0
- package/templates/apps/api-rest/package.json +27 -0
- package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
- package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
- package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
- package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
- package/templates/apps/api-rest/template.json +6 -0
- package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
- package/templates/apps/api-rest/tsconfig.json +5 -0
- package/templates/apps/api-saas/README.md +106 -0
- package/templates/apps/api-saas/app.config.ts +51 -0
- package/templates/apps/api-saas/database/schema.ts +50 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas/package.json +31 -0
- package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
- package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas/template.json +6 -0
- package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
- package/templates/apps/api-saas/tsconfig.json +5 -0
- package/templates/apps/api-search/README.md +68 -0
- package/templates/apps/api-search/app.config.ts +31 -0
- package/templates/apps/api-search/database/schema.ts +47 -0
- package/templates/apps/api-search/lib/search.ts +27 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
- package/templates/apps/api-search/package.json +29 -0
- package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
- package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
- package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
- package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
- package/templates/apps/api-search/template.json +6 -0
- package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
- package/templates/apps/api-search/tsconfig.json +5 -0
- package/templates/apps/api-versioning/README.md +51 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
- package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
- package/templates/apps/api-versioning/app.config.ts +24 -0
- package/templates/apps/api-versioning/database/schema.ts +38 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
- package/templates/apps/api-versioning/package.json +29 -0
- package/templates/apps/api-versioning/template.json +6 -0
- package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
- package/templates/apps/api-versioning/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/.env +6 -0
- package/templates/apps/api-webhooks/README.md +106 -0
- package/templates/apps/api-webhooks/app.config.ts +16 -0
- package/templates/apps/api-webhooks/database/schema.ts +49 -0
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
- package/templates/apps/api-webhooks/package.json +28 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
- package/templates/apps/api-webhooks/template.json +6 -0
- package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
- package/templates/apps/api-webhooks/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
- package/templates/apps/changelog/README.md +77 -0
- package/templates/apps/changelog/app.config.ts +27 -0
- package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
- package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
- package/templates/apps/changelog/package.json +30 -0
- package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
- package/templates/apps/changelog/src/globals.css +66 -0
- package/templates/apps/changelog/src/globals.d.ts +16 -0
- package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
- package/templates/apps/changelog/src/lib/locale.ts +55 -0
- package/templates/apps/changelog/src/lib/releases.ts +21 -0
- package/templates/apps/changelog/src/locales/de.ts +22 -0
- package/templates/apps/changelog/src/locales/en.ts +30 -0
- package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
- package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
- package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
- package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
- package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
- package/templates/apps/changelog/src/pages/index.tsx +55 -0
- package/templates/apps/changelog/src/pages/layout.tsx +84 -0
- package/templates/apps/changelog/template.json +6 -0
- package/templates/apps/changelog/tsconfig.json +11 -0
- package/templates/apps/edge-functions/README.md +55 -0
- package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
- package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
- package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
- package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
- package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
- package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
- package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
- package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
- package/templates/apps/edge-functions/package.json +21 -0
- package/templates/apps/edge-functions/template.json +6 -0
- package/templates/apps/edge-functions/tsconfig.json +10 -0
- package/templates/apps/frontend-admin/README.md +40 -0
- package/templates/apps/frontend-admin/app.config.ts +37 -0
- package/templates/apps/frontend-admin/package.json +30 -0
- package/templates/apps/frontend-admin/src/config.ts +8 -0
- package/templates/apps/frontend-admin/src/globals.css +76 -0
- package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
- package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
- package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
- package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
- package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
- package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
- package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
- package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
- package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
- package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-admin/template.json +6 -0
- package/templates/apps/frontend-admin/tsconfig.json +11 -0
- package/templates/apps/frontend-app/README.md +98 -0
- package/templates/apps/frontend-app/app.config.ts +43 -0
- package/templates/apps/frontend-app/package.json +30 -0
- package/templates/apps/frontend-app/src/locales/de.ts +36 -0
- package/templates/apps/frontend-app/src/locales/en.ts +43 -0
- package/templates/apps/frontend-app/src/locales/index.ts +15 -0
- package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
- package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
- package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
- package/templates/apps/frontend-app/template.json +6 -0
- package/templates/apps/frontend-app/tsconfig.json +11 -0
- package/templates/apps/frontend-blank/README.md +18 -0
- package/templates/apps/frontend-blank/app.config.ts +29 -0
- package/templates/apps/frontend-blank/package.json +29 -0
- package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
- package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
- package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
- package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
- package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
- package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
- package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
- package/templates/apps/frontend-blank/template.json +6 -0
- package/templates/apps/frontend-blank/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/README.md +65 -0
- package/templates/apps/frontend-contact/app.config.ts +25 -0
- package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
- package/templates/apps/frontend-contact/package.json +33 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
- package/templates/apps/frontend-contact/src/config.ts +12 -0
- package/templates/apps/frontend-contact/src/globals.css +84 -0
- package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
- package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
- package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
- package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
- package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
- package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
- package/templates/apps/frontend-contact/template.json +6 -0
- package/templates/apps/frontend-contact/tsconfig.json +11 -0
- package/templates/apps/frontend-dashboard/README.md +54 -0
- package/templates/apps/frontend-dashboard/app.config.ts +39 -0
- package/templates/apps/frontend-dashboard/package.json +29 -0
- package/templates/apps/frontend-dashboard/src/config.ts +8 -0
- package/templates/apps/frontend-dashboard/src/globals.css +72 -0
- package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
- package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
- package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
- package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
- package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-dashboard/template.json +6 -0
- package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
- package/templates/apps/frontend-docs/README.md +19 -0
- package/templates/apps/frontend-docs/app.config.ts +25 -0
- package/templates/apps/frontend-docs/package.json +29 -0
- package/templates/apps/frontend-docs/src/globals.css +32 -0
- package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
- package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
- package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
- package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
- package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-docs/template.json +6 -0
- package/templates/apps/frontend-docs/tsconfig.json +11 -0
- package/templates/apps/frontend-i18n/README.md +61 -0
- package/templates/apps/frontend-i18n/app.config.ts +33 -0
- package/templates/apps/frontend-i18n/package.json +28 -0
- package/templates/apps/frontend-i18n/src/globals.css +46 -0
- package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
- package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
- package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
- package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
- package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
- package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
- package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
- package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
- package/templates/apps/frontend-i18n/template.json +6 -0
- package/templates/apps/frontend-i18n/tsconfig.json +11 -0
- package/templates/apps/frontend-landing/README.md +17 -0
- package/templates/apps/frontend-landing/app.config.ts +25 -0
- package/templates/apps/frontend-landing/package.json +29 -0
- package/templates/apps/frontend-landing/src/globals.css +23 -0
- package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
- package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
- package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
- package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
- package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-landing/template.json +6 -0
- package/templates/apps/frontend-landing/tsconfig.json +11 -0
- package/templates/apps/frontend-spa/README.md +45 -0
- package/templates/apps/frontend-spa/app.config.ts +27 -0
- package/templates/apps/frontend-spa/package.json +29 -0
- package/templates/apps/frontend-spa/src/globals.css +84 -0
- package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
- package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
- package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
- package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
- package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
- package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-spa/template.json +6 -0
- package/templates/apps/frontend-spa/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr/README.md +68 -0
- package/templates/apps/frontend-ssr/app.config.ts +32 -0
- package/templates/apps/frontend-ssr/package.json +29 -0
- package/templates/apps/frontend-ssr/src/globals.css +67 -0
- package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
- package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
- package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
- package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
- package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
- package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
- package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
- package/templates/apps/frontend-ssr/template.json +6 -0
- package/templates/apps/frontend-ssr/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr-api/README.md +50 -0
- package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
- package/templates/apps/frontend-ssr-api/package.json +30 -0
- package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
- package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
- package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
- package/templates/apps/frontend-ssr-api/template.json +6 -0
- package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
- package/templates/apps/frontend-static-blog/README.md +49 -0
- package/templates/apps/frontend-static-blog/app.config.ts +34 -0
- package/templates/apps/frontend-static-blog/package.json +28 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
- package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
- package/templates/apps/frontend-static-blog/src/globals.css +75 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
- package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
- package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
- package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
- package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
- package/templates/apps/frontend-static-blog/template.json +6 -0
- package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
- package/templates/baselines/bare/.env.example +37 -0
- package/templates/baselines/bare/README.md +47 -0
- package/templates/baselines/bare/baseline.json +31 -0
- package/templates/baselines/bare/deploy/README.md +43 -0
- package/templates/baselines/bare/deploy/voltro.service.example +36 -0
- package/templates/baselines/compose/.env.example +46 -0
- package/templates/baselines/compose/README.md +69 -0
- package/templates/baselines/compose/baseline.json +51 -0
- package/templates/baselines/compose/docker/.dockerignore +38 -0
- package/templates/baselines/compose/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose/docker-compose.dev.yml +89 -0
- package/templates/baselines/compose/docker-compose.prod.yml +87 -0
- package/templates/baselines/compose/docker-compose.yml +41 -0
- package/templates/baselines/compose-mariadb/.env.example +57 -0
- package/templates/baselines/compose-mariadb/README.md +78 -0
- package/templates/baselines/compose-mariadb/baseline.json +51 -0
- package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
- package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
- package/templates/baselines/helm/.env.example +39 -0
- package/templates/baselines/helm/README.md +98 -0
- package/templates/baselines/helm/baseline.json +53 -0
- package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
- package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
- package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
- package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
- package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
- package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
- package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
- package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
- package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
- package/templates/baselines/helm/deploy/README.md +94 -0
- package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Feature-flag backend for the {{projectName}} project.
|
|
2
|
+
//
|
|
3
|
+
// `flagsPlugin` declares flags AS CODE and gates rpc calls two ways:
|
|
4
|
+
// - declaratively via `gatedBy` (an off flag fails typed FlagDisabled
|
|
5
|
+
// BEFORE the handler runs), and
|
|
6
|
+
// - in-handler via `requireFlag(ctx, key)` / `isFlagEnabled(ctx, key)`.
|
|
7
|
+
// A flag is a bare boolean (kill-switch) OR a rich definition with a
|
|
8
|
+
// deterministic `% rollout` and OR-of-rules `targeting`. `store: 'memory'`
|
|
9
|
+
// keeps this zero-infra; `store: 'postgres'` adds runtime-toggleable flags
|
|
10
|
+
// overlaid on the code baseline.
|
|
11
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
12
|
+
import { flagsPlugin } from '@voltro/plugin-flags'
|
|
13
|
+
|
|
14
|
+
export const env = defineEnv({
|
|
15
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
type: 'api' as const,
|
|
20
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
21
|
+
store: 'memory' as const,
|
|
22
|
+
plugins: [
|
|
23
|
+
flagsPlugin({
|
|
24
|
+
flags: {
|
|
25
|
+
// Bare boolean = kill-switch. `newEditor` is ON — the in-handler
|
|
26
|
+
// requireFlag guard on notes.create passes.
|
|
27
|
+
newEditor: true,
|
|
28
|
+
// OFF — the gatedBy map below blocks notes.export with FlagDisabled.
|
|
29
|
+
betaExport: false,
|
|
30
|
+
// Targeting: only subjects whose metadata says plan=pro see it.
|
|
31
|
+
proDashboard: { targeting: [{ metadata: { plan: 'pro' } }], description: 'Pro-plan dashboard.' },
|
|
32
|
+
// Deterministic % rollout — a given subject is stably in or out.
|
|
33
|
+
gradualRollout: { rollout: 25, description: '25% gradual rollout, bucketed by subject.' },
|
|
34
|
+
},
|
|
35
|
+
// Declarative gate: tag → flag. An off flag fails FlagDisabled pre-handler.
|
|
36
|
+
gatedBy: { 'notes.export': 'betaExport' },
|
|
37
|
+
store: 'memory',
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
env,
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Schema for the {{projectName}} feature-flags backend — a tiny `notes`
|
|
2
|
+
// domain to gate. `actors` + `tenants` are the framework core tables.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
databaseHandle,
|
|
6
|
+
id,
|
|
7
|
+
table,
|
|
8
|
+
text,
|
|
9
|
+
timestamp,
|
|
10
|
+
type InferRow,
|
|
11
|
+
} from '@voltro/database'
|
|
12
|
+
import { tenant } from '@voltro/plugin-multitenancy'
|
|
13
|
+
|
|
14
|
+
export const actors = table('actors', {
|
|
15
|
+
id: id(),
|
|
16
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
17
|
+
displayName: text().nullable(),
|
|
18
|
+
createdAt: timestamp().default('now'),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const tenants = table('tenants', {
|
|
22
|
+
id: id(),
|
|
23
|
+
name: text(),
|
|
24
|
+
createdAt: timestamp().default('now'),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const notes = table('notes', {
|
|
28
|
+
id: id({ prefix: 'note' }),
|
|
29
|
+
title: text(),
|
|
30
|
+
body: text(),
|
|
31
|
+
})
|
|
32
|
+
.with(tenant())
|
|
33
|
+
.reactive()
|
|
34
|
+
|
|
35
|
+
export type Note = InferRow<typeof notes>
|
|
36
|
+
|
|
37
|
+
export const database = databaseHandle({ actors, tenants, notes })
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EffectStore } from '@voltro/runtime'
|
|
2
|
+
import type { AppContext } from '@voltro/runtime'
|
|
3
|
+
import { requireFlag } from '@voltro/plugin-flags'
|
|
4
|
+
import { Effect } from 'effect'
|
|
5
|
+
|
|
6
|
+
const execute = (
|
|
7
|
+
input: { tenantId: string; title: string; body: string },
|
|
8
|
+
ctx: AppContext,
|
|
9
|
+
) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
// In-handler flag guard — short-circuits with typed FlagDisabled when the
|
|
12
|
+
// flag is off. Same shape as `requireScope` / `requireEntitlement`.
|
|
13
|
+
yield* requireFlag(ctx, 'newEditor')
|
|
14
|
+
|
|
15
|
+
const store = yield* EffectStore
|
|
16
|
+
const row = yield* store.insert('notes', {
|
|
17
|
+
title: input.title,
|
|
18
|
+
body: input.body,
|
|
19
|
+
tenantId: input.tenantId,
|
|
20
|
+
})
|
|
21
|
+
return {
|
|
22
|
+
id: row['id'] as string,
|
|
23
|
+
title: row['title'] as string,
|
|
24
|
+
body: row['body'] as string,
|
|
25
|
+
tenantId: row['tenantId'] as string,
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export default execute
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Creates a note — but only if the `newEditor` flag is on for the caller.
|
|
2
|
+
// The executor uses the IN-HANDLER guard `requireFlag(ctx, 'newEditor')`, which
|
|
3
|
+
// fails typed `FlagDisabled` when the flag is off. Declaring `error: FlagDisabled`
|
|
4
|
+
// here surfaces that to the client as a typed, pattern-matchable failure.
|
|
5
|
+
//
|
|
6
|
+
// `FlagDisabled` is imported from the browser-safe `/errors` subpath (no
|
|
7
|
+
// node/db imports) so this descriptor stays safe for the client bundle.
|
|
8
|
+
|
|
9
|
+
import { defineMutation } from '@voltro/protocol'
|
|
10
|
+
import { FlagDisabled } from '@voltro/plugin-flags/errors'
|
|
11
|
+
import { Schema } from 'effect'
|
|
12
|
+
|
|
13
|
+
export const createNote = defineMutation({
|
|
14
|
+
name: 'notes.create',
|
|
15
|
+
target: { table: 'notes', op: 'insert' },
|
|
16
|
+
input: Schema.Struct({
|
|
17
|
+
tenantId: Schema.String,
|
|
18
|
+
title: Schema.NonEmptyString,
|
|
19
|
+
body: Schema.String,
|
|
20
|
+
}),
|
|
21
|
+
output: Schema.Struct({
|
|
22
|
+
id: Schema.String,
|
|
23
|
+
title: Schema.String,
|
|
24
|
+
body: Schema.String,
|
|
25
|
+
tenantId: Schema.String,
|
|
26
|
+
}),
|
|
27
|
+
error: FlagDisabled,
|
|
28
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"migrate": "voltro migrate",
|
|
9
|
+
"test": "voltro test",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@effect/platform": "^0.96.1",
|
|
14
|
+
"@effect/rpc": "^0.75.1",
|
|
15
|
+
"@voltro/cli": "0.1.0",
|
|
16
|
+
"@voltro/database": "0.1.0",
|
|
17
|
+
"@voltro/env": "0.1.0",
|
|
18
|
+
"@voltro/plugin-flags": "0.1.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.1.0",
|
|
20
|
+
"@voltro/protocol": "0.1.0",
|
|
21
|
+
"@voltro/runtime": "0.1.0",
|
|
22
|
+
"effect": "^3.21.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@voltro/testing": "0.1.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-feature-flags",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Feature flags wired turnkey — @voltro/plugin-flags: flags as code (kill-switch / % rollout / targeting), a declarative gatedBy map that fails typed FlagDisabled before the handler, an in-handler requireFlag guard, and the flags.evaluate query for useFlag() on the web. Memory store, zero infra.",
|
|
5
|
+
"tags": ["api", "feature-flags", "rollout", "kill-switch", "targeting", "plugin"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Unit test for `notes.create` — driven through `@voltro/testing`.
|
|
2
|
+
//
|
|
3
|
+
// This executor is Effect-native: it `yield*`s the flag guard
|
|
4
|
+
// `requireFlag(ctx, 'newEditor')` (fails typed `FlagDisabled` when the flag is
|
|
5
|
+
// off for the caller) and then `yield* EffectStore` to insert. The flag registry
|
|
6
|
+
// is populated by flagsPlugin at BOOT from app.config.ts — a step the unit
|
|
7
|
+
// harness deliberately doesn't run, so under test every flag reads as off and
|
|
8
|
+
// `requireFlag` fails `FlagDisabled`. We assert that REAL guard behavior (no
|
|
9
|
+
// faking the plugin) + pin the descriptor's contract. `makeTestContext` exposes
|
|
10
|
+
// the mixin-wrapped store as `ctx.store`, provided to the Effect as the
|
|
11
|
+
// `EffectStore` layer via `makeEffectStoreLayer`. Run with `voltro test`.
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { Cause, Effect, Exit, Option, Schema } from 'effect'
|
|
15
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
16
|
+
import { makeEffectStoreLayer } from '@voltro/runtime'
|
|
17
|
+
import { database } from '../database/schema' // registers actors / tenants / notes
|
|
18
|
+
import { createNote } from '../mutations/notes.create.mutation'
|
|
19
|
+
import createNoteHandler from '../mutations/notes.create.mutation.server'
|
|
20
|
+
import { FlagDisabled } from '@voltro/plugin-flags/errors'
|
|
21
|
+
|
|
22
|
+
// Run the Effect-native executor with the test store provided as EffectStore,
|
|
23
|
+
// returning the raw Exit so a typed failure can be inspected WITHOUT the
|
|
24
|
+
// `FiberFailure` wrapper `Effect.runPromise` throws (which would defeat a plain
|
|
25
|
+
// `instanceof FlagDisabled` assertion). Unwrap the Cause to get the real error.
|
|
26
|
+
const runExit = (
|
|
27
|
+
input: { tenantId: string; title: string; body: string },
|
|
28
|
+
ctx: ReturnType<typeof makeTestContext>,
|
|
29
|
+
) => Effect.runPromiseExit(createNoteHandler(input, ctx).pipe(Effect.provide(makeEffectStoreLayer(ctx.store))))
|
|
30
|
+
|
|
31
|
+
describe('notes.create', () => {
|
|
32
|
+
it('fails typed FlagDisabled when the newEditor flag is off — nothing written', async () => {
|
|
33
|
+
// No plugin boot under unit test ⇒ the flag registry is empty ⇒ the flag is
|
|
34
|
+
// off ⇒ the in-handler requireFlag guard short-circuits before the insert.
|
|
35
|
+
const ctx = makeTestContext({
|
|
36
|
+
subject: { type: 'user', id: 'u1', tenantId: 'acme' },
|
|
37
|
+
store: mockStore({ notes: [] }),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const exit = await runExit({ tenantId: 'acme', title: 'Hello', body: 'World' }, ctx)
|
|
41
|
+
|
|
42
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
43
|
+
const failure = Exit.isFailure(exit) ? Cause.failureOption(exit.cause) : Option.none()
|
|
44
|
+
expect(Option.getOrThrow(failure)).toBeInstanceOf(FlagDisabled)
|
|
45
|
+
|
|
46
|
+
const rows = await ctx.store.query(database.notes.descriptor)
|
|
47
|
+
expect(rows).toHaveLength(0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('pins the descriptor: name, input decode + the declared FlagDisabled error', () => {
|
|
51
|
+
expect(createNote.name).toBe('notes.create')
|
|
52
|
+
|
|
53
|
+
const decoded = Schema.decodeUnknownSync(createNote.input)({
|
|
54
|
+
tenantId: 'acme',
|
|
55
|
+
title: 'ok',
|
|
56
|
+
body: 'b',
|
|
57
|
+
})
|
|
58
|
+
expect(decoded).toEqual({ tenantId: 'acme', title: 'ok', body: 'b' })
|
|
59
|
+
// Empty title is rejected at the input Schema (NonEmptyString).
|
|
60
|
+
expect(() =>
|
|
61
|
+
Schema.decodeUnknownSync(createNote.input)({ tenantId: 'acme', title: '', body: '' }),
|
|
62
|
+
).toThrow()
|
|
63
|
+
|
|
64
|
+
const disabled = new FlagDisabled({ flag: 'newEditor' })
|
|
65
|
+
expect(Schema.is(FlagDisabled)(disabled)).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# AES-256-GCM key for `.encrypted()` columns (32 bytes / 64 hex chars).
|
|
2
|
+
# DEV PLACEHOLDER — generate your own (`openssl rand -hex 32`) and keep it in a
|
|
3
|
+
# real secret store for production. LOSE THIS KEY, LOSE THE ENCRYPTED DATA.
|
|
4
|
+
VOLTRO_FIELD_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-governance**) — data governance with
|
|
4
|
+
`@voltro/plugin-governance`: field encryption, GDPR, consent, retention.
|
|
5
|
+
|
|
6
|
+
## Boot
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm install
|
|
10
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
11
|
+
# → http://localhost:4000 (store: memory — zero infra)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The `.env` ships a DEV encryption key. Generate your own with
|
|
15
|
+
`openssl rand -hex 32` and keep it in a secret store for production.
|
|
16
|
+
|
|
17
|
+
## What this shows
|
|
18
|
+
|
|
19
|
+
- **Field encryption** — `profiles.ssn` is `text().encrypted()`. You pass
|
|
20
|
+
plaintext; the store middleware encrypts it (AES-256-GCM) on write and
|
|
21
|
+
decrypts on read, so handlers always see plaintext while the column holds an
|
|
22
|
+
opaque `enc:v1:…` string at rest. `governancePlugin({ fieldEncryption: true })`
|
|
23
|
+
registers the cipher from `VOLTRO_FIELD_ENCRYPTION_KEY`. Boot fails loud if an
|
|
24
|
+
`.encrypted()` column exists but no cipher is registered.
|
|
25
|
+
- **GDPR** — admin-gated `governance.export` / `governance.erase` walk the
|
|
26
|
+
declared `subjectScopes` (`[{ table: 'profiles', subjectField: 'id' }]`) to
|
|
27
|
+
bundle or erase everything belonging to a subject.
|
|
28
|
+
- **Consent ledger** — `governance.consent` records a grant; `governance.hasConsent`
|
|
29
|
+
checks it. `GovernanceService` exposes the same in-handler.
|
|
30
|
+
- **Retention** — `retention: [{ table, ttlMs, action }]` runs a periodic
|
|
31
|
+
sweep that deletes (or anonymises) rows past their TTL.
|
|
32
|
+
|
|
33
|
+
## Try it
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# create a profile, then read it back — ssn round-trips as plaintext:
|
|
37
|
+
ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
38
|
+
-d '{"tag":"profiles.create","input":{"name":"Ada","email":"ada@acme.com","ssn":"123-45-6789"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
|
|
39
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
40
|
+
-d "{\"tag\":\"profiles.get\",\"input\":{\"id\":\"$ID\"}}"
|
|
41
|
+
# → { ok:true, result:{ …, ssn:"123-45-6789" } } (plaintext — cipher decrypted it)
|
|
42
|
+
|
|
43
|
+
# record consent (a mutation):
|
|
44
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
45
|
+
-d '{"tag":"governance.consent","input":{"purpose":"marketing","granted":true}}'
|
|
46
|
+
# → { ok:true, result:{ ok:true } }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`governance.hasConsent` is a reactive QUERY (not invoke-able) — subscribe to it
|
|
50
|
+
on the web, or call `GovernanceService.hasConsent(subjectId, purpose)` in a handler.
|
|
51
|
+
|
|
52
|
+
`governance.export` / `governance.erase` are admin-gated — call them as a
|
|
53
|
+
subject with the `admin:full` scope (see [`api-rbac`](https://voltro.cloud/docs/templates/api-rbac)).
|
|
54
|
+
|
|
55
|
+
## Rules
|
|
56
|
+
|
|
57
|
+
- **Don't encrypt what you filter on.** An `.encrypted()` column is ciphertext
|
|
58
|
+
on disk — you can't `WHERE`/`ORDER BY` it in SQL. Encrypt fields you read back
|
|
59
|
+
whole (PII, tokens, notes).
|
|
60
|
+
- **The key is everything.** GCM fails closed on a bad key — you get an error,
|
|
61
|
+
never silent corruption. Back the key up; rotating it needs re-encrypting.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
import { eq } from '@voltro/database'
|
|
3
|
+
import { database } from '../database/schema'
|
|
4
|
+
|
|
5
|
+
const execute = async (input: { id: string }, ctx: AppContext) => {
|
|
6
|
+
const rows = await ctx.store.query(database.profiles.where(eq('id', input.id)).descriptor)
|
|
7
|
+
const p = rows[0]
|
|
8
|
+
if (!p) return null
|
|
9
|
+
return {
|
|
10
|
+
id: p['id'] as string,
|
|
11
|
+
name: p['name'] as string,
|
|
12
|
+
email: p['email'] as string,
|
|
13
|
+
ssn: p['ssn'] as string, // decrypted by the store middleware
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default execute
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Reads a profile back — `ssn` comes out as PLAINTEXT. The store middleware
|
|
2
|
+
// decrypts the `.encrypted()` column on read; the handler never touches the
|
|
3
|
+
// `enc:v1:…` ciphertext. (In a real app you'd gate this read behind auth.)
|
|
4
|
+
|
|
5
|
+
import { defineAction } from '@voltro/protocol'
|
|
6
|
+
import { Schema } from 'effect'
|
|
7
|
+
|
|
8
|
+
export const getProfile = defineAction({
|
|
9
|
+
name: 'profiles.get',
|
|
10
|
+
input: Schema.Struct({ id: Schema.String }),
|
|
11
|
+
output: Schema.NullOr(Schema.Struct({
|
|
12
|
+
id: Schema.String,
|
|
13
|
+
name: Schema.String,
|
|
14
|
+
email: Schema.String,
|
|
15
|
+
ssn: Schema.String,
|
|
16
|
+
})),
|
|
17
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Data-governance backend for the {{projectName}} project — field encryption,
|
|
2
|
+
// GDPR export/erase, a consent ledger, and retention sweeps in one plugin.
|
|
3
|
+
//
|
|
4
|
+
// `@voltro/plugin-governance` does four things:
|
|
5
|
+
// - fieldEncryption: registers the AES-256-GCM cipher for `.encrypted()`
|
|
6
|
+
// columns (key from the secret VOLTRO_FIELD_ENCRYPTION_KEY). Handlers see
|
|
7
|
+
// plaintext; the column stores an opaque `enc:v1:…` string at rest.
|
|
8
|
+
// - GDPR: synthesizes admin-gated `governance.export` / `governance.erase`
|
|
9
|
+
// that walk the declared `subjectScopes`.
|
|
10
|
+
// - consent: `governance.consent` / `governance.hasConsent` + the service.
|
|
11
|
+
// - retention: a periodic sweep that deletes/anonymises rows past their TTL.
|
|
12
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
13
|
+
import { governancePlugin } from '@voltro/plugin-governance'
|
|
14
|
+
|
|
15
|
+
export const env = defineEnv({
|
|
16
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
type: 'api' as const,
|
|
21
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
22
|
+
store: 'memory' as const,
|
|
23
|
+
plugins: [
|
|
24
|
+
governancePlugin({
|
|
25
|
+
// Reads the secret VOLTRO_FIELD_ENCRYPTION_KEY (see .env). LOSE THE KEY,
|
|
26
|
+
// LOSE THE DATA — GCM fails closed, it never silently corrupts.
|
|
27
|
+
fieldEncryption: true,
|
|
28
|
+
// GDPR export/erase walk these (table + the column holding the subject id).
|
|
29
|
+
subjectScopes: [{ table: 'profiles', subjectField: 'id' }],
|
|
30
|
+
// Delete profiles untouched for a year (the sweep runs periodically).
|
|
31
|
+
retention: [{ table: 'profiles', ttlMs: 365 * 86_400_000, action: 'delete' }],
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
env,
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Schema for the {{projectName}} governance backend — a `profiles` table with
|
|
2
|
+
// an ENCRYPTED column. `actors` + `tenants` are the framework core tables.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
databaseHandle,
|
|
6
|
+
id,
|
|
7
|
+
table,
|
|
8
|
+
text,
|
|
9
|
+
timestamp,
|
|
10
|
+
type InferRow,
|
|
11
|
+
} from '@voltro/database'
|
|
12
|
+
import { audit } from '@voltro/plugin-audit'
|
|
13
|
+
|
|
14
|
+
export const actors = table('actors', {
|
|
15
|
+
id: id(),
|
|
16
|
+
kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
|
|
17
|
+
displayName: text().nullable(),
|
|
18
|
+
createdAt: timestamp().default('now'),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const tenants = table('tenants', {
|
|
22
|
+
id: id(),
|
|
23
|
+
name: text(),
|
|
24
|
+
createdAt: timestamp().default('now'),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const profiles = table('profiles', {
|
|
28
|
+
id: id({ prefix: 'prof' }),
|
|
29
|
+
name: text(),
|
|
30
|
+
email: text(),
|
|
31
|
+
// .encrypted() → AES-256-GCM at rest. The handler always sees plaintext; the
|
|
32
|
+
// stored value is an opaque `enc:v1:…` string. Encrypt only what you read
|
|
33
|
+
// back WHOLE (you can't filter/sort on it in SQL — it's ciphertext on disk).
|
|
34
|
+
ssn: text().encrypted(),
|
|
35
|
+
})
|
|
36
|
+
.with(audit()) // createdAt drives the retention sweep
|
|
37
|
+
.reactive()
|
|
38
|
+
|
|
39
|
+
export type Profile = InferRow<typeof profiles>
|
|
40
|
+
|
|
41
|
+
export const database = databaseHandle({ actors, tenants, profiles })
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AppContext } from '@voltro/runtime'
|
|
2
|
+
|
|
3
|
+
const execute = async (
|
|
4
|
+
input: { name: string; email: string; ssn: string },
|
|
5
|
+
ctx: AppContext,
|
|
6
|
+
) => {
|
|
7
|
+
// ssn is encrypted on write by the store middleware — we just pass plaintext.
|
|
8
|
+
const row = await ctx.store.insert('profiles', {
|
|
9
|
+
name: input.name,
|
|
10
|
+
email: input.email,
|
|
11
|
+
ssn: input.ssn,
|
|
12
|
+
})
|
|
13
|
+
// Don't echo the ssn back here — read it via profiles.get when you need it.
|
|
14
|
+
return { id: row['id'] as string, name: row['name'] as string, email: row['email'] as string }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default execute
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Creates a profile. `ssn` is an `.encrypted()` column — you pass plaintext,
|
|
2
|
+
// the store middleware encrypts it on write (the cipher is registered by
|
|
3
|
+
// governancePlugin's fieldEncryption). Nothing special in the handler.
|
|
4
|
+
|
|
5
|
+
import { defineMutation } from '@voltro/protocol'
|
|
6
|
+
import { Schema } from 'effect'
|
|
7
|
+
|
|
8
|
+
export const createProfile = defineMutation({
|
|
9
|
+
name: 'profiles.create',
|
|
10
|
+
target: { table: 'profiles', op: 'insert' },
|
|
11
|
+
input: Schema.Struct({
|
|
12
|
+
name: Schema.NonEmptyString,
|
|
13
|
+
email: Schema.String,
|
|
14
|
+
ssn: Schema.String,
|
|
15
|
+
}),
|
|
16
|
+
output: Schema.Struct({
|
|
17
|
+
id: Schema.String,
|
|
18
|
+
name: Schema.String,
|
|
19
|
+
email: Schema.String,
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"migrate": "voltro migrate",
|
|
9
|
+
"test": "voltro test",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@effect/platform": "^0.96.1",
|
|
14
|
+
"@effect/rpc": "^0.75.1",
|
|
15
|
+
"@voltro/cli": "0.1.0",
|
|
16
|
+
"@voltro/database": "0.1.0",
|
|
17
|
+
"@voltro/env": "0.1.0",
|
|
18
|
+
"@voltro/plugin-audit": "0.1.0",
|
|
19
|
+
"@voltro/plugin-governance": "0.1.0",
|
|
20
|
+
"@voltro/protocol": "0.1.0",
|
|
21
|
+
"@voltro/runtime": "0.1.0",
|
|
22
|
+
"effect": "^3.21.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@voltro/testing": "0.1.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "api-governance",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Data governance in one plugin — @voltro/plugin-governance: AES-256-GCM field encryption for .encrypted() columns (handler sees plaintext, ciphertext at rest), admin-gated GDPR governance.export/erase over declared subjectScopes, a consent ledger, and retention TTL sweeps. Memory store, zero infra.",
|
|
5
|
+
"tags": ["api", "governance", "encryption", "gdpr", "compliance", "retention", "plugin"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Unit test for `profiles.create` — driven through `@voltro/testing`.
|
|
2
|
+
//
|
|
3
|
+
// The governance features (field encryption, retention TTL sweep, GDPR
|
|
4
|
+
// export/erase) are wired by governancePlugin at boot — store middleware +
|
|
5
|
+
// scheduled sweeps that the unit harness does NOT run. So this test targets the
|
|
6
|
+
// executor's OWN observable behavior: a plain audited insert that echoes back
|
|
7
|
+
// { id, name, email } (never the ssn). `makeTestContext` hands the handler the
|
|
8
|
+
// SAME mixin-wrapped `ctx.store` it gets in production, so the audit() mixin
|
|
9
|
+
// auto-fills createdAt/createdBy exactly as at runtime.
|
|
10
|
+
// Run with `voltro test` (vitest).
|
|
11
|
+
|
|
12
|
+
import { afterAll, beforeAll, describe, it, expect } from 'vitest'
|
|
13
|
+
import { makeFieldCipher, setFieldCipher } from '@voltro/runtime'
|
|
14
|
+
import { makeTestContext, mockStore, invoke } from '@voltro/testing'
|
|
15
|
+
import { database } from '../database/schema' // registers actors / tenants / profiles
|
|
16
|
+
import { createProfile } from '../mutations/profiles.create.mutation'
|
|
17
|
+
import createProfileHandler from '../mutations/profiles.create.mutation.server'
|
|
18
|
+
|
|
19
|
+
const subject = { type: 'user', id: 'admin_1', tenantId: 'acme' } as const
|
|
20
|
+
|
|
21
|
+
// `profiles.ssn` is an `.encrypted()` column, so the store middleware refuses to
|
|
22
|
+
// write it unless a field cipher is registered — normally `governancePlugin({
|
|
23
|
+
// fieldEncryption: true })` does that at boot from VOLTRO_FIELD_ENCRYPTION_KEY,
|
|
24
|
+
// a step the unit harness doesn't run. We register the SAME AES-256-GCM cipher
|
|
25
|
+
// process-globally here so the encrypted insert exercises the real write path
|
|
26
|
+
// (plaintext in, ciphertext at rest), then clear it so the global doesn't leak.
|
|
27
|
+
beforeAll(() => setFieldCipher(makeFieldCipher('unit-test-field-encryption-key')))
|
|
28
|
+
afterAll(() => setFieldCipher(undefined))
|
|
29
|
+
|
|
30
|
+
describe('profiles.create', () => {
|
|
31
|
+
it('inserts a profile + echoes back { id, name, email } with a prof_ TypeID', async () => {
|
|
32
|
+
const ctx = makeTestContext({ subject, store: mockStore({ profiles: [] }) })
|
|
33
|
+
|
|
34
|
+
const row = (await invoke(
|
|
35
|
+
createProfile,
|
|
36
|
+
createProfileHandler,
|
|
37
|
+
{ name: 'Ada Lovelace', email: 'ada@example.com', ssn: '123-45-6789' },
|
|
38
|
+
ctx,
|
|
39
|
+
)) as { id: string; name: string; email: string }
|
|
40
|
+
|
|
41
|
+
expect(row.name).toBe('Ada Lovelace')
|
|
42
|
+
expect(row.email).toBe('ada@example.com')
|
|
43
|
+
expect(row.id).toMatch(/^prof_/)
|
|
44
|
+
// The handler deliberately does NOT echo the ssn — only id/name/email.
|
|
45
|
+
expect(row).not.toHaveProperty('ssn')
|
|
46
|
+
|
|
47
|
+
// The row is really persisted — the live query sees it.
|
|
48
|
+
const rows = await ctx.store.query(database.profiles.descriptor)
|
|
49
|
+
expect(rows).toHaveLength(1)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('rejects an empty name at the input-schema decode (NonEmptyString)', async () => {
|
|
53
|
+
const ctx = makeTestContext({ subject, store: mockStore({ profiles: [] }) })
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
invoke(createProfile, createProfileHandler, { name: '', email: 'x@y.z', ssn: '000' }, ctx),
|
|
57
|
+
).rejects.toThrow()
|
|
58
|
+
})
|
|
59
|
+
})
|