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,376 @@
1
+ /**
2
+ * Generic billing webhook handler.
3
+ *
4
+ * Accepts webhooks at /api/webhooks/billing from any configured billing provider
5
+ * (Stripe, Paddle, Polar). The active provider verifies the signature and
6
+ * normalizes the event, and this handler updates subscriptions in the database.
7
+ *
8
+ * The Stripe-specific webhook at /api/webhooks/stripe is kept for backward
9
+ * compatibility with existing deployments.
10
+ */
11
+
12
+ import type { SupabaseClient } from '@supabase/supabase-js';
13
+ import { Hono } from 'hono';
14
+ import { getBillingProvider, isBillingConfigured } from '../../billing';
15
+ import { paymentFailedEmail, subscriptionConfirmEmail } from '../../emails/templates';
16
+ import { sendEmail } from '../../lib/email';
17
+ import { logger } from '../../lib/logger';
18
+ import { supabaseAdmin } from '../../lib/supabase';
19
+ import type { HonoVariables } from '../../types';
20
+
21
+ // Helper to access tables not yet in generated types
22
+ // biome-ignore lint/suspicious/noExplicitAny: Tables not yet in generated Database types
23
+ const adminDb = supabaseAdmin as SupabaseClient<any>;
24
+
25
+ const billingWebhookRoutes = new Hono<{ Variables: HonoVariables }>();
26
+
27
+ /**
28
+ * Handle billing webhook events from the active provider.
29
+ *
30
+ * IMPORTANT: This endpoint must receive the raw body for signature verification.
31
+ * The body parser middleware should be skipped for this route.
32
+ */
33
+ billingWebhookRoutes.post('/', async (c) => {
34
+ if (!isBillingConfigured()) {
35
+ return c.json({ error: 'Billing is not configured' }, 503);
36
+ }
37
+
38
+ const provider = getBillingProvider();
39
+
40
+ // Each provider uses different signature headers:
41
+ // - Stripe: stripe-signature
42
+ // - Paddle: paddle-signature
43
+ // - Polar: webhook-id + webhook-timestamp + webhook-signature (combined below)
44
+ let signature = '';
45
+ switch (provider.type) {
46
+ case 'stripe':
47
+ signature = c.req.header('stripe-signature') ?? '';
48
+ break;
49
+ case 'paddle':
50
+ signature = c.req.header('paddle-signature') ?? '';
51
+ break;
52
+ case 'polar': {
53
+ // Polar uses three headers; combine them into a single string for the provider
54
+ const webhookId = c.req.header('webhook-id') ?? '';
55
+ const webhookTimestamp = c.req.header('webhook-timestamp') ?? '';
56
+ const webhookSignature = c.req.header('webhook-signature') ?? '';
57
+ signature = `${webhookId}.${webhookTimestamp}.${webhookSignature}`;
58
+ break;
59
+ }
60
+ }
61
+
62
+ if (!signature || signature === '..') {
63
+ logger.warn({ provider: provider.type }, 'Billing webhook received without signature');
64
+ return c.json({ error: 'Missing webhook signature' }, 400);
65
+ }
66
+
67
+ let event: { type: string; data: Record<string, unknown> };
68
+
69
+ try {
70
+ const rawBody = await c.req.text();
71
+ event = await provider.handleWebhook({ body: rawBody, signature });
72
+ } catch (error) {
73
+ const message = error instanceof Error ? error.message : 'Unknown error';
74
+ logger.error(
75
+ { error: message, provider: provider.type },
76
+ 'Billing webhook signature verification failed'
77
+ );
78
+ return c.json({ error: 'Invalid signature' }, 400);
79
+ }
80
+
81
+ logger.info({ type: event.type, provider: provider.type }, 'Processing billing webhook event');
82
+
83
+ try {
84
+ switch (event.type) {
85
+ case 'checkout.completed':
86
+ await handleCheckoutCompleted(event.data);
87
+ break;
88
+
89
+ case 'subscription.created':
90
+ case 'subscription.updated':
91
+ await handleSubscriptionUpsert(event.data);
92
+ break;
93
+
94
+ case 'subscription.deleted':
95
+ await handleSubscriptionDeleted(event.data);
96
+ break;
97
+
98
+ case 'payment.failed':
99
+ await handlePaymentFailed(event.data);
100
+ break;
101
+
102
+ default:
103
+ logger.debug({ type: event.type }, 'Unhandled billing webhook event type');
104
+ }
105
+
106
+ return c.json({ received: true });
107
+ } catch (error) {
108
+ const message = error instanceof Error ? error.message : 'Unknown error';
109
+ logger.error(
110
+ { error: message, eventType: event.type, provider: provider.type },
111
+ 'Failed to process billing webhook event'
112
+ );
113
+ return c.json({ error: 'Webhook handler failed' }, 500);
114
+ }
115
+ });
116
+
117
+ // ============================================================================
118
+ // EVENT HANDLERS (provider-agnostic)
119
+ // ============================================================================
120
+
121
+ /**
122
+ * Handle a completed checkout from any provider.
123
+ * The data contains normalized fields: customerId, subscriptionId, customerEmail.
124
+ */
125
+ async function handleCheckoutCompleted(data: Record<string, unknown>): Promise<void> {
126
+ const customerId = data.customerId as string | null;
127
+ const subscriptionId = data.subscriptionId as string | null;
128
+ const customerEmail = data.customerEmail as string | null;
129
+
130
+ if (!customerId || !subscriptionId) {
131
+ logger.debug('Checkout completed event missing customerId or subscriptionId');
132
+ return;
133
+ }
134
+
135
+ logger.info({ customerId, subscriptionId }, 'Checkout completed');
136
+
137
+ // Find customer in our database
138
+ const { data: customer, error: customerError } = await adminDb
139
+ .from('customers')
140
+ .select('id')
141
+ .eq('stripe_customer_id', customerId)
142
+ .single();
143
+
144
+ if (customerError || !customer) {
145
+ logger.error({ customerId, error: customerError }, 'Customer not found for checkout');
146
+ throw new Error(`Customer not found: ${customerId}`);
147
+ }
148
+
149
+ // If the provider sent subscription data inline, upsert it
150
+ if (data.status && data.priceId) {
151
+ await upsertSubscriptionFromData(customer.id, data);
152
+ } else {
153
+ // For providers like Stripe that require a separate fetch, get the full subscription
154
+ try {
155
+ const provider = getBillingProvider();
156
+ const subInfo = await provider.getSubscription({ customerId, subscriptionId });
157
+ if (subInfo) {
158
+ await upsertSubscriptionFromInfo(customer.id, subscriptionId, subInfo);
159
+ }
160
+ } catch (error) {
161
+ logger.warn({ error, subscriptionId }, 'Could not fetch subscription details after checkout');
162
+ }
163
+ }
164
+
165
+ // Send confirmation email
166
+ if (customerEmail) {
167
+ try {
168
+ const provider = getBillingProvider();
169
+ const subInfo = await provider.getSubscription({ customerId, subscriptionId });
170
+ if (subInfo) {
171
+ const template = subscriptionConfirmEmail({
172
+ planName: subInfo.product.name,
173
+ amount: `$${(subInfo.price.unitAmount / 100).toFixed(2)}`,
174
+ interval: subInfo.price.interval,
175
+ });
176
+ sendEmail({ to: customerEmail, ...template }).catch((err) => {
177
+ logger.error({ error: err }, 'Failed to send subscription confirmation email');
178
+ });
179
+ }
180
+ } catch (emailErr) {
181
+ logger.error({ error: emailErr }, 'Failed to prepare subscription confirmation email');
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Handle subscription create/update from any provider.
188
+ */
189
+ async function handleSubscriptionUpsert(data: Record<string, unknown>): Promise<void> {
190
+ const customerId = data.customerId as string | null;
191
+ const subscriptionId = data.subscriptionId as string | null;
192
+
193
+ if (!customerId || !subscriptionId) {
194
+ logger.warn('Subscription event missing customerId or subscriptionId');
195
+ return;
196
+ }
197
+
198
+ logger.info({ subscriptionId, status: data.status }, 'Subscription upserted');
199
+
200
+ // Find customer in our database
201
+ const { data: customer, error: customerError } = await adminDb
202
+ .from('customers')
203
+ .select('id')
204
+ .eq('stripe_customer_id', customerId)
205
+ .single();
206
+
207
+ if (customerError || !customer) {
208
+ logger.error({ customerId, error: customerError }, 'Customer not found for subscription');
209
+ throw new Error(`Customer not found: ${customerId}`);
210
+ }
211
+
212
+ await upsertSubscriptionFromData(customer.id, data);
213
+ }
214
+
215
+ /**
216
+ * Handle subscription deletion/cancellation.
217
+ */
218
+ async function handleSubscriptionDeleted(data: Record<string, unknown>): Promise<void> {
219
+ const subscriptionId = data.subscriptionId as string | null;
220
+
221
+ if (!subscriptionId) {
222
+ logger.warn('Subscription delete event missing subscriptionId');
223
+ return;
224
+ }
225
+
226
+ logger.info({ subscriptionId }, 'Subscription deleted');
227
+
228
+ const { error } = await adminDb
229
+ .from('subscriptions')
230
+ .update({
231
+ status: 'canceled',
232
+ canceled_at: new Date().toISOString(),
233
+ })
234
+ .eq('stripe_subscription_id', subscriptionId);
235
+
236
+ if (error) {
237
+ logger.error({ error, subscriptionId }, 'Failed to update canceled subscription');
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Handle a failed payment.
244
+ */
245
+ async function handlePaymentFailed(data: Record<string, unknown>): Promise<void> {
246
+ const subscriptionId = data.subscriptionId as string | null;
247
+
248
+ if (!subscriptionId) {
249
+ logger.debug('Payment failed event without subscription');
250
+ return;
251
+ }
252
+
253
+ logger.warn({ subscriptionId }, 'Payment failed for subscription');
254
+
255
+ const { error } = await adminDb
256
+ .from('subscriptions')
257
+ .update({ status: 'past_due' })
258
+ .eq('stripe_subscription_id', subscriptionId);
259
+
260
+ if (error) {
261
+ logger.error({ error, subscriptionId }, 'Failed to update subscription status to past_due');
262
+ throw error;
263
+ }
264
+
265
+ // Send payment failed email
266
+ try {
267
+ const { data: sub } = await adminDb
268
+ .from('subscriptions')
269
+ .select('customer_id')
270
+ .eq('stripe_subscription_id', subscriptionId)
271
+ .single();
272
+
273
+ if (sub) {
274
+ const { data: customer } = await adminDb
275
+ .from('customers')
276
+ .select('email')
277
+ .eq('id', sub.customer_id)
278
+ .single();
279
+
280
+ if (customer?.email) {
281
+ const baseUrl = process.env.SITE_URL || 'http://localhost:5173';
282
+ const template = paymentFailedEmail({
283
+ planName: 'your',
284
+ billingUrl: `${baseUrl}/settings/billing`,
285
+ });
286
+ sendEmail({ to: customer.email, ...template }).catch((err) => {
287
+ logger.error({ error: err }, 'Failed to send payment failed email');
288
+ });
289
+ }
290
+ }
291
+ } catch (emailErr) {
292
+ logger.error({ error: emailErr }, 'Failed to prepare payment failed email');
293
+ }
294
+ }
295
+
296
+ // ============================================================================
297
+ // HELPERS
298
+ // ============================================================================
299
+
300
+ /**
301
+ * Upsert a subscription record using normalized webhook event data.
302
+ * Used when the webhook payload contains all needed subscription fields.
303
+ */
304
+ async function upsertSubscriptionFromData(
305
+ dbCustomerId: string,
306
+ data: Record<string, unknown>
307
+ ): Promise<void> {
308
+ const subscriptionId = data.subscriptionId as string;
309
+ const priceId = data.priceId as string;
310
+
311
+ if (!priceId) {
312
+ throw new Error('Subscription event has no priceId');
313
+ }
314
+
315
+ const subscriptionData = {
316
+ customer_id: dbCustomerId,
317
+ stripe_subscription_id: subscriptionId,
318
+ stripe_price_id: priceId,
319
+ status: (data.status as string) ?? 'active',
320
+ current_period_start: (data.currentPeriodStart as string) ?? null,
321
+ current_period_end: (data.currentPeriodEnd as string) ?? null,
322
+ cancel_at_period_end: (data.cancelAtPeriodEnd as boolean) ?? false,
323
+ canceled_at: (data.canceledAt as string) ?? null,
324
+ };
325
+
326
+ const { error } = await adminDb.from('subscriptions').upsert(subscriptionData, {
327
+ onConflict: 'stripe_subscription_id',
328
+ });
329
+
330
+ if (error) {
331
+ logger.error({ error, subscriptionId }, 'Failed to upsert subscription');
332
+ throw error;
333
+ }
334
+
335
+ logger.info({ subscriptionId, status: data.status }, 'Subscription record upserted');
336
+ }
337
+
338
+ /**
339
+ * Upsert a subscription record using SubscriptionInfo from the provider API.
340
+ * Used when we need to fetch subscription details separately (e.g., after checkout).
341
+ */
342
+ async function upsertSubscriptionFromInfo(
343
+ dbCustomerId: string,
344
+ subscriptionId: string,
345
+ info: {
346
+ status: string;
347
+ priceId: string;
348
+ currentPeriodEnd: string | null;
349
+ cancelAtPeriodEnd: boolean;
350
+ }
351
+ ): Promise<void> {
352
+ const subscriptionData = {
353
+ customer_id: dbCustomerId,
354
+ stripe_subscription_id: subscriptionId,
355
+ stripe_price_id: info.priceId,
356
+ status: info.status,
357
+ current_period_end: info.currentPeriodEnd,
358
+ cancel_at_period_end: info.cancelAtPeriodEnd,
359
+ };
360
+
361
+ const { error } = await adminDb.from('subscriptions').upsert(subscriptionData, {
362
+ onConflict: 'stripe_subscription_id',
363
+ });
364
+
365
+ if (error) {
366
+ logger.error({ error, subscriptionId }, 'Failed to upsert subscription from provider info');
367
+ throw error;
368
+ }
369
+
370
+ logger.info(
371
+ { subscriptionId, status: info.status },
372
+ 'Subscription record upserted from provider'
373
+ );
374
+ }
375
+
376
+ export { billingWebhookRoutes };
@@ -0,0 +1,276 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { Hono } from 'hono';
3
+ import type Stripe from 'stripe';
4
+ import { paymentFailedEmail, subscriptionConfirmEmail } from '../../emails/templates';
5
+ import { sendEmail } from '../../lib/email';
6
+ import { logger } from '../../lib/logger';
7
+ import { getStripe, verifyWebhookSignature } from '../../lib/stripe';
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 stripeWebhookRoutes = new Hono<{ Variables: HonoVariables }>();
16
+
17
+ /**
18
+ * Handle Stripe webhook events
19
+ *
20
+ * IMPORTANT: This endpoint must receive the raw body for signature verification.
21
+ * The body parser middleware should be skipped for this route.
22
+ */
23
+ stripeWebhookRoutes.post('/', async (c) => {
24
+ const signature = c.req.header('stripe-signature');
25
+
26
+ if (!signature) {
27
+ logger.warn('Stripe webhook received without signature');
28
+ return c.json({ error: 'Missing stripe-signature header' }, 400);
29
+ }
30
+
31
+ let event: Stripe.Event;
32
+
33
+ try {
34
+ // Get raw body for signature verification
35
+ const rawBody = await c.req.text();
36
+ event = verifyWebhookSignature(rawBody, signature);
37
+ } catch (error) {
38
+ const message = error instanceof Error ? error.message : 'Unknown error';
39
+ logger.error({ error: message }, 'Stripe webhook signature verification failed');
40
+ return c.json({ error: 'Invalid signature' }, 400);
41
+ }
42
+
43
+ logger.info({ type: event.type, id: event.id }, 'Processing Stripe webhook event');
44
+
45
+ try {
46
+ switch (event.type) {
47
+ case 'checkout.session.completed':
48
+ await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);
49
+ break;
50
+
51
+ case 'customer.subscription.created':
52
+ case 'customer.subscription.updated':
53
+ await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
54
+ break;
55
+
56
+ case 'customer.subscription.deleted':
57
+ await handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
58
+ break;
59
+
60
+ case 'invoice.payment_failed':
61
+ await handlePaymentFailed(event.data.object as Stripe.Invoice);
62
+ break;
63
+
64
+ default:
65
+ logger.debug({ type: event.type }, 'Unhandled webhook event type');
66
+ }
67
+
68
+ return c.json({ received: true });
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : 'Unknown error';
71
+ logger.error({ error: message, eventType: event.type }, 'Failed to process webhook event');
72
+ return c.json({ error: 'Webhook handler failed' }, 500);
73
+ }
74
+ });
75
+
76
+ // ============================================================================
77
+ // EVENT HANDLERS
78
+ // ============================================================================
79
+
80
+ async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
81
+ if (session.mode !== 'subscription') {
82
+ logger.debug('Ignoring non-subscription checkout session');
83
+ return;
84
+ }
85
+
86
+ const subscriptionId = session.subscription as string;
87
+ const customerId = session.customer as string;
88
+
89
+ logger.info({ subscriptionId, customerId }, 'Checkout session completed');
90
+
91
+ // Fetch full subscription details
92
+ const stripe = getStripe();
93
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId);
94
+
95
+ // Find customer in our database
96
+ const { data: customer, error: customerError } = await adminDb
97
+ .from('customers')
98
+ .select('id')
99
+ .eq('stripe_customer_id', customerId)
100
+ .single();
101
+
102
+ if (customerError || !customer) {
103
+ logger.error({ customerId, error: customerError }, 'Customer not found for subscription');
104
+ throw new Error(`Customer not found: ${customerId}`);
105
+ }
106
+
107
+ // Create or update subscription record
108
+ await upsertSubscription(customer.id, subscription);
109
+
110
+ // Send subscription confirmation email
111
+ const stripeObj = getStripe();
112
+ try {
113
+ const price = await stripeObj.prices.retrieve(subscription.items.data[0]?.price.id, {
114
+ expand: ['product'],
115
+ });
116
+ const product = price.product as Stripe.Product;
117
+ const customerEmail = (session as Stripe.Checkout.Session).customer_details?.email;
118
+
119
+ if (customerEmail) {
120
+ const template = subscriptionConfirmEmail({
121
+ planName: product.name,
122
+ amount: `$${((price.unit_amount ?? 0) / 100).toFixed(2)}`,
123
+ interval: price.recurring?.interval ?? 'month',
124
+ });
125
+ sendEmail({ to: customerEmail, ...template }).catch((err) => {
126
+ logger.error({ error: err }, 'Failed to send subscription confirmation email');
127
+ });
128
+ }
129
+ } catch (emailErr) {
130
+ logger.error({ error: emailErr }, 'Failed to prepare subscription confirmation email');
131
+ }
132
+ }
133
+
134
+ async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
135
+ const customerId = subscription.customer as string;
136
+
137
+ logger.info(
138
+ { subscriptionId: subscription.id, status: subscription.status },
139
+ 'Subscription updated'
140
+ );
141
+
142
+ // Find customer in our database
143
+ const { data: customer, error: customerError } = await adminDb
144
+ .from('customers')
145
+ .select('id')
146
+ .eq('stripe_customer_id', customerId)
147
+ .single();
148
+
149
+ if (customerError || !customer) {
150
+ logger.error(
151
+ { customerId, error: customerError },
152
+ 'Customer not found for subscription update'
153
+ );
154
+ throw new Error(`Customer not found: ${customerId}`);
155
+ }
156
+
157
+ await upsertSubscription(customer.id, subscription);
158
+ }
159
+
160
+ async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
161
+ logger.info({ subscriptionId: subscription.id }, 'Subscription deleted');
162
+
163
+ const { error } = await adminDb
164
+ .from('subscriptions')
165
+ .update({
166
+ status: 'canceled',
167
+ canceled_at: new Date().toISOString(),
168
+ })
169
+ .eq('stripe_subscription_id', subscription.id);
170
+
171
+ if (error) {
172
+ logger.error(
173
+ { error, subscriptionId: subscription.id },
174
+ 'Failed to update canceled subscription'
175
+ );
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ async function handlePaymentFailed(invoice: Stripe.Invoice) {
181
+ // In Stripe 20.x, subscription is accessed via parent property
182
+ const invoiceWithSub = invoice as unknown as {
183
+ subscription?: string | { id: string } | null;
184
+ };
185
+ const subscriptionId =
186
+ typeof invoiceWithSub.subscription === 'string'
187
+ ? invoiceWithSub.subscription
188
+ : invoiceWithSub.subscription?.id;
189
+
190
+ if (!subscriptionId) {
191
+ logger.debug('Ignoring invoice without subscription');
192
+ return;
193
+ }
194
+
195
+ logger.warn({ subscriptionId, invoiceId: invoice.id }, 'Payment failed for subscription');
196
+
197
+ const { error } = await adminDb
198
+ .from('subscriptions')
199
+ .update({ status: 'past_due' })
200
+ .eq('stripe_subscription_id', subscriptionId);
201
+
202
+ if (error) {
203
+ logger.error({ error, subscriptionId }, 'Failed to update subscription status to past_due');
204
+ throw error;
205
+ }
206
+
207
+ // Send payment failed email
208
+ try {
209
+ const stripe = getStripe();
210
+ const sub = await stripe.subscriptions.retrieve(subscriptionId, {
211
+ expand: ['items.data.price.product'],
212
+ });
213
+ const customerObj = await stripe.customers.retrieve(sub.customer as string);
214
+ if (customerObj && !customerObj.deleted && customerObj.email) {
215
+ const product = sub.items.data[0]?.price.product as Stripe.Product;
216
+ const baseUrl = process.env.SITE_URL || 'http://localhost:5173';
217
+ const template = paymentFailedEmail({
218
+ planName: product?.name ?? 'your',
219
+ billingUrl: `${baseUrl}/settings/billing`,
220
+ });
221
+ sendEmail({ to: customerObj.email, ...template }).catch((err) => {
222
+ logger.error({ error: err }, 'Failed to send payment failed email');
223
+ });
224
+ }
225
+ } catch (emailErr) {
226
+ logger.error({ error: emailErr }, 'Failed to prepare payment failed email');
227
+ }
228
+ }
229
+
230
+ // ============================================================================
231
+ // HELPERS
232
+ // ============================================================================
233
+
234
+ async function upsertSubscription(customerId: string, subscription: Stripe.Subscription) {
235
+ const priceId = subscription.items.data[0]?.price.id;
236
+
237
+ if (!priceId) {
238
+ throw new Error('Subscription has no price');
239
+ }
240
+
241
+ const currentPeriodStart = (subscription as unknown as { current_period_start?: number })
242
+ .current_period_start;
243
+ const currentPeriodEnd = (subscription as unknown as { current_period_end?: number })
244
+ .current_period_end;
245
+
246
+ const subscriptionData = {
247
+ customer_id: customerId,
248
+ stripe_subscription_id: subscription.id,
249
+ stripe_price_id: priceId,
250
+ status: subscription.status,
251
+ current_period_start: currentPeriodStart
252
+ ? new Date(currentPeriodStart * 1000).toISOString()
253
+ : null,
254
+ current_period_end: currentPeriodEnd ? new Date(currentPeriodEnd * 1000).toISOString() : null,
255
+ cancel_at_period_end: subscription.cancel_at_period_end,
256
+ canceled_at: subscription.canceled_at
257
+ ? new Date(subscription.canceled_at * 1000).toISOString()
258
+ : null,
259
+ };
260
+
261
+ const { error } = await adminDb.from('subscriptions').upsert(subscriptionData, {
262
+ onConflict: 'stripe_subscription_id',
263
+ });
264
+
265
+ if (error) {
266
+ logger.error({ error, subscriptionId: subscription.id }, 'Failed to upsert subscription');
267
+ throw error;
268
+ }
269
+
270
+ logger.info(
271
+ { subscriptionId: subscription.id, status: subscription.status },
272
+ 'Subscription record upserted'
273
+ );
274
+ }
275
+
276
+ export { stripeWebhookRoutes };
@@ -0,0 +1,11 @@
1
+ import type { User } from '@supabase/supabase-js';
2
+ import type { getSupabaseClient } from './lib/supabase';
3
+
4
+ /**
5
+ * Shared Hono context variables for authenticated routes.
6
+ * Used to type the user and supabase client available via c.get().
7
+ */
8
+ export type HonoVariables = {
9
+ user: User | null;
10
+ supabase: ReturnType<typeof getSupabaseClient>;
11
+ };