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.
Files changed (495) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +188 -0
  3. package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
  4. package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
  5. package/carbon/.claude/settings.json +29 -0
  6. package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
  7. package/carbon/.dockerignore +57 -0
  8. package/carbon/.env.example +219 -0
  9. package/carbon/.github/copilot-instructions.md +3 -0
  10. package/carbon/.github/workflows/deploy.yml +346 -0
  11. package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
  12. package/carbon/.windsurfrules +74 -0
  13. package/carbon/AGENTS.md +422 -0
  14. package/carbon/CLAUDE.md +3 -0
  15. package/carbon/DEVELOPMENT.md +187 -0
  16. package/carbon/Dockerfile +98 -0
  17. package/carbon/LICENSE +21 -0
  18. package/carbon/PRODUCTION.md +364 -0
  19. package/carbon/README.md +193 -0
  20. package/carbon/TESTING.md +138 -0
  21. package/carbon/backup/Dockerfile +75 -0
  22. package/carbon/backup/backup.sh +95 -0
  23. package/carbon/backup/compose-backup.sh +140 -0
  24. package/carbon/biome.json +83 -0
  25. package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
  26. package/carbon/cloud-init/k3s/master-init.sh +215 -0
  27. package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
  28. package/carbon/cloud-init/k3s/worker-init.sh +147 -0
  29. package/carbon/components.json +24 -0
  30. package/carbon/content/blog/authentication-guide.mdx +34 -0
  31. package/carbon/content/blog/getting-started.mdx +41 -0
  32. package/carbon/content/changelog/v0-1-0.mdx +40 -0
  33. package/carbon/content/docs/analytics.mdx +90 -0
  34. package/carbon/content/docs/authentication.mdx +231 -0
  35. package/carbon/content/docs/background-jobs.mdx +116 -0
  36. package/carbon/content/docs/cli.mdx +630 -0
  37. package/carbon/content/docs/database.mdx +236 -0
  38. package/carbon/content/docs/deployment.mdx +227 -0
  39. package/carbon/content/docs/development.mdx +238 -0
  40. package/carbon/content/docs/environments.mdx +84 -0
  41. package/carbon/content/docs/getting-started.mdx +112 -0
  42. package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
  43. package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
  44. package/carbon/content/docs/optional-services.mdx +160 -0
  45. package/carbon/db/Dockerfile +23 -0
  46. package/carbon/docker-compose.dev-init.yml +5 -0
  47. package/carbon/docker-compose.metabase.override.yml +14 -0
  48. package/carbon/docker-compose.metabase.prod.yml +28 -0
  49. package/carbon/docker-compose.metabase.yml +84 -0
  50. package/carbon/docker-compose.n8n.override.yml +14 -0
  51. package/carbon/docker-compose.n8n.prod.yml +31 -0
  52. package/carbon/docker-compose.n8n.yml +97 -0
  53. package/carbon/docker-compose.observability.override.yml +18 -0
  54. package/carbon/docker-compose.observability.prod.yml +28 -0
  55. package/carbon/docker-compose.observability.yml +125 -0
  56. package/carbon/docker-compose.override.yml +28 -0
  57. package/carbon/docker-compose.prod.yml +294 -0
  58. package/carbon/docker-compose.yml +508 -0
  59. package/carbon/docker-entrypoint.sh +12 -0
  60. package/carbon/ha/activate-standby.sh +52 -0
  61. package/carbon/ha/primary-init.sql +36 -0
  62. package/carbon/ha/standby-init.sh +74 -0
  63. package/carbon/k8s/LICENSE +99 -0
  64. package/carbon/k8s/README.md +272 -0
  65. package/carbon/k8s/base/app/deployment.yaml +130 -0
  66. package/carbon/k8s/base/app/hpa.yaml +44 -0
  67. package/carbon/k8s/base/app/kustomization.yaml +10 -0
  68. package/carbon/k8s/base/app/network-policy.yaml +124 -0
  69. package/carbon/k8s/base/app/pdb.yaml +14 -0
  70. package/carbon/k8s/base/app/rbac.yaml +36 -0
  71. package/carbon/k8s/base/app/service.yaml +16 -0
  72. package/carbon/k8s/base/backup/cronjob.yaml +120 -0
  73. package/carbon/k8s/base/backup/kustomization.yaml +7 -0
  74. package/carbon/k8s/base/backup/network-policy.yaml +31 -0
  75. package/carbon/k8s/base/backup/rbac.yaml +7 -0
  76. package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
  77. package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
  78. package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
  79. package/carbon/k8s/base/config/configmap.yaml +48 -0
  80. package/carbon/k8s/base/config/kustomization.yaml +8 -0
  81. package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
  82. package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
  83. package/carbon/k8s/base/kustomization.yaml +43 -0
  84. package/carbon/k8s/base/namespace.yaml +7 -0
  85. package/carbon/k8s/base/network-policies.yaml +95 -0
  86. package/carbon/k8s/base/registry/kustomization.yaml +5 -0
  87. package/carbon/k8s/base/registry/local-registry.yaml +193 -0
  88. package/carbon/k8s/base/traefik/certificate.yaml +21 -0
  89. package/carbon/k8s/base/traefik/configmap.yaml +13 -0
  90. package/carbon/k8s/base/traefik/deployment.yaml +165 -0
  91. package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
  92. package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
  93. package/carbon/k8s/base/traefik/middleware.yaml +109 -0
  94. package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
  95. package/carbon/k8s/base/traefik/service.yaml +38 -0
  96. package/carbon/k8s/flux/README.md +49 -0
  97. package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
  98. package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
  99. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
  100. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
  101. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
  102. package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
  103. package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
  104. package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
  105. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
  106. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
  107. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
  108. package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
  109. package/carbon/k8s/infra/kustomization.yaml +21 -0
  110. package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
  111. package/carbon/k8s/overlays/local/configmap.yaml +23 -0
  112. package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
  113. package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
  114. package/carbon/k8s/overlays/local/namespace.yaml +8 -0
  115. package/carbon/k8s/overlays/local/secrets.yaml +32 -0
  116. package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
  117. package/carbon/k8s/test-local.sh +318 -0
  118. package/carbon/k8s/values/supabase.values.yaml +133 -0
  119. package/carbon/package.json +154 -0
  120. package/carbon/runtime.Dockerfile +17 -0
  121. package/carbon/scripts/_dev-jwt.mjs +45 -0
  122. package/carbon/scripts/check-rls.ts +53 -0
  123. package/carbon/scripts/dev-init.js +191 -0
  124. package/carbon/scripts/dev.js +191 -0
  125. package/carbon/scripts/docker-down.js +63 -0
  126. package/carbon/scripts/docker-logs.js +59 -0
  127. package/carbon/scripts/docker-up.js +222 -0
  128. package/carbon/scripts/generate-dev-configs.sh +131 -0
  129. package/carbon/scripts/generate-rss.ts +116 -0
  130. package/carbon/scripts/generate-sitemap.ts +102 -0
  131. package/carbon/scripts/k8s-apply.js +71 -0
  132. package/carbon/scripts/k8s-delete.js +75 -0
  133. package/carbon/scripts/lib/manifest.js +176 -0
  134. package/carbon/scripts/secret-scan.mjs +278 -0
  135. package/carbon/scripts/validate-dev-configs.sh +101 -0
  136. package/carbon/src/client/App.tsx +202 -0
  137. package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
  138. package/carbon/src/client/assets/hyperformant-light.svg +29 -0
  139. package/carbon/src/client/assets/logos/aider.svg +1 -0
  140. package/carbon/src/client/assets/logos/antigravity.svg +1 -0
  141. package/carbon/src/client/assets/logos/bolt.svg +1 -0
  142. package/carbon/src/client/assets/logos/claude-code.svg +1 -0
  143. package/carbon/src/client/assets/logos/copilot.svg +1 -0
  144. package/carbon/src/client/assets/logos/cursor.svg +1 -0
  145. package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
  146. package/carbon/src/client/assets/logos/lovable.svg +1 -0
  147. package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
  148. package/carbon/src/client/assets/logos/v0.svg +1 -0
  149. package/carbon/src/client/assets/logos/windsurf.svg +1 -0
  150. package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
  151. package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
  152. package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
  153. package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
  154. package/carbon/src/client/components/AppSidebar.tsx +760 -0
  155. package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
  156. package/carbon/src/client/components/CTAFooter.tsx +59 -0
  157. package/carbon/src/client/components/ComparisonSection.tsx +132 -0
  158. package/carbon/src/client/components/ContentPanel.tsx +66 -0
  159. package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
  160. package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
  161. package/carbon/src/client/components/FAQSection.tsx +76 -0
  162. package/carbon/src/client/components/FileUpload.tsx +210 -0
  163. package/carbon/src/client/components/HeaderActions.tsx +17 -0
  164. package/carbon/src/client/components/Hero.tsx +608 -0
  165. package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
  166. package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
  167. package/carbon/src/client/components/Logo.tsx +87 -0
  168. package/carbon/src/client/components/LogoStrip.tsx +76 -0
  169. package/carbon/src/client/components/MetricsStrip.tsx +48 -0
  170. package/carbon/src/client/components/Nav.tsx +195 -0
  171. package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
  172. package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
  173. package/carbon/src/client/components/PageHeader.tsx +24 -0
  174. package/carbon/src/client/components/PlanGate.tsx +36 -0
  175. package/carbon/src/client/components/PricingSection.tsx +371 -0
  176. package/carbon/src/client/components/SEO.tsx +34 -0
  177. package/carbon/src/client/components/SmoothScroll.tsx +45 -0
  178. package/carbon/src/client/components/TechStackSection.tsx +165 -0
  179. package/carbon/src/client/components/WorkflowSection.tsx +604 -0
  180. package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
  181. package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
  182. package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
  183. package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
  184. package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
  185. package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
  186. package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
  187. package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
  188. package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
  189. package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
  190. package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
  191. package/carbon/src/client/components/effects/index.ts +2 -0
  192. package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
  193. package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
  194. package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
  195. package/carbon/src/client/components/scroll/index.ts +2 -0
  196. package/carbon/src/client/components/ui/accordion.tsx +71 -0
  197. package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
  198. package/carbon/src/client/components/ui/alert.tsx +73 -0
  199. package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
  200. package/carbon/src/client/components/ui/avatar.tsx +91 -0
  201. package/carbon/src/client/components/ui/badge.tsx +50 -0
  202. package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
  203. package/carbon/src/client/components/ui/button-group.tsx +78 -0
  204. package/carbon/src/client/components/ui/button.tsx +120 -0
  205. package/carbon/src/client/components/ui/calendar.tsx +182 -0
  206. package/carbon/src/client/components/ui/card.tsx +85 -0
  207. package/carbon/src/client/components/ui/carousel.tsx +227 -0
  208. package/carbon/src/client/components/ui/chart.tsx +357 -0
  209. package/carbon/src/client/components/ui/checkbox.tsx +27 -0
  210. package/carbon/src/client/components/ui/collapsible.tsx +15 -0
  211. package/carbon/src/client/components/ui/command.tsx +178 -0
  212. package/carbon/src/client/components/ui/context-menu.tsx +233 -0
  213. package/carbon/src/client/components/ui/dialog.tsx +132 -0
  214. package/carbon/src/client/components/ui/drawer.tsx +118 -0
  215. package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
  216. package/carbon/src/client/components/ui/empty.tsx +94 -0
  217. package/carbon/src/client/components/ui/field.tsx +226 -0
  218. package/carbon/src/client/components/ui/hover-card.tsx +44 -0
  219. package/carbon/src/client/components/ui/input-group.tsx +146 -0
  220. package/carbon/src/client/components/ui/input-otp.tsx +83 -0
  221. package/carbon/src/client/components/ui/input.tsx +20 -0
  222. package/carbon/src/client/components/ui/item.tsx +188 -0
  223. package/carbon/src/client/components/ui/kbd.tsx +26 -0
  224. package/carbon/src/client/components/ui/label.tsx +21 -0
  225. package/carbon/src/client/components/ui/menubar.tsx +250 -0
  226. package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
  227. package/carbon/src/client/components/ui/pagination.tsx +102 -0
  228. package/carbon/src/client/components/ui/popover.tsx +75 -0
  229. package/carbon/src/client/components/ui/progress.tsx +66 -0
  230. package/carbon/src/client/components/ui/radio-group.tsx +36 -0
  231. package/carbon/src/client/components/ui/resizable.tsx +46 -0
  232. package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
  233. package/carbon/src/client/components/ui/select.tsx +186 -0
  234. package/carbon/src/client/components/ui/separator.tsx +21 -0
  235. package/carbon/src/client/components/ui/sheet.tsx +124 -0
  236. package/carbon/src/client/components/ui/sidebar.tsx +702 -0
  237. package/carbon/src/client/components/ui/skeleton.tsx +13 -0
  238. package/carbon/src/client/components/ui/slider.tsx +57 -0
  239. package/carbon/src/client/components/ui/sonner.tsx +45 -0
  240. package/carbon/src/client/components/ui/spinner.tsx +15 -0
  241. package/carbon/src/client/components/ui/switch.tsx +30 -0
  242. package/carbon/src/client/components/ui/table.tsx +89 -0
  243. package/carbon/src/client/components/ui/tabs.tsx +73 -0
  244. package/carbon/src/client/components/ui/textarea.tsx +18 -0
  245. package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
  246. package/carbon/src/client/components/ui/toggle.tsx +44 -0
  247. package/carbon/src/client/components/ui/tooltip.tsx +56 -0
  248. package/carbon/src/client/hooks/api/index.ts +14 -0
  249. package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
  250. package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
  251. package/carbon/src/client/hooks/use-mobile.ts +21 -0
  252. package/carbon/src/client/hooks/useMousePosition.ts +91 -0
  253. package/carbon/src/client/hooks/useNotifications.tsx +124 -0
  254. package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
  255. package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
  256. package/carbon/src/client/hooks/usePackageManager.ts +69 -0
  257. package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
  258. package/carbon/src/client/hooks/useRunningServices.ts +114 -0
  259. package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
  260. package/carbon/src/client/index.css +467 -0
  261. package/carbon/src/client/index.html +56 -0
  262. package/carbon/src/client/lib/admin-services.ts +151 -0
  263. package/carbon/src/client/lib/api.ts +32 -0
  264. package/carbon/src/client/lib/blog.ts +35 -0
  265. package/carbon/src/client/lib/changelog.ts +33 -0
  266. package/carbon/src/client/lib/docs-search.ts +101 -0
  267. package/carbon/src/client/lib/docs.ts +37 -0
  268. package/carbon/src/client/lib/i18n.ts +32 -0
  269. package/carbon/src/client/lib/supabase.ts +72 -0
  270. package/carbon/src/client/lib/tailwind-colors.ts +357 -0
  271. package/carbon/src/client/lib/theme.ts +117 -0
  272. package/carbon/src/client/lib/utils.ts +22 -0
  273. package/carbon/src/client/locales/de.json +529 -0
  274. package/carbon/src/client/locales/en.json +461 -0
  275. package/carbon/src/client/locales/es.json +529 -0
  276. package/carbon/src/client/locales/fr.json +529 -0
  277. package/carbon/src/client/locales/pt.json +529 -0
  278. package/carbon/src/client/main.tsx +56 -0
  279. package/carbon/src/client/mdx.d.ts +13 -0
  280. package/carbon/src/client/pages/ApiDocs.tsx +76 -0
  281. package/carbon/src/client/pages/AuthCallback.tsx +34 -0
  282. package/carbon/src/client/pages/Blog.tsx +167 -0
  283. package/carbon/src/client/pages/Changelog.tsx +171 -0
  284. package/carbon/src/client/pages/Charts.tsx +388 -0
  285. package/carbon/src/client/pages/Checkout.tsx +227 -0
  286. package/carbon/src/client/pages/Contact.tsx +174 -0
  287. package/carbon/src/client/pages/Dashboard.tsx +368 -0
  288. package/carbon/src/client/pages/Docs.tsx +372 -0
  289. package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
  290. package/carbon/src/client/pages/Home.tsx +187 -0
  291. package/carbon/src/client/pages/Legal.tsx +100 -0
  292. package/carbon/src/client/pages/Login.tsx +408 -0
  293. package/carbon/src/client/pages/MFAVerify.tsx +156 -0
  294. package/carbon/src/client/pages/NotFound.tsx +21 -0
  295. package/carbon/src/client/pages/Onboarding.tsx +246 -0
  296. package/carbon/src/client/pages/ResetPassword.tsx +200 -0
  297. package/carbon/src/client/pages/UIComponents.tsx +390 -0
  298. package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
  299. package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
  300. package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
  301. package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
  302. package/carbon/src/client/pages/admin/Logs.tsx +18 -0
  303. package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
  304. package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
  305. package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
  306. package/carbon/src/client/pages/admin/Settings.tsx +314 -0
  307. package/carbon/src/client/pages/admin/Theme.tsx +465 -0
  308. package/carbon/src/client/pages/admin/Users.tsx +365 -0
  309. package/carbon/src/client/pages/api-docs.css +156 -0
  310. package/carbon/src/client/pages/organizations/Details.tsx +200 -0
  311. package/carbon/src/client/pages/organizations/Members.tsx +402 -0
  312. package/carbon/src/client/pages/settings/Billing.tsx +473 -0
  313. package/carbon/src/client/pages/settings/Profile.tsx +160 -0
  314. package/carbon/src/client/pages/settings/Security.tsx +341 -0
  315. package/carbon/src/client/public/favicon.svg +19 -0
  316. package/carbon/src/client/public/robots.txt +8 -0
  317. package/carbon/src/server/billing/index.ts +104 -0
  318. package/carbon/src/server/billing/provider.ts +81 -0
  319. package/carbon/src/server/billing/providers/paddle.ts +314 -0
  320. package/carbon/src/server/billing/providers/polar.ts +325 -0
  321. package/carbon/src/server/billing/providers/stripe.ts +233 -0
  322. package/carbon/src/server/emails/templates.ts +116 -0
  323. package/carbon/src/server/index.ts +554 -0
  324. package/carbon/src/server/lib/auth.ts +6 -0
  325. package/carbon/src/server/lib/email.ts +64 -0
  326. package/carbon/src/server/lib/env.ts +112 -0
  327. package/carbon/src/server/lib/errors.ts +21 -0
  328. package/carbon/src/server/lib/logger.ts +17 -0
  329. package/carbon/src/server/lib/rate-limiter.ts +288 -0
  330. package/carbon/src/server/lib/request.ts +34 -0
  331. package/carbon/src/server/lib/stripe.ts +42 -0
  332. package/carbon/src/server/lib/supabase.ts +65 -0
  333. package/carbon/src/server/middleware/requirePlan.ts +80 -0
  334. package/carbon/src/server/routes/_internal/services-status.ts +958 -0
  335. package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
  336. package/carbon/src/server/routes/health.ts +48 -0
  337. package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
  338. package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
  339. package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
  340. package/carbon/src/server/routes/v1/auth.ts +390 -0
  341. package/carbon/src/server/routes/v1/billing.ts +718 -0
  342. package/carbon/src/server/routes/v1/contact.ts +93 -0
  343. package/carbon/src/server/routes/v1/index.ts +1333 -0
  344. package/carbon/src/server/routes/v1/newsletter.ts +181 -0
  345. package/carbon/src/server/routes/v1/performance.ts +157 -0
  346. package/carbon/src/server/routes/v1/stats.ts +170 -0
  347. package/carbon/src/server/routes/v1/theme.ts +106 -0
  348. package/carbon/src/server/routes/webhooks/billing.ts +376 -0
  349. package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
  350. package/carbon/src/server/types.ts +11 -0
  351. package/carbon/src/shared/pricing.ts +155 -0
  352. package/carbon/src/shared/types.ts +338 -0
  353. package/carbon/supabase/migrations/00001_init.sql +717 -0
  354. package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
  355. package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
  356. package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
  357. package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
  358. package/carbon/supabase/seed.sql +16 -0
  359. package/carbon/tests/_helpers/app.ts +45 -0
  360. package/carbon/tests/_helpers/env.ts +37 -0
  361. package/carbon/tests/_helpers/factories.ts +69 -0
  362. package/carbon/tests/_helpers/jwt.ts +23 -0
  363. package/carbon/tests/_helpers/setup-integration.ts +20 -0
  364. package/carbon/tests/_helpers/setup-rtl.ts +12 -0
  365. package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
  366. package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
  367. package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
  368. package/carbon/tests/integration/server/routes/health.test.ts +61 -0
  369. package/carbon/tests/structural/i18n-parity.test.ts +42 -0
  370. package/carbon/tests/unit/client/utils.test.ts +49 -0
  371. package/carbon/tests/unit/shared/pricing.test.ts +93 -0
  372. package/carbon/tsconfig.json +27 -0
  373. package/carbon/tsconfig.server.json +27 -0
  374. package/carbon/tsconfig.test.json +9 -0
  375. package/carbon/vite.config.ts +110 -0
  376. package/carbon/vitest.config.ts +74 -0
  377. package/carbon/volumes/db/jwt.sql +57 -0
  378. package/carbon/volumes/db/metabase-init.sh +29 -0
  379. package/carbon/volumes/db/n8n-init.sh +25 -0
  380. package/carbon/volumes/db/realtime.sql +33 -0
  381. package/carbon/volumes/db/roles.sql +93 -0
  382. package/carbon/volumes/db/set-passwords.sh +12 -0
  383. package/carbon/volumes/db/super-admin.dev.sql +113 -0
  384. package/carbon/volumes/db/super-admin.generated.sql +113 -0
  385. package/carbon/volumes/db/super-admin.sql +114 -0
  386. package/carbon/volumes/grafana/dashboards/logs.json +179 -0
  387. package/carbon/volumes/grafana/dashboards/overview.json +523 -0
  388. package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
  389. package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
  390. package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
  391. package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
  392. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
  393. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
  394. package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
  395. package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
  396. package/carbon/volumes/kong/kong.yml +208 -0
  397. package/carbon/volumes/loki/loki-config.yml +58 -0
  398. package/carbon/volumes/n8n/hooks.js +131 -0
  399. package/carbon/volumes/n8n/scripts/setup.sh +66 -0
  400. package/carbon/volumes/prometheus/prometheus.yml +43 -0
  401. package/carbon/volumes/promtail/promtail-config.yml +64 -0
  402. package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
  403. package/carbon/volumes/traefik/middlewares.yml +95 -0
  404. package/carbon/volumes/traefik/vite-dev.yml +20 -0
  405. package/package.json +95 -0
  406. package/src/access.js +354 -0
  407. package/src/activate.js +187 -0
  408. package/src/add.js +718 -0
  409. package/src/backup.js +786 -0
  410. package/src/cli.js +350 -0
  411. package/src/configure.js +967 -0
  412. package/src/console.js +155 -0
  413. package/src/create.js +1499 -0
  414. package/src/deploy.js +311 -0
  415. package/src/destroy.js +2033 -0
  416. package/src/diagnose.js +735 -0
  417. package/src/down.js +80 -0
  418. package/src/failover.js +1032 -0
  419. package/src/lib/backup-s3.js +179 -0
  420. package/src/lib/build.js +33 -0
  421. package/src/lib/checksum.js +28 -0
  422. package/src/lib/ci-setup.js +666 -0
  423. package/src/lib/cli/help.js +129 -0
  424. package/src/lib/cli/parse-flags.js +160 -0
  425. package/src/lib/cli/select-action.js +65 -0
  426. package/src/lib/cli/select-environment.js +108 -0
  427. package/src/lib/cli/tty-guard.js +75 -0
  428. package/src/lib/cloudflare.js +447 -0
  429. package/src/lib/colors.js +52 -0
  430. package/src/lib/command.js +361 -0
  431. package/src/lib/config.js +359 -0
  432. package/src/lib/cost.js +103 -0
  433. package/src/lib/deploy/bundle.js +231 -0
  434. package/src/lib/deploy/compose/acme-verify.js +121 -0
  435. package/src/lib/deploy/compose/build-args.js +78 -0
  436. package/src/lib/deploy/compose/ha.js +1874 -0
  437. package/src/lib/deploy/compose/index.js +1294 -0
  438. package/src/lib/deploy/compose/reconcile.js +74 -0
  439. package/src/lib/deploy/github.js +382 -0
  440. package/src/lib/deploy/image.js +191 -0
  441. package/src/lib/deploy/index.js +119 -0
  442. package/src/lib/deploy/k8s/LICENSE +99 -0
  443. package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
  444. package/src/lib/deploy/k8s/ha/index.js +1000 -0
  445. package/src/lib/deploy/k8s/index.js +62 -0
  446. package/src/lib/deploy/k8s/k3s.js +2515 -0
  447. package/src/lib/deploy/orchestrator.js +1401 -0
  448. package/src/lib/deploy/prompts.js +545 -0
  449. package/src/lib/deploy/remote-build.js +130 -0
  450. package/src/lib/deploy/state.js +120 -0
  451. package/src/lib/deploy/utils.js +328 -0
  452. package/src/lib/deploy-logger.js +93 -0
  453. package/src/lib/dns-propagation.js +48 -0
  454. package/src/lib/environment.js +58 -0
  455. package/src/lib/fetch-retry.js +103 -0
  456. package/src/lib/github-environments.js +335 -0
  457. package/src/lib/hetzner-dns.js +377 -0
  458. package/src/lib/hetzner-guided-setup.js +275 -0
  459. package/src/lib/host-keys.js +37 -0
  460. package/src/lib/iac/cloud-init.js +52 -0
  461. package/src/lib/iac/index.js +325 -0
  462. package/src/lib/iac/programs/hetzner-compose.js +130 -0
  463. package/src/lib/iac/programs/hetzner-k8s.js +320 -0
  464. package/src/lib/kubectl.js +81 -0
  465. package/src/lib/licensing/index.js +259 -0
  466. package/src/lib/licensing/tiers.js +181 -0
  467. package/src/lib/licensing/validator.js +171 -0
  468. package/src/lib/merge-package-json.js +90 -0
  469. package/src/lib/operator-ip.js +381 -0
  470. package/src/lib/package-manager.js +111 -0
  471. package/src/lib/perf.js +71 -0
  472. package/src/lib/project-guard.js +39 -0
  473. package/src/lib/project.js +334 -0
  474. package/src/lib/providers/base.js +276 -0
  475. package/src/lib/providers/hetzner-s3.js +656 -0
  476. package/src/lib/providers/hetzner.js +755 -0
  477. package/src/lib/providers/index.js +164 -0
  478. package/src/lib/s3.js +510 -0
  479. package/src/lib/secret-scan.js +583 -0
  480. package/src/lib/secrets.js +63 -0
  481. package/src/lib/server-types.js +195 -0
  482. package/src/lib/shell.js +91 -0
  483. package/src/lib/ssh.js +241 -0
  484. package/src/lib/tracker.js +242 -0
  485. package/src/lib/upgrade-policy.js +170 -0
  486. package/src/lib/validators.js +105 -0
  487. package/src/lib/version.js +5 -0
  488. package/src/remove.js +292 -0
  489. package/src/reset.js +97 -0
  490. package/src/restore.js +871 -0
  491. package/src/scale.js +1734 -0
  492. package/src/shell.js +222 -0
  493. package/src/status.js +981 -0
  494. package/src/up.js +264 -0
  495. 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 };