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,314 @@
1
+ /**
2
+ * Paddle billing provider implementation.
3
+ *
4
+ * Uses Paddle API v2 via fetch (no SDK dependency). Supports both sandbox
5
+ * and production environments via PADDLE_ENVIRONMENT env var.
6
+ *
7
+ * Paddle API docs: https://developer.paddle.com/api-reference/overview
8
+ */
9
+
10
+ import { createHmac } from 'node:crypto';
11
+ import { env } from '../../lib/env';
12
+ import { logger } from '../../lib/logger';
13
+ import type { BillingProvider, CheckoutResult, PortalResult, SubscriptionInfo } from '../provider';
14
+
15
+ // Paddle API v2 base URLs
16
+ const PADDLE_API_URLS = {
17
+ sandbox: 'https://sandbox-api.paddle.com',
18
+ production: 'https://api.paddle.com',
19
+ } as const;
20
+
21
+ export class PaddleProvider implements BillingProvider {
22
+ readonly type = 'paddle' as const;
23
+
24
+ private get apiKey(): string {
25
+ if (!env.PADDLE_API_KEY) {
26
+ throw new Error('Paddle is not configured. Set PADDLE_API_KEY in your environment.');
27
+ }
28
+ return env.PADDLE_API_KEY;
29
+ }
30
+
31
+ private get baseUrl(): string {
32
+ const environment = env.PADDLE_ENVIRONMENT ?? 'sandbox';
33
+ return PADDLE_API_URLS[environment];
34
+ }
35
+
36
+ isConfigured(): boolean {
37
+ return !!env.PADDLE_API_KEY;
38
+ }
39
+
40
+ async createCheckout(params: {
41
+ customerId: string;
42
+ priceId: string;
43
+ successUrl: string;
44
+ cancelUrl: string;
45
+ metadata: Record<string, string>;
46
+ }): Promise<CheckoutResult> {
47
+ // Paddle uses Transactions API to create checkout sessions
48
+ const response = await this.request('POST', '/transactions', {
49
+ customer_id: params.customerId,
50
+ items: [{ price_id: params.priceId, quantity: 1 }],
51
+ checkout: {
52
+ url: params.successUrl,
53
+ },
54
+ custom_data: params.metadata,
55
+ });
56
+
57
+ // biome-ignore lint/suspicious/noExplicitAny: Paddle API response shape
58
+ const data = response.data as any;
59
+
60
+ return {
61
+ url: (data.checkout?.url as string) ?? '',
62
+ sessionId: data.id as string,
63
+ };
64
+ }
65
+
66
+ async createPortalSession(params: {
67
+ customerId: string;
68
+ returnUrl: string;
69
+ }): Promise<PortalResult> {
70
+ // Paddle provides a hosted customer portal via the Customers API
71
+ const response = await this.request('POST', `/customers/${params.customerId}/portal-sessions`, {
72
+ return_url: params.returnUrl,
73
+ });
74
+
75
+ // SECURITY: The portal URL is a short-lived link from Paddle
76
+ // biome-ignore lint/suspicious/noExplicitAny: Paddle API response shape
77
+ const data = response.data as any;
78
+ return { url: (data.urls?.general?.overview as string) ?? '' };
79
+ }
80
+
81
+ async getSubscription(params: {
82
+ customerId: string;
83
+ subscriptionId: string;
84
+ }): Promise<SubscriptionInfo | null> {
85
+ try {
86
+ const response = await this.request(
87
+ 'GET',
88
+ `/subscriptions/${params.subscriptionId}?include=next_transaction`
89
+ );
90
+
91
+ // biome-ignore lint/suspicious/noExplicitAny: Paddle API response shape
92
+ const sub = response.data as any;
93
+ if (!sub) return null;
94
+
95
+ const item = sub.items?.[0];
96
+ const price = item?.price;
97
+ const product = price?.product;
98
+
99
+ return {
100
+ id: sub.id as string,
101
+ status: this.normalizePaddleStatus(String(sub.status ?? '')),
102
+ planId: this.resolvePlanId(String(price?.id ?? '')),
103
+ priceId: String(price?.id ?? ''),
104
+ currentPeriodEnd: (sub.current_billing_period?.ends_at as string) ?? null,
105
+ cancelAtPeriodEnd: sub.scheduled_change?.action === 'cancel',
106
+ price: {
107
+ unitAmount: Number.parseInt(String(price?.unit_price?.amount ?? '0'), 10),
108
+ currency: String(price?.unit_price?.currency_code ?? 'USD'),
109
+ interval: String(price?.billing_cycle?.interval ?? 'month'),
110
+ },
111
+ product: {
112
+ id: String(product?.id ?? ''),
113
+ name: String(product?.name ?? ''),
114
+ },
115
+ };
116
+ } catch (error) {
117
+ logger.error(
118
+ { error, subscriptionId: params.subscriptionId },
119
+ 'Failed to fetch Paddle subscription'
120
+ );
121
+ return null;
122
+ }
123
+ }
124
+
125
+ async handleWebhook(params: {
126
+ body: string;
127
+ signature: string;
128
+ }): Promise<{ type: string; data: Record<string, unknown> }> {
129
+ // Verify Paddle webhook signature
130
+ this.verifyWebhookSignature(params.body, params.signature);
131
+
132
+ const payload = JSON.parse(params.body);
133
+ const eventType = payload.event_type as string;
134
+ const normalized = this.normalizeEventType(eventType);
135
+
136
+ return {
137
+ type: normalized,
138
+ data: this.extractEventData(eventType, payload.data ?? {}),
139
+ };
140
+ }
141
+
142
+ async createCustomer(params: {
143
+ email: string;
144
+ name: string;
145
+ metadata: Record<string, string>;
146
+ }): Promise<string> {
147
+ const response = await this.request('POST', '/customers', {
148
+ email: params.email,
149
+ name: params.name,
150
+ custom_data: params.metadata,
151
+ });
152
+
153
+ return response.data.id as string;
154
+ }
155
+
156
+ async deleteCustomer(_customerId: string): Promise<void> {
157
+ // Paddle does not support customer deletion via API.
158
+ // Customers can be archived but not removed. Log and no-op.
159
+ logger.info({ customerId: _customerId }, 'Paddle does not support customer deletion; skipping');
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Private helpers
164
+ // ---------------------------------------------------------------------------
165
+
166
+ /** Make an authenticated request to the Paddle API v2. */
167
+ private async request(
168
+ method: string,
169
+ path: string,
170
+ body?: Record<string, unknown>
171
+ ): Promise<{ data: Record<string, unknown> }> {
172
+ const url = `${this.baseUrl}${path}`;
173
+
174
+ const headers: Record<string, string> = {
175
+ Authorization: `Bearer ${this.apiKey}`,
176
+ 'Content-Type': 'application/json',
177
+ };
178
+
179
+ const options: RequestInit = { method, headers };
180
+ if (body && method !== 'GET') {
181
+ options.body = JSON.stringify(body);
182
+ }
183
+
184
+ const response = await fetch(url, options);
185
+
186
+ if (!response.ok) {
187
+ const errorBody = await response.text();
188
+ logger.error({ status: response.status, body: errorBody, path }, 'Paddle API request failed');
189
+ throw new Error(`Paddle API error (${response.status}): ${errorBody}`);
190
+ }
191
+
192
+ // biome-ignore lint/suspicious/noExplicitAny: Paddle API response shape varies
193
+ const json = (await response.json()) as any;
194
+ return { data: json.data ?? json };
195
+ }
196
+
197
+ /**
198
+ * Verify the Paddle webhook signature (Paddle Billing v2 format).
199
+ *
200
+ * Paddle v2 sends the signature in a header with format:
201
+ * `ts=<timestamp>;h1=<hmac_hex>`
202
+ *
203
+ * The HMAC is computed over `<timestamp>:<raw_body>` using the webhook secret.
204
+ */
205
+ private verifyWebhookSignature(body: string, signatureHeader: string): void {
206
+ if (!env.PADDLE_WEBHOOK_SECRET) {
207
+ throw new Error('Paddle webhook secret is not configured');
208
+ }
209
+
210
+ // Parse ts=...; h1=...
211
+ const parts = signatureHeader.split(';');
212
+ const tsEntry = parts.find((p) => p.trim().startsWith('ts='));
213
+ const h1Entry = parts.find((p) => p.trim().startsWith('h1='));
214
+
215
+ if (!tsEntry || !h1Entry) {
216
+ throw new Error('Invalid Paddle webhook signature format');
217
+ }
218
+
219
+ const timestamp = tsEntry.trim().slice(3);
220
+ const providedHash = h1Entry.trim().slice(3);
221
+
222
+ const payload = `${timestamp}:${body}`;
223
+ const expectedHash = createHmac('sha256', env.PADDLE_WEBHOOK_SECRET)
224
+ .update(payload)
225
+ .digest('hex');
226
+
227
+ if (expectedHash !== providedHash) {
228
+ throw new Error('Invalid Paddle webhook signature');
229
+ }
230
+ }
231
+
232
+ /** Map Paddle event types to normalized billing event names. */
233
+ private normalizeEventType(paddleType: string): string {
234
+ const map: Record<string, string> = {
235
+ 'transaction.completed': 'checkout.completed',
236
+ 'subscription.created': 'subscription.created',
237
+ 'subscription.updated': 'subscription.updated',
238
+ 'subscription.canceled': 'subscription.deleted',
239
+ 'subscription.past_due': 'payment.failed',
240
+ 'transaction.payment_failed': 'payment.failed',
241
+ };
242
+ return map[paddleType] ?? paddleType;
243
+ }
244
+
245
+ /** Map Paddle subscription status to a normalized string. */
246
+ private normalizePaddleStatus(paddleStatus: string): string {
247
+ const map: Record<string, string> = {
248
+ active: 'active',
249
+ paused: 'paused',
250
+ past_due: 'past_due',
251
+ canceled: 'canceled',
252
+ trialing: 'trialing',
253
+ };
254
+ return map[paddleStatus] ?? paddleStatus;
255
+ }
256
+
257
+ /** Map a Paddle price ID to a plan ID using configured env vars. */
258
+ private resolvePlanId(priceId: string): string {
259
+ if (priceId === env.PADDLE_PRICE_STARTER) return 'starter';
260
+ if (priceId === env.PADDLE_PRICE_PRO) return 'pro';
261
+ return 'free';
262
+ }
263
+
264
+ /** Extract relevant data from a Paddle webhook event payload. */
265
+ // biome-ignore lint/suspicious/noExplicitAny: Paddle webhook shapes vary by event type
266
+ private extractEventData(eventType: string, data: any): Record<string, unknown> {
267
+ switch (eventType) {
268
+ case 'transaction.completed':
269
+ return {
270
+ provider: 'paddle',
271
+ mode: 'subscription',
272
+ customerId: data.customer_id ?? null,
273
+ subscriptionId: data.subscription_id ?? null,
274
+ customerEmail: null, // Paddle transactions don't always include email
275
+ };
276
+
277
+ case 'subscription.created':
278
+ case 'subscription.updated': {
279
+ const item = data.items?.[0];
280
+ const priceId = item?.price?.id ?? '';
281
+ return {
282
+ provider: 'paddle',
283
+ subscriptionId: data.id,
284
+ customerId: data.customer_id ?? null,
285
+ status: this.normalizePaddleStatus(data.status ?? ''),
286
+ priceId,
287
+ currentPeriodStart: data.current_billing_period?.starts_at ?? null,
288
+ currentPeriodEnd: data.current_billing_period?.ends_at ?? null,
289
+ cancelAtPeriodEnd: data.scheduled_change?.action === 'cancel',
290
+ canceledAt: data.canceled_at ?? null,
291
+ };
292
+ }
293
+
294
+ case 'subscription.canceled':
295
+ return {
296
+ provider: 'paddle',
297
+ subscriptionId: data.id,
298
+ customerId: data.customer_id ?? null,
299
+ status: 'canceled',
300
+ };
301
+
302
+ case 'subscription.past_due':
303
+ case 'transaction.payment_failed':
304
+ return {
305
+ provider: 'paddle',
306
+ subscriptionId: data.subscription_id ?? data.id ?? null,
307
+ invoiceId: data.id ?? null,
308
+ };
309
+
310
+ default:
311
+ return { provider: 'paddle', raw: data };
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Polar billing provider implementation.
3
+ *
4
+ * Uses the Polar API via fetch (no SDK dependency). Polar is a modern billing
5
+ * platform for developers. API docs: https://docs.polar.sh/api-reference
6
+ */
7
+
8
+ import { createHmac, timingSafeEqual } from 'node:crypto';
9
+ import { env } from '../../lib/env';
10
+ import { logger } from '../../lib/logger';
11
+ import type { BillingProvider, CheckoutResult, PortalResult, SubscriptionInfo } from '../provider';
12
+
13
+ const POLAR_API_URL = 'https://api.polar.sh';
14
+
15
+ export class PolarProvider implements BillingProvider {
16
+ readonly type = 'polar' as const;
17
+
18
+ private get accessToken(): string {
19
+ if (!env.POLAR_ACCESS_TOKEN) {
20
+ throw new Error('Polar is not configured. Set POLAR_ACCESS_TOKEN in your environment.');
21
+ }
22
+ return env.POLAR_ACCESS_TOKEN;
23
+ }
24
+
25
+ isConfigured(): boolean {
26
+ return !!env.POLAR_ACCESS_TOKEN;
27
+ }
28
+
29
+ async createCheckout(params: {
30
+ customerId: string;
31
+ priceId: string;
32
+ successUrl: string;
33
+ cancelUrl: string;
34
+ metadata: Record<string, string>;
35
+ }): Promise<CheckoutResult> {
36
+ // Polar uses the Checkouts API
37
+ const response = await this.request('POST', '/v1/checkouts', {
38
+ product_price_id: params.priceId,
39
+ customer_id: params.customerId,
40
+ success_url: params.successUrl,
41
+ metadata: params.metadata,
42
+ });
43
+
44
+ const data = response.data;
45
+
46
+ return {
47
+ url: data.url as string,
48
+ sessionId: data.id as string,
49
+ };
50
+ }
51
+
52
+ async createPortalSession(params: {
53
+ customerId: string;
54
+ returnUrl: string;
55
+ }): Promise<PortalResult> {
56
+ // Polar provides customer portal sessions via the Customers API
57
+ const response = await this.request(
58
+ 'POST',
59
+ `/v1/customers/${params.customerId}/portal/sessions`,
60
+ {
61
+ return_url: params.returnUrl,
62
+ }
63
+ );
64
+
65
+ return { url: response.data.url as string };
66
+ }
67
+
68
+ async getSubscription(params: {
69
+ customerId: string;
70
+ subscriptionId: string;
71
+ }): Promise<SubscriptionInfo | null> {
72
+ try {
73
+ const response = await this.request('GET', `/v1/subscriptions/${params.subscriptionId}`);
74
+
75
+ const sub = response.data;
76
+ if (!sub) return null;
77
+
78
+ const price = sub.price as Record<string, unknown> | undefined;
79
+ const product = sub.product as Record<string, unknown> | undefined;
80
+
81
+ return {
82
+ id: sub.id as string,
83
+ status: this.normalizePolarStatus(sub.status as string),
84
+ planId: this.resolvePlanId((price?.id as string) ?? ''),
85
+ priceId: (price?.id as string) ?? '',
86
+ currentPeriodEnd: (sub.current_period_end as string) ?? null,
87
+ cancelAtPeriodEnd: (sub.cancel_at_period_end as boolean) ?? false,
88
+ price: {
89
+ unitAmount: (price?.price_amount as number) ?? 0,
90
+ currency: (price?.price_currency as string) ?? 'usd',
91
+ interval: (price?.recurring_interval as string) ?? 'month',
92
+ },
93
+ product: {
94
+ id: (product?.id as string) ?? '',
95
+ name: (product?.name as string) ?? '',
96
+ },
97
+ };
98
+ } catch (error) {
99
+ logger.error(
100
+ { error, subscriptionId: params.subscriptionId },
101
+ 'Failed to fetch Polar subscription'
102
+ );
103
+ return null;
104
+ }
105
+ }
106
+
107
+ async handleWebhook(params: {
108
+ body: string;
109
+ signature: string;
110
+ }): Promise<{ type: string; data: Record<string, unknown> }> {
111
+ // Verify Polar webhook signature (HMAC-SHA256)
112
+ this.verifyWebhookSignature(params.body, params.signature);
113
+
114
+ const payload = JSON.parse(params.body);
115
+ const eventType = payload.type as string;
116
+ const normalized = this.normalizeEventType(eventType);
117
+
118
+ return {
119
+ type: normalized,
120
+ data: this.extractEventData(eventType, payload.data ?? {}),
121
+ };
122
+ }
123
+
124
+ async createCustomer(params: {
125
+ email: string;
126
+ name: string;
127
+ metadata: Record<string, string>;
128
+ }): Promise<string> {
129
+ const body: Record<string, unknown> = {
130
+ email: params.email,
131
+ name: params.name,
132
+ metadata: params.metadata,
133
+ };
134
+
135
+ // Polar requires an organization_id for customer creation
136
+ if (env.POLAR_ORGANIZATION_ID) {
137
+ body.organization_id = env.POLAR_ORGANIZATION_ID;
138
+ }
139
+
140
+ const response = await this.request('POST', '/v1/customers', body);
141
+ return response.data.id as string;
142
+ }
143
+
144
+ async deleteCustomer(customerId: string): Promise<void> {
145
+ try {
146
+ await this.request('DELETE', `/v1/customers/${customerId}`);
147
+ } catch (error) {
148
+ // Polar may return 404 if customer was already removed
149
+ logger.warn(
150
+ { error, customerId },
151
+ 'Failed to delete Polar customer (may already be deleted)'
152
+ );
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Private helpers
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /** Make an authenticated request to the Polar API. */
161
+ private async request(
162
+ method: string,
163
+ path: string,
164
+ body?: Record<string, unknown>
165
+ ): Promise<{ data: Record<string, unknown> }> {
166
+ const url = `${POLAR_API_URL}${path}`;
167
+
168
+ const headers: Record<string, string> = {
169
+ Authorization: `Bearer ${this.accessToken}`,
170
+ 'Content-Type': 'application/json',
171
+ };
172
+
173
+ const options: RequestInit = { method, headers };
174
+ if (body && method !== 'GET') {
175
+ options.body = JSON.stringify(body);
176
+ }
177
+
178
+ const response = await fetch(url, options);
179
+
180
+ if (!response.ok) {
181
+ const errorBody = await response.text();
182
+ logger.error({ status: response.status, body: errorBody, path }, 'Polar API request failed');
183
+ throw new Error(`Polar API error (${response.status}): ${errorBody}`);
184
+ }
185
+
186
+ // DELETE responses may have no body
187
+ if (response.status === 204) {
188
+ return { data: {} };
189
+ }
190
+
191
+ // biome-ignore lint/suspicious/noExplicitAny: Polar API response shape varies
192
+ const json = (await response.json()) as any;
193
+ return { data: json };
194
+ }
195
+
196
+ /**
197
+ * Verify Polar webhook signature.
198
+ *
199
+ * Polar sends a webhook-id, webhook-timestamp, and webhook-signature header.
200
+ * The signature is an HMAC-SHA256 over `<webhook-id>.<timestamp>.<body>` using
201
+ * the webhook secret (base64-decoded).
202
+ *
203
+ * The signature header may contain multiple signatures separated by spaces,
204
+ * each prefixed with `v1,`. We check if any match.
205
+ */
206
+ private verifyWebhookSignature(body: string, signatureHeader: string): void {
207
+ if (!env.POLAR_WEBHOOK_SECRET) {
208
+ throw new Error('Polar webhook secret is not configured');
209
+ }
210
+
211
+ // The signatureHeader passed here is the combined value we construct
212
+ // in the billing webhook route: `${webhookId}.${timestamp}.${signature}`
213
+ const parts = signatureHeader.split('.');
214
+ if (parts.length < 3) {
215
+ throw new Error('Invalid Polar webhook signature format');
216
+ }
217
+
218
+ const webhookId = parts[0];
219
+ const timestamp = parts[1];
220
+ const signatures = parts.slice(2).join('.');
221
+
222
+ // Compute expected HMAC
223
+ // Polar webhook secrets are base64-encoded and prefixed with "whsec_"
224
+ const secretRaw = env.POLAR_WEBHOOK_SECRET.startsWith('whsec_')
225
+ ? env.POLAR_WEBHOOK_SECRET.slice(6)
226
+ : env.POLAR_WEBHOOK_SECRET;
227
+ const secretBytes = Buffer.from(secretRaw, 'base64');
228
+ const payload = `${webhookId}.${timestamp}.${body}`;
229
+ const expectedSig = createHmac('sha256', secretBytes).update(payload).digest('base64');
230
+
231
+ // The signature header may contain multiple v1 signatures
232
+ const providedSigs = signatures.split(' ').map((s) => {
233
+ if (s.startsWith('v1,')) return s.slice(3);
234
+ return s;
235
+ });
236
+
237
+ const expectedBuf = Buffer.from(expectedSig);
238
+ const matched = providedSigs.some((sig) => {
239
+ const sigBuf = Buffer.from(sig);
240
+ if (sigBuf.length !== expectedBuf.length) return false;
241
+ return timingSafeEqual(sigBuf, expectedBuf);
242
+ });
243
+
244
+ if (!matched) {
245
+ throw new Error('Invalid Polar webhook signature');
246
+ }
247
+ }
248
+
249
+ /** Map Polar event types to normalized billing event names. */
250
+ private normalizeEventType(polarType: string): string {
251
+ const map: Record<string, string> = {
252
+ 'checkout.created': 'checkout.completed',
253
+ 'subscription.created': 'subscription.created',
254
+ 'subscription.updated': 'subscription.updated',
255
+ 'subscription.revoked': 'subscription.deleted',
256
+ 'subscription.canceled': 'subscription.deleted',
257
+ };
258
+ return map[polarType] ?? polarType;
259
+ }
260
+
261
+ /** Map Polar subscription status to a normalized string. */
262
+ private normalizePolarStatus(polarStatus: string): string {
263
+ const map: Record<string, string> = {
264
+ active: 'active',
265
+ canceled: 'canceled',
266
+ revoked: 'canceled',
267
+ incomplete: 'incomplete',
268
+ incomplete_expired: 'expired',
269
+ trialing: 'trialing',
270
+ past_due: 'past_due',
271
+ unpaid: 'past_due',
272
+ };
273
+ return map[polarStatus] ?? polarStatus;
274
+ }
275
+
276
+ /** Map a Polar price ID to a plan ID using configured env vars. */
277
+ private resolvePlanId(priceId: string): string {
278
+ if (priceId === env.POLAR_PRICE_STARTER) return 'starter';
279
+ if (priceId === env.POLAR_PRICE_PRO) return 'pro';
280
+ return 'free';
281
+ }
282
+
283
+ /** Extract relevant data from a Polar webhook event payload. */
284
+ // biome-ignore lint/suspicious/noExplicitAny: Polar webhook shapes vary by event type
285
+ private extractEventData(eventType: string, data: any): Record<string, unknown> {
286
+ switch (eventType) {
287
+ case 'checkout.created':
288
+ return {
289
+ provider: 'polar',
290
+ mode: 'subscription',
291
+ customerId: data.customer_id ?? null,
292
+ subscriptionId: data.subscription_id ?? null,
293
+ customerEmail: data.customer_email ?? null,
294
+ };
295
+
296
+ case 'subscription.created':
297
+ case 'subscription.updated': {
298
+ const price = data.price;
299
+ return {
300
+ provider: 'polar',
301
+ subscriptionId: data.id,
302
+ customerId: data.customer_id ?? null,
303
+ status: this.normalizePolarStatus(data.status ?? ''),
304
+ priceId: price?.id ?? '',
305
+ currentPeriodStart: data.current_period_start ?? null,
306
+ currentPeriodEnd: data.current_period_end ?? null,
307
+ cancelAtPeriodEnd: data.cancel_at_period_end ?? false,
308
+ canceledAt: data.ended_at ?? null,
309
+ };
310
+ }
311
+
312
+ case 'subscription.revoked':
313
+ case 'subscription.canceled':
314
+ return {
315
+ provider: 'polar',
316
+ subscriptionId: data.id,
317
+ customerId: data.customer_id ?? null,
318
+ status: 'canceled',
319
+ };
320
+
321
+ default:
322
+ return { provider: 'polar', raw: data };
323
+ }
324
+ }
325
+ }