@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,42 @@
|
|
|
1
|
+
// Rate-limited backend for the {{projectName}} project.
|
|
2
|
+
//
|
|
3
|
+
// `rateLimitPlugin` runs on the rpc interceptors — the one surface that sees
|
|
4
|
+
// every call AND the resolved subject. A `default` fallback applies to every
|
|
5
|
+
// procedure; `rules` override per endpoint (exact tag or `/regex/`), with a
|
|
6
|
+
// choice of algorithm (sliding-window / fixed-window / token-bucket+burst) and
|
|
7
|
+
// composite keying (`by: 'subject' | 'tenant' | 'apiKey' | 'global'` or an
|
|
8
|
+
// array). Over-limit calls fail a typed `RateLimited` error — the plugin merges
|
|
9
|
+
// it into every procedure's wire error union, so the client decodes it typed.
|
|
10
|
+
// `store: 'memory'` is single-node; switch to 'postgres' / 'redis' for a cluster.
|
|
11
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
12
|
+
import { rateLimitPlugin } from '@voltro/plugin-ratelimit'
|
|
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
|
+
rateLimitPlugin({
|
|
24
|
+
// Global fallback: 100 calls/min, per subject.
|
|
25
|
+
default: { limit: 100, window: '1m' },
|
|
26
|
+
rules: [
|
|
27
|
+
// Tight, per-endpoint: notes.create → 3/min per TENANT, token-bucket
|
|
28
|
+
// (the bucket starts full, so a burst of 3 is allowed, then refills).
|
|
29
|
+
{
|
|
30
|
+
match: 'notes.create',
|
|
31
|
+
limit: 3,
|
|
32
|
+
window: '1m',
|
|
33
|
+
algorithm: 'token-bucket',
|
|
34
|
+
burst: 3,
|
|
35
|
+
by: 'tenant',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
store: 'memory',
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
env,
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Schema for the {{projectName}} rate-limited backend — a tiny `notes` domain
|
|
2
|
+
// to throttle. `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,16 @@
|
|
|
1
|
+
import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
|
|
2
|
+
import type { AppContext } from '@voltro/runtime'
|
|
3
|
+
|
|
4
|
+
const execute = async (
|
|
5
|
+
input: { tenantId: string; title: string; body: string },
|
|
6
|
+
ctx: AppContext,
|
|
7
|
+
) => {
|
|
8
|
+
assertOwnTenant(input.tenantId, ctx.request.subject)
|
|
9
|
+
return ctx.store.insert('notes', {
|
|
10
|
+
title: input.title,
|
|
11
|
+
body: input.body,
|
|
12
|
+
tenantId: input.tenantId,
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default execute
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Creates a note. There's NO rate-limit code here — the limit is enforced by
|
|
2
|
+
// the plugin's rpc interceptor (see app.config.ts). When the caller is over
|
|
3
|
+
// the limit, the call fails a typed `RateLimited` error that the plugin merges
|
|
4
|
+
// into this procedure's wire error union automatically — you don't declare it.
|
|
5
|
+
// (`TenantMismatch` is the explicit error this handler can throw.)
|
|
6
|
+
|
|
7
|
+
import { defineMutation } from '@voltro/protocol'
|
|
8
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
9
|
+
import { Schema } from 'effect'
|
|
10
|
+
|
|
11
|
+
export const createNote = defineMutation({
|
|
12
|
+
name: 'notes.create',
|
|
13
|
+
target: {
|
|
14
|
+
table: 'notes',
|
|
15
|
+
op: 'insert',
|
|
16
|
+
shape: (input: { tenantId: string; title: string; body: string }) => ({
|
|
17
|
+
title: input.title,
|
|
18
|
+
body: input.body,
|
|
19
|
+
tenantId: input.tenantId,
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
input: Schema.Struct({
|
|
23
|
+
tenantId: Schema.String,
|
|
24
|
+
title: Schema.NonEmptyString,
|
|
25
|
+
body: Schema.String,
|
|
26
|
+
}),
|
|
27
|
+
output: Schema.Struct({
|
|
28
|
+
id: Schema.String,
|
|
29
|
+
title: Schema.String,
|
|
30
|
+
body: Schema.String,
|
|
31
|
+
tenantId: Schema.String,
|
|
32
|
+
}),
|
|
33
|
+
error: TenantMismatch,
|
|
34
|
+
})
|
|
@@ -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-multitenancy": "0.1.0",
|
|
19
|
+
"@voltro/plugin-ratelimit": "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-ratelimit",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Per-endpoint / per-subject / per-tenant rate limiting — @voltro/plugin-ratelimit intercepts rpc calls: a default fallback + rules (exact tag or regex), sliding-window / token-bucket algorithms, composite keying (by subject/tenant/apiKey/global), and a typed RateLimited error auto-merged into the wire error union. Memory store, zero infra.",
|
|
5
|
+
"tags": ["api", "rate-limit", "throttle", "token-bucket", "plugin"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Unit test for `notes.create` — driven through `@voltro/testing`.
|
|
2
|
+
//
|
|
3
|
+
// The rate limit itself is enforced by the plugin's rpc INTERCEPTOR (a
|
|
4
|
+
// dispatch-time middleware wired in app.config.ts), NOT by this executor — and
|
|
5
|
+
// `invoke` reproduces only the input-Schema decode, not middleware (see its
|
|
6
|
+
// header). So we test the executor's OWN observable behavior: the tenant-guarded
|
|
7
|
+
// insert. `makeTestContext` hands the handler the SAME mixin-wrapped `ctx.store`
|
|
8
|
+
// it gets in production, so tenant auto-scoping behaves as at runtime.
|
|
9
|
+
// Run with `voltro test` (vitest).
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { makeTestContext, mockStore, invoke } from '@voltro/testing'
|
|
13
|
+
import { database } from '../database/schema' // registers actors / tenants / notes
|
|
14
|
+
import { createNote } from '../mutations/notes.create.mutation'
|
|
15
|
+
import createNoteHandler from '../mutations/notes.create.mutation.server'
|
|
16
|
+
import { TenantMismatch } from '@voltro/plugin-multitenancy/guard'
|
|
17
|
+
|
|
18
|
+
const subject = { type: 'user', id: 'u1', tenantId: 'acme' } as const
|
|
19
|
+
|
|
20
|
+
describe('notes.create', () => {
|
|
21
|
+
it('inserts a note stamped with the caller tenant + a note_ TypeID', async () => {
|
|
22
|
+
const ctx = makeTestContext({ subject, store: mockStore({ notes: [] }) })
|
|
23
|
+
|
|
24
|
+
const row = (await invoke(
|
|
25
|
+
createNote,
|
|
26
|
+
createNoteHandler,
|
|
27
|
+
{ tenantId: 'acme', title: 'Ship it', body: 'World' },
|
|
28
|
+
ctx,
|
|
29
|
+
)) as { id: string; title: string; body: string; tenantId: string }
|
|
30
|
+
|
|
31
|
+
expect(row.title).toBe('Ship it')
|
|
32
|
+
expect(row.tenantId).toBe('acme')
|
|
33
|
+
expect(row.id).toMatch(/^note_/)
|
|
34
|
+
|
|
35
|
+
// The row is really in the store — the live tenant-scoped query sees it.
|
|
36
|
+
const rows = await ctx.store.query(database.notes.descriptor)
|
|
37
|
+
expect(rows).toHaveLength(1)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('rejects a cross-tenant write with a typed TenantMismatch — nothing written', async () => {
|
|
41
|
+
const ctx = makeTestContext({ subject, store: mockStore({ notes: [] }) })
|
|
42
|
+
|
|
43
|
+
await expect(
|
|
44
|
+
invoke(createNote, createNoteHandler, { tenantId: 'evil-corp', title: 'x', body: '' }, ctx),
|
|
45
|
+
).rejects.toBeInstanceOf(TenantMismatch)
|
|
46
|
+
|
|
47
|
+
const rows = await ctx.store.select('notes').unscoped().all()
|
|
48
|
+
expect(rows).toHaveLength(0)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('rejects an empty title at the input-schema decode (NonEmptyString)', async () => {
|
|
52
|
+
const ctx = makeTestContext({ subject, store: mockStore({ notes: [] }) })
|
|
53
|
+
|
|
54
|
+
await expect(
|
|
55
|
+
invoke(createNote, createNoteHandler, { tenantId: 'acme', title: '', body: '' }, ctx),
|
|
56
|
+
).rejects.toThrow()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('isolates tenants — a second tenant never sees the first tenant row', async () => {
|
|
60
|
+
const ctx = makeTestContext({
|
|
61
|
+
subject: { type: 'user', id: 'u1', tenantId: 't1' },
|
|
62
|
+
store: mockStore({ notes: [] }),
|
|
63
|
+
})
|
|
64
|
+
await invoke(createNote, createNoteHandler, { tenantId: 't1', title: 'secret', body: '' }, ctx)
|
|
65
|
+
|
|
66
|
+
const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.notes.descriptor))
|
|
67
|
+
expect(seenByT2).toHaveLength(0)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Voltro API scaffold (template: **api-rbac**) — role-based access control with
|
|
4
|
+
`@voltro/plugin-rbac`.
|
|
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
|
+
## What this shows
|
|
15
|
+
|
|
16
|
+
- **Roles → scopes, as config** — `app.config.ts` declares a role map
|
|
17
|
+
(`viewer → notes:read`, `editor → notes:read + notes:write`,
|
|
18
|
+
`admin → admin:full`). An rpc interceptor resolves each caller's roles to
|
|
19
|
+
scopes and stamps them on the subject.
|
|
20
|
+
- **Guard with `permission()`** — `notes.create` calls
|
|
21
|
+
`permission(ctx, 'notes:write')`, the Effect-native guard. It fails typed
|
|
22
|
+
`Forbidden` when the caller lacks the scope; `admin:full` is a blanket bypass.
|
|
23
|
+
Companions: `anyPermission(ctx, [...])` (OR), `assertPermission` (sync throw),
|
|
24
|
+
`can(ctx, scope)` (boolean).
|
|
25
|
+
- **Where roles come from** — the shipped `resolveRoles` is a DEMO that maps
|
|
26
|
+
the request's **tenant** to a role so you can try each with the `x-tenant`
|
|
27
|
+
header. Production reads the caller's real roles: the default resolver reads
|
|
28
|
+
`subject.metadata.roles` (set by your auth strategy), or do a DB lookup.
|
|
29
|
+
|
|
30
|
+
## Try it
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# admin tenant (acme) → admin:full → notes:write passes:
|
|
34
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' -H 'x-tenant: acme' \
|
|
35
|
+
-d '{"tag":"notes.create","input":{"title":"hi","body":"x"}}'
|
|
36
|
+
# → { ok:true, result:{ id, … } }
|
|
37
|
+
|
|
38
|
+
# readers tenant → viewer → notes:write missing → typed Forbidden:
|
|
39
|
+
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' -H 'x-tenant: readers' \
|
|
40
|
+
-d '{"tag":"notes.create","input":{"title":"hi","body":"x"}}'
|
|
41
|
+
# → { ok:false, error:{ _tag:"Forbidden", required:"notes:write" } }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## On the web side
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { useCan } from '@voltro/plugin-rbac/web'
|
|
48
|
+
const canWrite = useCan('notes:write')
|
|
49
|
+
{canWrite && <NewNoteButton />} // hide affordances the user can't use
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`useCan` is a UI affordance only — the server `permission()` guard is the real
|
|
53
|
+
enforcement. Never trust the client.
|
|
54
|
+
|
|
55
|
+
## Persisted roles
|
|
56
|
+
|
|
57
|
+
Set `tables: true` to store role assignments in `roles` / `userRoles` tables
|
|
58
|
+
and resolve them from the DB (multitenant-aware). The shipped template stays
|
|
59
|
+
config-only.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// RBAC backend for the {{projectName}} project.
|
|
2
|
+
//
|
|
3
|
+
// `rbacPlugin` compiles a role→scope map; an rpc interceptor resolves each
|
|
4
|
+
// caller's ROLES to SCOPES and stamps them on the subject, so in-handler
|
|
5
|
+
// guards like `permission(ctx, 'notes:write')` (Effect-native, fails typed
|
|
6
|
+
// `Forbidden`) just work. The `admin:full` scope is a blanket bypass.
|
|
7
|
+
//
|
|
8
|
+
// Config-only — no tables, no infra.
|
|
9
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
10
|
+
import { rbacPlugin } from '@voltro/plugin-rbac'
|
|
11
|
+
|
|
12
|
+
export const env = defineEnv({
|
|
13
|
+
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// role → the scopes it grants. Scopes are `domain:action`; `admin:full` is the
|
|
17
|
+
// framework's blanket bypass (passes every permission check).
|
|
18
|
+
const roles = {
|
|
19
|
+
viewer: ['notes:read'],
|
|
20
|
+
editor: ['notes:read', 'notes:write'],
|
|
21
|
+
admin: ['admin:full'],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
type: 'api' as const,
|
|
26
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
27
|
+
store: 'memory' as const,
|
|
28
|
+
plugins: [
|
|
29
|
+
rbacPlugin({
|
|
30
|
+
roles,
|
|
31
|
+
// DEMO resolver — assigns a role by the request's tenant so you can try
|
|
32
|
+
// each role via the `x-tenant` header with zero auth setup. PRODUCTION
|
|
33
|
+
// reads the caller's REAL roles: the default resolver reads
|
|
34
|
+
// `subject.metadata.roles` (set by your auth strategy), or do a DB
|
|
35
|
+
// lookup here (`subject.id` / `subject.tenantId` → roles). Delete this
|
|
36
|
+
// whole `resolveRoles` to fall back to the metadata default.
|
|
37
|
+
resolveRoles: (subject) => {
|
|
38
|
+
const demo: Record<string, ReadonlyArray<string>> = {
|
|
39
|
+
acme: ['admin'],
|
|
40
|
+
editors: ['editor'],
|
|
41
|
+
readers: ['viewer'],
|
|
42
|
+
}
|
|
43
|
+
return demo[subject.tenantId ?? ''] ?? ['viewer']
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
env,
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Schema for the {{projectName}} RBAC backend — a tiny `notes` domain to
|
|
2
|
+
// guard. `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,22 @@
|
|
|
1
|
+
import { EffectStore } from '@voltro/runtime'
|
|
2
|
+
import type { AppContext } from '@voltro/runtime'
|
|
3
|
+
import { permission } from '@voltro/plugin-rbac'
|
|
4
|
+
import { Effect } from 'effect'
|
|
5
|
+
|
|
6
|
+
const execute = (input: { title: string; body: string }, ctx: AppContext) =>
|
|
7
|
+
Effect.gen(function* () {
|
|
8
|
+
// RBAC guard — passes for editor/admin, fails typed Forbidden for viewer.
|
|
9
|
+
yield* permission(ctx, 'notes:write')
|
|
10
|
+
|
|
11
|
+
const store = yield* EffectStore
|
|
12
|
+
// tenant() auto-fills tenantId from the subject — no need to pass it.
|
|
13
|
+
const row = yield* store.insert('notes', { title: input.title, body: input.body })
|
|
14
|
+
return {
|
|
15
|
+
id: row['id'] as string,
|
|
16
|
+
title: row['title'] as string,
|
|
17
|
+
body: row['body'] as string,
|
|
18
|
+
tenantId: row['tenantId'] as string,
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export default execute
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Creates a note — requires the `notes:write` scope. The executor guards with
|
|
2
|
+
// `permission(ctx, 'notes:write')`, the Effect-native RBAC guard that fails
|
|
3
|
+
// typed `Forbidden` when the caller's resolved scopes don't include it.
|
|
4
|
+
// Declaring `error: Forbidden` surfaces that to the client typed.
|
|
5
|
+
//
|
|
6
|
+
// `Forbidden` comes from the browser-safe `/errors` subpath.
|
|
7
|
+
|
|
8
|
+
import { defineMutation } from '@voltro/protocol'
|
|
9
|
+
import { Forbidden } from '@voltro/plugin-rbac/errors'
|
|
10
|
+
import { Schema } from 'effect'
|
|
11
|
+
|
|
12
|
+
export const createNote = defineMutation({
|
|
13
|
+
name: 'notes.create',
|
|
14
|
+
target: { table: 'notes', op: 'insert' },
|
|
15
|
+
input: Schema.Struct({
|
|
16
|
+
title: Schema.NonEmptyString,
|
|
17
|
+
body: Schema.String,
|
|
18
|
+
}),
|
|
19
|
+
output: Schema.Struct({
|
|
20
|
+
id: Schema.String,
|
|
21
|
+
title: Schema.String,
|
|
22
|
+
body: Schema.String,
|
|
23
|
+
tenantId: Schema.String,
|
|
24
|
+
}),
|
|
25
|
+
error: Forbidden,
|
|
26
|
+
})
|
|
@@ -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-multitenancy": "0.1.0",
|
|
19
|
+
"@voltro/plugin-rbac": "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-rbac",
|
|
3
|
+
"kind": "api",
|
|
4
|
+
"summary": "Role-based access control — @voltro/plugin-rbac compiles a role→scope map and an interceptor resolves the caller's roles to scopes; guard handlers with permission(ctx, 'notes:write') (Effect-native, fails typed Forbidden). admin:full bypasses. Memory/config-only, zero infra; useCan() for web UI affordance.",
|
|
5
|
+
"tags": ["api", "rbac", "authz", "roles", "permissions", "plugin"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Unit test for `notes.create` — driven through `@voltro/testing`.
|
|
2
|
+
//
|
|
3
|
+
// This executor is Effect-native: it `yield*`s the RBAC guard
|
|
4
|
+
// `permission(ctx, 'notes:write')` (fails typed `Forbidden` for a caller
|
|
5
|
+
// lacking the scope) and then `yield* EffectStore` to insert. `makeTestContext`
|
|
6
|
+
// exposes the mixin-wrapped store as `ctx.store`; we provide it to the Effect as
|
|
7
|
+
// the `EffectStore` layer via `makeEffectStoreLayer` — the SAME wiring the CLI
|
|
8
|
+
// dev/serve dispatcher does — so the handler runs exactly as at runtime.
|
|
9
|
+
//
|
|
10
|
+
// In production the rbac INTERCEPTOR resolves the caller's roles → scopes and
|
|
11
|
+
// stashes them on the subject before the handler runs. That interceptor is a
|
|
12
|
+
// dispatch-time concern the unit harness doesn't run, so we drive the handler's
|
|
13
|
+
// OWN guard directly by putting the resolved scope on `subject.scopes` — exactly
|
|
14
|
+
// what the interceptor would have stashed. Run with `voltro test` (vitest).
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
import { Cause, Effect, Exit, Option, Schema } from 'effect'
|
|
18
|
+
import { makeTestContext, mockStore } from '@voltro/testing'
|
|
19
|
+
import { makeEffectStoreLayer } from '@voltro/runtime'
|
|
20
|
+
import { database } from '../database/schema' // registers actors / tenants / notes
|
|
21
|
+
import { createNote } from '../mutations/notes.create.mutation'
|
|
22
|
+
import createNoteHandler from '../mutations/notes.create.mutation.server'
|
|
23
|
+
import { Forbidden } from '@voltro/plugin-rbac/errors'
|
|
24
|
+
|
|
25
|
+
// Run the Effect-native executor with the test store provided as EffectStore.
|
|
26
|
+
const runExec = (input: { title: string; body: string }, ctx: ReturnType<typeof makeTestContext>) =>
|
|
27
|
+
Effect.runPromise(createNoteHandler(input, ctx).pipe(Effect.provide(makeEffectStoreLayer(ctx.store))))
|
|
28
|
+
|
|
29
|
+
// Same, but returns the raw Exit so a typed failure can be inspected WITHOUT
|
|
30
|
+
// the `FiberFailure` wrapper `runPromise` throws (which would defeat a plain
|
|
31
|
+
// `instanceof Forbidden` assertion). Unwrap the Cause to get the real error.
|
|
32
|
+
const runExit = (input: { title: string; body: string }, ctx: ReturnType<typeof makeTestContext>) =>
|
|
33
|
+
Effect.runPromiseExit(createNoteHandler(input, ctx).pipe(Effect.provide(makeEffectStoreLayer(ctx.store))))
|
|
34
|
+
|
|
35
|
+
describe('notes.create', () => {
|
|
36
|
+
it('inserts for a caller holding notes:write — tenant-stamped, note_ TypeID', async () => {
|
|
37
|
+
const ctx = makeTestContext({
|
|
38
|
+
subject: { type: 'user', id: 'editor_1', tenantId: 'acme', scopes: ['notes:write'] },
|
|
39
|
+
store: mockStore({ notes: [] }),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const row = await runExec({ title: 'Ship it', body: 'World' }, ctx)
|
|
43
|
+
|
|
44
|
+
expect(row.title).toBe('Ship it')
|
|
45
|
+
expect(row.tenantId).toBe('acme') // tenant() auto-filled from the subject
|
|
46
|
+
expect(row.id).toMatch(/^note_/)
|
|
47
|
+
|
|
48
|
+
// The row is really in the store — the live tenant-scoped query sees it.
|
|
49
|
+
const rows = await ctx.store.query(database.notes.descriptor)
|
|
50
|
+
expect(rows).toHaveLength(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('rejects a caller WITHOUT notes:write with typed Forbidden — nothing written', async () => {
|
|
54
|
+
const ctx = makeTestContext({
|
|
55
|
+
subject: { type: 'user', id: 'viewer_1', tenantId: 'acme', scopes: [] },
|
|
56
|
+
store: mockStore({ notes: [] }),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const exit = await runExit({ title: 'x', body: '' }, ctx)
|
|
60
|
+
|
|
61
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
62
|
+
const failure = Exit.isFailure(exit) ? Cause.failureOption(exit.cause) : Option.none()
|
|
63
|
+
expect(Option.getOrThrow(failure)).toBeInstanceOf(Forbidden)
|
|
64
|
+
|
|
65
|
+
const rows = await ctx.store.query(database.notes.descriptor)
|
|
66
|
+
expect(rows).toHaveLength(0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('pins the descriptor: name, input decode + the declared Forbidden error', () => {
|
|
70
|
+
expect(createNote.name).toBe('notes.create')
|
|
71
|
+
|
|
72
|
+
const decoded = Schema.decodeUnknownSync(createNote.input)({ title: 'ok', body: 'b' })
|
|
73
|
+
expect(decoded).toEqual({ title: 'ok', body: 'b' })
|
|
74
|
+
// Empty title is rejected at the input Schema (NonEmptyString).
|
|
75
|
+
expect(() => Schema.decodeUnknownSync(createNote.input)({ title: '', body: '' })).toThrow()
|
|
76
|
+
|
|
77
|
+
const forbidden = new Forbidden({ required: 'notes:write' })
|
|
78
|
+
expect(Schema.is(Forbidden)(forbidden)).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — public REST API
|
|
2
|
+
|
|
3
|
+
A turnkey **public REST API** built on `defineRestRoute`, with an
|
|
4
|
+
auto-generated **OpenAPI 3.1 spec + Swagger UI**. Unlike the reactive
|
|
5
|
+
rpc/WebSocket surface (`api-backend`), these are plain raw-HTTP endpoints a
|
|
6
|
+
third party calls with a URL + JSON.
|
|
7
|
+
|
|
8
|
+
## What it demonstrates
|
|
9
|
+
|
|
10
|
+
- **`defineRestRoute`** — four endpoints over a `products` catalog:
|
|
11
|
+
- `GET /v1/products?limit=&cursor=` — public, cursor-paginated list
|
|
12
|
+
- `GET /v1/products/:id` — public, one product or `404`
|
|
13
|
+
- `POST /v1/products` — **scope-guarded** + **idempotent** create
|
|
14
|
+
- `DELETE /v1/products/:id` — **scope-guarded** delete
|
|
15
|
+
- **Query string + path params + JSON body** parsing → one decoded `input`.
|
|
16
|
+
- **Scope guards** — `requireScope('products:write')` returns `403` unless the
|
|
17
|
+
caller's API key carries the scope.
|
|
18
|
+
- **`Idempotency-Key`** — a retried `POST` replays the first response instead
|
|
19
|
+
of inserting twice (`app.config.ts` `idempotency: true`).
|
|
20
|
+
- **`@voltro/plugin-openapi`** — `GET /openapi.json` (the spec) + `GET /docs`
|
|
21
|
+
(Swagger UI), generated from the route descriptors. No hand-written docs.
|
|
22
|
+
|
|
23
|
+
REST routes are the ONE primitive that is **not auto-discovered** — they're
|
|
24
|
+
registered explicitly via `restRoutes` in `app.config.ts`.
|
|
25
|
+
|
|
26
|
+
## Run it
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
voltro dev .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Boots on `http://localhost:4000`. Then:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# The spec + interactive docs
|
|
36
|
+
open http://localhost:4000/docs
|
|
37
|
+
curl -s http://localhost:4000/openapi.json | jq '.info, (.paths | keys)'
|
|
38
|
+
|
|
39
|
+
# Public reads — no auth
|
|
40
|
+
curl -s http://localhost:4000/v1/products # → { "data": [], "nextCursor": null }
|
|
41
|
+
curl -s http://localhost:4000/v1/products/prod_xyz # → 404
|
|
42
|
+
|
|
43
|
+
# Guarded create — the demo API key carries products:write
|
|
44
|
+
curl -s -X POST http://localhost:4000/v1/products \
|
|
45
|
+
-H 'Authorization: Bearer restdemo_devkey' \
|
|
46
|
+
-H 'Content-Type: application/json' \
|
|
47
|
+
-d '{"name":"Widget","priceCents":1999}' # → 201 the created product
|
|
48
|
+
|
|
49
|
+
# Same call WITHOUT the key → 403 (the guard rejects)
|
|
50
|
+
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:4000/v1/products \
|
|
51
|
+
-H 'Content-Type: application/json' -d '{"name":"x","priceCents":1}' # → 403
|
|
52
|
+
|
|
53
|
+
# Idempotency — repeat with the same key header → the SAME response replayed
|
|
54
|
+
curl -s -X POST http://localhost:4000/v1/products \
|
|
55
|
+
-H 'Authorization: Bearer restdemo_devkey' \
|
|
56
|
+
-H 'Idempotency-Key: order-42' \
|
|
57
|
+
-H 'Content-Type: application/json' \
|
|
58
|
+
-d '{"name":"Once","priceCents":500}'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Auth — the demo key is DEV-ONLY
|
|
62
|
+
|
|
63
|
+
`app.config.ts` ships ONE hardcoded API key (`restdemo_devkey` → scopes
|
|
64
|
+
`products:read` + `products:write`) so the guarded routes work on first boot.
|
|
65
|
+
The `apiKeyStrategy` SHA-256-hashes the bearer token and matches the hash to a
|
|
66
|
+
fixed Subject.
|
|
67
|
+
|
|
68
|
+
**Before you deploy**, delete the demo constant and do one of:
|
|
69
|
+
|
|
70
|
+
- look the token hash up against your own key store (store key **hashes**,
|
|
71
|
+
never raw tokens), or
|
|
72
|
+
- flip on the framework's first-class API keys — set `apiKeys: true` in
|
|
73
|
+
`app.config.ts` for admin-gated issue/list/revoke against `_voltro_api_keys`,
|
|
74
|
+
with hash-only storage built in.
|
|
75
|
+
|
|
76
|
+
## Make it real
|
|
77
|
+
|
|
78
|
+
- `store: 'memory'` → `store: 'postgres'` (data survives restarts).
|
|
79
|
+
- Replace `products` in `database/schema.ts` with your domain; add `tenant()`
|
|
80
|
+
if the API is multi-tenant (then scope each handler by the key's `tenantId`).
|
|
81
|
+
- Add routes — drop another `*.route.tsx`, `export default defineRestRoute(…)`,
|
|
82
|
+
and add it to the `routes` array in `app.config.ts` (it joins both the mount
|
|
83
|
+
and the OpenAPI spec automatically).
|
|
84
|
+
- Version/deprecate — a descriptor's `deprecated` sets a `Deprecation: true`
|
|
85
|
+
header; `sunset` sets `Sunset:` and `410`s past the date.
|