vibecarbon 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/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- package/src/upgrade.js +721 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Theme Settings Migration
|
|
2
|
+
-- Adds the app_theme key to app_settings and allows public read access.
|
|
3
|
+
|
|
4
|
+
-- Insert default theme (empty object = use CSS defaults)
|
|
5
|
+
INSERT INTO app_settings (key, value)
|
|
6
|
+
VALUES ('app_theme', '{}'::jsonb)
|
|
7
|
+
ON CONFLICT (key) DO NOTHING;
|
|
8
|
+
|
|
9
|
+
-- Allow public read so all users get the custom theme on page load
|
|
10
|
+
DROP POLICY IF EXISTS "Public can read whitelisted settings" ON app_settings;
|
|
11
|
+
CREATE POLICY "Public can read whitelisted settings"
|
|
12
|
+
ON app_settings FOR SELECT
|
|
13
|
+
USING (key IN ('mfa_enabled', 'app_theme'));
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- pg_cron: Background job scheduling via PostgreSQL
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
|
|
5
|
+
-- Enable the pg_cron extension (available in Supabase PostgreSQL)
|
|
6
|
+
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
|
7
|
+
|
|
8
|
+
-- Grant usage to postgres role (required for Supabase)
|
|
9
|
+
GRANT USAGE ON SCHEMA cron TO postgres;
|
|
10
|
+
|
|
11
|
+
-- =============================================================================
|
|
12
|
+
-- Job execution history table (readable by super admins via API)
|
|
13
|
+
-- =============================================================================
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS public.cron_job_history (
|
|
16
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
17
|
+
job_name TEXT NOT NULL,
|
|
18
|
+
status TEXT NOT NULL DEFAULT 'running', -- running, succeeded, failed
|
|
19
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
20
|
+
finished_at TIMESTAMPTZ,
|
|
21
|
+
duration_ms INTEGER,
|
|
22
|
+
result TEXT,
|
|
23
|
+
error TEXT,
|
|
24
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_cron_job_history_job_name ON public.cron_job_history (job_name);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_cron_job_history_started_at ON public.cron_job_history (started_at DESC);
|
|
29
|
+
|
|
30
|
+
ALTER TABLE public.cron_job_history ENABLE ROW LEVEL SECURITY;
|
|
31
|
+
|
|
32
|
+
-- Only super admins can read job history
|
|
33
|
+
DROP POLICY IF EXISTS "Super admins can read job history" ON public.cron_job_history;
|
|
34
|
+
CREATE POLICY "Super admins can read job history"
|
|
35
|
+
ON public.cron_job_history FOR SELECT
|
|
36
|
+
USING (public.is_super_admin());
|
|
37
|
+
|
|
38
|
+
-- =============================================================================
|
|
39
|
+
-- Helper function to log job execution
|
|
40
|
+
-- =============================================================================
|
|
41
|
+
|
|
42
|
+
CREATE OR REPLACE FUNCTION public.log_cron_job(
|
|
43
|
+
p_job_name TEXT,
|
|
44
|
+
p_status TEXT DEFAULT 'succeeded',
|
|
45
|
+
p_result TEXT DEFAULT NULL,
|
|
46
|
+
p_error TEXT DEFAULT NULL
|
|
47
|
+
)
|
|
48
|
+
RETURNS void
|
|
49
|
+
LANGUAGE plpgsql
|
|
50
|
+
SECURITY DEFINER
|
|
51
|
+
AS $$
|
|
52
|
+
BEGIN
|
|
53
|
+
INSERT INTO public.cron_job_history (job_name, status, finished_at, result, error)
|
|
54
|
+
VALUES (p_job_name, p_status, now(), p_result, p_error);
|
|
55
|
+
END;
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
-- =============================================================================
|
|
59
|
+
-- Schedule default jobs
|
|
60
|
+
-- =============================================================================
|
|
61
|
+
|
|
62
|
+
-- 1. Clean up old failed login attempts (every 6 hours)
|
|
63
|
+
-- Replaces the Node.js setInterval in server/index.ts
|
|
64
|
+
SELECT cron.schedule(
|
|
65
|
+
'cleanup-login-attempts',
|
|
66
|
+
'0 */6 * * *', -- every 6 hours
|
|
67
|
+
$$
|
|
68
|
+
DO $body$
|
|
69
|
+
DECLARE
|
|
70
|
+
deleted_count INTEGER;
|
|
71
|
+
BEGIN
|
|
72
|
+
DELETE FROM public.failed_login_attempts
|
|
73
|
+
WHERE created_at < now() - interval '24 hours';
|
|
74
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
75
|
+
PERFORM public.log_cron_job('cleanup-login-attempts', 'succeeded',
|
|
76
|
+
'Deleted ' || deleted_count || ' old login attempts');
|
|
77
|
+
EXCEPTION WHEN OTHERS THEN
|
|
78
|
+
PERFORM public.log_cron_job('cleanup-login-attempts', 'failed', NULL, SQLERRM);
|
|
79
|
+
END $body$;
|
|
80
|
+
$$
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
-- 2. Clean up expired notifications (daily at 3am)
|
|
84
|
+
SELECT cron.schedule(
|
|
85
|
+
'cleanup-expired-notifications',
|
|
86
|
+
'0 3 * * *', -- daily at 3:00 AM
|
|
87
|
+
$$
|
|
88
|
+
DO $body$
|
|
89
|
+
DECLARE
|
|
90
|
+
deleted_count INTEGER;
|
|
91
|
+
BEGIN
|
|
92
|
+
DELETE FROM public.notifications
|
|
93
|
+
WHERE expires_at IS NOT NULL AND expires_at < now();
|
|
94
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
95
|
+
PERFORM public.log_cron_job('cleanup-expired-notifications', 'succeeded',
|
|
96
|
+
'Deleted ' || deleted_count || ' expired notifications');
|
|
97
|
+
EXCEPTION WHEN OTHERS THEN
|
|
98
|
+
PERFORM public.log_cron_job('cleanup-expired-notifications', 'failed', NULL, SQLERRM);
|
|
99
|
+
END $body$;
|
|
100
|
+
$$
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
-- 3. Clean up old cron job history (weekly, keep last 30 days)
|
|
104
|
+
SELECT cron.schedule(
|
|
105
|
+
'cleanup-job-history',
|
|
106
|
+
'0 4 * * 0', -- Sundays at 4:00 AM
|
|
107
|
+
$$
|
|
108
|
+
DO $body$
|
|
109
|
+
DECLARE
|
|
110
|
+
deleted_count INTEGER;
|
|
111
|
+
BEGIN
|
|
112
|
+
DELETE FROM public.cron_job_history
|
|
113
|
+
WHERE created_at < now() - interval '30 days';
|
|
114
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
115
|
+
PERFORM public.log_cron_job('cleanup-job-history', 'succeeded',
|
|
116
|
+
'Deleted ' || deleted_count || ' old history entries');
|
|
117
|
+
EXCEPTION WHEN OTHERS THEN
|
|
118
|
+
PERFORM public.log_cron_job('cleanup-job-history', 'failed', NULL, SQLERRM);
|
|
119
|
+
END $body$;
|
|
120
|
+
$$
|
|
121
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Contact form submissions + Newsletter subscribers
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
|
|
5
|
+
-- ========== CONTACT SUBMISSIONS ==========
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS public.contact_submissions (
|
|
8
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
email TEXT NOT NULL,
|
|
11
|
+
subject TEXT NOT NULL,
|
|
12
|
+
message TEXT NOT NULL,
|
|
13
|
+
status TEXT NOT NULL DEFAULT 'unread', -- unread, read, replied, archived
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
15
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_contact_submissions_status ON public.contact_submissions (status);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_contact_submissions_created_at ON public.contact_submissions (created_at DESC);
|
|
20
|
+
|
|
21
|
+
ALTER TABLE public.contact_submissions ENABLE ROW LEVEL SECURITY;
|
|
22
|
+
|
|
23
|
+
-- Only super admins can read/manage contact submissions
|
|
24
|
+
DROP POLICY IF EXISTS "Super admins can manage contact submissions" ON public.contact_submissions;
|
|
25
|
+
CREATE POLICY "Super admins can manage contact submissions"
|
|
26
|
+
ON public.contact_submissions FOR ALL
|
|
27
|
+
USING (public.is_super_admin());
|
|
28
|
+
|
|
29
|
+
-- Allow anonymous inserts (public contact form)
|
|
30
|
+
DROP POLICY IF EXISTS "Anyone can submit contact form" ON public.contact_submissions;
|
|
31
|
+
CREATE POLICY "Anyone can submit contact form"
|
|
32
|
+
ON public.contact_submissions FOR INSERT
|
|
33
|
+
WITH CHECK (true);
|
|
34
|
+
|
|
35
|
+
DROP TRIGGER IF EXISTS update_contact_submissions_updated_at ON public.contact_submissions;
|
|
36
|
+
CREATE TRIGGER update_contact_submissions_updated_at
|
|
37
|
+
BEFORE UPDATE ON public.contact_submissions
|
|
38
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
39
|
+
|
|
40
|
+
-- ========== NEWSLETTER SUBSCRIBERS ==========
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS public.newsletter_subscribers (
|
|
43
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
44
|
+
email TEXT NOT NULL UNIQUE,
|
|
45
|
+
name TEXT,
|
|
46
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending, active, unsubscribed
|
|
47
|
+
confirmation_token UUID DEFAULT gen_random_uuid(),
|
|
48
|
+
subscribed_at TIMESTAMPTZ,
|
|
49
|
+
unsubscribed_at TIMESTAMPTZ,
|
|
50
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
51
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_status ON public.newsletter_subscribers (status);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_email ON public.newsletter_subscribers (email);
|
|
56
|
+
|
|
57
|
+
ALTER TABLE public.newsletter_subscribers ENABLE ROW LEVEL SECURITY;
|
|
58
|
+
|
|
59
|
+
-- Only super admins can read/manage subscribers
|
|
60
|
+
DROP POLICY IF EXISTS "Super admins can manage newsletter subscribers" ON public.newsletter_subscribers;
|
|
61
|
+
CREATE POLICY "Super admins can manage newsletter subscribers"
|
|
62
|
+
ON public.newsletter_subscribers FOR ALL
|
|
63
|
+
USING (public.is_super_admin());
|
|
64
|
+
|
|
65
|
+
-- Allow anonymous inserts (public signup form)
|
|
66
|
+
DROP POLICY IF EXISTS "Anyone can subscribe to newsletter" ON public.newsletter_subscribers;
|
|
67
|
+
CREATE POLICY "Anyone can subscribe to newsletter"
|
|
68
|
+
ON public.newsletter_subscribers FOR INSERT
|
|
69
|
+
WITH CHECK (true);
|
|
70
|
+
|
|
71
|
+
-- Allow anonymous updates for confirmation/unsubscribe (by token or email)
|
|
72
|
+
DROP POLICY IF EXISTS "Anyone can confirm or unsubscribe" ON public.newsletter_subscribers;
|
|
73
|
+
CREATE POLICY "Anyone can confirm or unsubscribe"
|
|
74
|
+
ON public.newsletter_subscribers FOR UPDATE
|
|
75
|
+
USING (true)
|
|
76
|
+
WITH CHECK (true);
|
|
77
|
+
|
|
78
|
+
DROP TRIGGER IF EXISTS update_newsletter_subscribers_updated_at ON public.newsletter_subscribers;
|
|
79
|
+
CREATE TRIGGER update_newsletter_subscribers_updated_at
|
|
80
|
+
BEFORE UPDATE ON public.newsletter_subscribers
|
|
81
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Localization: enabled languages
|
|
2
|
+
--
|
|
3
|
+
-- Promotes the language list from a build-time env var
|
|
4
|
+
-- (VITE_ENABLED_LANGUAGES) to a runtime DB-backed setting so admins can
|
|
5
|
+
-- swap languages from the admin Settings UI without redeploying.
|
|
6
|
+
--
|
|
7
|
+
-- The on/off master switch (`localization_enabled`) was added earlier.
|
|
8
|
+
-- This adds the companion list of which languages the switcher should
|
|
9
|
+
-- offer when it's on. With <2 codes the switcher auto-hides at the
|
|
10
|
+
-- component level (LanguageSwitcher.tsx), so a single-language project
|
|
11
|
+
-- has no visible switcher even with localization_enabled=true.
|
|
12
|
+
|
|
13
|
+
INSERT INTO app_settings (key, value)
|
|
14
|
+
VALUES ('enabled_languages', '{"codes": ["en"]}'::jsonb)
|
|
15
|
+
ON CONFLICT (key) DO NOTHING;
|
|
16
|
+
|
|
17
|
+
-- Public read so unauthenticated nav can render the right switcher
|
|
18
|
+
-- options before login (mirrors mfa_enabled / app_theme / localization_enabled).
|
|
19
|
+
DROP POLICY IF EXISTS "Public can read whitelisted settings" ON app_settings;
|
|
20
|
+
CREATE POLICY "Public can read whitelisted settings"
|
|
21
|
+
ON app_settings FOR SELECT
|
|
22
|
+
USING (key IN ('mfa_enabled', 'app_theme', 'localization_enabled', 'enabled_languages'));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Seed data for development
|
|
2
|
+
-- This file is optional and can be run to populate initial test data
|
|
3
|
+
|
|
4
|
+
-- Note: In a real application, organizations and memberships should be
|
|
5
|
+
-- created through the application after users sign up.
|
|
6
|
+
|
|
7
|
+
-- Example: Create a demo organization (run this manually after first user signup)
|
|
8
|
+
-- INSERT INTO organizations (name, slug, plan)
|
|
9
|
+
-- VALUES ('Demo Organization', 'demo-org', 'FREE');
|
|
10
|
+
|
|
11
|
+
-- Then add the first user as owner:
|
|
12
|
+
-- INSERT INTO memberships (user_id, organization_id, role)
|
|
13
|
+
-- SELECT
|
|
14
|
+
-- auth.uid(),
|
|
15
|
+
-- (SELECT id FROM organizations WHERE slug = 'demo-org'),
|
|
16
|
+
-- 'OWNER';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { HonoVariables } from '../../src/server/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mount a route module onto a fresh Hono instance at a given prefix.
|
|
6
|
+
*
|
|
7
|
+
* Integration tests get a clean app per test — no global middleware
|
|
8
|
+
* (CORS, rate-limit, security-headers) so assertions stay focused on the
|
|
9
|
+
* route under test. If you need those, mount them on the returned app
|
|
10
|
+
* before issuing requests.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* import { healthRoutes } from '@server/routes/health';
|
|
14
|
+
* const app = mountRoute('/api/health', healthRoutes);
|
|
15
|
+
* const res = await app.request('/api/health/');
|
|
16
|
+
* expect(res.status).toBe(200);
|
|
17
|
+
*/
|
|
18
|
+
export function mountRoute(prefix: string, routes: Hono<{ Variables: HonoVariables }>): Hono {
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.route(prefix, routes);
|
|
21
|
+
return app;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a JSON POST Request for app.request(...). Saves the boilerplate of
|
|
26
|
+
* setting `method`, `headers`, and JSON-encoding the body in every test.
|
|
27
|
+
*
|
|
28
|
+
* Example:
|
|
29
|
+
* const res = await app.request('/api/v1/contact/submit', jsonPost({
|
|
30
|
+
* name: 'Ada', email: 'ada@example.com', subject: 'Hi', message: 'Hello there.',
|
|
31
|
+
* }));
|
|
32
|
+
*/
|
|
33
|
+
export function jsonPost(body: unknown, init: RequestInit = {}): RequestInit {
|
|
34
|
+
return {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'content-type': 'application/json',
|
|
38
|
+
...(init.headers ?? {}),
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
...init,
|
|
42
|
+
// Re-apply method/headers/body after the spread so callers can't
|
|
43
|
+
// accidentally clobber them with an empty init.
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporarily set environment variables for a test, returning a restore
|
|
3
|
+
* function. Closure-based — no globals, safe to call from concurrent tests
|
|
4
|
+
* as long as the variables don't overlap.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
* it('falls back to default SITE_URL', () => {
|
|
8
|
+
* const restore = mockEnv({ SITE_URL: undefined });
|
|
9
|
+
* try {
|
|
10
|
+
* // ... assertion that exercises the fallback path
|
|
11
|
+
* } finally {
|
|
12
|
+
* restore();
|
|
13
|
+
* }
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
export function mockEnv(vars: Record<string, string | undefined>): () => void {
|
|
17
|
+
const original: Record<string, string | undefined> = {};
|
|
18
|
+
|
|
19
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
20
|
+
original[key] = process.env[key];
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
delete process.env[key];
|
|
23
|
+
} else {
|
|
24
|
+
process.env[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
for (const [key, value] of Object.entries(original)) {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
delete process.env[key];
|
|
32
|
+
} else {
|
|
33
|
+
process.env[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight factories that produce plausible domain objects for tests.
|
|
5
|
+
*
|
|
6
|
+
* These don't touch the DB — they return plain TS objects you can hand to
|
|
7
|
+
* a mocked Supabase client's resolved value (e.g. `mockResolvedValue({
|
|
8
|
+
* data: makeUser(), error: null })`).
|
|
9
|
+
*
|
|
10
|
+
* Add new factories here as you write tests; the test-maintainer agent
|
|
11
|
+
* follows this pattern when extending coverage.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function rid(prefix: string): string {
|
|
15
|
+
return `${prefix}-${randomBytes(4).toString('hex')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TestUser {
|
|
19
|
+
id: string;
|
|
20
|
+
email: string;
|
|
21
|
+
user_metadata: { full_name?: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makeUser(overrides: Partial<TestUser> = {}): TestUser {
|
|
25
|
+
return {
|
|
26
|
+
id: rid('user'),
|
|
27
|
+
email: `${rid('user')}@example.test`,
|
|
28
|
+
user_metadata: { full_name: 'Test User' },
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TestOrg {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
slug: string;
|
|
37
|
+
owner_id: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function makeOrg(overrides: Partial<TestOrg> = {}): TestOrg {
|
|
41
|
+
const id = rid('org');
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
name: 'Test Org',
|
|
45
|
+
slug: id,
|
|
46
|
+
owner_id: rid('user'),
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ContactSubmission {
|
|
52
|
+
name: string;
|
|
53
|
+
email: string;
|
|
54
|
+
subject: string;
|
|
55
|
+
message: string;
|
|
56
|
+
website?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function makeContactSubmission(
|
|
60
|
+
overrides: Partial<ContactSubmission> = {},
|
|
61
|
+
): ContactSubmission {
|
|
62
|
+
return {
|
|
63
|
+
name: 'Ada Lovelace',
|
|
64
|
+
email: 'ada@example.test',
|
|
65
|
+
subject: 'Hello',
|
|
66
|
+
message: 'Just dropping a line — this is a test message.',
|
|
67
|
+
...overrides,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build an HS256-signed JWT for tests.
|
|
5
|
+
*
|
|
6
|
+
* The default secret matches what the auth middleware's mock paths expect
|
|
7
|
+
* — pass an explicit `secret` if you're testing the real JWT verifier.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* const token = mockJwt({ sub: 'user-123', role: 'authenticated' });
|
|
11
|
+
* const res = await app.request('/api/v1/me', {
|
|
12
|
+
* headers: { authorization: `Bearer ${token}` },
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export function mockJwt(payload: Record<string, unknown>, secret = 'test-secret'): string {
|
|
16
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
17
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
18
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
19
|
+
const signature = createHmac('sha256', secret)
|
|
20
|
+
.update(`${encodedHeader}.${encodedPayload}`)
|
|
21
|
+
.digest('base64url');
|
|
22
|
+
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Vitest setupFile for the `integration` project.
|
|
2
|
+
//
|
|
3
|
+
// The server modules (env, supabase, email) validate required env vars at
|
|
4
|
+
// import time via zod. Tests don't connect to real Supabase/SMTP — but the
|
|
5
|
+
// schemas still need plausible values to pass `.url()` / `.min(1)` checks
|
|
6
|
+
// before `vi.mock()` swaps in the test doubles.
|
|
7
|
+
//
|
|
8
|
+
// Set them once here so individual tests stay focused on behavior.
|
|
9
|
+
const defaults: Record<string, string> = {
|
|
10
|
+
SUPABASE_URL: 'http://supabase.test',
|
|
11
|
+
SUPABASE_ANON_KEY: 'test-anon-key',
|
|
12
|
+
SUPABASE_SERVICE_ROLE_KEY: 'test-service-role-key',
|
|
13
|
+
NODE_ENV: 'test',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
17
|
+
if (process.env[key] === undefined) {
|
|
18
|
+
process.env[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Vitest setupFile for the `component` project.
|
|
2
|
+
//
|
|
3
|
+
// 1. Loads @testing-library/jest-dom so matchers like .toBeInTheDocument()
|
|
4
|
+
// are typed and available.
|
|
5
|
+
// 2. Runs RTL's cleanup() after each test so DOM state doesn't leak.
|
|
6
|
+
import '@testing-library/jest-dom/vitest';
|
|
7
|
+
import { cleanup } from '@testing-library/react';
|
|
8
|
+
import { afterEach } from 'vitest';
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup();
|
|
12
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|
4
|
+
|
|
5
|
+
// React's default rendering logs the caught error to console.error in dev,
|
|
6
|
+
// which clutters test output but isn't an assertion failure. Silence it for
|
|
7
|
+
// the error path only.
|
|
8
|
+
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
errorSpy.mockRestore();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function Boom(): never {
|
|
17
|
+
throw new Error('kaboom');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('<ErrorBoundary />', () => {
|
|
21
|
+
it('renders children when no error is thrown', () => {
|
|
22
|
+
render(
|
|
23
|
+
<ErrorBoundary>
|
|
24
|
+
<p>healthy</p>
|
|
25
|
+
</ErrorBoundary>,
|
|
26
|
+
);
|
|
27
|
+
expect(screen.getByText('healthy')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders the fallback UI when a child throws', () => {
|
|
31
|
+
render(
|
|
32
|
+
<ErrorBoundary>
|
|
33
|
+
<Boom />
|
|
34
|
+
</ErrorBoundary>,
|
|
35
|
+
);
|
|
36
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByRole('button', { name: /reload page/i })).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('logs the caught error so it surfaces in observability', () => {
|
|
41
|
+
render(
|
|
42
|
+
<ErrorBoundary>
|
|
43
|
+
<Boom />
|
|
44
|
+
</ErrorBoundary>,
|
|
45
|
+
);
|
|
46
|
+
// Two console.error calls land here: one from React's own dev warning,
|
|
47
|
+
// and one from ErrorBoundary.componentDidCatch. Assert ours is present.
|
|
48
|
+
const fromBoundary = errorSpy.mock.calls.find(
|
|
49
|
+
(args) => typeof args[0] === 'string' && args[0].includes('ErrorBoundary caught an error'),
|
|
50
|
+
);
|
|
51
|
+
expect(fromBoundary).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
type AuthSettings,
|
|
7
|
+
authSettingsQueryKey,
|
|
8
|
+
useAuthSettings,
|
|
9
|
+
useMfaRequired,
|
|
10
|
+
} from '@/hooks/api/useAuthSettings';
|
|
11
|
+
|
|
12
|
+
// Wraps any rendered hook in a fresh QueryClient — retry: false so a
|
|
13
|
+
// fetch rejection surfaces in `error` immediately instead of after the
|
|
14
|
+
// useQuery defaults' multiple retries.
|
|
15
|
+
function makeWrapper() {
|
|
16
|
+
const client = new QueryClient({
|
|
17
|
+
defaultOptions: { queries: { retry: false } },
|
|
18
|
+
});
|
|
19
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
20
|
+
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const okSettings: AuthSettings = {
|
|
25
|
+
settings: {
|
|
26
|
+
mfaEnabled: true,
|
|
27
|
+
localizationEnabled: false,
|
|
28
|
+
enabledLanguages: ['en'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.stubGlobal(
|
|
34
|
+
'fetch',
|
|
35
|
+
vi.fn(async () => new Response(JSON.stringify(okSettings), { status: 200 })),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('useAuthSettings', () => {
|
|
44
|
+
it('returns settings on success', async () => {
|
|
45
|
+
const { result } = renderHook(() => useAuthSettings(), { wrapper: makeWrapper() });
|
|
46
|
+
|
|
47
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
48
|
+
|
|
49
|
+
expect(result.current.data).toEqual(okSettings);
|
|
50
|
+
expect(result.current.error).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('hits /api/v1/auth/settings exactly once when called multiple times (TanStack dedupes)', async () => {
|
|
54
|
+
const wrapper = makeWrapper();
|
|
55
|
+
const a = renderHook(() => useAuthSettings(), { wrapper });
|
|
56
|
+
const b = renderHook(() => useAuthSettings(), { wrapper });
|
|
57
|
+
|
|
58
|
+
await waitFor(() => expect(a.result.current.isLoading).toBe(false));
|
|
59
|
+
await waitFor(() => expect(b.result.current.isLoading).toBe(false));
|
|
60
|
+
|
|
61
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(fetch).toHaveBeenCalledWith('/api/v1/auth/settings');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('surfaces an error when fetch returns non-2xx', async () => {
|
|
66
|
+
vi.stubGlobal(
|
|
67
|
+
'fetch',
|
|
68
|
+
vi.fn(async () => new Response('boom', { status: 500 })),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useAuthSettings(), { wrapper: makeWrapper() });
|
|
72
|
+
|
|
73
|
+
// The hook sets `retry: 1` (per-query options override the wrapper's
|
|
74
|
+
// `retry: false` default), so we get one retry with TanStack's 1s
|
|
75
|
+
// exponential-backoff delay before isError flips. Generous waitFor
|
|
76
|
+
// timeout covers it.
|
|
77
|
+
await waitFor(() => expect(result.current.isError).toBe(true), { timeout: 5_000 });
|
|
78
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not fetch when `enabled: false` is passed', async () => {
|
|
82
|
+
const { result } = renderHook(() => useAuthSettings({ enabled: false }), {
|
|
83
|
+
wrapper: makeWrapper(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.current.fetchStatus).toBe('idle');
|
|
87
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('exports a stable query key (cache invalidation contract)', () => {
|
|
91
|
+
// Renaming this key is a breaking change for any external invalidation
|
|
92
|
+
// callers — pin it so regressions surface in review.
|
|
93
|
+
expect(authSettingsQueryKey).toEqual(['auth', 'settings']);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('useMfaRequired', () => {
|
|
98
|
+
it('derives mfaRequired from the underlying settings', async () => {
|
|
99
|
+
const { result } = renderHook(() => useMfaRequired(), { wrapper: makeWrapper() });
|
|
100
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
101
|
+
expect(result.current.mfaRequired).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('defaults to false when settings are missing', async () => {
|
|
105
|
+
vi.stubGlobal(
|
|
106
|
+
'fetch',
|
|
107
|
+
vi.fn(
|
|
108
|
+
async () =>
|
|
109
|
+
new Response(JSON.stringify({ settings: null } satisfies AuthSettings), {
|
|
110
|
+
status: 200,
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const { result } = renderHook(() => useMfaRequired(), { wrapper: makeWrapper() });
|
|
116
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
117
|
+
expect(result.current.mfaRequired).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|