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,447 @@
1
+ /**
2
+ * Cloudflare API operations
3
+ * Shared across deploy.js and destroy.js
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import { fetchWithRetry } from './fetch-retry.js';
8
+
9
+ // ============================================================================
10
+ // ZONE AND ACCOUNT OPERATIONS
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Get all zones in the Cloudflare account
15
+ *
16
+ * @param {string} apiToken - Cloudflare API token
17
+ * @returns {Promise<Array>} - Array of zone objects
18
+ */
19
+ export async function getZones(apiToken) {
20
+ const response = await fetchWithRetry('https://api.cloudflare.com/client/v4/zones', {
21
+ headers: {
22
+ Authorization: `Bearer ${apiToken}`,
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ });
26
+
27
+ if (!response.ok) {
28
+ const error = await response.json();
29
+ throw new Error(`Cloudflare API error: ${JSON.stringify(error.errors)}`);
30
+ }
31
+
32
+ const data = await response.json();
33
+ return data.result;
34
+ }
35
+
36
+ /**
37
+ * Get the Cloudflare account ID
38
+ *
39
+ * @param {string} apiToken - Cloudflare API token
40
+ * @returns {Promise<string|null>} - Account ID or null if not found
41
+ */
42
+ export async function getAccountId(apiToken) {
43
+ const response = await fetchWithRetry('https://api.cloudflare.com/client/v4/accounts', {
44
+ headers: {
45
+ Authorization: `Bearer ${apiToken}`,
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ });
49
+
50
+ const data = await response.json();
51
+ if (data.result && data.result.length > 0) {
52
+ return data.result[0].id;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ // ============================================================================
58
+ // DNS RECORD OPERATIONS
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Create or update a DNS record
63
+ *
64
+ * @param {string} apiToken - Cloudflare API token
65
+ * @param {string} zoneId - Zone ID
66
+ * @param {object} config - Record configuration
67
+ * @param {string} config.type - Record type (A, CNAME, etc.)
68
+ * @param {string} config.name - Record name
69
+ * @param {string} config.content - Record content (IP address, etc.)
70
+ * @param {boolean} [config.proxied=true] - Whether to proxy through Cloudflare
71
+ * @param {number} [config.ttl=1] - TTL (1 = auto)
72
+ * @returns {Promise<object>} - API response
73
+ */
74
+ export async function createDNSRecord(apiToken, zoneId, config) {
75
+ const { type, name, content, proxied = true, ttl = 1 } = config;
76
+
77
+ // Check if record already exists
78
+ const listResponse = await fetchWithRetry(
79
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=${type}&name=${name}`,
80
+ {
81
+ headers: {
82
+ Authorization: `Bearer ${apiToken}`,
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ },
86
+ );
87
+
88
+ const listData = await listResponse.json();
89
+ const existing = listData.result?.find((r) => r.name === name && r.type === type);
90
+
91
+ if (existing) {
92
+ // Update existing record
93
+ const updateResponse = await fetchWithRetry(
94
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${existing.id}`,
95
+ {
96
+ method: 'PATCH',
97
+ headers: {
98
+ Authorization: `Bearer ${apiToken}`,
99
+ 'Content-Type': 'application/json',
100
+ },
101
+ body: JSON.stringify({ content, proxied, ttl }),
102
+ },
103
+ );
104
+ return updateResponse.json();
105
+ }
106
+
107
+ // Create new record
108
+ const response = await fetchWithRetry(
109
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
110
+ {
111
+ method: 'POST',
112
+ headers: {
113
+ Authorization: `Bearer ${apiToken}`,
114
+ 'Content-Type': 'application/json',
115
+ },
116
+ body: JSON.stringify({ type, name, content, proxied, ttl }),
117
+ },
118
+ );
119
+
120
+ return response.json();
121
+ }
122
+
123
+ /**
124
+ * Delete a DNS record, but only if its target IP belongs to this caller.
125
+ *
126
+ * Cloudflare zones can hold multiple A records for the same name (e.g. when
127
+ * two e2e scenarios share a root domain and each owns a subdomain).
128
+ * Deleting unconditionally wipes neighbours' records too — the blast-radius
129
+ * bug observed in the 2026-05-16 matrix where compose-ha's destroy zeroed
130
+ * compose's `e1.carbonstack.dev` mid-verify.
131
+ *
132
+ * Ownership rule: a record is "ours" iff its `content` (the target IP) is
133
+ * in `ownedIps`. Records pointing elsewhere are preserved with a `skipped`
134
+ * count. Pass an empty array to refuse all deletes (the safer default when
135
+ * envConfig has no server IPs to compare against).
136
+ *
137
+ * @param {string} apiToken - Cloudflare API token
138
+ * @param {string} zoneId - Zone ID
139
+ * @param {string} name - Record name (FQDN)
140
+ * @param {string[]} ownedIps - IPs that belong to this caller's stack
141
+ * @param {string} [type='A'] - Record type
142
+ * @returns {Promise<{deleted: number, skipped: number, total: number, skippedTargets: string[]}>}
143
+ */
144
+ export async function deleteDNSRecord(apiToken, zoneId, name, ownedIps, type = 'A') {
145
+ const listResponse = await fetchWithRetry(
146
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=${type}&name=${name}`,
147
+ {
148
+ headers: {
149
+ Authorization: `Bearer ${apiToken}`,
150
+ 'Content-Type': 'application/json',
151
+ },
152
+ },
153
+ );
154
+
155
+ const listData = await listResponse.json();
156
+ const records = (listData.result || []).filter((r) => r.name === name && r.type === type);
157
+ const total = records.length;
158
+
159
+ if (total === 0) {
160
+ return { deleted: 0, skipped: 0, total: 0, skippedTargets: [] };
161
+ }
162
+
163
+ const owned = new Set(ownedIps || []);
164
+ let deleted = 0;
165
+ const skippedTargets = [];
166
+
167
+ for (const record of records) {
168
+ if (!owned.has(record.content)) {
169
+ skippedTargets.push(record.content);
170
+ continue;
171
+ }
172
+ const response = await fetchWithRetry(
173
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${record.id}`,
174
+ {
175
+ method: 'DELETE',
176
+ headers: {
177
+ Authorization: `Bearer ${apiToken}`,
178
+ 'Content-Type': 'application/json',
179
+ },
180
+ },
181
+ );
182
+ if (response.ok) deleted++;
183
+ }
184
+
185
+ return { deleted, skipped: skippedTargets.length, total, skippedTargets };
186
+ }
187
+
188
+ // ============================================================================
189
+ // HEALTH CHECK OPERATIONS
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Create a health check
194
+ *
195
+ * @param {string} apiToken - Cloudflare API token
196
+ * @param {string} zoneId - Zone ID
197
+ * @param {object} config - Health check configuration
198
+ * @returns {Promise<object>} - API response
199
+ */
200
+ export async function createHealthCheck(apiToken, zoneId, config) {
201
+ const { name, address, path = '/api/health', port = 443 } = config;
202
+
203
+ const response = await fetchWithRetry(
204
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/healthchecks`,
205
+ {
206
+ method: 'POST',
207
+ headers: {
208
+ Authorization: `Bearer ${apiToken}`,
209
+ 'Content-Type': 'application/json',
210
+ },
211
+ body: JSON.stringify({
212
+ name,
213
+ address,
214
+ type: 'HTTPS',
215
+ port,
216
+ method: 'GET',
217
+ path,
218
+ interval: 60,
219
+ timeout: 5,
220
+ retries: 2,
221
+ consecutive_fails: 2,
222
+ consecutive_successes: 1,
223
+ }),
224
+ },
225
+ );
226
+
227
+ return response.json();
228
+ }
229
+
230
+ /**
231
+ * Delete a health check
232
+ *
233
+ * @param {string} apiToken - Cloudflare API token
234
+ * @param {string} zoneId - Zone ID
235
+ * @param {string} name - Health check name
236
+ * @returns {Promise<boolean>} - True if deleted
237
+ */
238
+ export async function deleteHealthCheck(apiToken, zoneId, name) {
239
+ // List health checks
240
+ const listResponse = await fetchWithRetry(
241
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/healthchecks`,
242
+ {
243
+ headers: {
244
+ Authorization: `Bearer ${apiToken}`,
245
+ 'Content-Type': 'application/json',
246
+ },
247
+ },
248
+ );
249
+
250
+ const listData = await listResponse.json();
251
+ const healthCheck = listData.result?.find((h) => h.name === name);
252
+
253
+ if (!healthCheck) return false;
254
+
255
+ const response = await fetchWithRetry(
256
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/healthchecks/${healthCheck.id}`,
257
+ {
258
+ method: 'DELETE',
259
+ headers: {
260
+ Authorization: `Bearer ${apiToken}`,
261
+ 'Content-Type': 'application/json',
262
+ },
263
+ },
264
+ );
265
+
266
+ return response.ok;
267
+ }
268
+
269
+ // ============================================================================
270
+ // HIGH-LEVEL SETUP FUNCTIONS
271
+ // ============================================================================
272
+
273
+ /**
274
+ * Set up Cloudflare for HA deployment with DNS records and health checks
275
+ *
276
+ * @param {string} apiToken - Cloudflare API token
277
+ * @param {string} zoneId - Zone ID
278
+ * @param {string} domain - Domain name
279
+ * @param {Array} servers - Array of server objects with name and ip
280
+ * @returns {Promise<object>} - Setup result
281
+ */
282
+ export async function setupHA(apiToken, zoneId, domain, servers) {
283
+ const s = p.spinner();
284
+
285
+ try {
286
+ // Create A record pointing to primary server.
287
+ //
288
+ // proxied: false (gray cloud, DNS-only). Was `true` (orange cloud)
289
+ // back when we used Cloudflare Load Balancers for automatic
290
+ // failover — the proxy was load-bearing because LB rules ran at
291
+ // CF's edge. Cloudflare LBs were removed 2026-03-26 in favor of
292
+ // manual failover (rewriting this A record on demand), so the
293
+ // proxy is no longer needed and is actively harmful: with orange
294
+ // cloud, CF intercepts the apex and serves a 404 from its edge
295
+ // until it can verify the origin's cert and a route exists. The
296
+ // wildcard below was already gray-cloud — apex matches it now so
297
+ // both are direct-to-origin via Traefik + Let's Encrypt.
298
+ // RCA: k8s-ha 2026-04-30 deploy probe got 404 for 1200s — DNS
299
+ // resolved fine, but CF proxy returned 404 instead of forwarding.
300
+ s.start(`Creating DNS record for ${domain}`);
301
+ await createDNSRecord(apiToken, zoneId, {
302
+ type: 'A',
303
+ name: domain,
304
+ content: servers[0].ip,
305
+ proxied: false,
306
+ });
307
+ s.stop('Primary DNS record created');
308
+
309
+ // Create wildcard A record for subdomains (registry, api, etc.)
310
+ // DNS-only (not proxied) — Traefik handles TLS via Let's Encrypt
311
+ await createDNSRecord(apiToken, zoneId, {
312
+ type: 'A',
313
+ name: `*.${domain}`,
314
+ content: servers[0].ip,
315
+ proxied: false,
316
+ });
317
+
318
+ // Create health checks for monitoring both regions
319
+ for (const server of servers) {
320
+ s.start(`Creating health check for ${server.name}`);
321
+ await createHealthCheck(apiToken, zoneId, {
322
+ name: `${server.name}-health`,
323
+ address: server.ip,
324
+ path: '/api/health',
325
+ });
326
+ s.stop(`Health check created for ${server.name}`);
327
+ }
328
+
329
+ return {
330
+ success: true,
331
+ mode: 'dns',
332
+ primaryIp: servers[0].ip,
333
+ standbyIp: servers[1]?.ip,
334
+ };
335
+ } catch (error) {
336
+ s.stop(`Cloudflare DNS setup failed: ${error.message}`);
337
+ return { success: false, error: error.message };
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Set up Cloudflare for simple single-server deployment
343
+ *
344
+ * @param {string} apiToken - Cloudflare API token
345
+ * @param {string} zoneId - Zone ID
346
+ * @param {string} domain - Domain name
347
+ * @param {string} serverIp - Server IP address
348
+ * @returns {Promise<object>} - Setup result
349
+ */
350
+ /**
351
+ * Set the zone-level SSL mode on Cloudflare. Used when we want Cloudflare's
352
+ * edge to terminate TLS (so the origin can skip ACME / cert-manager).
353
+ *
354
+ * - 'off' — no encryption (don't use in prod)
355
+ * - 'flexible' — CF↔browser HTTPS, CF↔origin HTTP (allows origin to run
356
+ * without any TLS; not recommended — redirect loops with origin-side
357
+ * HTTPS)
358
+ * - 'full' — CF↔browser HTTPS, CF↔origin HTTPS with ANY cert (self-signed
359
+ * OK). Our sweet spot when the user opts into the TLS-proxy path.
360
+ * - 'strict' — CF↔origin HTTPS with a valid cert. Requires real ACME,
361
+ * defeats the purpose of TLS proxy.
362
+ *
363
+ * @param {string} apiToken
364
+ * @param {string} zoneId
365
+ * @param {'off'|'flexible'|'full'|'strict'} mode
366
+ */
367
+ export async function setZoneSslMode(apiToken, zoneId, mode) {
368
+ const response = await fetchWithRetry(
369
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/settings/ssl`,
370
+ {
371
+ method: 'PATCH',
372
+ headers: {
373
+ Authorization: `Bearer ${apiToken}`,
374
+ 'Content-Type': 'application/json',
375
+ },
376
+ body: JSON.stringify({ value: mode }),
377
+ },
378
+ );
379
+ const data = await response.json();
380
+ if (!data.success) {
381
+ throw new Error(
382
+ `Cloudflare zone SSL mode update failed: ${JSON.stringify(data.errors ?? data)}`,
383
+ );
384
+ }
385
+ return data.result;
386
+ }
387
+
388
+ export async function setupSimple(apiToken, zoneId, domain, serverIp, options = {}) {
389
+ // proxied default flipped to false (DNS-only / gray-cloud) on
390
+ // 2026-04-26 after e2e run #3 surfaced the orange-cloud
391
+ // chicken-and-egg: with proxied=true, dig resolves to CF edge IPs
392
+ // (172.67.x / 104.21.x), not the origin floating IP. CF then
393
+ // intercepts HTTPS, terminates with its Universal SSL cert, and
394
+ // tries to talk to origin per the zone's SSL mode (default Full
395
+ // (strict) for new zones — requires a valid origin cert). Origin's
396
+ // cert-manager can't issue via HTTP-01 because LE follows CF's
397
+ // Always-Use-HTTPS redirect and gets rejected at origin since cert
398
+ // isn't issued yet. Loop. With proxied=false, dig resolves to the
399
+ // origin IP directly, LE HTTP-01 hits origin on :80, traefik serves
400
+ // the challenge, cert issues, HTTPS works. Callers that want CF's
401
+ // edge TLS can opt in via options.proxied=true (they're then
402
+ // responsible for setting the zone SSL mode + ensuring origin
403
+ // accepts CF's challenge path).
404
+ const { proxied = false, onProgress } = options;
405
+ // When caller provides onProgress, skip internal spinners (caller manages UI)
406
+ const s = onProgress ? null : p.spinner();
407
+
408
+ try {
409
+ // Create A record for main domain
410
+ s?.start(`Creating DNS records for ${domain}`);
411
+ onProgress?.(`Creating DNS records for ${domain}`);
412
+ const result = await createDNSRecord(apiToken, zoneId, {
413
+ type: 'A',
414
+ name: domain,
415
+ content: serverIp,
416
+ proxied,
417
+ });
418
+
419
+ // Create wildcard A record for all subdomains (api, registry, etc.)
420
+ // DNS-only (not proxied) — Cloudflare free/pro plans don't proxy wildcards,
421
+ // and Traefik handles TLS via Let's Encrypt anyway.
422
+ await createDNSRecord(apiToken, zoneId, {
423
+ type: 'A',
424
+ name: `*.${domain}`,
425
+ content: serverIp,
426
+ proxied: false,
427
+ });
428
+ s?.stop('DNS records created (root + wildcard)');
429
+ onProgress?.('DNS records created (root + wildcard)');
430
+
431
+ // Create health check (informational, doesn't affect routing without LB)
432
+ s?.start('Creating health check');
433
+ onProgress?.('Creating health check');
434
+ await createHealthCheck(apiToken, zoneId, {
435
+ name: `${domain}-health`,
436
+ address: serverIp,
437
+ path: '/api/health',
438
+ });
439
+ s?.stop('Health check created');
440
+ onProgress?.('Health check created');
441
+
442
+ return { success: true, mode: 'simple', record: result.result };
443
+ } catch (error) {
444
+ s?.stop(`Cloudflare setup failed: ${error.message}`);
445
+ return { success: false, error: error.message };
446
+ }
447
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared ANSI color codes and formatting utilities for CLI output
3
+ */
4
+
5
+ // ANSI escape codes
6
+ export const colors = {
7
+ reset: '\x1b[0m',
8
+ bold: '\x1b[1m',
9
+ dim: '\x1b[2m',
10
+ red: '\x1b[31m',
11
+ brightRed: '\x1b[91m',
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ cyan: '\x1b[36m',
15
+ };
16
+
17
+ // Color formatting helpers
18
+ export const c = {
19
+ error: (s) => `${colors.red}${s}${colors.reset}`,
20
+ success: (s) => `${colors.green}${s}${colors.reset}`,
21
+ warning: (s) => `${colors.yellow}${s}${colors.reset}`,
22
+ info: (s) => `${colors.cyan}${s}${colors.reset}`,
23
+ bold: (s) => `${colors.bold}${s}${colors.reset}`,
24
+ dim: (s) => `${colors.dim}${s}${colors.reset}`,
25
+ // Bright red + bold — for prominent destruction-warning headlines
26
+ // (destroy preview banner, "WARNING: data will be lost" footer). More
27
+ // vibrant than `c.error` (which stays as the muted bullet color).
28
+ danger: (s) => `${colors.bold}${colors.brightRed}${s}${colors.reset}`,
29
+ // Composite styles
30
+ boldCyan: (s) => `${colors.bold}${colors.cyan}${s}${colors.reset}`,
31
+ boldYellow: (s) => `${colors.bold}${colors.yellow}${s}${colors.reset}`,
32
+ boldCyanUnderline: (s) => `${colors.bold}${colors.cyan}\x1b[4m${s}${colors.reset}`,
33
+ };
34
+
35
+ // Branded banner for CLI commands
36
+ export function printBanner() {
37
+ const bgCyan = '\x1b[46m';
38
+ const border = 2;
39
+ const inner = 44;
40
+ const width = inner + border * 2;
41
+ const full = `${bgCyan}${' '.repeat(width)}${colors.reset}`;
42
+ const side = `${bgCyan}${' '.repeat(border)}${colors.reset}`;
43
+ const empty = `${side}${' '.repeat(inner)}${side}`;
44
+ const text = ' v i b e c a r b o n ';
45
+ console.log();
46
+ console.log(full);
47
+ console.log(empty);
48
+ console.log(`${side}${colors.bold}${colors.cyan}${text}${colors.reset}${side}`);
49
+ console.log(empty);
50
+ console.log(full);
51
+ console.log();
52
+ }