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,718 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { planIdFromPriceId, plans } from '../../../shared/pricing';
|
|
5
|
+
import { getBillingProvider, getProviderPriceIds, isBillingConfigured } from '../../billing';
|
|
6
|
+
import { env } from '../../lib/env';
|
|
7
|
+
import { sanitizeError } from '../../lib/errors';
|
|
8
|
+
import { supabaseAdmin } from '../../lib/supabase';
|
|
9
|
+
import type { HonoVariables } from '../../types';
|
|
10
|
+
|
|
11
|
+
// Helper to access tables not yet in generated types
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Tables not yet in generated Database types
|
|
13
|
+
const adminDb = supabaseAdmin as SupabaseClient<any>;
|
|
14
|
+
|
|
15
|
+
const billingRoutes = new Hono<{ Variables: HonoVariables }>();
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// VALIDATION SCHEMAS
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const checkoutSchema = z.object({
|
|
22
|
+
priceId: z.string().min(1, 'Price ID is required'),
|
|
23
|
+
type: z.enum(['user', 'organization']),
|
|
24
|
+
organizationId: z.string().uuid().optional(),
|
|
25
|
+
successUrl: z.string().url().optional(),
|
|
26
|
+
cancelUrl: z.string().url().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const portalSchema = z.object({
|
|
30
|
+
type: z.enum(['user', 'organization']),
|
|
31
|
+
organizationId: z.string().uuid().optional(),
|
|
32
|
+
returnUrl: z.string().url().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// HELPER FUNCTIONS
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate that a redirect URL is same-origin to prevent open redirects.
|
|
41
|
+
* Returns the URL if valid, or null if it's external.
|
|
42
|
+
*/
|
|
43
|
+
function validateRedirectUrl(url: string, baseUrl: string): string | null {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(url);
|
|
46
|
+
const base = new URL(baseUrl);
|
|
47
|
+
if (parsed.origin === base.origin) return url;
|
|
48
|
+
return null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get or create a billing customer for a user or organization.
|
|
56
|
+
*
|
|
57
|
+
* Uses the active billing provider to create the customer in the external
|
|
58
|
+
* system, then stores the provider's customer ID in the `stripe_customer_id`
|
|
59
|
+
* column (which holds any provider's customer ID despite its name).
|
|
60
|
+
*/
|
|
61
|
+
async function getOrCreateCustomer(
|
|
62
|
+
type: 'user' | 'organization',
|
|
63
|
+
userId: string,
|
|
64
|
+
email: string,
|
|
65
|
+
organizationId?: string
|
|
66
|
+
): Promise<{ customerId: string; providerCustomerId: string }> {
|
|
67
|
+
const provider = getBillingProvider();
|
|
68
|
+
|
|
69
|
+
// Build query based on type
|
|
70
|
+
const query = adminDb.from('customers').select('id, stripe_customer_id');
|
|
71
|
+
|
|
72
|
+
if (type === 'user') {
|
|
73
|
+
query.eq('user_id', userId).is('organization_id', null);
|
|
74
|
+
} else {
|
|
75
|
+
if (!organizationId) {
|
|
76
|
+
throw new Error('Organization ID is required for organization billing');
|
|
77
|
+
}
|
|
78
|
+
query.eq('organization_id', organizationId).is('user_id', null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { data: existingCustomer, error: fetchError } = await query.maybeSingle();
|
|
82
|
+
|
|
83
|
+
if (fetchError) {
|
|
84
|
+
throw new Error(`Failed to fetch customer: ${fetchError.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Return existing customer if found
|
|
88
|
+
if (existingCustomer) {
|
|
89
|
+
return {
|
|
90
|
+
customerId: existingCustomer.id,
|
|
91
|
+
providerCustomerId: existingCustomer.stripe_customer_id,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get organization name if applicable
|
|
96
|
+
let customerName = email;
|
|
97
|
+
if (type === 'organization' && organizationId) {
|
|
98
|
+
const { data: org } = await adminDb
|
|
99
|
+
.from('organizations')
|
|
100
|
+
.select('name')
|
|
101
|
+
.eq('id', organizationId)
|
|
102
|
+
.single();
|
|
103
|
+
if (org) {
|
|
104
|
+
customerName = org.name;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create customer in the billing provider
|
|
109
|
+
const providerCustomerId = await provider.createCustomer({
|
|
110
|
+
email,
|
|
111
|
+
name: customerName,
|
|
112
|
+
metadata: {
|
|
113
|
+
type,
|
|
114
|
+
user_id: userId,
|
|
115
|
+
organization_id: organizationId || '',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Create database record
|
|
120
|
+
// NOTE: stripe_customer_id column stores the provider's customer ID regardless
|
|
121
|
+
// of which provider is active. The column name is kept for backward compatibility.
|
|
122
|
+
const insertData =
|
|
123
|
+
type === 'user'
|
|
124
|
+
? { user_id: userId, stripe_customer_id: providerCustomerId, email }
|
|
125
|
+
: { organization_id: organizationId, stripe_customer_id: providerCustomerId, email };
|
|
126
|
+
|
|
127
|
+
const { data: newCustomer, error: insertError } = await adminDb
|
|
128
|
+
.from('customers')
|
|
129
|
+
.insert(insertData)
|
|
130
|
+
.select('id, stripe_customer_id')
|
|
131
|
+
.single();
|
|
132
|
+
|
|
133
|
+
if (insertError) {
|
|
134
|
+
// Clean up provider customer if DB insert fails
|
|
135
|
+
try {
|
|
136
|
+
await provider.deleteCustomer(providerCustomerId);
|
|
137
|
+
} catch {
|
|
138
|
+
// Best-effort cleanup; the orphaned customer can be cleaned up manually
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Failed to create customer record: ${insertError.message}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
customerId: newCustomer.id,
|
|
145
|
+
providerCustomerId: newCustomer.stripe_customer_id,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// STRIPE PRICE CACHE (kept for backward compatibility with Stripe subscriptions)
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
const priceCache = new Map<string, { data: unknown; expiresAt: number }>();
|
|
154
|
+
const PRICE_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour (prices rarely change)
|
|
155
|
+
|
|
156
|
+
async function getCachedPrice(priceId: string): Promise<unknown> {
|
|
157
|
+
const cached = priceCache.get(priceId);
|
|
158
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
159
|
+
return cached.data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Price caching via direct Stripe API is only available for the Stripe provider.
|
|
163
|
+
// For other providers, the subscription endpoint uses getSubscription() instead.
|
|
164
|
+
const provider = getBillingProvider();
|
|
165
|
+
if (provider.type !== 'stripe') {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Dynamic import to avoid loading stripe module when not needed
|
|
170
|
+
const { getStripe } = await import('../../lib/stripe');
|
|
171
|
+
const stripe = getStripe();
|
|
172
|
+
const price = await stripe.prices.retrieve(priceId, { expand: ['product'] });
|
|
173
|
+
priceCache.set(priceId, { data: price, expiresAt: Date.now() + PRICE_CACHE_TTL_MS });
|
|
174
|
+
return price;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// BILLING STATUS
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if billing is configured
|
|
183
|
+
*/
|
|
184
|
+
billingRoutes.get('/status', (c) => {
|
|
185
|
+
return c.json({
|
|
186
|
+
configured: isBillingConfigured(),
|
|
187
|
+
provider: env.BILLING_PROVIDER,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// PRICES ENDPOINT
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get available plans and their price IDs for the active provider
|
|
197
|
+
*/
|
|
198
|
+
billingRoutes.get('/prices', (c) => {
|
|
199
|
+
const configured = isBillingConfigured();
|
|
200
|
+
const priceIds = getProviderPriceIds();
|
|
201
|
+
const priceMap: Record<string, { monthly?: string }> = {};
|
|
202
|
+
|
|
203
|
+
if (configured) {
|
|
204
|
+
if (priceIds.starter) {
|
|
205
|
+
priceMap.starter = { monthly: priceIds.starter };
|
|
206
|
+
}
|
|
207
|
+
if (priceIds.pro) {
|
|
208
|
+
priceMap.pro = { monthly: priceIds.pro };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return c.json({
|
|
213
|
+
configured,
|
|
214
|
+
provider: env.BILLING_PROVIDER,
|
|
215
|
+
plans: plans.map((plan) => ({
|
|
216
|
+
...plan,
|
|
217
|
+
// Keep stripePriceIds key for backward compatibility with existing clients
|
|
218
|
+
stripePriceIds: priceMap[plan.id] ?? null,
|
|
219
|
+
priceIds: priceMap[plan.id] ?? null,
|
|
220
|
+
})),
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// SUBSCRIPTION ENDPOINTS
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get current subscription for user or organization
|
|
230
|
+
*/
|
|
231
|
+
billingRoutes.get('/subscription', async (c) => {
|
|
232
|
+
const user = c.get('user');
|
|
233
|
+
|
|
234
|
+
if (!user) {
|
|
235
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!isBillingConfigured()) {
|
|
239
|
+
return c.json({ error: 'Billing is not configured' }, 503);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const type = c.req.query('type') || 'user';
|
|
243
|
+
const organizationId = c.req.query('organizationId');
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Build customer query
|
|
247
|
+
let customerQuery = adminDb.from('customers').select('id, stripe_customer_id');
|
|
248
|
+
|
|
249
|
+
if (type === 'user') {
|
|
250
|
+
customerQuery = customerQuery.eq('user_id', user.id).is('organization_id', null);
|
|
251
|
+
} else {
|
|
252
|
+
if (!organizationId) {
|
|
253
|
+
return c.json({ error: 'Organization ID is required' }, 400);
|
|
254
|
+
}
|
|
255
|
+
customerQuery = customerQuery.eq('organization_id', organizationId).is('user_id', null);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { data: customer } = await customerQuery.maybeSingle();
|
|
259
|
+
|
|
260
|
+
if (!customer) {
|
|
261
|
+
return c.json({
|
|
262
|
+
subscription: null,
|
|
263
|
+
status: 'none',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Get subscription from database
|
|
268
|
+
const { data: subscription } = await adminDb
|
|
269
|
+
.from('subscriptions')
|
|
270
|
+
.select('*')
|
|
271
|
+
.eq('customer_id', customer.id)
|
|
272
|
+
.eq('status', 'active')
|
|
273
|
+
.maybeSingle();
|
|
274
|
+
|
|
275
|
+
if (!subscription) {
|
|
276
|
+
return c.json({
|
|
277
|
+
subscription: null,
|
|
278
|
+
status: 'none',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const providerPriceIds = getProviderPriceIds();
|
|
283
|
+
|
|
284
|
+
// Try to get price details from provider or use cached Stripe price
|
|
285
|
+
const provider = getBillingProvider();
|
|
286
|
+
let priceInfo: { unitAmount: number; currency: string; interval: string } | null = null;
|
|
287
|
+
let productInfo: { id: string; name: string } | null = null;
|
|
288
|
+
|
|
289
|
+
if (provider.type === 'stripe') {
|
|
290
|
+
// Use the existing Stripe price cache for backward compatibility
|
|
291
|
+
const price = (await getCachedPrice(subscription.stripe_price_id)) as {
|
|
292
|
+
id: string;
|
|
293
|
+
unit_amount: number | null;
|
|
294
|
+
currency: string;
|
|
295
|
+
recurring: { interval: string } | null;
|
|
296
|
+
product: unknown;
|
|
297
|
+
} | null;
|
|
298
|
+
|
|
299
|
+
if (price) {
|
|
300
|
+
priceInfo = {
|
|
301
|
+
unitAmount: price.unit_amount ?? 0,
|
|
302
|
+
currency: price.currency,
|
|
303
|
+
interval: price.recurring?.interval ?? 'month',
|
|
304
|
+
};
|
|
305
|
+
const product = price.product as { id: string; name: string } | undefined;
|
|
306
|
+
productInfo = product ? { id: product.id, name: product.name } : null;
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// For Paddle/Polar, fetch subscription details from the provider API
|
|
310
|
+
try {
|
|
311
|
+
const subInfo = await provider.getSubscription({
|
|
312
|
+
customerId: customer.stripe_customer_id,
|
|
313
|
+
subscriptionId: subscription.stripe_subscription_id,
|
|
314
|
+
});
|
|
315
|
+
if (subInfo) {
|
|
316
|
+
priceInfo = subInfo.price;
|
|
317
|
+
productInfo = subInfo.product;
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// Fall back to database data if provider API is unreachable
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const resolvedPlanId = planIdFromPriceId(subscription.stripe_price_id, providerPriceIds);
|
|
325
|
+
|
|
326
|
+
return c.json({
|
|
327
|
+
subscription: {
|
|
328
|
+
id: subscription.id,
|
|
329
|
+
status: subscription.status,
|
|
330
|
+
priceId: subscription.stripe_price_id,
|
|
331
|
+
planId: resolvedPlanId,
|
|
332
|
+
currentPeriodEnd: subscription.current_period_end,
|
|
333
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
334
|
+
product: productInfo,
|
|
335
|
+
price: priceInfo
|
|
336
|
+
? {
|
|
337
|
+
id: subscription.stripe_price_id,
|
|
338
|
+
unitAmount: priceInfo.unitAmount,
|
|
339
|
+
currency: priceInfo.currency,
|
|
340
|
+
interval: priceInfo.interval,
|
|
341
|
+
}
|
|
342
|
+
: null,
|
|
343
|
+
},
|
|
344
|
+
status: subscription.status,
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return c.json({ error: sanitizeError(error, 'Failed to fetch subscription') }, 500);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ============================================================================
|
|
352
|
+
// CHECKOUT
|
|
353
|
+
// ============================================================================
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Create a checkout session via the active billing provider
|
|
357
|
+
*/
|
|
358
|
+
billingRoutes.post('/checkout', async (c) => {
|
|
359
|
+
const user = c.get('user');
|
|
360
|
+
|
|
361
|
+
if (!user) {
|
|
362
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!isBillingConfigured()) {
|
|
366
|
+
return c.json({ error: 'Billing is not configured' }, 503);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Parse and validate request body
|
|
370
|
+
let body: z.infer<typeof checkoutSchema>;
|
|
371
|
+
try {
|
|
372
|
+
const rawBody = await c.req.json();
|
|
373
|
+
const result = checkoutSchema.safeParse(rawBody);
|
|
374
|
+
|
|
375
|
+
if (!result.success) {
|
|
376
|
+
const errors = result.error.issues.map((e: { message: string }) => e.message).join(', ');
|
|
377
|
+
return c.json({ error: errors }, 400);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
body = result.data;
|
|
381
|
+
} catch {
|
|
382
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Validate organization access if billing for organization
|
|
386
|
+
if (body.type === 'organization') {
|
|
387
|
+
if (!body.organizationId) {
|
|
388
|
+
return c.json({ error: 'Organization ID is required for organization billing' }, 400);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Check user is admin/owner of organization
|
|
392
|
+
const supabase = c.get('supabase');
|
|
393
|
+
const { data: membership } = await supabase
|
|
394
|
+
.from('memberships')
|
|
395
|
+
.select('role')
|
|
396
|
+
.eq('user_id', user.id)
|
|
397
|
+
.eq('organization_id', body.organizationId)
|
|
398
|
+
.single();
|
|
399
|
+
|
|
400
|
+
if (!membership || !['OWNER', 'ADMIN'].includes(membership.role)) {
|
|
401
|
+
return c.json({ error: 'You must be an admin to manage organization billing' }, 403);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const provider = getBillingProvider();
|
|
407
|
+
|
|
408
|
+
// Get or create customer (user.email guaranteed for authenticated users)
|
|
409
|
+
const { providerCustomerId } = await getOrCreateCustomer(
|
|
410
|
+
body.type,
|
|
411
|
+
user.id,
|
|
412
|
+
user.email ?? '',
|
|
413
|
+
body.organizationId
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Default URLs - validate user-supplied URLs are same-origin to prevent open redirects
|
|
417
|
+
const baseUrl = env.NODE_ENV === 'production' ? process.env.SITE_URL : 'http://localhost:5173';
|
|
418
|
+
const defaultSuccessUrl = `${baseUrl}/settings/billing?success=true`;
|
|
419
|
+
const defaultCancelUrl = `${baseUrl}/settings/billing?canceled=true`;
|
|
420
|
+
const successUrl =
|
|
421
|
+
(body.successUrl && baseUrl ? validateRedirectUrl(body.successUrl, baseUrl) : null) ||
|
|
422
|
+
defaultSuccessUrl;
|
|
423
|
+
const cancelUrl =
|
|
424
|
+
(body.cancelUrl && baseUrl ? validateRedirectUrl(body.cancelUrl, baseUrl) : null) ||
|
|
425
|
+
defaultCancelUrl;
|
|
426
|
+
|
|
427
|
+
// Create checkout session via the provider
|
|
428
|
+
const result = await provider.createCheckout({
|
|
429
|
+
customerId: providerCustomerId,
|
|
430
|
+
priceId: body.priceId,
|
|
431
|
+
successUrl,
|
|
432
|
+
cancelUrl,
|
|
433
|
+
metadata: {
|
|
434
|
+
type: body.type,
|
|
435
|
+
user_id: user.id,
|
|
436
|
+
organization_id: body.organizationId || '',
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return c.json({
|
|
441
|
+
sessionId: result.sessionId,
|
|
442
|
+
url: result.url,
|
|
443
|
+
});
|
|
444
|
+
} catch (error) {
|
|
445
|
+
return c.json({ error: sanitizeError(error, 'Failed to create checkout session') }, 500);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// SETUP (add payment method without subscribing — Stripe-only feature)
|
|
451
|
+
// ============================================================================
|
|
452
|
+
|
|
453
|
+
const setupSchema = z.object({
|
|
454
|
+
type: z.enum(['user', 'organization']),
|
|
455
|
+
organizationId: z.string().uuid().optional(),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Create a Stripe Checkout session in setup mode to collect a payment method.
|
|
460
|
+
*
|
|
461
|
+
* NOTE: This endpoint is Stripe-specific. Paddle and Polar handle payment
|
|
462
|
+
* method collection differently (during checkout or via their portal).
|
|
463
|
+
* For non-Stripe providers, this endpoint returns 501.
|
|
464
|
+
*/
|
|
465
|
+
billingRoutes.post('/setup', async (c) => {
|
|
466
|
+
const user = c.get('user');
|
|
467
|
+
|
|
468
|
+
if (!user) {
|
|
469
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!isBillingConfigured()) {
|
|
473
|
+
return c.json({ error: 'Billing is not configured' }, 503);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Setup mode is only supported by Stripe
|
|
477
|
+
const provider = getBillingProvider();
|
|
478
|
+
if (provider.type !== 'stripe') {
|
|
479
|
+
return c.json(
|
|
480
|
+
{ error: 'Setup mode is only supported with Stripe. Use the customer portal instead.' },
|
|
481
|
+
501
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let body: z.infer<typeof setupSchema>;
|
|
486
|
+
try {
|
|
487
|
+
const rawBody = await c.req.json();
|
|
488
|
+
const result = setupSchema.safeParse(rawBody);
|
|
489
|
+
|
|
490
|
+
if (!result.success) {
|
|
491
|
+
const errors = result.error.issues.map((e: { message: string }) => e.message).join(', ');
|
|
492
|
+
return c.json({ error: errors }, 400);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
body = result.data;
|
|
496
|
+
} catch {
|
|
497
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (body.type === 'organization') {
|
|
501
|
+
if (!body.organizationId) {
|
|
502
|
+
return c.json({ error: 'Organization ID is required for organization billing' }, 400);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const supabase = c.get('supabase');
|
|
506
|
+
const { data: membership } = await supabase
|
|
507
|
+
.from('memberships')
|
|
508
|
+
.select('role')
|
|
509
|
+
.eq('user_id', user.id)
|
|
510
|
+
.eq('organization_id', body.organizationId)
|
|
511
|
+
.single();
|
|
512
|
+
|
|
513
|
+
if (!membership || !['OWNER', 'ADMIN'].includes(membership.role)) {
|
|
514
|
+
return c.json({ error: 'You must be an admin to manage organization billing' }, 403);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
// Import Stripe directly for setup mode (Stripe-specific feature)
|
|
520
|
+
const { getStripe } = await import('../../lib/stripe');
|
|
521
|
+
const stripe = getStripe();
|
|
522
|
+
|
|
523
|
+
const { providerCustomerId } = await getOrCreateCustomer(
|
|
524
|
+
body.type,
|
|
525
|
+
user.id,
|
|
526
|
+
user.email ?? '',
|
|
527
|
+
body.organizationId
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const baseUrl = env.NODE_ENV === 'production' ? process.env.SITE_URL : 'http://localhost:5173';
|
|
531
|
+
const returnUrl = `${baseUrl}/settings/billing`;
|
|
532
|
+
|
|
533
|
+
const session = await stripe.checkout.sessions.create({
|
|
534
|
+
customer: providerCustomerId,
|
|
535
|
+
mode: 'setup',
|
|
536
|
+
payment_method_types: ['card'],
|
|
537
|
+
success_url: `${returnUrl}?setup=success`,
|
|
538
|
+
cancel_url: `${returnUrl}?setup=canceled`,
|
|
539
|
+
metadata: {
|
|
540
|
+
type: body.type,
|
|
541
|
+
user_id: user.id,
|
|
542
|
+
organization_id: body.organizationId || '',
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
return c.json({
|
|
547
|
+
sessionId: session.id,
|
|
548
|
+
url: session.url,
|
|
549
|
+
});
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return c.json({ error: sanitizeError(error, 'Failed to create setup session') }, 500);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// CUSTOMER PORTAL
|
|
557
|
+
// ============================================================================
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Create a customer portal session via the active billing provider
|
|
561
|
+
*/
|
|
562
|
+
billingRoutes.post('/portal', async (c) => {
|
|
563
|
+
const user = c.get('user');
|
|
564
|
+
|
|
565
|
+
if (!user) {
|
|
566
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!isBillingConfigured()) {
|
|
570
|
+
return c.json({ error: 'Billing is not configured' }, 503);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Parse and validate request body
|
|
574
|
+
let body: z.infer<typeof portalSchema>;
|
|
575
|
+
try {
|
|
576
|
+
const rawBody = await c.req.json();
|
|
577
|
+
const result = portalSchema.safeParse(rawBody);
|
|
578
|
+
|
|
579
|
+
if (!result.success) {
|
|
580
|
+
const errors = result.error.issues.map((e: { message: string }) => e.message).join(', ');
|
|
581
|
+
return c.json({ error: errors }, 400);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
body = result.data;
|
|
585
|
+
} catch {
|
|
586
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Validate organization access if billing for organization
|
|
590
|
+
if (body.type === 'organization') {
|
|
591
|
+
if (!body.organizationId) {
|
|
592
|
+
return c.json({ error: 'Organization ID is required for organization billing' }, 400);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const supabase = c.get('supabase');
|
|
596
|
+
const { data: membership } = await supabase
|
|
597
|
+
.from('memberships')
|
|
598
|
+
.select('role')
|
|
599
|
+
.eq('user_id', user.id)
|
|
600
|
+
.eq('organization_id', body.organizationId)
|
|
601
|
+
.single();
|
|
602
|
+
|
|
603
|
+
if (!membership || !['OWNER', 'ADMIN'].includes(membership.role)) {
|
|
604
|
+
return c.json({ error: 'You must be an admin to manage organization billing' }, 403);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
// Find existing customer
|
|
610
|
+
const query = adminDb.from('customers').select('stripe_customer_id');
|
|
611
|
+
|
|
612
|
+
if (body.type === 'user') {
|
|
613
|
+
query.eq('user_id', user.id).is('organization_id', null);
|
|
614
|
+
} else {
|
|
615
|
+
query.eq('organization_id', body.organizationId).is('user_id', null);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const { data: customer, error: fetchError } = await query.maybeSingle();
|
|
619
|
+
|
|
620
|
+
if (fetchError) {
|
|
621
|
+
return c.json({ error: sanitizeError(fetchError, 'Failed to fetch customer') }, 500);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!customer) {
|
|
625
|
+
return c.json({ error: 'No billing account found. Please subscribe first.' }, 404);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const provider = getBillingProvider();
|
|
629
|
+
const baseUrl = env.NODE_ENV === 'production' ? process.env.SITE_URL : 'http://localhost:5173';
|
|
630
|
+
const defaultReturnUrl = `${baseUrl}/settings/billing`;
|
|
631
|
+
const returnUrl =
|
|
632
|
+
(body.returnUrl && baseUrl ? validateRedirectUrl(body.returnUrl, baseUrl) : null) ||
|
|
633
|
+
defaultReturnUrl;
|
|
634
|
+
|
|
635
|
+
const result = await provider.createPortalSession({
|
|
636
|
+
customerId: customer.stripe_customer_id,
|
|
637
|
+
returnUrl,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
return c.json({
|
|
641
|
+
url: result.url,
|
|
642
|
+
});
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return c.json({ error: sanitizeError(error, 'Failed to create portal session') }, 500);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// ============================================================================
|
|
649
|
+
// LICENSE CHECKOUT (public, no auth — one-time license purchases)
|
|
650
|
+
// ============================================================================
|
|
651
|
+
|
|
652
|
+
const licenseCheckoutSchema = z.object({
|
|
653
|
+
email: z.string().email('Valid email is required'),
|
|
654
|
+
tier: z.enum(['diamond', 'fullerene']),
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const TIER_PRICE_IDS: Record<string, string | undefined> = {
|
|
658
|
+
diamond: env.DIAMOND_PRICE_ID,
|
|
659
|
+
fullerene: env.FULLERENE_PRICE_ID,
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Create a one-time payment checkout session for a license tier.
|
|
664
|
+
* No authentication required — collects email for the provider.
|
|
665
|
+
*/
|
|
666
|
+
billingRoutes.post('/license-checkout', async (c) => {
|
|
667
|
+
if (!isBillingConfigured()) {
|
|
668
|
+
return c.json({ error: 'Billing is not configured' }, 503);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let body: z.infer<typeof licenseCheckoutSchema>;
|
|
672
|
+
try {
|
|
673
|
+
const rawBody = await c.req.json();
|
|
674
|
+
const result = licenseCheckoutSchema.safeParse(rawBody);
|
|
675
|
+
if (!result.success) {
|
|
676
|
+
const errors = result.error.issues.map((e: { message: string }) => e.message).join(', ');
|
|
677
|
+
return c.json({ error: errors }, 400);
|
|
678
|
+
}
|
|
679
|
+
body = result.data;
|
|
680
|
+
} catch {
|
|
681
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const priceId = TIER_PRICE_IDS[body.tier];
|
|
685
|
+
if (!priceId) {
|
|
686
|
+
return c.json({ error: `Price not configured for ${body.tier} tier` }, 503);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
const provider = getBillingProvider();
|
|
691
|
+
|
|
692
|
+
// Create a customer with the provided email
|
|
693
|
+
const customerId = await provider.createCustomer({
|
|
694
|
+
email: body.email,
|
|
695
|
+
name: body.email,
|
|
696
|
+
metadata: { tier: body.tier, source: 'license-checkout' },
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const baseUrl = env.NODE_ENV === 'production' ? process.env.SITE_URL : 'http://localhost:5173';
|
|
700
|
+
const successUrl = `${baseUrl}/checkout?success=true&tier=${body.tier}`;
|
|
701
|
+
const cancelUrl = `${baseUrl}/checkout?tier=${body.tier}`;
|
|
702
|
+
|
|
703
|
+
const result = await provider.createCheckout({
|
|
704
|
+
customerId,
|
|
705
|
+
priceId,
|
|
706
|
+
successUrl,
|
|
707
|
+
cancelUrl,
|
|
708
|
+
metadata: { tier: body.tier, email: body.email, type: 'license' },
|
|
709
|
+
mode: 'payment',
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
return c.json({ url: result.url });
|
|
713
|
+
} catch (error) {
|
|
714
|
+
return c.json({ error: sanitizeError(error, 'Failed to create checkout session') }, 500);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
export { billingRoutes };
|