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,377 @@
1
+ /**
2
+ * Hetzner Cloud DNS Operations
3
+ *
4
+ * DNS management through the Hetzner Cloud API (api.hetzner.cloud/v1).
5
+ * Uses the same API token as server operations — no additional credentials needed.
6
+ *
7
+ * The Cloud API uses `/zones/{id}/rrsets` for record management (not `/records`).
8
+ * Updates are performed via delete + create (PUT on rrsets returns 422).
9
+ *
10
+ * API Documentation: https://docs.hetzner.cloud/
11
+ */
12
+
13
+ import * as p from '@clack/prompts';
14
+ import { fetchWithRetry } from './fetch-retry.js';
15
+ import { HetznerProvider } from './providers/hetzner.js';
16
+
17
+ const API_BASE = HetznerProvider.API_BASE;
18
+
19
+ // Default TTL for HA records (low for fast failover propagation)
20
+ const HA_TTL = 60;
21
+ // Default TTL for standard records (low to prevent stale DNS between test runs)
22
+ const DEFAULT_TTL = 60;
23
+
24
+ // ============================================================================
25
+ // HELPERS
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Convert an FQDN to a zone-relative name for the Hetzner DNS API.
30
+ *
31
+ * Hetzner DNS records use names relative to the zone:
32
+ * example.com (zone: example.com) → "@"
33
+ * *.example.com (zone: example.com) → "*"
34
+ * sub.example.com (zone: example.com) → "sub"
35
+ * a.b.example.com (zone: example.com) → "a.b"
36
+ *
37
+ * @param {string} fqdn - Fully qualified domain name
38
+ * @param {string} zoneName - Zone name (e.g. "example.com")
39
+ * @returns {string} Zone-relative record name
40
+ */
41
+ export function fqdnToRelative(fqdn, zoneName) {
42
+ // Strip trailing dots
43
+ const clean = fqdn.replace(/\.$/, '');
44
+ const zone = zoneName.replace(/\.$/, '');
45
+
46
+ if (clean === zone) return '@';
47
+ if (clean === `*.${zone}`) return '*';
48
+ if (clean.endsWith(`.${zone}`)) {
49
+ return clean.slice(0, -(zone.length + 1));
50
+ }
51
+ // If no match, return as-is (caller may have already passed a relative name)
52
+ return clean;
53
+ }
54
+
55
+ // ============================================================================
56
+ // LOW-LEVEL API FUNCTIONS
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Get all DNS zones in the Hetzner Cloud project.
61
+ *
62
+ * @param {string} apiToken - Hetzner Cloud API token
63
+ * @returns {Promise<Array>} Array of zone objects
64
+ */
65
+ export async function getZones(apiToken) {
66
+ const zones = [];
67
+ let page = 1;
68
+
69
+ while (true) {
70
+ const response = await fetchWithRetry(`${API_BASE}/zones?page=${page}&per_page=50`, {
71
+ headers: { Authorization: `Bearer ${apiToken}` },
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const error = await response.json().catch(() => ({}));
76
+ throw new Error(`Hetzner DNS API error: ${error.error?.message || response.statusText}`);
77
+ }
78
+
79
+ const data = await response.json();
80
+ zones.push(...(data.zones || []));
81
+
82
+ if (!data.meta?.pagination?.next_page) break;
83
+ page = data.meta.pagination.next_page;
84
+ }
85
+
86
+ return zones;
87
+ }
88
+
89
+ /**
90
+ * Get a single DNS zone by ID.
91
+ *
92
+ * @param {string} apiToken - Hetzner Cloud API token
93
+ * @param {string} zoneId - Zone ID
94
+ * @returns {Promise<object>} Zone object (includes `name`)
95
+ */
96
+ export async function getZone(apiToken, zoneId) {
97
+ const response = await fetchWithRetry(`${API_BASE}/zones/${zoneId}`, {
98
+ headers: { Authorization: `Bearer ${apiToken}` },
99
+ });
100
+
101
+ if (!response.ok) {
102
+ const error = await response.json().catch(() => ({}));
103
+ throw new Error(`Hetzner DNS API error: ${error.error?.message || response.statusText}`);
104
+ }
105
+
106
+ const data = await response.json();
107
+ return data.zone;
108
+ }
109
+
110
+ /**
111
+ * List all rrsets (record sets) in a DNS zone.
112
+ *
113
+ * @param {string} apiToken - Hetzner Cloud API token
114
+ * @param {string} zoneId - Zone ID
115
+ * @returns {Promise<Array>} Array of rrset objects
116
+ */
117
+ export async function getRrsets(apiToken, zoneId) {
118
+ const rrsets = [];
119
+ let page = 1;
120
+
121
+ while (true) {
122
+ const response = await fetchWithRetry(
123
+ `${API_BASE}/zones/${zoneId}/rrsets?page=${page}&per_page=100`,
124
+ {
125
+ headers: { Authorization: `Bearer ${apiToken}` },
126
+ },
127
+ );
128
+
129
+ if (!response.ok) {
130
+ const error = await response.json().catch(() => ({}));
131
+ throw new Error(`Hetzner DNS API error: ${error.error?.message || response.statusText}`);
132
+ }
133
+
134
+ const data = await response.json();
135
+ rrsets.push(...(data.rrsets || []));
136
+
137
+ if (!data.meta?.pagination?.next_page) break;
138
+ page = data.meta.pagination.next_page;
139
+ }
140
+
141
+ return rrsets;
142
+ }
143
+
144
+ /**
145
+ * Create or update a DNS record.
146
+ *
147
+ * Uses the /rrsets endpoint. If a matching rrset (same type + name) exists,
148
+ * deletes it first then creates a new one (PUT is not supported on rrsets).
149
+ *
150
+ * @param {string} apiToken - Hetzner Cloud API token
151
+ * @param {string} zoneId - Zone ID
152
+ * @param {string} zoneName - Zone name (e.g. "example.com") for FQDN→relative conversion
153
+ * @param {object} config - Record config
154
+ * @param {string} config.type - Record type (A, AAAA, CNAME, etc.)
155
+ * @param {string} config.name - Record name (FQDN — converted to relative internally)
156
+ * @param {string} config.value - Record value (e.g. IP address)
157
+ * @param {number} [config.ttl] - TTL in seconds (default: 3600)
158
+ * @returns {Promise<object>} Created rrset object
159
+ */
160
+ export async function createDNSRecord(apiToken, zoneId, zoneName, config) {
161
+ const { type, name, value, ttl = DEFAULT_TTL } = config;
162
+ const relativeName = fqdnToRelative(name, zoneName);
163
+
164
+ // Delete existing rrset if present (PUT is not supported, so we delete + create)
165
+ const existing = await getRrsets(apiToken, zoneId);
166
+ const match = existing.find((r) => r.type === type && r.name === relativeName);
167
+
168
+ if (match) {
169
+ const delResponse = await fetchWithRetry(
170
+ `${API_BASE}/zones/${zoneId}/rrsets/${relativeName}/${type}`,
171
+ {
172
+ method: 'DELETE',
173
+ headers: { Authorization: `Bearer ${apiToken}` },
174
+ },
175
+ );
176
+ if (!delResponse.ok) {
177
+ const error = await delResponse.json().catch(() => ({}));
178
+ throw new Error(
179
+ `Failed to remove existing DNS record: ${error.error?.message || delResponse.statusText}`,
180
+ );
181
+ }
182
+ }
183
+
184
+ // Create new rrset
185
+ const response = await fetchWithRetry(`${API_BASE}/zones/${zoneId}/rrsets`, {
186
+ method: 'POST',
187
+ headers: {
188
+ Authorization: `Bearer ${apiToken}`,
189
+ 'Content-Type': 'application/json',
190
+ },
191
+ body: JSON.stringify({
192
+ name: relativeName,
193
+ type,
194
+ ttl,
195
+ records: [{ value }],
196
+ }),
197
+ });
198
+
199
+ if (!response.ok) {
200
+ const error = await response.json().catch(() => ({}));
201
+ throw new Error(`Failed to create DNS record: ${error.error?.message || response.statusText}`);
202
+ }
203
+
204
+ const data = await response.json();
205
+ return data.rrset;
206
+ }
207
+
208
+ /**
209
+ * Delete a DNS rrset by FQDN and type — but only if every value in the
210
+ * rrset belongs to this caller's stack.
211
+ *
212
+ * Hetzner stores DNS records as rrsets (one row per name+type, with an
213
+ * array of values). The destroy path historically wiped the rrset
214
+ * unconditionally, which was fine for single-tenant zones but blasted
215
+ * shared zones when e2e scenarios reused the same root domain
216
+ * (the 2026-05-16 matrix saw compose-ha's destroy zero compose's
217
+ * `e1.carbonstack.dev` mid-verify).
218
+ *
219
+ * Ownership rule:
220
+ * - If every value in the rrset is in `ownedIps`, DELETE the rrset.
221
+ * - If any value is NOT owned, preserve the entire rrset (we don't
222
+ * PATCH out individual records — Hetzner's API forces delete+recreate
223
+ * anyway, and a stale recreate risks dropping the others' values).
224
+ * - If `ownedIps` is empty, preserve (safer default when envConfig
225
+ * has no IPs to verify against).
226
+ *
227
+ * @param {string} apiToken - Hetzner Cloud API token
228
+ * @param {string} zoneId - Zone ID
229
+ * @param {string} zoneName - Zone name for FQDN→relative conversion
230
+ * @param {string} name - Record name (FQDN)
231
+ * @param {string[]} ownedIps - IPs that belong to this caller's stack
232
+ * @param {string} [type='A'] - Record type
233
+ * @returns {Promise<{deleted: boolean, skipped: boolean, foundValues: string[], skippedTargets: string[]}>}
234
+ */
235
+ export async function deleteDNSRecord(apiToken, zoneId, zoneName, name, ownedIps, type = 'A') {
236
+ const relativeName = fqdnToRelative(name, zoneName);
237
+
238
+ // Inspect the rrset before deleting. The list endpoint surfaces all
239
+ // values for a (name, type) tuple as a single rrset row.
240
+ const rrsets = await getRrsets(apiToken, zoneId);
241
+ const match = rrsets.find((r) => r.type === type && r.name === relativeName);
242
+
243
+ if (!match) {
244
+ return { deleted: false, skipped: false, foundValues: [], skippedTargets: [] };
245
+ }
246
+
247
+ const values = (match.records || []).map((r) => r.value);
248
+ const owned = new Set(ownedIps || []);
249
+ const unowned = values.filter((v) => !owned.has(v));
250
+
251
+ if (unowned.length > 0) {
252
+ // At least one value belongs to someone else — leave the whole rrset
253
+ // alone. Surfaces to the caller so they can log what was preserved.
254
+ return { deleted: false, skipped: true, foundValues: values, skippedTargets: unowned };
255
+ }
256
+
257
+ const response = await fetchWithRetry(
258
+ `${API_BASE}/zones/${zoneId}/rrsets/${relativeName}/${type}`,
259
+ {
260
+ method: 'DELETE',
261
+ headers: { Authorization: `Bearer ${apiToken}` },
262
+ },
263
+ );
264
+
265
+ return {
266
+ deleted: response.ok,
267
+ skipped: false,
268
+ foundValues: values,
269
+ skippedTargets: [],
270
+ };
271
+ }
272
+
273
+ // ============================================================================
274
+ // HIGH-LEVEL SETUP FUNCTIONS
275
+ // ============================================================================
276
+
277
+ /**
278
+ * Set up Hetzner DNS for a simple single-server deployment.
279
+ * Creates root A and wildcard A records.
280
+ *
281
+ * @param {string} apiToken - Hetzner Cloud API token
282
+ * @param {string} zoneId - DNS zone ID
283
+ * @param {string} domain - Domain name (e.g. "app.example.com")
284
+ * @param {string} serverIp - Server IP address
285
+ * @param {object} [options] - Options
286
+ * @param {Function} [options.onProgress] - Progress callback (skips internal spinners)
287
+ * @returns {Promise<object>} Setup result
288
+ */
289
+ export async function setupSimple(apiToken, zoneId, domain, serverIp, options = {}) {
290
+ const { onProgress } = options;
291
+ const s = onProgress ? null : p.spinner();
292
+
293
+ try {
294
+ s?.start(`Creating DNS records for ${domain}`);
295
+ onProgress?.(`Creating DNS records for ${domain}`);
296
+
297
+ // Fetch zone name for FQDN→relative conversion
298
+ const zone = await getZone(apiToken, zoneId);
299
+ const zoneName = zone.name;
300
+
301
+ // Root A record
302
+ await createDNSRecord(apiToken, zoneId, zoneName, {
303
+ type: 'A',
304
+ name: domain,
305
+ value: serverIp,
306
+ ttl: DEFAULT_TTL,
307
+ });
308
+
309
+ // Wildcard A record for subdomains (registry, api, etc.)
310
+ await createDNSRecord(apiToken, zoneId, zoneName, {
311
+ type: 'A',
312
+ name: `*.${domain}`,
313
+ value: serverIp,
314
+ ttl: DEFAULT_TTL,
315
+ });
316
+
317
+ s?.stop('DNS records created (root + wildcard)');
318
+ onProgress?.('DNS records created (root + wildcard)');
319
+
320
+ return { success: true, mode: 'simple' };
321
+ } catch (error) {
322
+ s?.stop(`Hetzner DNS setup failed: ${error.message}`);
323
+ return { success: false, error: error.message };
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Set up Hetzner DNS for an HA deployment.
329
+ * Creates root A and wildcard A records pointing to the primary server,
330
+ * with a low TTL for fast failover propagation.
331
+ *
332
+ * Note: Hetzner DNS has no health checks or load balancers.
333
+ * Failover is triggered manually via `vibecarbon failover`.
334
+ *
335
+ * @param {string} apiToken - Hetzner Cloud API token
336
+ * @param {string} zoneId - DNS zone ID
337
+ * @param {string} domain - Domain name
338
+ * @param {Array} servers - Array of { name, ip } objects (primary first)
339
+ * @returns {Promise<object>} Setup result
340
+ */
341
+ export async function setupHA(apiToken, zoneId, domain, servers) {
342
+ const s = p.spinner();
343
+
344
+ try {
345
+ // Fetch zone name for FQDN→relative conversion
346
+ const zone = await getZone(apiToken, zoneId);
347
+ const zoneName = zone.name;
348
+
349
+ // Root A record (low TTL for fast failover)
350
+ s.start(`Creating DNS record for ${domain}`);
351
+ await createDNSRecord(apiToken, zoneId, zoneName, {
352
+ type: 'A',
353
+ name: domain,
354
+ value: servers[0].ip,
355
+ ttl: HA_TTL,
356
+ });
357
+ s.stop('Primary DNS record created');
358
+
359
+ // Wildcard A record (low TTL for fast failover)
360
+ await createDNSRecord(apiToken, zoneId, zoneName, {
361
+ type: 'A',
362
+ name: `*.${domain}`,
363
+ value: servers[0].ip,
364
+ ttl: HA_TTL,
365
+ });
366
+
367
+ return {
368
+ success: true,
369
+ mode: 'dns',
370
+ primaryIp: servers[0].ip,
371
+ standbyIp: servers[1]?.ip,
372
+ };
373
+ } catch (error) {
374
+ s.stop(`Hetzner DNS setup failed: ${error.message}`);
375
+ return { success: false, error: error.message };
376
+ }
377
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Hetzner Guided Setup - Interactive prompts for first-time setup
3
+ *
4
+ * This module provides improved DX for users setting up Hetzner credentials:
5
+ * 1. Creating a Hetzner API token
6
+ * 2. Creating S3 Object Storage credentials
7
+ *
8
+ * Features:
9
+ * - Clear visual guides with step-by-step instructions
10
+ * - Better validation and error messages
11
+ * - Lookup chain: env var → ~/.vibecarbon/credentials.json → interactive prompt
12
+ * - Single "Save keys for future deploys?" prompt applies to all credentials
13
+ */
14
+
15
+ import * as p from '@clack/prompts';
16
+ import { c } from './colors.js';
17
+ import { loadCredentials, saveCredentials } from './config.js';
18
+
19
+ /**
20
+ * Validate a Hetzner API token by making a lightweight API call.
21
+ * @param {string} token
22
+ * @returns {Promise<{ valid: boolean, error?: string }>}
23
+ */
24
+ async function validateHetznerToken(token) {
25
+ try {
26
+ const res = await fetch('https://api.hetzner.cloud/v1/locations?per_page=1', {
27
+ headers: { Authorization: `Bearer ${token}` },
28
+ });
29
+ if (res.ok) return { valid: true };
30
+ if (res.status === 401) return { valid: false, error: 'Token is invalid or expired' };
31
+ return { valid: false, error: `Hetzner API returned status ${res.status}` };
32
+ } catch {
33
+ return { valid: true, unreachable: true };
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Display improved visual guide for API token creation
39
+ */
40
+ function displayApiTokenGuide(projectName) {
41
+ p.log.info('');
42
+ p.log.info(`${c.boldCyan('🔑 Hetzner API Token Setup')}`);
43
+ p.log.info('');
44
+ p.log.info(c.dim('─'.repeat(60)));
45
+ p.log.info('');
46
+ p.log.info(`${c.bold('1.')} Open: ${c.boldCyanUnderline('https://console.hetzner.cloud/')}`);
47
+ p.log.info(
48
+ `${c.dim(' └─')} Select ${c.info('+ New project')} and call it ${c.info(projectName || 'your-project')}`,
49
+ );
50
+ p.log.info('');
51
+ p.log.info(`${c.bold('2.')} In the sidebar: ${c.info('Security')}`);
52
+ p.log.info(`${c.dim(' └─')} Click the ${c.info('API tokens')} tab`);
53
+ p.log.info('');
54
+ p.log.info(`${c.bold('3.')} Click ${c.boldCyan('Generate API token')}`);
55
+ p.log.info(`${c.dim(' ├─')} Description: ${c.info(`"${projectName || 'vibecarbon'}-deploy"`)}`);
56
+ p.log.info(`${c.dim(' └─')} Permissions: ${c.info('Read & Write')} ${c.dim('(required)')}`);
57
+ p.log.info('');
58
+ p.log.info(`${c.bold('4.')} ${c.boldYellow('⚠️ Copy the token immediately')}`);
59
+ p.log.info(`${c.dim(' └─')} ${c.dim('It will only be shown once!')}`);
60
+ p.log.info('');
61
+ p.log.info(c.dim('─'.repeat(60)));
62
+ p.log.info('');
63
+ }
64
+
65
+ /**
66
+ * Display improved visual guide for S3 credentials creation
67
+ */
68
+ function displayS3CredentialsGuide(projectName) {
69
+ p.log.info('');
70
+ p.log.info(`${c.boldCyan('🗄️ Hetzner Object Storage Setup')}`);
71
+ p.log.info('');
72
+ p.log.info(c.dim('─'.repeat(60)));
73
+ p.log.info('');
74
+ p.log.info(
75
+ `${c.bold('1.')} Click the ${c.info('S3 credentials')} tab ${c.dim('(you should already be on Security)')}`,
76
+ );
77
+ p.log.info('');
78
+ p.log.info(`${c.bold('2.')} Click ${c.boldCyan('Generate credentials')}`);
79
+ p.log.info(`${c.dim(' └─')} Description: ${c.info(`"${projectName || 'vibecarbon'}-s3"`)}`);
80
+
81
+ p.log.info('');
82
+ p.log.info(`${c.bold('3.')} ${c.boldYellow('⚠️ Copy BOTH credentials immediately:')}`);
83
+ p.log.info(`${c.dim(' ├─')} ${c.info('Access Key')} ${c.dim('(visible anytime)')}`);
84
+ p.log.info(`${c.dim(' └─')} ${c.info('Secret Key')} ${c.dim('(only shown once!)')}`);
85
+ p.log.info('');
86
+ p.log.info(c.dim('─'.repeat(60)));
87
+ p.log.info('');
88
+ p.note('Object Storage is required for database backups and file uploads', 'Why S3?');
89
+ p.log.info('');
90
+ }
91
+
92
+ // Session-level save preference: null = not asked yet, true/false = user's answer.
93
+ // Once the user answers "Save keys for future deploys?" the decision applies to all
94
+ // credentials prompted during this process.
95
+ let _savePreference = null;
96
+
97
+ /**
98
+ * Ask the user once whether to save credentials, then reuse the answer.
99
+ * @param {object} credentials - Credential object to merge-save
100
+ */
101
+ export async function saveIfWanted(credentials) {
102
+ if (_savePreference === null) {
103
+ const shouldSave = await p.confirm({
104
+ message: 'Save keys for future deploys?',
105
+ initialValue: true,
106
+ });
107
+ _savePreference = shouldSave && !p.isCancel(shouldSave);
108
+ }
109
+
110
+ if (_savePreference) {
111
+ saveCredentials(credentials);
112
+ p.log.info(c.dim('Saved to ~/.vibecarbon/credentials.json'));
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Reset the session-level save preference (for testing)
118
+ */
119
+ export function resetSavePreference() {
120
+ _savePreference = null;
121
+ }
122
+
123
+ /**
124
+ * Get API token with improved visual guidance
125
+ * Lookup order: env var → credentials file → interactive prompt
126
+ *
127
+ * @param {string} [projectName] - Project name for display in guide
128
+ * @param {{ save?: boolean }} [options] - Options (save: offer to save, default true)
129
+ * @returns {Promise<string|null>}
130
+ */
131
+ export async function getApiToken(projectName, options = {}) {
132
+ const { save = true } = options;
133
+
134
+ // 1. Check environment variable first
135
+ const envToken = process.env.HETZNER_API_TOKEN;
136
+ if (envToken) {
137
+ const check = await validateHetznerToken(envToken);
138
+ if (!check.valid) {
139
+ p.log.warn(`HETZNER_API_TOKEN environment variable is set but invalid: ${check.error}`);
140
+ // Fall through to credentials file / interactive prompt
141
+ } else {
142
+ if (check.unreachable) {
143
+ p.log.warn('Could not reach Hetzner API to verify token — proceeding with saved token');
144
+ }
145
+ p.log.info('✓ Using Hetzner API token from HETZNER_API_TOKEN environment variable');
146
+ return envToken;
147
+ }
148
+ }
149
+
150
+ // 2. Check credentials file
151
+ const creds = loadCredentials();
152
+ if (creds.hetzner?.apiToken) {
153
+ const check = await validateHetznerToken(creds.hetzner.apiToken);
154
+ if (!check.valid) {
155
+ p.log.warn(`Saved Hetzner API token is invalid: ${check.error}. Please enter a new one.`);
156
+ // Fall through to interactive prompt
157
+ } else {
158
+ if (check.unreachable) {
159
+ p.log.warn('Could not reach Hetzner API to verify token — proceeding with saved token');
160
+ }
161
+ p.log.info('✓ Using Hetzner API token from ~/.vibecarbon/credentials.json');
162
+ return creds.hetzner.apiToken;
163
+ }
164
+ }
165
+
166
+ // 3. Interactive prompt (with retry on invalid token)
167
+ displayApiTokenGuide(projectName);
168
+
169
+ let token;
170
+ while (true) {
171
+ token = await p.password({
172
+ message: 'Paste your Hetzner API token here',
173
+ validate: (v) => {
174
+ if (!v || v.length < 10) return 'API token is required';
175
+ if (v.length !== 64)
176
+ return 'Token should be 64 characters - please check you copied it correctly';
177
+ return undefined;
178
+ },
179
+ });
180
+
181
+ if (p.isCancel(token)) {
182
+ p.cancel('Operation cancelled.');
183
+ process.exit(0);
184
+ }
185
+
186
+ const s = p.spinner();
187
+ s.start('Verifying API token...');
188
+ const check = await validateHetznerToken(token);
189
+ if (check.valid) {
190
+ if (check.unreachable) {
191
+ s.stop('Could not reach Hetzner API — proceeding with unverified token');
192
+ } else {
193
+ s.stop('API token verified');
194
+ }
195
+ break;
196
+ }
197
+ s.stop(`Token invalid: ${check.error}`);
198
+ p.log.warn('Please try again with a valid token.');
199
+ }
200
+
201
+ if (save) {
202
+ await saveIfWanted({ hetzner: { apiToken: token } });
203
+ }
204
+
205
+ return token;
206
+ }
207
+
208
+ /**
209
+ * Get S3 credentials with improved visual guidance
210
+ * Lookup order: env vars → credentials file → interactive prompt
211
+ *
212
+ * @param {string} [projectName] - Project name for display in guide
213
+ * @param {{ save?: boolean, force?: boolean }} [options] - Options
214
+ * save: offer to save credentials (default true)
215
+ * force: skip env/file lookup and go straight to interactive prompt (default false)
216
+ * @returns {Promise<{accessKey: string, secretKey: string}|null>}
217
+ */
218
+ export async function getS3Credentials(projectName, options = {}) {
219
+ const { save = true, force = false } = options;
220
+
221
+ if (!force) {
222
+ // 1. Check environment variables first
223
+ const envAccessKey = process.env.S3_ACCESS_KEY;
224
+ const envSecretKey = process.env.S3_SECRET_KEY;
225
+
226
+ if (envAccessKey && envSecretKey) {
227
+ p.log.info('✓ Using S3 credentials from environment variables');
228
+ return { accessKey: envAccessKey, secretKey: envSecretKey };
229
+ }
230
+
231
+ // 2. Check credentials file
232
+ const creds = loadCredentials();
233
+ if (creds.s3?.accessKey && creds.s3?.secretKey) {
234
+ p.log.info('✓ Using S3 credentials from ~/.vibecarbon/credentials.json');
235
+ return { accessKey: creds.s3.accessKey, secretKey: creds.s3.secretKey };
236
+ }
237
+ }
238
+
239
+ // 3. Interactive prompt
240
+ displayS3CredentialsGuide(projectName);
241
+
242
+ const accessKey = await p.text({
243
+ message: 'Paste your S3 Access Key here',
244
+ validate: (v) => {
245
+ if (!v || v.length < 10) return 'Access Key is required';
246
+ return undefined;
247
+ },
248
+ });
249
+
250
+ if (p.isCancel(accessKey)) {
251
+ p.cancel('Operation cancelled.');
252
+ process.exit(0);
253
+ }
254
+
255
+ const secretKey = await p.password({
256
+ message: 'Paste your S3 Secret Key here',
257
+ validate: (v) => {
258
+ if (!v || v.length < 10) return 'Secret Key is required';
259
+ return undefined;
260
+ },
261
+ });
262
+
263
+ if (p.isCancel(secretKey)) {
264
+ p.cancel('Operation cancelled.');
265
+ process.exit(0);
266
+ }
267
+
268
+ p.log.success('S3 credentials received!');
269
+
270
+ if (save) {
271
+ await saveIfWanted({ s3: { accessKey, secretKey } });
272
+ }
273
+
274
+ return { accessKey, secretKey };
275
+ }
@@ -0,0 +1,37 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ /**
5
+ * Absolute path to the per-environment known_hosts file for SSH strict-checking.
6
+ *
7
+ * NOTE: Permission enforcement below relies on POSIX modes and is a no-op on
8
+ * Windows. This CLI targets Linux/macOS developer machines and Linux servers.
9
+ *
10
+ * @param {string} env - environment name
11
+ * @param {string} [cwd=process.cwd()] - project root
12
+ * @returns {string}
13
+ */
14
+ export function knownHostsPath(env, cwd = process.cwd()) {
15
+ return join(cwd, '.vibecarbon', `known_hosts_${env}`);
16
+ }
17
+
18
+ /**
19
+ * Overwrite the per-environment known_hosts file with the given host-key lines.
20
+ *
21
+ * Enforces `.vibecarbon/` mode 0o700 and file mode 0o600 unconditionally —
22
+ * `mkdirSync`/`writeFileSync` only apply `mode` on initial creation, so we
23
+ * follow each with `chmodSync` to guarantee the mode even when the target
24
+ * already exists (which it will on every re-pin after first deploy).
25
+ *
26
+ * @param {string} env
27
+ * @param {string[]} hostKeyLines - one host-key entry per element
28
+ * @param {string} [cwd=process.cwd()]
29
+ */
30
+ export function pinHostKey(env, hostKeyLines, cwd = process.cwd()) {
31
+ const path = knownHostsPath(env, cwd);
32
+ const dir = dirname(path);
33
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
34
+ chmodSync(dir, 0o700);
35
+ writeFileSync(path, `${hostKeyLines.join('\n')}\n`, { mode: 0o600 });
36
+ chmodSync(path, 0o600);
37
+ }