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,112 @@
1
+ import { z } from 'zod';
2
+
3
+ const envSchema = z.object({
4
+ // Supabase (required)
5
+ SUPABASE_URL: z.string().url(),
6
+ SUPABASE_ANON_KEY: z.string().min(1),
7
+ SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
8
+
9
+ // Public site URL — used in CSP connectSrc so the client can reach Supabase
10
+ // via the public domain (Traefik → Kong). Defaults to empty string in dev
11
+ // (CSP is disabled in development).
12
+ SITE_URL: z.string().url().optional(),
13
+
14
+ // Server
15
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
16
+ PORT: z.coerce.number().default(3000),
17
+
18
+ // Port configuration (for running multiple projects)
19
+ DEV_PORT_OFFSET: z.coerce.number().default(0),
20
+ DEV_VITE_PORT: z.coerce.number().optional(),
21
+ DEV_API_PORT: z.coerce.number().optional(),
22
+ DEV_DB_PORT: z.coerce.number().optional(),
23
+ DEV_KONG_PORT: z.coerce.number().optional(),
24
+ DEV_TRAEFIK_PORT: z.coerce.number().optional(),
25
+ DEV_GRAFANA_PORT: z.coerce.number().optional(),
26
+ DEV_PROMETHEUS_PORT: z.coerce.number().optional(),
27
+ DEV_LOKI_PORT: z.coerce.number().optional(),
28
+
29
+ // OAuth (optional - configure in Supabase dashboard)
30
+ GOOGLE_ENABLED: z.coerce.boolean().default(false),
31
+ MICROSOFT_ENABLED: z.coerce.boolean().default(false),
32
+ GITHUB_ENABLED: z.coerce.boolean().default(false),
33
+ APPLE_ENABLED: z.coerce.boolean().default(false),
34
+ DISCORD_ENABLED: z.coerce.boolean().default(false),
35
+ MAGIC_LINK_ENABLED: z.coerce.boolean().default(true),
36
+
37
+ // SMTP (shared with Supabase Auth)
38
+ SMTP_HOST: z.string().optional(),
39
+ SMTP_PORT: z.coerce.number().optional(),
40
+ SMTP_USER: z.string().optional(),
41
+ SMTP_PASS: z.string().optional(),
42
+ SMTP_ADMIN_EMAIL: z.string().email().optional(),
43
+ SMTP_SENDER_NAME: z.string().optional(),
44
+
45
+ // Billing provider selection (defaults to stripe for backward compatibility)
46
+ BILLING_PROVIDER: z.enum(['stripe', 'paddle', 'polar']).default('stripe'),
47
+
48
+ // Stripe (optional)
49
+ STRIPE_SECRET_KEY: z.string().optional(),
50
+ STRIPE_WEBHOOK_SECRET: z.string().optional(),
51
+ STRIPE_PRICE_STARTER: z.string().optional(),
52
+ STRIPE_PRICE_PRO: z.string().optional(),
53
+
54
+ // Paddle (optional - alternative billing provider)
55
+ PADDLE_API_KEY: z.string().optional(),
56
+ PADDLE_WEBHOOK_SECRET: z.string().optional(),
57
+ PADDLE_ENVIRONMENT: z.enum(['sandbox', 'production']).default('sandbox'),
58
+ PADDLE_PRICE_STARTER: z.string().optional(),
59
+ PADDLE_PRICE_PRO: z.string().optional(),
60
+
61
+ // Polar (optional - alternative billing provider)
62
+ POLAR_ACCESS_TOKEN: z.string().optional(),
63
+ POLAR_WEBHOOK_SECRET: z.string().optional(),
64
+ POLAR_ORGANIZATION_ID: z.string().optional(),
65
+ POLAR_PRICE_STARTER: z.string().optional(),
66
+ POLAR_PRICE_PRO: z.string().optional(),
67
+
68
+ // License tier price IDs (for one-time license purchases)
69
+ DIAMOND_PRICE_ID: z.string().optional(),
70
+ FULLERENE_PRICE_ID: z.string().optional(),
71
+
72
+ // Redis (optional - for distributed rate limiting)
73
+ // Format: redis://[:password@]host:port or redis://[:password@]host:port/db
74
+ REDIS_URL: z.string().url().optional(),
75
+ });
76
+
77
+ // Apply port offset to localhost URLs in development
78
+ // SUPABASE_URL and SITE_URL are hardcoded to default ports in .env but
79
+ // DEV_PORT_OFFSET shifts Docker ports (Kong on 8000+offset, Vite on 5173+offset).
80
+ const portOffset = Number.parseInt(process.env.DEV_PORT_OFFSET || '0', 10);
81
+ if (process.env.NODE_ENV !== 'production' && portOffset !== 0) {
82
+ for (const key of ['SUPABASE_URL', 'SITE_URL'] as const) {
83
+ const raw = process.env[key];
84
+ if (!raw) continue;
85
+ try {
86
+ const url = new URL(raw);
87
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
88
+ url.port = String(Number.parseInt(url.port || '80', 10) + portOffset);
89
+ process.env[key] = url.toString().replace(/\/$/, '');
90
+ }
91
+ } catch {
92
+ // Leave invalid URLs for Zod to catch
93
+ }
94
+ }
95
+ }
96
+
97
+ // Validate environment variables
98
+ const parsed = envSchema.safeParse(process.env);
99
+
100
+ if (!parsed.success) {
101
+ // In production, don't expose which specific environment variables failed validation
102
+ // This prevents information disclosure about the application's configuration
103
+ if (process.env.NODE_ENV === 'production') {
104
+ console.error('Server configuration error. Please check environment variables.');
105
+ } else {
106
+ console.error('Invalid environment variables:');
107
+ console.error(parsed.error.flatten().fieldErrors);
108
+ }
109
+ process.exit(1);
110
+ }
111
+
112
+ export const env = parsed.data;
@@ -0,0 +1,21 @@
1
+ import { env } from './env';
2
+ import { logger } from './logger';
3
+
4
+ /**
5
+ * Sanitize error messages for API responses.
6
+ * In production, logs the full error and returns a generic message.
7
+ * In development, returns the actual error message for debugging.
8
+ */
9
+ export function sanitizeError(error: unknown, fallback = 'An unexpected error occurred'): string {
10
+ if (env.NODE_ENV === 'production') {
11
+ // Log the full error for debugging
12
+ logger.error({ error }, 'API error');
13
+ return fallback;
14
+ }
15
+ // In development, return the actual error message
16
+ if (error instanceof Error) return error.message;
17
+ if (typeof error === 'object' && error !== null && 'message' in error) {
18
+ return String((error as { message: unknown }).message);
19
+ }
20
+ return fallback;
21
+ }
@@ -0,0 +1,17 @@
1
+ import pino from 'pino';
2
+
3
+ const isProduction = process.env.NODE_ENV === 'production';
4
+
5
+ export const logger = pino({
6
+ level: isProduction ? 'info' : 'debug',
7
+ ...(isProduction
8
+ ? {}
9
+ : {
10
+ transport: {
11
+ target: 'pino-pretty',
12
+ options: {
13
+ colorize: true,
14
+ },
15
+ },
16
+ }),
17
+ });
@@ -0,0 +1,288 @@
1
+ import type { Context, Next } from 'hono';
2
+ import Redis from 'ioredis';
3
+ import { env } from './env';
4
+ import { logger } from './logger';
5
+
6
+ // =============================================================================
7
+ // RATE LIMIT STORE INTERFACE
8
+ // =============================================================================
9
+
10
+ interface RateLimitResult {
11
+ count: number;
12
+ resetTime: number;
13
+ allowed: boolean;
14
+ }
15
+
16
+ interface RateLimitStore {
17
+ increment(key: string, windowMs: number, max: number): Promise<RateLimitResult>;
18
+ isHealthy(): boolean;
19
+ }
20
+
21
+ // =============================================================================
22
+ // IN-MEMORY STORE (fallback when Redis unavailable)
23
+ // =============================================================================
24
+
25
+ class InMemoryStore implements RateLimitStore {
26
+ private store: Map<string, { count: number; resetTime: number }> = new Map();
27
+
28
+ constructor() {
29
+ // Clean up old entries every minute
30
+ setInterval(() => {
31
+ const now = Date.now();
32
+ for (const [key, value] of this.store.entries()) {
33
+ if (value.resetTime < now) {
34
+ this.store.delete(key);
35
+ }
36
+ }
37
+ }, 60000);
38
+ }
39
+
40
+ async increment(key: string, windowMs: number, max: number): Promise<RateLimitResult> {
41
+ const now = Date.now();
42
+ const entry = this.store.get(key);
43
+
44
+ if (!entry || entry.resetTime < now) {
45
+ const resetTime = now + windowMs;
46
+ this.store.set(key, { count: 1, resetTime });
47
+ return { count: 1, resetTime, allowed: true };
48
+ }
49
+
50
+ entry.count++;
51
+ return {
52
+ count: entry.count,
53
+ resetTime: entry.resetTime,
54
+ allowed: entry.count <= max,
55
+ };
56
+ }
57
+
58
+ isHealthy(): boolean {
59
+ return true;
60
+ }
61
+ }
62
+
63
+ // =============================================================================
64
+ // REDIS STORE (for distributed rate limiting)
65
+ // =============================================================================
66
+
67
+ class RedisStore implements RateLimitStore {
68
+ private client: Redis;
69
+ private healthy = false;
70
+
71
+ constructor(redisUrl: string) {
72
+ this.client = new Redis(redisUrl, {
73
+ maxRetriesPerRequest: 1,
74
+ retryStrategy: (times) => {
75
+ if (times > 3) {
76
+ logger.warn('Redis connection failed, rate limiting will use in-memory fallback');
77
+ return null; // Stop retrying
78
+ }
79
+ return Math.min(times * 100, 1000);
80
+ },
81
+ lazyConnect: true,
82
+ });
83
+
84
+ this.client.on('connect', () => {
85
+ this.healthy = true;
86
+ logger.info('Redis connected for rate limiting');
87
+ });
88
+
89
+ this.client.on('error', (err) => {
90
+ this.healthy = false;
91
+ logger.warn({ err: err.message }, 'Redis error');
92
+ });
93
+
94
+ this.client.on('close', () => {
95
+ this.healthy = false;
96
+ });
97
+
98
+ // Attempt connection
99
+ this.client.connect().catch(() => {
100
+ // Connection failed, will use fallback
101
+ });
102
+ }
103
+
104
+ async increment(key: string, windowMs: number, max: number): Promise<RateLimitResult> {
105
+ const now = Date.now();
106
+ const windowKey = `ratelimit:${key}:${Math.floor(now / windowMs)}`;
107
+ const resetTime = Math.ceil(now / windowMs) * windowMs + windowMs;
108
+
109
+ try {
110
+ // Use Redis MULTI for atomic increment and TTL set
111
+ const results = await this.client
112
+ .multi()
113
+ .incr(windowKey)
114
+ .pexpire(windowKey, windowMs + 1000) // Extra 1s buffer
115
+ .exec();
116
+
117
+ if (!results || results[0][0]) {
118
+ throw new Error('Redis transaction failed');
119
+ }
120
+
121
+ const count = results[0][1] as number;
122
+ return {
123
+ count,
124
+ resetTime,
125
+ allowed: count <= max,
126
+ };
127
+ } catch (err) {
128
+ // Redis operation failed, mark as unhealthy
129
+ this.healthy = false;
130
+ throw err;
131
+ }
132
+ }
133
+
134
+ isHealthy(): boolean {
135
+ return this.healthy;
136
+ }
137
+ }
138
+
139
+ // =============================================================================
140
+ // HYBRID STORE (Redis with in-memory fallback)
141
+ // =============================================================================
142
+
143
+ class HybridStore implements RateLimitStore {
144
+ private redisStore: RedisStore | null = null;
145
+ private memoryStore: InMemoryStore;
146
+ private warnedAboutFallback = false;
147
+
148
+ constructor(redisUrl?: string) {
149
+ this.memoryStore = new InMemoryStore();
150
+
151
+ if (redisUrl) {
152
+ this.redisStore = new RedisStore(redisUrl);
153
+ } else if (env.NODE_ENV === 'production') {
154
+ logger.warn(
155
+ 'REDIS_URL not configured. Rate limiting uses in-memory store which does not scale across multiple instances. ' +
156
+ 'For production with multiple replicas, configure Redis: vibecarbon add redis'
157
+ );
158
+ }
159
+ }
160
+
161
+ async increment(key: string, windowMs: number, max: number): Promise<RateLimitResult> {
162
+ // Try Redis first if available
163
+ if (this.redisStore?.isHealthy()) {
164
+ try {
165
+ return await this.redisStore.increment(key, windowMs, max);
166
+ } catch {
167
+ // Fall through to memory store
168
+ if (!this.warnedAboutFallback) {
169
+ this.warnedAboutFallback = true;
170
+ logger.warn('Redis unavailable, falling back to in-memory rate limiting');
171
+ }
172
+ }
173
+ }
174
+
175
+ // Use in-memory fallback
176
+ return this.memoryStore.increment(key, windowMs, max);
177
+ }
178
+
179
+ isHealthy(): boolean {
180
+ return this.redisStore?.isHealthy() || this.memoryStore.isHealthy();
181
+ }
182
+ }
183
+
184
+ // =============================================================================
185
+ // SINGLETON STORE INSTANCE
186
+ // =============================================================================
187
+
188
+ let store: HybridStore | null = null;
189
+
190
+ function getStore(): HybridStore {
191
+ if (!store) {
192
+ store = new HybridStore(env.REDIS_URL);
193
+ }
194
+ return store;
195
+ }
196
+
197
+ // =============================================================================
198
+ // IP EXTRACTION
199
+ // =============================================================================
200
+
201
+ // Track if we've warned about missing IP headers (log once per process)
202
+ let warnedAboutMissingIP = false;
203
+
204
+ /**
205
+ * Extract client IP from request headers
206
+ *
207
+ * Security note: Only trust x-forwarded-for when behind a trusted reverse proxy (Traefik).
208
+ * The leftmost IP in x-forwarded-for is the original client IP.
209
+ *
210
+ * Fallback behavior:
211
+ * - In development: Falls back to 'localhost' (expected when not behind proxy)
212
+ * - In production: Falls back to 'unknown' but logs a warning (indicates misconfiguration)
213
+ *
214
+ * When IP is 'unknown', all such requests share a rate limit bucket - this is intentional
215
+ * as it provides more restrictive rate limiting for requests that bypass proper headers.
216
+ */
217
+ function getClientIP(c: Context): string {
218
+ // x-forwarded-for format: "client, proxy1, proxy2"
219
+ const xff = c.req.header('x-forwarded-for');
220
+ if (xff) {
221
+ // Take leftmost (original client) IP, trim whitespace
222
+ const clientIP = xff.split(',')[0].trim();
223
+ // Basic validation - must look like an IP (IPv4 or IPv6)
224
+ if (/^[\d.:a-fA-F]+$/.test(clientIP)) {
225
+ return clientIP;
226
+ }
227
+ }
228
+
229
+ // Fallback to x-real-ip (set by some proxies like nginx)
230
+ const realIP = c.req.header('x-real-ip');
231
+ if (realIP && /^[\d.:a-fA-F]+$/.test(realIP)) {
232
+ return realIP;
233
+ }
234
+
235
+ // Fallback to cf-connecting-ip (Cloudflare)
236
+ const cfIP = c.req.header('cf-connecting-ip');
237
+ if (cfIP && /^[\d.:a-fA-F]+$/.test(cfIP)) {
238
+ return cfIP;
239
+ }
240
+
241
+ // In development, localhost is expected
242
+ if (env.NODE_ENV === 'development') {
243
+ return 'localhost';
244
+ }
245
+
246
+ // In production, warn about missing IP headers (indicates proxy misconfiguration)
247
+ if (!warnedAboutMissingIP) {
248
+ warnedAboutMissingIP = true;
249
+ logger.warn(
250
+ 'Could not determine client IP from request headers. ' +
251
+ 'Ensure reverse proxy (Traefik) is configured to set X-Forwarded-For or X-Real-IP headers. ' +
252
+ 'Requests without identifiable IPs will share a rate limit bucket.'
253
+ );
254
+ }
255
+
256
+ return 'unknown';
257
+ }
258
+
259
+ // =============================================================================
260
+ // RATE LIMITER MIDDLEWARE
261
+ // =============================================================================
262
+
263
+ export function createRateLimiter(options: { windowMs: number; max: number }) {
264
+ return async (c: Context, next: Next) => {
265
+ const ip = getClientIP(c);
266
+ const key = `${ip}:${c.req.path}`;
267
+ const now = Date.now();
268
+
269
+ const result = await getStore().increment(key, options.windowMs, options.max);
270
+ const remaining = Math.max(0, options.max - result.count);
271
+ const reset = Math.ceil(result.resetTime / 1000);
272
+
273
+ c.res.headers.set('X-RateLimit-Limit', String(options.max));
274
+ c.res.headers.set('X-RateLimit-Remaining', String(remaining));
275
+ c.res.headers.set('X-RateLimit-Reset', String(reset));
276
+
277
+ if (!result.allowed) {
278
+ // Log rate limit violations for security monitoring
279
+ logger.warn({ ip, path: c.req.path, count: result.count }, 'Rate limit exceeded');
280
+ return c.json(
281
+ { error: 'Too many requests', retryAfter: Math.ceil((result.resetTime - now) / 1000) },
282
+ 429
283
+ );
284
+ }
285
+
286
+ await next();
287
+ };
288
+ }
@@ -0,0 +1,34 @@
1
+ import { logger } from './logger';
2
+
3
+ /**
4
+ * Extract client IP address from request headers.
5
+ * Checks common proxy headers (x-forwarded-for, x-real-ip, cf-connecting-ip).
6
+ */
7
+ export function getClientIp(c: { req: { header: (name: string) => string | undefined } }): string {
8
+ // Cloudflare
9
+ const cfIp = c.req.header('cf-connecting-ip');
10
+ if (cfIp && isValidIp(cfIp)) return cfIp;
11
+
12
+ // Standard proxy headers
13
+ const forwarded = c.req.header('x-forwarded-for');
14
+ if (forwarded) {
15
+ const first = forwarded.split(',')[0].trim();
16
+ if (isValidIp(first)) return first;
17
+ }
18
+
19
+ const realIp = c.req.header('x-real-ip');
20
+ if (realIp && isValidIp(realIp)) return realIp;
21
+
22
+ logger.warn(
23
+ 'Could not extract client IP from request headers (x-forwarded-for, x-real-ip, cf-connecting-ip). ' +
24
+ 'Using fallback. Check proxy configuration if this happens frequently.'
25
+ );
26
+ return '127.0.0.1';
27
+ }
28
+
29
+ const ipv4Regex = /^(?:\d{1,3}\.){3}\d{1,3}$/;
30
+ const ipv6Regex = /^[a-fA-F0-9:]+$/;
31
+
32
+ function isValidIp(ip: string): boolean {
33
+ return ipv4Regex.test(ip) || ipv6Regex.test(ip);
34
+ }
@@ -0,0 +1,42 @@
1
+ import Stripe from 'stripe';
2
+ import { env } from './env';
3
+
4
+ /**
5
+ * Stripe client instance.
6
+ * Only initialized if STRIPE_SECRET_KEY is configured.
7
+ */
8
+ export const stripe = env.STRIPE_SECRET_KEY
9
+ ? new Stripe(env.STRIPE_SECRET_KEY, {
10
+ typescript: true,
11
+ })
12
+ : null;
13
+
14
+ /**
15
+ * Get the Stripe client, throwing an error if not configured.
16
+ * Use this in routes that require Stripe to be configured.
17
+ */
18
+ export function getStripe(): Stripe {
19
+ if (!stripe) {
20
+ throw new Error('Stripe is not configured. Set STRIPE_SECRET_KEY in your environment.');
21
+ }
22
+ return stripe;
23
+ }
24
+
25
+ /**
26
+ * Check if Stripe is configured.
27
+ */
28
+ export function isStripeConfigured(): boolean {
29
+ return stripe !== null;
30
+ }
31
+
32
+ /**
33
+ * Verify Stripe webhook signature.
34
+ * Returns the event if valid, throws an error if invalid.
35
+ */
36
+ export function verifyWebhookSignature(payload: string | Buffer, signature: string): Stripe.Event {
37
+ if (!env.STRIPE_WEBHOOK_SECRET) {
38
+ throw new Error('Stripe webhook secret is not configured');
39
+ }
40
+
41
+ return getStripe().webhooks.constructEvent(payload, signature, env.STRIPE_WEBHOOK_SECRET);
42
+ }
@@ -0,0 +1,65 @@
1
+ import { createClient, type SupabaseClient } from '@supabase/supabase-js';
2
+ import type { Database } from '../../shared/types';
3
+ import { env } from './env';
4
+
5
+ // Server-side Supabase client with service role key
6
+ // This bypasses RLS - use only for admin operations
7
+ export const supabaseAdmin: SupabaseClient<Database> = createClient<Database>(
8
+ env.SUPABASE_URL,
9
+ env.SUPABASE_SERVICE_ROLE_KEY,
10
+ {
11
+ auth: {
12
+ autoRefreshToken: false,
13
+ persistSession: false,
14
+ },
15
+ }
16
+ );
17
+
18
+ // Create a client for a specific user's context (respects RLS)
19
+ // Uses SERVICE_ROLE_KEY to pass Kong's ACL gate for PostgREST, but RLS is still
20
+ // enforced because PostgREST reads the user's role from the Authorization JWT.
21
+ export function createSupabaseClient(accessToken: string): SupabaseClient<Database> {
22
+ return createClient<Database>(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, {
23
+ global: {
24
+ headers: {
25
+ Authorization: `Bearer ${accessToken}`,
26
+ },
27
+ },
28
+ auth: {
29
+ autoRefreshToken: false,
30
+ persistSession: false,
31
+ },
32
+ });
33
+ }
34
+
35
+ // Create a fresh client for auth operations (signInWithPassword, etc.)
36
+ // IMPORTANT: Never use supabaseAdmin for signInWithPassword — it stores the
37
+ // user's session on the singleton, contaminating all subsequent admin queries
38
+ // with the user's "authenticated" role instead of the "service_role" key.
39
+ export function createAuthClient(): SupabaseClient<Database> {
40
+ return createClient<Database>(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, {
41
+ auth: {
42
+ autoRefreshToken: false,
43
+ persistSession: false,
44
+ },
45
+ });
46
+ }
47
+
48
+ // Helper to get Supabase client from request headers
49
+ export function getSupabaseClient(authHeader: string | undefined): SupabaseClient<Database> {
50
+ if (!authHeader?.startsWith('Bearer ')) {
51
+ // No valid auth header — return an anon client so RLS is enforced.
52
+ // Routes must still reject unauthenticated requests via c.get('user'),
53
+ // but this ensures any accidental query falls under anon RLS policies
54
+ // rather than bypassing them entirely with the service role key.
55
+ return createClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY, {
56
+ auth: {
57
+ autoRefreshToken: false,
58
+ persistSession: false,
59
+ },
60
+ });
61
+ }
62
+
63
+ const token = authHeader.replace('Bearer ', '');
64
+ return createSupabaseClient(token);
65
+ }
@@ -0,0 +1,80 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { Context, Next } from 'hono';
3
+ import type { PlanId } from '../../shared/pricing';
4
+ import { planIdFromStripePriceId } from '../../shared/pricing';
5
+ import { env } from '../lib/env';
6
+ import { isStripeConfigured } from '../lib/stripe';
7
+ import { supabaseAdmin } from '../lib/supabase';
8
+ import type { HonoVariables } from '../types';
9
+
10
+ // biome-ignore lint/suspicious/noExplicitAny: Tables not yet in generated Database types
11
+ const adminDb = supabaseAdmin as SupabaseClient<any>;
12
+
13
+ const planHierarchy: Record<PlanId, number> = {
14
+ free: 0,
15
+ starter: 1,
16
+ pro: 2,
17
+ };
18
+
19
+ /**
20
+ * Hono middleware that gates API routes behind a minimum plan.
21
+ *
22
+ * Usage:
23
+ * myRoutes.use('/premium-endpoint', requirePlan('pro'));
24
+ * myRoutes.get('/starter-feature', requirePlan('starter'), handler);
25
+ */
26
+ export function requirePlan(minimumPlan: PlanId) {
27
+ return async (c: Context<{ Variables: HonoVariables }>, next: Next) => {
28
+ // If billing isn't configured, allow all requests (dev mode / self-hosted)
29
+ if (!isStripeConfigured()) {
30
+ await next();
31
+ return;
32
+ }
33
+
34
+ const user = c.get('user');
35
+ if (!user) {
36
+ return c.json({ error: 'Unauthorized' }, 401);
37
+ }
38
+
39
+ // Free plan requires no subscription check
40
+ if (minimumPlan === 'free') {
41
+ await next();
42
+ return;
43
+ }
44
+
45
+ // Find customer for this user
46
+ const { data: customer } = await adminDb
47
+ .from('customers')
48
+ .select('id')
49
+ .eq('user_id', user.id)
50
+ .is('organization_id', null)
51
+ .maybeSingle();
52
+
53
+ if (!customer) {
54
+ return c.json({ error: 'Upgrade required', requiredPlan: minimumPlan }, 403);
55
+ }
56
+
57
+ // Find active subscription
58
+ const { data: subscription } = await adminDb
59
+ .from('subscriptions')
60
+ .select('stripe_price_id, status')
61
+ .eq('customer_id', customer.id)
62
+ .in('status', ['active', 'trialing'])
63
+ .maybeSingle();
64
+
65
+ if (!subscription) {
66
+ return c.json({ error: 'Upgrade required', requiredPlan: minimumPlan }, 403);
67
+ }
68
+
69
+ const currentPlan = planIdFromStripePriceId(subscription.stripe_price_id, {
70
+ starter: env.STRIPE_PRICE_STARTER,
71
+ pro: env.STRIPE_PRICE_PRO,
72
+ });
73
+
74
+ if (planHierarchy[currentPlan] < planHierarchy[minimumPlan]) {
75
+ return c.json({ error: 'Upgrade required', requiredPlan: minimumPlan, currentPlan }, 403);
76
+ }
77
+
78
+ await next();
79
+ };
80
+ }