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,162 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { jsonPost, mountRoute } from '../../../_helpers/app';
3
+ import { makeContactSubmission } from '../../../_helpers/factories';
4
+
5
+ // `vi.hoisted` runs before the vi.mock factories below, so the same object
6
+ // reference is shared with the mocked env module. Per-test mutations to
7
+ // `fakeEnv` are visible inside the route under test.
8
+ const { insertMock, sendEmailMock, fakeEnv } = vi.hoisted(() => ({
9
+ insertMock: vi.fn(),
10
+ sendEmailMock: vi.fn().mockResolvedValue(undefined),
11
+ fakeEnv: {
12
+ SMTP_ADMIN_EMAIL: undefined as string | undefined,
13
+ } as Record<string, unknown>,
14
+ }));
15
+
16
+ vi.mock('@server/lib/supabase', () => ({
17
+ supabaseAdmin: {
18
+ from: () => ({ insert: insertMock }),
19
+ },
20
+ }));
21
+
22
+ // Email is fired-and-forgotten on success — stub it so tests don't try
23
+ // to open an SMTP connection.
24
+ vi.mock('@server/lib/email', () => ({
25
+ sendEmail: sendEmailMock,
26
+ }));
27
+
28
+ // The route reads `env.SMTP_ADMIN_EMAIL` at request time. The real env
29
+ // module evaluates zod schemas at import time, so vi.stubEnv can't shift
30
+ // values after the fact — mock the module instead with a mutable object.
31
+ vi.mock('@server/lib/env', () => ({
32
+ env: fakeEnv,
33
+ }));
34
+
35
+ // The route file's `createRateLimiter` returns a Hono middleware. The
36
+ // real implementation builds an in-memory rate-limit store on a 60s
37
+ // setInterval — replace with a pass-through so the timer never starts.
38
+ vi.mock('@server/lib/rate-limiter', () => ({
39
+ createRateLimiter: () => async (_c: unknown, next: () => Promise<void>) => {
40
+ await next();
41
+ },
42
+ }));
43
+
44
+ vi.mock('@server/lib/logger', () => ({
45
+ logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn(), debug: vi.fn() },
46
+ }));
47
+
48
+ const { contactRoutes } = await import('@server/routes/v1/contact');
49
+ const app = mountRoute('/api/v1/contact', contactRoutes);
50
+
51
+ beforeEach(() => {
52
+ insertMock.mockReset();
53
+ sendEmailMock.mockClear();
54
+ fakeEnv.SMTP_ADMIN_EMAIL = undefined;
55
+ });
56
+
57
+ describe('POST /api/v1/contact/submit', () => {
58
+ it('inserts the submission and returns 200 on the happy path', async () => {
59
+ insertMock.mockResolvedValueOnce({ error: null });
60
+
61
+ const payload = makeContactSubmission();
62
+ const res = await app.request('/api/v1/contact/submit', jsonPost(payload));
63
+
64
+ expect(res.status).toBe(200);
65
+ expect(await res.json()).toEqual({ success: true });
66
+ expect(insertMock).toHaveBeenCalledTimes(1);
67
+ expect(insertMock).toHaveBeenCalledWith({
68
+ name: payload.name,
69
+ email: payload.email,
70
+ subject: payload.subject,
71
+ message: payload.message,
72
+ });
73
+ });
74
+
75
+ it('returns 400 with field errors when validation fails', async () => {
76
+ const res = await app.request(
77
+ '/api/v1/contact/submit',
78
+ jsonPost({ name: '', email: 'not-an-email', subject: '', message: 'short' }),
79
+ );
80
+
81
+ expect(res.status).toBe(400);
82
+ const body = (await res.json()) as { error: string };
83
+ expect(body.error).toContain('Name is required');
84
+ expect(insertMock).not.toHaveBeenCalled();
85
+ });
86
+
87
+ it('returns 400 on invalid JSON', async () => {
88
+ const res = await app.request('/api/v1/contact/submit', {
89
+ method: 'POST',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: '{not valid',
92
+ });
93
+ expect(res.status).toBe(400);
94
+ const body = (await res.json()) as { error: string };
95
+ expect(body.error).toBe('Invalid JSON body');
96
+ });
97
+
98
+ it('rejects bot submissions that fill the honeypot field (zod max(0))', async () => {
99
+ // The schema's `website: z.string().max(0).optional()` is the actual
100
+ // bot trap — anything non-empty fails validation with 400. The
101
+ // route's defensive `if (body.website)` is dead code as the schema
102
+ // stands; we test the live protection here.
103
+ const res = await app.request(
104
+ '/api/v1/contact/submit',
105
+ jsonPost(makeContactSubmission({ website: 'http://spam.example' })),
106
+ );
107
+
108
+ expect(res.status).toBe(400);
109
+ expect(insertMock).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('returns 500 when the insert fails', async () => {
113
+ insertMock.mockResolvedValueOnce({ error: { message: 'db down' } });
114
+
115
+ const res = await app.request('/api/v1/contact/submit', jsonPost(makeContactSubmission()));
116
+
117
+ expect(res.status).toBe(500);
118
+ const body = (await res.json()) as { error: string };
119
+ expect(body.error).toBeTypeOf('string');
120
+ });
121
+
122
+ it('fires the admin notification email when SMTP_ADMIN_EMAIL is set', async () => {
123
+ fakeEnv.SMTP_ADMIN_EMAIL = 'admin@example.test';
124
+ insertMock.mockResolvedValueOnce({ error: null });
125
+
126
+ const payload = makeContactSubmission({ subject: 'Bug report' });
127
+ const res = await app.request('/api/v1/contact/submit', jsonPost(payload));
128
+
129
+ expect(res.status).toBe(200);
130
+ // Email is sent async — wait a tick for the .catch chain to run.
131
+ await new Promise((r) => setTimeout(r, 0));
132
+ expect(sendEmailMock).toHaveBeenCalledTimes(1);
133
+ expect(sendEmailMock).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ to: 'admin@example.test',
136
+ subject: expect.stringContaining('Bug report'),
137
+ replyTo: payload.email,
138
+ }),
139
+ );
140
+ });
141
+
142
+ it('escapes HTML in the notification email body', async () => {
143
+ fakeEnv.SMTP_ADMIN_EMAIL = 'admin@example.test';
144
+ insertMock.mockResolvedValueOnce({ error: null });
145
+
146
+ await app.request(
147
+ '/api/v1/contact/submit',
148
+ jsonPost(
149
+ makeContactSubmission({
150
+ name: '<script>alert(1)</script>',
151
+ message: 'hi & bye <b>bold</b>'.padEnd(20, ' '),
152
+ }),
153
+ ),
154
+ );
155
+ await new Promise((r) => setTimeout(r, 0));
156
+
157
+ const html = sendEmailMock.mock.calls[0][0].html as string;
158
+ expect(html).not.toContain('<script>');
159
+ expect(html).toContain('&lt;script&gt;');
160
+ expect(html).toContain('hi &amp; bye &lt;b&gt;bold&lt;/b&gt;');
161
+ });
162
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { mountRoute } from '../../../_helpers/app';
3
+
4
+ // supabaseAdmin is invoked only by the readiness check. We give it a
5
+ // chainable shape so .from(...).select(...).limit(...) resolves to a
6
+ // controllable result; the test overrides the resolved value per case.
7
+ const limitMock = vi.fn();
8
+ vi.mock('@server/lib/supabase', () => ({
9
+ supabaseAdmin: {
10
+ from: () => ({ select: () => ({ limit: limitMock }) }),
11
+ },
12
+ }));
13
+
14
+ // Quiet the route's error log on the failure path — it's expected.
15
+ vi.mock('@server/lib/logger', () => ({
16
+ logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn(), debug: vi.fn() },
17
+ }));
18
+
19
+ const { healthRoutes } = await import('@server/routes/health');
20
+ const app = mountRoute('/api/health', healthRoutes);
21
+
22
+ describe('GET /api/health', () => {
23
+ it('returns 200 with a timestamp (liveness probe is independent of deps)', async () => {
24
+ // Hono mounts: app.route('/api/health', healthRoutes) + healthRoutes.get('/').
25
+ // Match the path without a trailing slash — Hono's router is strict.
26
+ const res = await app.request('/api/health');
27
+ expect(res.status).toBe(200);
28
+ const body = (await res.json()) as { status: string; timestamp: string };
29
+ expect(body.status).toBe('ok');
30
+ expect(new Date(body.timestamp).toString()).not.toBe('Invalid Date');
31
+ });
32
+ });
33
+
34
+ describe('GET /api/health/ready', () => {
35
+ it('returns 200 when Supabase responds without error', async () => {
36
+ limitMock.mockResolvedValueOnce({ data: [], error: null });
37
+ const res = await app.request('/api/health/ready');
38
+ expect(res.status).toBe(200);
39
+ const body = (await res.json()) as {
40
+ status: string;
41
+ services: { database: string; supabase: string };
42
+ };
43
+ expect(body.status).toBe('ready');
44
+ expect(body.services.database).toBe('connected');
45
+ expect(body.services.supabase).toBe('connected');
46
+ });
47
+
48
+ it('returns 503 when Supabase responds with an error', async () => {
49
+ limitMock.mockResolvedValueOnce({ data: null, error: new Error('db down') });
50
+ const res = await app.request('/api/health/ready');
51
+ expect(res.status).toBe(503);
52
+ const body = (await res.json()) as { status: string };
53
+ expect(body.status).toBe('not_ready');
54
+ });
55
+
56
+ it('returns 503 when the Supabase client throws', async () => {
57
+ limitMock.mockRejectedValueOnce(new Error('connection refused'));
58
+ const res = await app.request('/api/health/ready');
59
+ expect(res.status).toBe(503);
60
+ });
61
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import de from '../../src/client/locales/de.json';
3
+ import en from '../../src/client/locales/en.json';
4
+ import es from '../../src/client/locales/es.json';
5
+ import fr from '../../src/client/locales/fr.json';
6
+ import pt from '../../src/client/locales/pt.json';
7
+
8
+ // Structural invariant: every locale file must have the same key set as
9
+ // en.json. A missing key falls back to the key path at runtime (ugly); an
10
+ // extra key in a translation file means the source string was renamed or
11
+ // dropped but the translation wasn't updated. Either way it's a bug.
12
+ //
13
+ // This is the pattern for "cross-cutting invariant" tests — they don't
14
+ // exercise behavior, they assert that the shape of related artifacts
15
+ // agrees. Lives in tests/structural/ and runs under the `unit` project.
16
+
17
+ /** Recursively collect every leaf key path, e.g. "auth.signIn", "landing.hero.badge". */
18
+ function collectKeys(obj: unknown, prefix = ''): string[] {
19
+ if (typeof obj !== 'object' || obj === null) return [prefix];
20
+ return Object.entries(obj as Record<string, unknown>).flatMap(([key, val]) =>
21
+ collectKeys(val, prefix ? `${prefix}.${key}` : key),
22
+ );
23
+ }
24
+
25
+ const enKeys = new Set(collectKeys(en));
26
+ const locales: Record<string, unknown> = { es, fr, de, pt };
27
+
28
+ describe('i18n locale parity', () => {
29
+ for (const [lang, locale] of Object.entries(locales)) {
30
+ const keys = new Set(collectKeys(locale));
31
+
32
+ it(`${lang}.json contains every key from en.json`, () => {
33
+ const missing = [...enKeys].filter((k) => !keys.has(k));
34
+ expect(missing, `${lang} missing: ${missing.slice(0, 10).join(', ')}`).toHaveLength(0);
35
+ });
36
+
37
+ it(`${lang}.json has no extra keys absent from en.json`, () => {
38
+ const extra = [...keys].filter((k) => !enKeys.has(k));
39
+ expect(extra, `${lang} extra: ${extra.slice(0, 10).join(', ')}`).toHaveLength(0);
40
+ });
41
+ }
42
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { cn, getUserInitials } from '@/lib/utils';
3
+
4
+ describe('cn (className merger)', () => {
5
+ it('joins truthy class names', () => {
6
+ expect(cn('a', 'b', 'c')).toBe('a b c');
7
+ });
8
+
9
+ it('drops falsy values', () => {
10
+ expect(cn('a', false, null, undefined, '', 'b')).toBe('a b');
11
+ });
12
+
13
+ it('lets later Tailwind utilities override earlier ones (tw-merge)', () => {
14
+ // The whole reason we use tw-merge instead of plain clsx: conflicting
15
+ // utility classes resolve to the last one in.
16
+ expect(cn('px-2', 'px-4')).toBe('px-4');
17
+ expect(cn('text-sm text-red-500', 'text-blue-500')).toBe('text-sm text-blue-500');
18
+ });
19
+ });
20
+
21
+ describe('getUserInitials', () => {
22
+ it('returns "?" when user is null', () => {
23
+ expect(getUserInitials(null)).toBe('?');
24
+ });
25
+
26
+ it('uses the first two name parts when full_name is present', () => {
27
+ expect(
28
+ getUserInitials({ email: 'a@b.c', user_metadata: { full_name: 'Ada Lovelace' } }),
29
+ ).toBe('AL');
30
+ });
31
+
32
+ it('caps initials at two characters even for long names', () => {
33
+ expect(
34
+ getUserInitials({
35
+ email: 'm@a.c',
36
+ user_metadata: { full_name: 'Mary Anne Evans George Eliot' },
37
+ }),
38
+ ).toBe('MA');
39
+ });
40
+
41
+ it('falls back to the email first character when no full_name', () => {
42
+ expect(getUserInitials({ email: 'ada@example.com' })).toBe('A');
43
+ });
44
+
45
+ it('returns "?" when neither name nor email is usable', () => {
46
+ expect(getUserInitials({ email: '' })).toBe('?');
47
+ expect(getUserInitials({})).toBe('?');
48
+ });
49
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ formatPrice,
4
+ getPlan,
5
+ planIdFromPriceId,
6
+ plans,
7
+ } from '@shared/pricing';
8
+
9
+ describe('plans catalog', () => {
10
+ it('exposes free, starter, and pro tiers', () => {
11
+ expect(plans.map((p) => p.id)).toEqual(['free', 'starter', 'pro']);
12
+ });
13
+
14
+ it('only `starter` is flagged as popular', () => {
15
+ const popular = plans.filter((p) => p.popular);
16
+ expect(popular.map((p) => p.id)).toEqual(['starter']);
17
+ });
18
+
19
+ it('prices are non-negative integer cents', () => {
20
+ for (const plan of plans) {
21
+ expect(Number.isInteger(plan.price.monthly)).toBe(true);
22
+ expect(plan.price.monthly).toBeGreaterThanOrEqual(0);
23
+ }
24
+ });
25
+
26
+ it('higher tiers strictly include all features of lower tiers', () => {
27
+ // Encodes the product invariant: upgrading never removes a feature.
28
+ const tierOrder = ['free', 'starter', 'pro'] as const;
29
+ for (let i = 1; i < tierOrder.length; i++) {
30
+ const lower = getPlan(tierOrder[i - 1])!;
31
+ const higher = getPlan(tierOrder[i])!;
32
+ const lowerIncluded = lower.features.filter((f) => f.included).map((f) => f.text);
33
+ const higherIncluded = new Set(
34
+ higher.features.filter((f) => f.included).map((f) => f.text),
35
+ );
36
+ for (const feature of lowerIncluded) {
37
+ expect(higherIncluded.has(feature)).toBe(true);
38
+ }
39
+ }
40
+ });
41
+ });
42
+
43
+ describe('getPlan', () => {
44
+ it('returns the plan when the id matches', () => {
45
+ expect(getPlan('pro')?.name).toBe('Pro');
46
+ });
47
+
48
+ it('returns undefined for an unknown id', () => {
49
+ // @ts-expect-error — intentional: exercise runtime behavior for unknown id
50
+ expect(getPlan('enterprise')).toBeUndefined();
51
+ });
52
+ });
53
+
54
+ describe('formatPrice', () => {
55
+ it('returns "Free" for zero cents', () => {
56
+ expect(formatPrice(0)).toBe('Free');
57
+ });
58
+
59
+ it('formats whole-dollar amounts with /mo suffix', () => {
60
+ expect(formatPrice(1900)).toBe('$19/mo');
61
+ expect(formatPrice(7900)).toBe('$79/mo');
62
+ });
63
+
64
+ it('truncates fractional dollars (current behavior)', () => {
65
+ // Documenting current rounding: toFixed(0) on $19.50 -> "$20/mo" due
66
+ // to banker rounding via toFixed. If you change pricing math, update
67
+ // this assertion to whatever the new contract is.
68
+ expect(formatPrice(1950)).toBe('$20/mo');
69
+ });
70
+ });
71
+
72
+ describe('planIdFromPriceId', () => {
73
+ const priceIds = {
74
+ starter: 'price_starter_test',
75
+ pro: 'price_pro_test',
76
+ };
77
+
78
+ it('maps the starter price id to "starter"', () => {
79
+ expect(planIdFromPriceId('price_starter_test', priceIds)).toBe('starter');
80
+ });
81
+
82
+ it('maps the pro price id to "pro"', () => {
83
+ expect(planIdFromPriceId('price_pro_test', priceIds)).toBe('pro');
84
+ });
85
+
86
+ it('falls back to "free" for unknown price ids', () => {
87
+ expect(planIdFromPriceId('price_unknown', priceIds)).toBe('free');
88
+ });
89
+
90
+ it('falls back to "free" when the id mapping is empty', () => {
91
+ expect(planIdFromPriceId('anything', {})).toBe('free');
92
+ });
93
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "types": ["vite/client"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "ignoreDeprecations": "6.0",
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": ["./src/client/*"],
22
+ "@shared/*": ["./src/shared/*"],
23
+ "@content/*": ["./content/*"]
24
+ }
25
+ },
26
+ "include": ["src/client", "src/shared", "content"]
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "outDir": "./dist/server",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "ignoreDeprecations": "6.0",
20
+ "baseUrl": ".",
21
+ "paths": {
22
+ "@shared/*": ["./src/shared/*"]
23
+ }
24
+ },
25
+ "include": ["src/server", "src/shared"],
26
+ "exclude": ["src/client"]
27
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["vitest/globals", "@testing-library/jest-dom", "node"],
5
+ "noUnusedLocals": false,
6
+ "noUnusedParameters": false
7
+ },
8
+ "include": ["src", "tests", "vitest.config.ts"]
9
+ }
@@ -0,0 +1,110 @@
1
+ import path from 'node:path';
2
+ import mdx from '@mdx-js/rollup';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import react from '@vitejs/plugin-react';
5
+ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
6
+ import rehypeSlug from 'rehype-slug';
7
+ import remarkFrontmatter from 'remark-frontmatter';
8
+ import remarkGfm from 'remark-gfm';
9
+ import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
10
+ import { defineConfig, loadEnv } from 'vite';
11
+
12
+ // Calculate ports based on offset and overrides
13
+ function getPort(envVarName: string, defaultPort: number, offset: number): number {
14
+ const override = process.env[envVarName];
15
+ if (override) return Number.parseInt(override, 10);
16
+ return defaultPort + offset;
17
+ }
18
+
19
+ export default defineConfig(({ mode }) => {
20
+ // Load env from project root (where .env.local lives)
21
+ const env = loadEnv(mode, path.resolve(__dirname), '');
22
+
23
+ const portOffset = Number.parseInt(env.DEV_PORT_OFFSET || '0', 10);
24
+ const vitePort = getPort('DEV_VITE_PORT', 5173, portOffset);
25
+ const apiPort = getPort('DEV_API_PORT', 3000, portOffset);
26
+ const kongPort = getPort('DEV_KONG_PORT', 8000, portOffset);
27
+
28
+ // Override client-side env vars with offset-adjusted ports in development.
29
+ // VITE_SUPABASE_URL and SITE_URL are hardcoded to default ports in .env,
30
+ // but DEV_PORT_OFFSET shifts Docker ports (Kong on 8000+offset).
31
+ const devDefines: Record<string, string> =
32
+ mode === 'development' && portOffset !== 0
33
+ ? {
34
+ 'import.meta.env.VITE_SUPABASE_URL': JSON.stringify(`http://localhost:${kongPort}`),
35
+ 'import.meta.env.SITE_URL': JSON.stringify(`http://localhost:${vitePort}`),
36
+ }
37
+ : {};
38
+
39
+ return {
40
+ plugins: [
41
+ mdx({
42
+ remarkPlugins: [remarkFrontmatter, remarkGfm, remarkMdxFrontmatter],
43
+ rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]],
44
+ }),
45
+ react(),
46
+ tailwindcss(),
47
+ ],
48
+
49
+ define: devDefines,
50
+
51
+ root: 'src/client',
52
+ envDir: '../..',
53
+
54
+ build: {
55
+ outDir: '../../dist/client',
56
+ emptyOutDir: true,
57
+ // Security: Explicitly disable source maps in production builds
58
+ // Using mode check since NODE_ENV may not be set during Vite build
59
+ sourcemap: mode === 'development',
60
+ rollupOptions: {
61
+ output: {
62
+ manualChunks(id) {
63
+ if (
64
+ id.includes('node_modules/react-dom/') ||
65
+ id.includes('node_modules/react/') ||
66
+ id.includes('node_modules/react-router')
67
+ )
68
+ return 'vendor-react';
69
+ if (id.includes('node_modules/@supabase/')) return 'vendor-supabase';
70
+ if (id.includes('node_modules/@tanstack/react-query')) return 'vendor-query';
71
+ if (id.includes('node_modules/recharts')) return 'vendor-charts';
72
+ if (id.includes('node_modules/framer-motion')) return 'vendor-motion';
73
+ if (
74
+ id.includes('node_modules/cmdk') ||
75
+ id.includes('node_modules/vaul') ||
76
+ id.includes('node_modules/sonner') ||
77
+ id.includes('node_modules/embla-carousel-react')
78
+ )
79
+ return 'vendor-ui';
80
+ },
81
+ },
82
+ },
83
+ },
84
+
85
+ resolve: {
86
+ alias: {
87
+ '@': path.resolve(__dirname, './src/client'),
88
+ '@shared': path.resolve(__dirname, './src/shared'),
89
+ '@content': path.resolve(__dirname, './content'),
90
+ },
91
+ },
92
+
93
+ server: {
94
+ port: vitePort,
95
+ // Listen on all interfaces so Docker containers can reach Vite via host.docker.internal
96
+ // This enables accessing the app at http://app.localhost through Traefik
97
+ host: true,
98
+ proxy: {
99
+ '/api': {
100
+ target: `http://localhost:${apiPort}`,
101
+ changeOrigin: true,
102
+ // Let React Router handle /api/docs — don't proxy it to the backend
103
+ bypass(req) {
104
+ if (req.url === '/api/docs') return req.url;
105
+ },
106
+ },
107
+ },
108
+ },
109
+ };
110
+ });
@@ -0,0 +1,74 @@
1
+ import { resolve } from 'node:path';
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ // Three-tier vitest setup mirroring vibecarbon's own layout (slimmed for an
5
+ // app — no real-infra e2e tier).
6
+ //
7
+ // unit pure functions, validators, shared lib — node env, fast
8
+ // component React components + hooks via RTL — jsdom env
9
+ // integration Hono route handlers w/ mocked externals — node env
10
+ //
11
+ // `pnpm test` runs the full set; `pnpm test:unit` etc. pick one tier.
12
+ //
13
+ // The `test-maintainer` subagent reads tests/ to learn this project's
14
+ // conventions — keep new tests in the matching tier directory so the agent
15
+ // can extend the pattern.
16
+ export default defineConfig({
17
+ resolve: {
18
+ alias: {
19
+ '@': resolve(__dirname, 'src/client'),
20
+ '@shared': resolve(__dirname, 'src/shared'),
21
+ '@server': resolve(__dirname, 'src/server'),
22
+ },
23
+ },
24
+ test: {
25
+ globals: true,
26
+ coverage: {
27
+ provider: 'v8',
28
+ reporter: ['text', 'html'],
29
+ include: ['src/**/*.{ts,tsx}'],
30
+ exclude: ['src/**/*.test.{ts,tsx}', 'src/**/*.d.ts', 'tests/**', 'node_modules'],
31
+ },
32
+ projects: [
33
+ {
34
+ extends: true,
35
+ test: {
36
+ name: 'unit',
37
+ // Pure, in-process. No DOM, no React. Validators, formatters,
38
+ // pricing math, shared types.
39
+ environment: 'node',
40
+ include: ['tests/unit/**/*.test.ts', 'tests/structural/**/*.test.ts'],
41
+ testTimeout: 5_000,
42
+ },
43
+ },
44
+ {
45
+ extends: true,
46
+ test: {
47
+ name: 'component',
48
+ // React components + custom hooks via React Testing Library.
49
+ // jsdom for DOM APIs; setup file wires @testing-library/jest-dom
50
+ // matchers + auto-cleanup between tests.
51
+ environment: 'jsdom',
52
+ include: ['tests/component/**/*.test.{ts,tsx}'],
53
+ setupFiles: ['tests/_helpers/setup-rtl.ts'],
54
+ testTimeout: 10_000,
55
+ },
56
+ },
57
+ {
58
+ extends: true,
59
+ test: {
60
+ name: 'integration',
61
+ // Hono route handlers exercised end-to-end via app.request(),
62
+ // with external services (Supabase, Stripe, SMTP) mocked at the
63
+ // module boundary. Tests the full request/response cycle —
64
+ // schema validation, status codes, error mapping, rate-limit
65
+ // wiring — without standing up real infra.
66
+ environment: 'node',
67
+ include: ['tests/integration/**/*.test.ts'],
68
+ setupFiles: ['tests/_helpers/setup-integration.ts'],
69
+ testTimeout: 15_000,
70
+ },
71
+ },
72
+ ],
73
+ },
74
+ });