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,583 @@
1
+ /**
2
+ * Secret-leak detector for vibecarbon-managed projects.
3
+ *
4
+ * Two layers of defense feed off this module:
5
+ * 1. The pre-commit hook scaffolded into every `vibecarbon create` project
6
+ * (.git/hooks/pre-commit calls scripts/secret-scan.mjs over staged files).
7
+ * Catches secrets before they enter git history.
8
+ * 2. The CLI deploy/add path. Runs against the working tree (filtered
9
+ * through `git ls-files`, so .gitignore is respected and the scan
10
+ * mirrors what `git push` would actually upload) and refuses the
11
+ * operation on any finding. Belt-and-suspenders against
12
+ * `git commit --no-verify`.
13
+ *
14
+ * No `--allow-secrets` bypass exists by design. If a finding is a true
15
+ * false positive, add the substring (or `regex:<pat>`) to .vibecarbonignore
16
+ * at the repo root. If the finding is real, rotate the credential and
17
+ * remove it from history before retrying.
18
+ *
19
+ * The patterns are intentionally biased toward LOW false-positive rates:
20
+ * - Each rule includes both a primary regex and (where useful) a context
21
+ * regex that the surrounding 256 chars must also match. This prevents
22
+ * a stray 40-char alphanumeric in a docs file from tripping the
23
+ * "Hetzner token" rule.
24
+ * - JWT detection is gated on the *decoded* payload claim — a JWT is
25
+ * only flagged if its base64url-decoded payload contains
26
+ * `"role":"service_role"` or similar. The previous JWT-shaped
27
+ * placeholders in carbon/k8s/overlays/local/secrets.yaml tripped
28
+ * GitHub's scanner because they had `service_role` in the payload —
29
+ * we now ship plain non-JWT placeholders, so this rule fires only on
30
+ * real Supabase keys.
31
+ * - Allowlist support: a `.vibecarbonignore` file at the repo root can
32
+ * contain literal substrings or `regex:<pattern>` lines that suppress
33
+ * matches. Same syntax as our docs.
34
+ */
35
+
36
+ import { execFileSync } from 'node:child_process';
37
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
38
+ import { extname, join, relative, sep } from 'node:path';
39
+
40
+ /**
41
+ * @typedef {object} SecretRule
42
+ * @property {string} id - stable identifier (used in allowlist + reports)
43
+ * @property {string} description - human-readable name shown to operators
44
+ * @property {RegExp} pattern - primary detection regex
45
+ * @property {RegExp} [context] - optional context regex (must also match
46
+ * the surrounding ±128 chars). Use to dampen false positives on rules
47
+ * that match generic shapes (e.g. 40-char alphanum strings).
48
+ * @property {(match: string, fullMatch: RegExpExecArray) => boolean} [predicate]
49
+ * - optional extra filter. Receives the matched substring; return false
50
+ * to discard.
51
+ */
52
+
53
+ /** @type {SecretRule[]} */
54
+ export const SECRET_RULES = [
55
+ {
56
+ id: 'aws-access-key',
57
+ description: 'AWS access key ID',
58
+ pattern: /\bAKIA[0-9A-Z]{16}\b/g,
59
+ },
60
+ {
61
+ id: 'aws-secret-key',
62
+ description: 'AWS secret access key',
63
+ pattern:
64
+ /(?:aws[_-]?secret[_-]?access[_-]?key|secretAccessKey)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
65
+ },
66
+ {
67
+ id: 'github-pat',
68
+ description: 'GitHub personal access token',
69
+ pattern: /\bgh[posru]_[A-Za-z0-9]{36,}\b/g,
70
+ },
71
+ {
72
+ id: 'stripe-live-key',
73
+ description: 'Stripe live secret key',
74
+ pattern: /\bsk_live_[A-Za-z0-9]{24,}\b/g,
75
+ },
76
+ {
77
+ id: 'stripe-test-key',
78
+ description: 'Stripe test secret key (still sensitive — can drain test ledger)',
79
+ pattern: /\bsk_test_[A-Za-z0-9]{24,}\b/g,
80
+ },
81
+ {
82
+ id: 'slack-token',
83
+ description: 'Slack token',
84
+ pattern: /\bxox[abprs]-[0-9A-Za-z-]{10,}\b/g,
85
+ },
86
+ {
87
+ id: 'openai-key',
88
+ description: 'OpenAI API key',
89
+ pattern: /\bsk-[A-Za-z0-9]{20,}\b/g,
90
+ },
91
+ {
92
+ id: 'anthropic-key',
93
+ description: 'Anthropic API key',
94
+ pattern: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/g,
95
+ },
96
+ {
97
+ id: 'google-service-account-json',
98
+ description: 'Google Cloud service account JSON',
99
+ pattern: /"type"\s*:\s*"service_account"/g,
100
+ context: /"private_key"\s*:\s*"-----BEGIN[ A-Z]+PRIVATE KEY-----/,
101
+ },
102
+ {
103
+ id: 'google-api-key',
104
+ description: 'Google API key',
105
+ pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g,
106
+ },
107
+ {
108
+ id: 'private-key-block',
109
+ description: 'PEM-encoded private key',
110
+ pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
111
+ },
112
+ {
113
+ id: 'hetzner-token',
114
+ description: 'Hetzner Cloud API token',
115
+ // Hetzner tokens are 64 alphanumeric chars. Gate on a nearby
116
+ // `hetzner` / `hcloud` identifier so we don't flag every 64-char
117
+ // string in test fixtures or hashes.
118
+ pattern: /['"]([A-Za-z0-9]{64})['"]/g,
119
+ context: /(?:hetzner|hcloud|HCLOUD_TOKEN|HETZNER_API_TOKEN)/i,
120
+ },
121
+ {
122
+ id: 'cloudflare-api-token',
123
+ description: 'Cloudflare scoped API token',
124
+ pattern: /\b[A-Za-z0-9_-]{40}\b/g,
125
+ context: /(?:cloudflare|cf[_-]?api[_-]?token|CLOUDFLARE_API_TOKEN)/i,
126
+ predicate: (m) => {
127
+ // Exclude hex (commit shas, hashes).
128
+ if (/^[a-f0-9]+$/i.test(m)) return false;
129
+ // Real Cloudflare API tokens are high-entropy random base62/64;
130
+ // P(all-lowercase) ≈ 10^-12 for a true 40-char token. All-lowercase
131
+ // 40-char strings with hyphen separators are the signature of
132
+ // hyphenated identifiers — kubernetes resource names, project
133
+ // identifiers, and the `${projectName}-${env}-${role}` server names
134
+ // vibecarbon writes to .vibecarbon.json (which collided with the
135
+ // `dnsProvider: 'cloudflare'` context match and false-flagged
136
+ // compose-ha warm-deploy in 2026-05-19 matrix run bq5c4h9l5).
137
+ return /[A-Z]/.test(m);
138
+ },
139
+ },
140
+ {
141
+ id: 'supabase-service-role-jwt',
142
+ description: 'Supabase service-role JWT (privileged)',
143
+ pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
144
+ predicate: (m) => {
145
+ const payload = decodeJwtPayload(m);
146
+ if (!payload) return false;
147
+ // Only fire on truly privileged role claims. `anon` keys are
148
+ // intentionally public.
149
+ if (payload.role === 'service_role') return true;
150
+ if (payload.role === 'admin') return true;
151
+ return false;
152
+ },
153
+ },
154
+ {
155
+ id: 'generic-secret-assignment',
156
+ description: 'High-entropy value assigned to a secret-named variable',
157
+ // Only fires when the surrounding identifier is suggestive AND the
158
+ // value has high entropy. This is the noisiest rule; the allowlist
159
+ // is the escape hatch.
160
+ pattern:
161
+ /(?:password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|credentials?)\s*[:=]\s*['"]([^'"\s]{20,})['"]/gi,
162
+ predicate: (_m, captured) => {
163
+ const value = captured?.[1];
164
+ if (!value) return false;
165
+ // Variable interpolations are not values. Both $VAR (shell, GH
166
+ // Actions) and ${expr} (Bash, JS template literals) refer to a
167
+ // value defined elsewhere — flagging them just trains operators
168
+ // to ignore the scanner.
169
+ if (/^\$/.test(value)) return false;
170
+ if (/^(?:REPLACE|TODO|CHANGE|YOUR|EXAMPLE|PLACEHOLDER|FIXME)/i.test(value)) return false;
171
+ if (/^(?:local|dev|test|sample|fake|demo)[-_]/i.test(value)) return false;
172
+ return shannonEntropy(value) >= 3.5;
173
+ },
174
+ },
175
+ ];
176
+
177
+ /**
178
+ * Decode a JWT's base64url-encoded payload to a JS object.
179
+ * @param {string} jwt
180
+ * @returns {Record<string, unknown> | null}
181
+ */
182
+ function decodeJwtPayload(jwt) {
183
+ const parts = jwt.split('.');
184
+ if (parts.length !== 3) return null;
185
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
186
+ const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
187
+ try {
188
+ const json = Buffer.from(padded, 'base64').toString('utf-8');
189
+ return JSON.parse(json);
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Shannon entropy (bits per symbol) over an ASCII-ish string. Random
197
+ * hex lives ~4.0, random base64 ~5.0; English prose hovers around
198
+ * 3.0-3.5.
199
+ * @param {string} s
200
+ * @returns {number}
201
+ */
202
+ function shannonEntropy(s) {
203
+ if (!s) return 0;
204
+ const freq = new Map();
205
+ for (const c of s) freq.set(c, (freq.get(c) ?? 0) + 1);
206
+ let h = 0;
207
+ for (const n of freq.values()) {
208
+ const p = n / s.length;
209
+ h -= p * Math.log2(p);
210
+ }
211
+ return h;
212
+ }
213
+
214
+ /**
215
+ * @typedef {object} SecretFinding
216
+ * @property {string} ruleId - which SecretRule fired
217
+ * @property {string} description - human-readable name
218
+ * @property {string} match - the matched substring (truncated)
219
+ * @property {number} index - byte offset within the scanned content
220
+ * @property {number} line - 1-indexed line number
221
+ * @property {number} column - 1-indexed column number
222
+ */
223
+
224
+ /**
225
+ * Scan a string buffer (typically a single file's contents) for secret
226
+ * patterns.
227
+ *
228
+ * @param {string} content
229
+ * @param {{allowlist?: string[]}} [options]
230
+ * @returns {SecretFinding[]}
231
+ */
232
+ export function scanContent(content, options = {}) {
233
+ const { allowlist = [] } = options;
234
+ if (!content || typeof content !== 'string') return [];
235
+ /** @type {SecretFinding[]} */
236
+ const findings = [];
237
+
238
+ for (const rule of SECRET_RULES) {
239
+ // Reset .lastIndex on the global regex between rules so multi-rule
240
+ // scans of the same content don't skip into mid-stream state.
241
+ rule.pattern.lastIndex = 0;
242
+ /** @type {RegExpExecArray | null} */
243
+ let m = rule.pattern.exec(content);
244
+ while (m !== null) {
245
+ const [whole] = m;
246
+ const { index } = m;
247
+
248
+ if (rule.context) {
249
+ const start = Math.max(0, index - 128);
250
+ const end = Math.min(content.length, index + whole.length + 128);
251
+ if (!rule.context.test(content.slice(start, end))) {
252
+ m = rule.pattern.exec(content);
253
+ continue;
254
+ }
255
+ }
256
+
257
+ if (rule.predicate && !rule.predicate(whole, m)) {
258
+ m = rule.pattern.exec(content);
259
+ continue;
260
+ }
261
+
262
+ if (matchesAllowlist(whole, allowlist)) {
263
+ m = rule.pattern.exec(content);
264
+ continue;
265
+ }
266
+
267
+ const { line, column } = locateOffset(content, index);
268
+ findings.push({
269
+ ruleId: rule.id,
270
+ description: rule.description,
271
+ match: whole.length > 64 ? `${whole.slice(0, 32)}…${whole.slice(-16)}` : whole,
272
+ index,
273
+ line,
274
+ column,
275
+ });
276
+
277
+ m = rule.pattern.exec(content);
278
+ }
279
+ }
280
+
281
+ return findings;
282
+ }
283
+
284
+ /**
285
+ * @param {string} value
286
+ * @param {string[]} allowlist
287
+ * @returns {boolean}
288
+ */
289
+ function matchesAllowlist(value, allowlist) {
290
+ for (const entry of allowlist) {
291
+ if (!entry || entry.startsWith('#')) continue;
292
+ if (entry.startsWith('regex:')) {
293
+ try {
294
+ const re = new RegExp(entry.slice('regex:'.length));
295
+ if (re.test(value)) return true;
296
+ } catch {
297
+ // ignore malformed regex entries
298
+ }
299
+ } else if (value.includes(entry)) {
300
+ return true;
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+
306
+ /**
307
+ * Convert a byte offset within `content` to (line, column).
308
+ * @param {string} content
309
+ * @param {number} offset
310
+ */
311
+ function locateOffset(content, offset) {
312
+ let line = 1;
313
+ let lineStart = 0;
314
+ for (let i = 0; i < offset; i++) {
315
+ if (content.charCodeAt(i) === 10) {
316
+ line++;
317
+ lineStart = i + 1;
318
+ }
319
+ }
320
+ return { line, column: offset - lineStart + 1 };
321
+ }
322
+
323
+ /**
324
+ * Load the allowlist from `.vibecarbonignore` at the given repo root.
325
+ * Returns an empty array if the file is missing.
326
+ *
327
+ * @param {string} cwd
328
+ * @returns {string[]}
329
+ */
330
+ export function loadAllowlist(cwd = process.cwd()) {
331
+ const path = join(cwd, '.vibecarbonignore');
332
+ try {
333
+ const raw = readFileSync(path, 'utf-8');
334
+ return raw
335
+ .split('\n')
336
+ .map((line) => line.trim())
337
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
338
+ } catch (err) {
339
+ if (err && /** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return [];
340
+ throw err;
341
+ }
342
+ }
343
+
344
+ // Directories we never want to descend into during a tree-walk scan.
345
+ // node_modules / build outputs / vendor stuff would slow us down (many
346
+ // of these contain test fixtures or minified blobs that trip the
347
+ // generic-secret rule). Each entry matches a basename.
348
+ const SKIP_DIRS = new Set([
349
+ 'node_modules',
350
+ '.git',
351
+ '.next',
352
+ '.nuxt',
353
+ '.cache',
354
+ '.parcel-cache',
355
+ '.vercel',
356
+ '.turbo',
357
+ '.pnpm-store',
358
+ 'dist',
359
+ 'build',
360
+ 'out',
361
+ 'coverage',
362
+ '.nyc_output',
363
+ '.pulumi',
364
+ '.terraform',
365
+ '.vibecarbon', // local CLI state — generated, not source
366
+ '.venv',
367
+ 'venv',
368
+ '__pycache__',
369
+ ]);
370
+
371
+ // File extensions we won't even open. Binary blobs that secrets shouldn't
372
+ // appear inside; opening them costs IO + risks regex misfires on bytes
373
+ // that happen to look like ASCII secrets.
374
+ const SKIP_EXTS = new Set([
375
+ '.png',
376
+ '.jpg',
377
+ '.jpeg',
378
+ '.gif',
379
+ '.webp',
380
+ '.ico',
381
+ '.pdf',
382
+ '.zip',
383
+ '.gz',
384
+ '.tar',
385
+ '.tgz',
386
+ '.7z',
387
+ '.rar',
388
+ '.mp3',
389
+ '.mp4',
390
+ '.mov',
391
+ '.avi',
392
+ '.webm',
393
+ '.woff',
394
+ '.woff2',
395
+ '.ttf',
396
+ '.otf',
397
+ '.eot',
398
+ '.so',
399
+ '.dll',
400
+ '.dylib',
401
+ '.bin',
402
+ '.wasm',
403
+ ]);
404
+
405
+ // Skip any individual file > 1 MiB. Real source files stay well under
406
+ // this; minified bundles can be large and noisy without containing
407
+ // real secrets.
408
+ const MAX_FILE_BYTES = 1_048_576;
409
+
410
+ /**
411
+ * Basenames that are universally gitignored across vibecarbon-generated
412
+ * projects (and ~all node/12-factor apps): runtime secret holders that
413
+ * are NEVER intended to be pushed. Only honored in the no-git fallback
414
+ * walk — when we have a real git tree, we trust `git ls-files
415
+ * --exclude-standard`, which already filters via the project's actual
416
+ * .gitignore (and would correctly fire if the user ever made the mistake
417
+ * of `git add .env`).
418
+ *
419
+ * Mirrors the .env stanza scaffolded by `vibecarbon create` into
420
+ * carbon/.gitignore: `.env`, `.env.local`, `.env.*.local`, `!.env.example`.
421
+ *
422
+ * @param {string} basename
423
+ * @returns {boolean}
424
+ */
425
+ function isUniversalLocalEnv(basename) {
426
+ if (basename === '.env' || basename === '.env.local') return true;
427
+ // .env.<anything>.local — production.local, development.local, etc.
428
+ if (/^\.env\..+\.local$/.test(basename)) return true;
429
+ return false;
430
+ }
431
+
432
+ /**
433
+ * @typedef {object} FileFinding
434
+ * @property {string} file - relative path from cwd
435
+ * @property {string} ruleId
436
+ * @property {string} description
437
+ * @property {string} match - truncated for display
438
+ * @property {number} line
439
+ * @property {number} column
440
+ */
441
+
442
+ /**
443
+ * Walk a directory tree and scan every text-like file under `cwd`.
444
+ *
445
+ * Mirrors what `git push` would actually upload by preferring
446
+ * `git ls-files --cached --others --exclude-standard` when `cwd` is a
447
+ * git working tree. This both (a) skips gitignored files like
448
+ * `.env.local` that may legitimately contain secrets and (b) matches
449
+ * exactly what the deploy step would push to GitHub. Falls back to a
450
+ * plain directory walk (with a built-in skip list) when git isn't
451
+ * available — useful for the standalone `carbon/` template, scanning
452
+ * unrelated directories, or pre-init projects.
453
+ *
454
+ * @param {string} cwd
455
+ * @param {{allowlist?: string[], extraSkipDirs?: Set<string>}} [options]
456
+ * @returns {FileFinding[]}
457
+ */
458
+ export function scanTree(cwd, options = {}) {
459
+ const allowlist = options.allowlist ?? loadAllowlist(cwd);
460
+ const skipDirs = new Set([...SKIP_DIRS, ...(options.extraSkipDirs ?? [])]);
461
+ /** @type {FileFinding[]} */
462
+ const findings = [];
463
+
464
+ /** @param {string} relPath */
465
+ const scanFile = (relPath) => {
466
+ const full = join(cwd, relPath);
467
+ if (SKIP_EXTS.has(extname(relPath).toLowerCase())) return;
468
+ const base = relPath.split('/').pop() || '';
469
+ if (base === 'package-lock.json' || base === 'pnpm-lock.yaml') return;
470
+ let content;
471
+ try {
472
+ const stat = statSync(full);
473
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
474
+ content = readFileSync(full, 'utf-8');
475
+ } catch {
476
+ return;
477
+ }
478
+ const fileFindings = scanContent(content, { allowlist });
479
+ if (fileFindings.length === 0) return;
480
+ const display = relPath.split(sep).join('/');
481
+ for (const f of fileFindings) {
482
+ findings.push({
483
+ file: display,
484
+ ruleId: f.ruleId,
485
+ description: f.description,
486
+ match: f.match,
487
+ line: f.line,
488
+ column: f.column,
489
+ });
490
+ }
491
+ };
492
+
493
+ // Prefer git's view of the tree when available — the most accurate
494
+ // proxy for "what `git push` would actually upload". `--cached`
495
+ // covers tracked files; `--others --exclude-standard` adds untracked
496
+ // files that aren't gitignored. Hidden behind a feature check so
497
+ // tests + non-git directories still work.
498
+ const gitFiles = listGitTrackedFiles(cwd);
499
+ if (gitFiles) {
500
+ for (const rel of gitFiles) scanFile(rel);
501
+ return findings;
502
+ }
503
+
504
+ /** @param {string} dir */
505
+ function walk(dir) {
506
+ let entries;
507
+ try {
508
+ entries = readdirSync(dir, { withFileTypes: true });
509
+ } catch {
510
+ return;
511
+ }
512
+ for (const ent of entries) {
513
+ const full = join(dir, ent.name);
514
+ if (ent.isDirectory()) {
515
+ if (skipDirs.has(ent.name)) continue;
516
+ walk(full);
517
+ continue;
518
+ }
519
+ if (!ent.isFile()) continue;
520
+ // Universal local-only env files: the project may have no
521
+ // .gitignore (e.g. created with -no-git), but these files would
522
+ // never reach a remote. Scanning them only produces guaranteed
523
+ // false positives that block legitimate `vibecarbon add`/`deploy`
524
+ // operations. The git-tracked path above (which we prefer when
525
+ // available) still surfaces the genuine "user accidentally
526
+ // committed .env" mistake.
527
+ if (isUniversalLocalEnv(ent.name)) continue;
528
+ const rel = relative(cwd, full);
529
+ scanFile(rel);
530
+ }
531
+ }
532
+ walk(cwd);
533
+ return findings;
534
+ }
535
+
536
+ /**
537
+ * Ask git for the list of files it would push. Returns null if `cwd`
538
+ * isn't a git working tree, git isn't on PATH, or the call fails for
539
+ * any other reason — the caller should fall back to a plain directory
540
+ * walk in that case.
541
+ *
542
+ * @param {string} cwd
543
+ * @returns {string[] | null}
544
+ */
545
+ function listGitTrackedFiles(cwd) {
546
+ if (!existsSync(join(cwd, '.git'))) return null;
547
+ try {
548
+ const stdout = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
549
+ cwd,
550
+ encoding: 'utf-8',
551
+ stdio: ['ignore', 'pipe', 'ignore'],
552
+ });
553
+ return stdout
554
+ .split('\n')
555
+ .map((s) => s.trim())
556
+ .filter(Boolean);
557
+ } catch {
558
+ return null;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Format an array of findings for terminal output. Returns a multi-line
564
+ * human-readable string ending in a newline.
565
+ *
566
+ * @param {FileFinding[]} findings
567
+ * @returns {string}
568
+ */
569
+ export function formatFindings(findings) {
570
+ if (!findings || findings.length === 0) return '';
571
+ const lines = [];
572
+ lines.push(`Detected ${findings.length} potential secret${findings.length === 1 ? '' : 's'}:`);
573
+ for (const f of findings) {
574
+ lines.push(` ${f.file}:${f.line}:${f.column} [${f.ruleId}] ${f.description}`);
575
+ lines.push(` → ${f.match}`);
576
+ }
577
+ lines.push('');
578
+ lines.push('If a finding is a false positive, add it to .vibecarbonignore at the repo root:');
579
+ lines.push(' - one entry per line');
580
+ lines.push(' - literal substring match, OR `regex:<pattern>` for a regex');
581
+ lines.push(' - `#` comments are ignored');
582
+ return `${lines.join('\n')}\n`;
583
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Secret generation utilities
3
+ */
4
+
5
+ import { createHmac, randomBytes } from 'node:crypto';
6
+ import bcrypt from 'bcryptjs';
7
+
8
+ /**
9
+ * Generate a secure random password
10
+ *
11
+ * @param {number} length - Password length (default: 32)
12
+ * @returns {string} - Generated password
13
+ */
14
+ export function generatePassword(length = 32) {
15
+ return randomBytes(length).toString('base64').slice(0, length).replace(/[+/=]/g, 'x');
16
+ }
17
+
18
+ /**
19
+ * Generate a JWT token with HS256 signing
20
+ *
21
+ * @param {string} secret - JWT signing secret
22
+ * @param {object} payload - Token payload
23
+ * @param {number} exp - Expiration time in seconds (default: 10 years)
24
+ * @returns {string} - Signed JWT token
25
+ */
26
+ export function generateJWT(secret, payload, exp = 315360000) {
27
+ const header = { alg: 'HS256', typ: 'JWT' };
28
+ const now = Math.floor(Date.now() / 1000);
29
+
30
+ const fullPayload = {
31
+ ...payload,
32
+ iat: now,
33
+ exp: now + exp,
34
+ };
35
+
36
+ const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
37
+ const base64Payload = Buffer.from(JSON.stringify(fullPayload)).toString('base64url');
38
+
39
+ const signature = createHmac('sha256', secret)
40
+ .update(`${base64Header}.${base64Payload}`)
41
+ .digest('base64url');
42
+
43
+ return `${base64Header}.${base64Payload}.${signature}`;
44
+ }
45
+
46
+ /**
47
+ * Generate a replication password for HA Postgres deployments.
48
+ * 24 random bytes → 32 base64url chars. URL/shell safe (no + / =).
49
+ */
50
+ export function generateReplPassword() {
51
+ return randomBytes(24).toString('base64url');
52
+ }
53
+
54
+ /**
55
+ * Generate a bcrypt hash of a password
56
+ * Uses the same algorithm as Supabase GoTrue (bcrypt with cost factor 10)
57
+ *
58
+ * @param {string} password - Plain text password
59
+ * @returns {string} - Bcrypt hash
60
+ */
61
+ export function hashPassword(password) {
62
+ return bcrypt.hashSync(password, 10);
63
+ }