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,13 @@
1
+ -- Theme Settings Migration
2
+ -- Adds the app_theme key to app_settings and allows public read access.
3
+
4
+ -- Insert default theme (empty object = use CSS defaults)
5
+ INSERT INTO app_settings (key, value)
6
+ VALUES ('app_theme', '{}'::jsonb)
7
+ ON CONFLICT (key) DO NOTHING;
8
+
9
+ -- Allow public read so all users get the custom theme on page load
10
+ DROP POLICY IF EXISTS "Public can read whitelisted settings" ON app_settings;
11
+ CREATE POLICY "Public can read whitelisted settings"
12
+ ON app_settings FOR SELECT
13
+ USING (key IN ('mfa_enabled', 'app_theme'));
@@ -0,0 +1,121 @@
1
+ -- =============================================================================
2
+ -- pg_cron: Background job scheduling via PostgreSQL
3
+ -- =============================================================================
4
+
5
+ -- Enable the pg_cron extension (available in Supabase PostgreSQL)
6
+ CREATE EXTENSION IF NOT EXISTS pg_cron;
7
+
8
+ -- Grant usage to postgres role (required for Supabase)
9
+ GRANT USAGE ON SCHEMA cron TO postgres;
10
+
11
+ -- =============================================================================
12
+ -- Job execution history table (readable by super admins via API)
13
+ -- =============================================================================
14
+
15
+ CREATE TABLE IF NOT EXISTS public.cron_job_history (
16
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17
+ job_name TEXT NOT NULL,
18
+ status TEXT NOT NULL DEFAULT 'running', -- running, succeeded, failed
19
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
20
+ finished_at TIMESTAMPTZ,
21
+ duration_ms INTEGER,
22
+ result TEXT,
23
+ error TEXT,
24
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
25
+ );
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_cron_job_history_job_name ON public.cron_job_history (job_name);
28
+ CREATE INDEX IF NOT EXISTS idx_cron_job_history_started_at ON public.cron_job_history (started_at DESC);
29
+
30
+ ALTER TABLE public.cron_job_history ENABLE ROW LEVEL SECURITY;
31
+
32
+ -- Only super admins can read job history
33
+ DROP POLICY IF EXISTS "Super admins can read job history" ON public.cron_job_history;
34
+ CREATE POLICY "Super admins can read job history"
35
+ ON public.cron_job_history FOR SELECT
36
+ USING (public.is_super_admin());
37
+
38
+ -- =============================================================================
39
+ -- Helper function to log job execution
40
+ -- =============================================================================
41
+
42
+ CREATE OR REPLACE FUNCTION public.log_cron_job(
43
+ p_job_name TEXT,
44
+ p_status TEXT DEFAULT 'succeeded',
45
+ p_result TEXT DEFAULT NULL,
46
+ p_error TEXT DEFAULT NULL
47
+ )
48
+ RETURNS void
49
+ LANGUAGE plpgsql
50
+ SECURITY DEFINER
51
+ AS $$
52
+ BEGIN
53
+ INSERT INTO public.cron_job_history (job_name, status, finished_at, result, error)
54
+ VALUES (p_job_name, p_status, now(), p_result, p_error);
55
+ END;
56
+ $$;
57
+
58
+ -- =============================================================================
59
+ -- Schedule default jobs
60
+ -- =============================================================================
61
+
62
+ -- 1. Clean up old failed login attempts (every 6 hours)
63
+ -- Replaces the Node.js setInterval in server/index.ts
64
+ SELECT cron.schedule(
65
+ 'cleanup-login-attempts',
66
+ '0 */6 * * *', -- every 6 hours
67
+ $$
68
+ DO $body$
69
+ DECLARE
70
+ deleted_count INTEGER;
71
+ BEGIN
72
+ DELETE FROM public.failed_login_attempts
73
+ WHERE created_at < now() - interval '24 hours';
74
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
75
+ PERFORM public.log_cron_job('cleanup-login-attempts', 'succeeded',
76
+ 'Deleted ' || deleted_count || ' old login attempts');
77
+ EXCEPTION WHEN OTHERS THEN
78
+ PERFORM public.log_cron_job('cleanup-login-attempts', 'failed', NULL, SQLERRM);
79
+ END $body$;
80
+ $$
81
+ );
82
+
83
+ -- 2. Clean up expired notifications (daily at 3am)
84
+ SELECT cron.schedule(
85
+ 'cleanup-expired-notifications',
86
+ '0 3 * * *', -- daily at 3:00 AM
87
+ $$
88
+ DO $body$
89
+ DECLARE
90
+ deleted_count INTEGER;
91
+ BEGIN
92
+ DELETE FROM public.notifications
93
+ WHERE expires_at IS NOT NULL AND expires_at < now();
94
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
95
+ PERFORM public.log_cron_job('cleanup-expired-notifications', 'succeeded',
96
+ 'Deleted ' || deleted_count || ' expired notifications');
97
+ EXCEPTION WHEN OTHERS THEN
98
+ PERFORM public.log_cron_job('cleanup-expired-notifications', 'failed', NULL, SQLERRM);
99
+ END $body$;
100
+ $$
101
+ );
102
+
103
+ -- 3. Clean up old cron job history (weekly, keep last 30 days)
104
+ SELECT cron.schedule(
105
+ 'cleanup-job-history',
106
+ '0 4 * * 0', -- Sundays at 4:00 AM
107
+ $$
108
+ DO $body$
109
+ DECLARE
110
+ deleted_count INTEGER;
111
+ BEGIN
112
+ DELETE FROM public.cron_job_history
113
+ WHERE created_at < now() - interval '30 days';
114
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
115
+ PERFORM public.log_cron_job('cleanup-job-history', 'succeeded',
116
+ 'Deleted ' || deleted_count || ' old history entries');
117
+ EXCEPTION WHEN OTHERS THEN
118
+ PERFORM public.log_cron_job('cleanup-job-history', 'failed', NULL, SQLERRM);
119
+ END $body$;
120
+ $$
121
+ );
@@ -0,0 +1,81 @@
1
+ -- =============================================================================
2
+ -- Contact form submissions + Newsletter subscribers
3
+ -- =============================================================================
4
+
5
+ -- ========== CONTACT SUBMISSIONS ==========
6
+
7
+ CREATE TABLE IF NOT EXISTS public.contact_submissions (
8
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ name TEXT NOT NULL,
10
+ email TEXT NOT NULL,
11
+ subject TEXT NOT NULL,
12
+ message TEXT NOT NULL,
13
+ status TEXT NOT NULL DEFAULT 'unread', -- unread, read, replied, archived
14
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
15
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_contact_submissions_status ON public.contact_submissions (status);
19
+ CREATE INDEX IF NOT EXISTS idx_contact_submissions_created_at ON public.contact_submissions (created_at DESC);
20
+
21
+ ALTER TABLE public.contact_submissions ENABLE ROW LEVEL SECURITY;
22
+
23
+ -- Only super admins can read/manage contact submissions
24
+ DROP POLICY IF EXISTS "Super admins can manage contact submissions" ON public.contact_submissions;
25
+ CREATE POLICY "Super admins can manage contact submissions"
26
+ ON public.contact_submissions FOR ALL
27
+ USING (public.is_super_admin());
28
+
29
+ -- Allow anonymous inserts (public contact form)
30
+ DROP POLICY IF EXISTS "Anyone can submit contact form" ON public.contact_submissions;
31
+ CREATE POLICY "Anyone can submit contact form"
32
+ ON public.contact_submissions FOR INSERT
33
+ WITH CHECK (true);
34
+
35
+ DROP TRIGGER IF EXISTS update_contact_submissions_updated_at ON public.contact_submissions;
36
+ CREATE TRIGGER update_contact_submissions_updated_at
37
+ BEFORE UPDATE ON public.contact_submissions
38
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
39
+
40
+ -- ========== NEWSLETTER SUBSCRIBERS ==========
41
+
42
+ CREATE TABLE IF NOT EXISTS public.newsletter_subscribers (
43
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
44
+ email TEXT NOT NULL UNIQUE,
45
+ name TEXT,
46
+ status TEXT NOT NULL DEFAULT 'pending', -- pending, active, unsubscribed
47
+ confirmation_token UUID DEFAULT gen_random_uuid(),
48
+ subscribed_at TIMESTAMPTZ,
49
+ unsubscribed_at TIMESTAMPTZ,
50
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
51
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_status ON public.newsletter_subscribers (status);
55
+ CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_email ON public.newsletter_subscribers (email);
56
+
57
+ ALTER TABLE public.newsletter_subscribers ENABLE ROW LEVEL SECURITY;
58
+
59
+ -- Only super admins can read/manage subscribers
60
+ DROP POLICY IF EXISTS "Super admins can manage newsletter subscribers" ON public.newsletter_subscribers;
61
+ CREATE POLICY "Super admins can manage newsletter subscribers"
62
+ ON public.newsletter_subscribers FOR ALL
63
+ USING (public.is_super_admin());
64
+
65
+ -- Allow anonymous inserts (public signup form)
66
+ DROP POLICY IF EXISTS "Anyone can subscribe to newsletter" ON public.newsletter_subscribers;
67
+ CREATE POLICY "Anyone can subscribe to newsletter"
68
+ ON public.newsletter_subscribers FOR INSERT
69
+ WITH CHECK (true);
70
+
71
+ -- Allow anonymous updates for confirmation/unsubscribe (by token or email)
72
+ DROP POLICY IF EXISTS "Anyone can confirm or unsubscribe" ON public.newsletter_subscribers;
73
+ CREATE POLICY "Anyone can confirm or unsubscribe"
74
+ ON public.newsletter_subscribers FOR UPDATE
75
+ USING (true)
76
+ WITH CHECK (true);
77
+
78
+ DROP TRIGGER IF EXISTS update_newsletter_subscribers_updated_at ON public.newsletter_subscribers;
79
+ CREATE TRIGGER update_newsletter_subscribers_updated_at
80
+ BEFORE UPDATE ON public.newsletter_subscribers
81
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
@@ -0,0 +1,22 @@
1
+ -- Localization: enabled languages
2
+ --
3
+ -- Promotes the language list from a build-time env var
4
+ -- (VITE_ENABLED_LANGUAGES) to a runtime DB-backed setting so admins can
5
+ -- swap languages from the admin Settings UI without redeploying.
6
+ --
7
+ -- The on/off master switch (`localization_enabled`) was added earlier.
8
+ -- This adds the companion list of which languages the switcher should
9
+ -- offer when it's on. With <2 codes the switcher auto-hides at the
10
+ -- component level (LanguageSwitcher.tsx), so a single-language project
11
+ -- has no visible switcher even with localization_enabled=true.
12
+
13
+ INSERT INTO app_settings (key, value)
14
+ VALUES ('enabled_languages', '{"codes": ["en"]}'::jsonb)
15
+ ON CONFLICT (key) DO NOTHING;
16
+
17
+ -- Public read so unauthenticated nav can render the right switcher
18
+ -- options before login (mirrors mfa_enabled / app_theme / localization_enabled).
19
+ DROP POLICY IF EXISTS "Public can read whitelisted settings" ON app_settings;
20
+ CREATE POLICY "Public can read whitelisted settings"
21
+ ON app_settings FOR SELECT
22
+ USING (key IN ('mfa_enabled', 'app_theme', 'localization_enabled', 'enabled_languages'));
@@ -0,0 +1,16 @@
1
+ -- Seed data for development
2
+ -- This file is optional and can be run to populate initial test data
3
+
4
+ -- Note: In a real application, organizations and memberships should be
5
+ -- created through the application after users sign up.
6
+
7
+ -- Example: Create a demo organization (run this manually after first user signup)
8
+ -- INSERT INTO organizations (name, slug, plan)
9
+ -- VALUES ('Demo Organization', 'demo-org', 'FREE');
10
+
11
+ -- Then add the first user as owner:
12
+ -- INSERT INTO memberships (user_id, organization_id, role)
13
+ -- SELECT
14
+ -- auth.uid(),
15
+ -- (SELECT id FROM organizations WHERE slug = 'demo-org'),
16
+ -- 'OWNER';
@@ -0,0 +1,45 @@
1
+ import { Hono } from 'hono';
2
+ import type { HonoVariables } from '../../src/server/types';
3
+
4
+ /**
5
+ * Mount a route module onto a fresh Hono instance at a given prefix.
6
+ *
7
+ * Integration tests get a clean app per test — no global middleware
8
+ * (CORS, rate-limit, security-headers) so assertions stay focused on the
9
+ * route under test. If you need those, mount them on the returned app
10
+ * before issuing requests.
11
+ *
12
+ * Example:
13
+ * import { healthRoutes } from '@server/routes/health';
14
+ * const app = mountRoute('/api/health', healthRoutes);
15
+ * const res = await app.request('/api/health/');
16
+ * expect(res.status).toBe(200);
17
+ */
18
+ export function mountRoute(prefix: string, routes: Hono<{ Variables: HonoVariables }>): Hono {
19
+ const app = new Hono();
20
+ app.route(prefix, routes);
21
+ return app;
22
+ }
23
+
24
+ /**
25
+ * Build a JSON POST Request for app.request(...). Saves the boilerplate of
26
+ * setting `method`, `headers`, and JSON-encoding the body in every test.
27
+ *
28
+ * Example:
29
+ * const res = await app.request('/api/v1/contact/submit', jsonPost({
30
+ * name: 'Ada', email: 'ada@example.com', subject: 'Hi', message: 'Hello there.',
31
+ * }));
32
+ */
33
+ export function jsonPost(body: unknown, init: RequestInit = {}): RequestInit {
34
+ return {
35
+ method: 'POST',
36
+ headers: {
37
+ 'content-type': 'application/json',
38
+ ...(init.headers ?? {}),
39
+ },
40
+ body: JSON.stringify(body),
41
+ ...init,
42
+ // Re-apply method/headers/body after the spread so callers can't
43
+ // accidentally clobber them with an empty init.
44
+ };
45
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Temporarily set environment variables for a test, returning a restore
3
+ * function. Closure-based — no globals, safe to call from concurrent tests
4
+ * as long as the variables don't overlap.
5
+ *
6
+ * Example:
7
+ * it('falls back to default SITE_URL', () => {
8
+ * const restore = mockEnv({ SITE_URL: undefined });
9
+ * try {
10
+ * // ... assertion that exercises the fallback path
11
+ * } finally {
12
+ * restore();
13
+ * }
14
+ * });
15
+ */
16
+ export function mockEnv(vars: Record<string, string | undefined>): () => void {
17
+ const original: Record<string, string | undefined> = {};
18
+
19
+ for (const [key, value] of Object.entries(vars)) {
20
+ original[key] = process.env[key];
21
+ if (value === undefined) {
22
+ delete process.env[key];
23
+ } else {
24
+ process.env[key] = value;
25
+ }
26
+ }
27
+
28
+ return () => {
29
+ for (const [key, value] of Object.entries(original)) {
30
+ if (value === undefined) {
31
+ delete process.env[key];
32
+ } else {
33
+ process.env[key] = value;
34
+ }
35
+ }
36
+ };
37
+ }
@@ -0,0 +1,69 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ /**
4
+ * Lightweight factories that produce plausible domain objects for tests.
5
+ *
6
+ * These don't touch the DB — they return plain TS objects you can hand to
7
+ * a mocked Supabase client's resolved value (e.g. `mockResolvedValue({
8
+ * data: makeUser(), error: null })`).
9
+ *
10
+ * Add new factories here as you write tests; the test-maintainer agent
11
+ * follows this pattern when extending coverage.
12
+ */
13
+
14
+ function rid(prefix: string): string {
15
+ return `${prefix}-${randomBytes(4).toString('hex')}`;
16
+ }
17
+
18
+ export interface TestUser {
19
+ id: string;
20
+ email: string;
21
+ user_metadata: { full_name?: string };
22
+ }
23
+
24
+ export function makeUser(overrides: Partial<TestUser> = {}): TestUser {
25
+ return {
26
+ id: rid('user'),
27
+ email: `${rid('user')}@example.test`,
28
+ user_metadata: { full_name: 'Test User' },
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ export interface TestOrg {
34
+ id: string;
35
+ name: string;
36
+ slug: string;
37
+ owner_id: string;
38
+ }
39
+
40
+ export function makeOrg(overrides: Partial<TestOrg> = {}): TestOrg {
41
+ const id = rid('org');
42
+ return {
43
+ id,
44
+ name: 'Test Org',
45
+ slug: id,
46
+ owner_id: rid('user'),
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ export interface ContactSubmission {
52
+ name: string;
53
+ email: string;
54
+ subject: string;
55
+ message: string;
56
+ website?: string;
57
+ }
58
+
59
+ export function makeContactSubmission(
60
+ overrides: Partial<ContactSubmission> = {},
61
+ ): ContactSubmission {
62
+ return {
63
+ name: 'Ada Lovelace',
64
+ email: 'ada@example.test',
65
+ subject: 'Hello',
66
+ message: 'Just dropping a line — this is a test message.',
67
+ ...overrides,
68
+ };
69
+ }
@@ -0,0 +1,23 @@
1
+ import { createHmac } from 'node:crypto';
2
+
3
+ /**
4
+ * Build an HS256-signed JWT for tests.
5
+ *
6
+ * The default secret matches what the auth middleware's mock paths expect
7
+ * — pass an explicit `secret` if you're testing the real JWT verifier.
8
+ *
9
+ * Example:
10
+ * const token = mockJwt({ sub: 'user-123', role: 'authenticated' });
11
+ * const res = await app.request('/api/v1/me', {
12
+ * headers: { authorization: `Bearer ${token}` },
13
+ * });
14
+ */
15
+ export function mockJwt(payload: Record<string, unknown>, secret = 'test-secret'): string {
16
+ const header = { alg: 'HS256', typ: 'JWT' };
17
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
18
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
19
+ const signature = createHmac('sha256', secret)
20
+ .update(`${encodedHeader}.${encodedPayload}`)
21
+ .digest('base64url');
22
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
23
+ }
@@ -0,0 +1,20 @@
1
+ // Vitest setupFile for the `integration` project.
2
+ //
3
+ // The server modules (env, supabase, email) validate required env vars at
4
+ // import time via zod. Tests don't connect to real Supabase/SMTP — but the
5
+ // schemas still need plausible values to pass `.url()` / `.min(1)` checks
6
+ // before `vi.mock()` swaps in the test doubles.
7
+ //
8
+ // Set them once here so individual tests stay focused on behavior.
9
+ const defaults: Record<string, string> = {
10
+ SUPABASE_URL: 'http://supabase.test',
11
+ SUPABASE_ANON_KEY: 'test-anon-key',
12
+ SUPABASE_SERVICE_ROLE_KEY: 'test-service-role-key',
13
+ NODE_ENV: 'test',
14
+ };
15
+
16
+ for (const [key, value] of Object.entries(defaults)) {
17
+ if (process.env[key] === undefined) {
18
+ process.env[key] = value;
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ // Vitest setupFile for the `component` project.
2
+ //
3
+ // 1. Loads @testing-library/jest-dom so matchers like .toBeInTheDocument()
4
+ // are typed and available.
5
+ // 2. Runs RTL's cleanup() after each test so DOM state doesn't leak.
6
+ import '@testing-library/jest-dom/vitest';
7
+ import { cleanup } from '@testing-library/react';
8
+ import { afterEach } from 'vitest';
9
+
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
@@ -0,0 +1,53 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { ErrorBoundary } from '@/components/ErrorBoundary';
4
+
5
+ // React's default rendering logs the caught error to console.error in dev,
6
+ // which clutters test output but isn't an assertion failure. Silence it for
7
+ // the error path only.
8
+ let errorSpy: ReturnType<typeof vi.spyOn>;
9
+ beforeEach(() => {
10
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
11
+ });
12
+ afterEach(() => {
13
+ errorSpy.mockRestore();
14
+ });
15
+
16
+ function Boom(): never {
17
+ throw new Error('kaboom');
18
+ }
19
+
20
+ describe('<ErrorBoundary />', () => {
21
+ it('renders children when no error is thrown', () => {
22
+ render(
23
+ <ErrorBoundary>
24
+ <p>healthy</p>
25
+ </ErrorBoundary>,
26
+ );
27
+ expect(screen.getByText('healthy')).toBeInTheDocument();
28
+ });
29
+
30
+ it('renders the fallback UI when a child throws', () => {
31
+ render(
32
+ <ErrorBoundary>
33
+ <Boom />
34
+ </ErrorBoundary>,
35
+ );
36
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
37
+ expect(screen.getByRole('button', { name: /reload page/i })).toBeInTheDocument();
38
+ });
39
+
40
+ it('logs the caught error so it surfaces in observability', () => {
41
+ render(
42
+ <ErrorBoundary>
43
+ <Boom />
44
+ </ErrorBoundary>,
45
+ );
46
+ // Two console.error calls land here: one from React's own dev warning,
47
+ // and one from ErrorBoundary.componentDidCatch. Assert ours is present.
48
+ const fromBoundary = errorSpy.mock.calls.find(
49
+ (args) => typeof args[0] === 'string' && args[0].includes('ErrorBoundary caught an error'),
50
+ );
51
+ expect(fromBoundary).toBeDefined();
52
+ });
53
+ });
@@ -0,0 +1,119 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import type { ReactNode } from 'react';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import {
6
+ type AuthSettings,
7
+ authSettingsQueryKey,
8
+ useAuthSettings,
9
+ useMfaRequired,
10
+ } from '@/hooks/api/useAuthSettings';
11
+
12
+ // Wraps any rendered hook in a fresh QueryClient — retry: false so a
13
+ // fetch rejection surfaces in `error` immediately instead of after the
14
+ // useQuery defaults' multiple retries.
15
+ function makeWrapper() {
16
+ const client = new QueryClient({
17
+ defaultOptions: { queries: { retry: false } },
18
+ });
19
+ return function Wrapper({ children }: { children: ReactNode }) {
20
+ return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
21
+ };
22
+ }
23
+
24
+ const okSettings: AuthSettings = {
25
+ settings: {
26
+ mfaEnabled: true,
27
+ localizationEnabled: false,
28
+ enabledLanguages: ['en'],
29
+ },
30
+ };
31
+
32
+ beforeEach(() => {
33
+ vi.stubGlobal(
34
+ 'fetch',
35
+ vi.fn(async () => new Response(JSON.stringify(okSettings), { status: 200 })),
36
+ );
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.unstubAllGlobals();
41
+ });
42
+
43
+ describe('useAuthSettings', () => {
44
+ it('returns settings on success', async () => {
45
+ const { result } = renderHook(() => useAuthSettings(), { wrapper: makeWrapper() });
46
+
47
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
48
+
49
+ expect(result.current.data).toEqual(okSettings);
50
+ expect(result.current.error).toBeNull();
51
+ });
52
+
53
+ it('hits /api/v1/auth/settings exactly once when called multiple times (TanStack dedupes)', async () => {
54
+ const wrapper = makeWrapper();
55
+ const a = renderHook(() => useAuthSettings(), { wrapper });
56
+ const b = renderHook(() => useAuthSettings(), { wrapper });
57
+
58
+ await waitFor(() => expect(a.result.current.isLoading).toBe(false));
59
+ await waitFor(() => expect(b.result.current.isLoading).toBe(false));
60
+
61
+ expect(fetch).toHaveBeenCalledTimes(1);
62
+ expect(fetch).toHaveBeenCalledWith('/api/v1/auth/settings');
63
+ });
64
+
65
+ it('surfaces an error when fetch returns non-2xx', async () => {
66
+ vi.stubGlobal(
67
+ 'fetch',
68
+ vi.fn(async () => new Response('boom', { status: 500 })),
69
+ );
70
+
71
+ const { result } = renderHook(() => useAuthSettings(), { wrapper: makeWrapper() });
72
+
73
+ // The hook sets `retry: 1` (per-query options override the wrapper's
74
+ // `retry: false` default), so we get one retry with TanStack's 1s
75
+ // exponential-backoff delay before isError flips. Generous waitFor
76
+ // timeout covers it.
77
+ await waitFor(() => expect(result.current.isError).toBe(true), { timeout: 5_000 });
78
+ expect(result.current.error).toBeInstanceOf(Error);
79
+ });
80
+
81
+ it('does not fetch when `enabled: false` is passed', async () => {
82
+ const { result } = renderHook(() => useAuthSettings({ enabled: false }), {
83
+ wrapper: makeWrapper(),
84
+ });
85
+
86
+ expect(result.current.fetchStatus).toBe('idle');
87
+ expect(fetch).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it('exports a stable query key (cache invalidation contract)', () => {
91
+ // Renaming this key is a breaking change for any external invalidation
92
+ // callers — pin it so regressions surface in review.
93
+ expect(authSettingsQueryKey).toEqual(['auth', 'settings']);
94
+ });
95
+ });
96
+
97
+ describe('useMfaRequired', () => {
98
+ it('derives mfaRequired from the underlying settings', async () => {
99
+ const { result } = renderHook(() => useMfaRequired(), { wrapper: makeWrapper() });
100
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
101
+ expect(result.current.mfaRequired).toBe(true);
102
+ });
103
+
104
+ it('defaults to false when settings are missing', async () => {
105
+ vi.stubGlobal(
106
+ 'fetch',
107
+ vi.fn(
108
+ async () =>
109
+ new Response(JSON.stringify({ settings: null } satisfies AuthSettings), {
110
+ status: 200,
111
+ }),
112
+ ),
113
+ );
114
+
115
+ const { result } = renderHook(() => useMfaRequired(), { wrapper: makeWrapper() });
116
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
117
+ expect(result.current.mfaRequired).toBe(false);
118
+ });
119
+ });