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,185 @@
1
+ import { Hono } from 'hono';
2
+ import { logger } from '../../lib/logger';
3
+ import { supabaseAdmin } from '../../lib/supabase';
4
+
5
+ /**
6
+ * Validate and sanitize the SITE_URL for redirect.
7
+ * Prevents open redirect vulnerabilities by ensuring the URL is well-formed
8
+ * and has a valid protocol.
9
+ */
10
+ function getValidatedSiteUrl(): string {
11
+ const siteUrl = process.env.SITE_URL || 'http://localhost:5173';
12
+
13
+ try {
14
+ const url = new URL(siteUrl);
15
+ // Only allow http and https protocols
16
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
17
+ logger.warn({ siteUrl }, 'Invalid SITE_URL protocol, falling back to localhost');
18
+ return 'http://localhost:5173';
19
+ }
20
+ return url.origin;
21
+ } catch {
22
+ logger.warn({ siteUrl }, 'Invalid SITE_URL format, falling back to localhost');
23
+ return 'http://localhost:5173';
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Internal endpoint for Traefik ForwardAuth middleware.
29
+ * Verifies that the user is authenticated and has the required role.
30
+ *
31
+ * Usage:
32
+ * GET /api/_internal/verify-role?role=admin
33
+ * GET /api/_internal/verify-role?roles=admin,moderator (any of these)
34
+ *
35
+ * Authentication:
36
+ * - Reads JWT from Authorization header (Bearer token)
37
+ * - Or from sb-access-token cookie (for browser sessions)
38
+ * - Or from X-Forwarded-Access-Token header (set by Traefik)
39
+ *
40
+ * Returns:
41
+ * 200 - User authenticated and has required role
42
+ * 401 - Not authenticated or invalid token
43
+ * 403 - Authenticated but missing required role
44
+ *
45
+ * Response headers (on success):
46
+ * X-User-Id: The authenticated user's ID
47
+ * X-User-Email: The authenticated user's email
48
+ * X-User-Role: The user's role from app_metadata
49
+ */
50
+
51
+ import type { HonoVariables } from '../../types';
52
+
53
+ const verifyRoleRoutes = new Hono<{ Variables: HonoVariables }>();
54
+
55
+ // Helper to extract JWT from various sources
56
+ function extractToken(req: Request, cookieHeader: string | undefined): string | null {
57
+ // 1. Check Authorization header
58
+ const authHeader = req.headers.get('Authorization');
59
+ if (authHeader?.startsWith('Bearer ')) {
60
+ return authHeader.slice(7);
61
+ }
62
+
63
+ // 2. Check X-Forwarded-Access-Token (set by Traefik ForwardAuth)
64
+ const forwardedToken = req.headers.get('X-Forwarded-Access-Token');
65
+ if (forwardedToken) {
66
+ return forwardedToken;
67
+ }
68
+
69
+ // 3. Check cookies for Supabase auth token
70
+ if (cookieHeader) {
71
+ const cookies = cookieHeader.split(';').reduce(
72
+ (acc, cookie) => {
73
+ const [key, value] = cookie.trim().split('=');
74
+ if (key && value) {
75
+ acc[key] = value;
76
+ }
77
+ return acc;
78
+ },
79
+ {} as Record<string, string>
80
+ );
81
+
82
+ // Check for our custom cookie-based auth token first (sb-auth-token)
83
+ // Then fall back to standard Supabase cookie patterns
84
+ const supabaseToken =
85
+ cookies['sb-auth-token'] ||
86
+ cookies['sb-access-token'] ||
87
+ Object.entries(cookies).find(
88
+ ([key]) => key.startsWith('sb-') && key.endsWith('-auth-token')
89
+ )?.[1];
90
+
91
+ if (supabaseToken) {
92
+ // The cookie value is JSON-encoded, containing access_token
93
+ try {
94
+ const parsed = JSON.parse(decodeURIComponent(supabaseToken));
95
+ if (parsed.access_token) {
96
+ return parsed.access_token;
97
+ }
98
+ } catch {
99
+ // If it's not JSON, use as-is
100
+ return supabaseToken;
101
+ }
102
+ }
103
+ }
104
+
105
+ return null;
106
+ }
107
+
108
+ verifyRoleRoutes.all('/', async (c) => {
109
+ const requiredRole = c.req.query('role');
110
+ const requiredRolesParam = c.req.query('roles');
111
+ const requiredRoles = requiredRole
112
+ ? [requiredRole]
113
+ : requiredRolesParam
114
+ ? requiredRolesParam.split(',').map((r) => r.trim())
115
+ : [];
116
+
117
+ if (requiredRoles.length === 0) {
118
+ logger.warn('verify-role called without role parameter');
119
+ return c.text('Bad Request: role or roles parameter required', 400);
120
+ }
121
+
122
+ // Extract token from request
123
+ const token = extractToken(c.req.raw, c.req.header('Cookie'));
124
+
125
+ if (!token) {
126
+ logger.debug('verify-role: No token found');
127
+
128
+ // For browser requests, redirect to login page with full return URL
129
+ // so the user is sent back to the original service (e.g., n8n.localhost)
130
+ const acceptHeader = c.req.header('Accept') || '';
131
+ if (acceptHeader.includes('text/html')) {
132
+ const forwardedProto = c.req.header('X-Forwarded-Proto') || 'http';
133
+ const forwardedHost = c.req.header('X-Forwarded-Host');
134
+ const forwardedUri = c.req.header('X-Forwarded-Uri') || '/';
135
+
136
+ // Build full return URL from Traefik's forwarded headers
137
+ const returnUrl = forwardedHost
138
+ ? `${forwardedProto}://${forwardedHost}${forwardedUri}`
139
+ : forwardedUri;
140
+
141
+ const siteUrl = getValidatedSiteUrl();
142
+ return c.redirect(`${siteUrl}/login?redirect=${encodeURIComponent(returnUrl)}`);
143
+ }
144
+
145
+ return c.text('Unauthorized', 401);
146
+ }
147
+
148
+ try {
149
+ // Verify the JWT and get user info using Supabase Admin
150
+ const {
151
+ data: { user },
152
+ error,
153
+ } = await supabaseAdmin.auth.getUser(token);
154
+
155
+ if (error || !user) {
156
+ logger.debug({ error: error?.message }, 'verify-role: Invalid token');
157
+ return c.text('Unauthorized', 401);
158
+ }
159
+
160
+ // Check user's role in app_metadata
161
+ const userRole = user.app_metadata?.role as string | undefined;
162
+ const hasRequiredRole = requiredRoles.includes(userRole || '');
163
+
164
+ if (!hasRequiredRole) {
165
+ logger.info(
166
+ { userId: user.id, userRole, requiredRoles },
167
+ 'verify-role: User lacks required role'
168
+ );
169
+ return c.text('Forbidden', 403);
170
+ }
171
+
172
+ // Success - set headers for downstream services
173
+ c.header('X-User-Id', user.id);
174
+ c.header('X-User-Email', user.email || '');
175
+ c.header('X-User-Role', userRole || '');
176
+
177
+ logger.debug({ userId: user.id, userRole }, 'verify-role: Access granted');
178
+ return c.text('OK', 200);
179
+ } catch (err) {
180
+ logger.error({ error: err }, 'verify-role: Unexpected error');
181
+ return c.text('Internal Server Error', 500);
182
+ }
183
+ });
184
+
185
+ export { verifyRoleRoutes };
@@ -0,0 +1,48 @@
1
+ import { Hono } from 'hono';
2
+ import { logger } from '../lib/logger';
3
+ import { supabaseAdmin } from '../lib/supabase';
4
+ import type { HonoVariables } from '../types';
5
+
6
+ const healthRoutes = new Hono<{ Variables: HonoVariables }>();
7
+
8
+ // Liveness probe: returns 200 if the server process is running.
9
+ // No external dependency checks — if this handler executes, the server is alive.
10
+ healthRoutes.get('/', (c) => {
11
+ return c.json({
12
+ status: 'ok',
13
+ timestamp: new Date().toISOString(),
14
+ });
15
+ });
16
+
17
+ // Readiness probe: returns 200 only when the app can serve traffic
18
+ // (i.e., database/Supabase is reachable).
19
+ healthRoutes.get('/ready', async (c) => {
20
+ try {
21
+ const { error } = await supabaseAdmin.from('organizations').select('id').limit(0);
22
+
23
+ if (error) {
24
+ throw error;
25
+ }
26
+
27
+ return c.json({
28
+ status: 'ready',
29
+ timestamp: new Date().toISOString(),
30
+ services: {
31
+ database: 'connected',
32
+ supabase: 'connected',
33
+ },
34
+ });
35
+ } catch (error) {
36
+ logger.error({ error }, 'Readiness check failed');
37
+
38
+ return c.json(
39
+ {
40
+ status: 'not_ready',
41
+ timestamp: new Date().toISOString(),
42
+ },
43
+ 503
44
+ );
45
+ }
46
+ });
47
+
48
+ export { healthRoutes };
@@ -0,0 +1,128 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { Hono } from 'hono';
3
+ import { z } from 'zod';
4
+ import { sanitizeError } from '../../../lib/errors';
5
+ import { supabaseAdmin } from '../../../lib/supabase';
6
+ import type { HonoVariables } from '../../../types';
7
+
8
+ // biome-ignore lint/suspicious/noExplicitAny: Tables not yet in generated Database types
9
+ const adminDb = supabaseAdmin as SupabaseClient<any>;
10
+
11
+ const adminContactRoutes = new Hono<{ Variables: HonoVariables }>();
12
+
13
+ /**
14
+ * Require super admin for all contact management endpoints
15
+ */
16
+ adminContactRoutes.use('*', async (c, next) => {
17
+ const user = c.get('user');
18
+ if (!user) return c.json({ error: 'Unauthorized' }, 401);
19
+
20
+ const { data } = await adminDb
21
+ .from('auth.users')
22
+ .select('raw_app_meta_data')
23
+ .eq('id', user.id)
24
+ .single();
25
+
26
+ const role = data?.raw_app_meta_data?.role;
27
+ if (role !== 'super_admin') {
28
+ return c.json({ error: 'Forbidden' }, 403);
29
+ }
30
+
31
+ await next();
32
+ });
33
+
34
+ /**
35
+ * List contact submissions with pagination
36
+ */
37
+ adminContactRoutes.get('/', async (c) => {
38
+ const page = Math.max(1, Number(c.req.query('page') || '1'));
39
+ const limit = Math.min(50, Math.max(1, Number(c.req.query('limit') || '20')));
40
+ const status = c.req.query('status');
41
+ const offset = (page - 1) * limit;
42
+
43
+ try {
44
+ let query = adminDb
45
+ .from('contact_submissions')
46
+ .select('*', { count: 'exact' })
47
+ .order('created_at', { ascending: false })
48
+ .range(offset, offset + limit - 1);
49
+
50
+ if (status) {
51
+ query = query.eq('status', status);
52
+ }
53
+
54
+ const { data, error, count } = await query;
55
+
56
+ if (error) {
57
+ return c.json({ error: sanitizeError(error, 'Failed to fetch submissions') }, 500);
58
+ }
59
+
60
+ return c.json({
61
+ submissions: data || [],
62
+ total: count || 0,
63
+ page,
64
+ limit,
65
+ });
66
+ } catch (error) {
67
+ return c.json({ error: sanitizeError(error, 'Failed to fetch submissions') }, 500);
68
+ }
69
+ });
70
+
71
+ /**
72
+ * Update submission status
73
+ */
74
+ const updateStatusSchema = z.object({
75
+ status: z.enum(['unread', 'read', 'replied', 'archived']),
76
+ });
77
+
78
+ adminContactRoutes.patch('/:id', async (c) => {
79
+ const id = c.req.param('id');
80
+
81
+ let body: z.infer<typeof updateStatusSchema>;
82
+ try {
83
+ const rawBody = await c.req.json();
84
+ const result = updateStatusSchema.safeParse(rawBody);
85
+ if (!result.success) {
86
+ return c.json({ error: result.error.issues.map((e) => e.message).join(', ') }, 400);
87
+ }
88
+ body = result.data;
89
+ } catch {
90
+ return c.json({ error: 'Invalid JSON body' }, 400);
91
+ }
92
+
93
+ try {
94
+ const { error } = await adminDb
95
+ .from('contact_submissions')
96
+ .update({ status: body.status })
97
+ .eq('id', id);
98
+
99
+ if (error) {
100
+ return c.json({ error: sanitizeError(error, 'Failed to update submission') }, 500);
101
+ }
102
+
103
+ return c.json({ success: true });
104
+ } catch (error) {
105
+ return c.json({ error: sanitizeError(error, 'Failed to update submission') }, 500);
106
+ }
107
+ });
108
+
109
+ /**
110
+ * Delete a submission
111
+ */
112
+ adminContactRoutes.delete('/:id', async (c) => {
113
+ const id = c.req.param('id');
114
+
115
+ try {
116
+ const { error } = await adminDb.from('contact_submissions').delete().eq('id', id);
117
+
118
+ if (error) {
119
+ return c.json({ error: sanitizeError(error, 'Failed to delete submission') }, 500);
120
+ }
121
+
122
+ return c.json({ success: true });
123
+ } catch (error) {
124
+ return c.json({ error: sanitizeError(error, 'Failed to delete submission') }, 500);
125
+ }
126
+ });
127
+
128
+ export { adminContactRoutes };
@@ -0,0 +1,171 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { Hono } from 'hono';
3
+ import { z } from 'zod';
4
+ import { sanitizeError } from '../../../lib/errors';
5
+ import { supabaseAdmin } from '../../../lib/supabase';
6
+ import type { HonoVariables } from '../../../types';
7
+
8
+ // biome-ignore lint/suspicious/noExplicitAny: Tables not yet in generated Database types
9
+ const adminDb = supabaseAdmin as SupabaseClient<any>;
10
+
11
+ const jobsRoutes = new Hono<{ Variables: HonoVariables }>();
12
+
13
+ /**
14
+ * Require super admin for all job management endpoints
15
+ */
16
+ jobsRoutes.use('*', async (c, next) => {
17
+ const user = c.get('user');
18
+ if (!user) return c.json({ error: 'Unauthorized' }, 401);
19
+
20
+ const { data } = await adminDb
21
+ .from('auth.users')
22
+ .select('raw_app_meta_data')
23
+ .eq('id', user.id)
24
+ .single();
25
+
26
+ const role = data?.raw_app_meta_data?.role;
27
+ if (role !== 'super_admin') {
28
+ return c.json({ error: 'Forbidden' }, 403);
29
+ }
30
+
31
+ await next();
32
+ });
33
+
34
+ /**
35
+ * List all scheduled cron jobs
36
+ */
37
+ jobsRoutes.get('/', async (c) => {
38
+ try {
39
+ // Query pg_cron's job table directly
40
+ const { data: jobs, error } = await adminDb.rpc('get_cron_jobs');
41
+
42
+ if (error) {
43
+ // Fallback: query cron.job directly via raw SQL isn't available via RPC,
44
+ // so we return the job history as a summary instead
45
+ const { data: history, error: historyError } = await adminDb
46
+ .from('cron_job_history')
47
+ .select('job_name, status, started_at, finished_at, duration_ms, result, error')
48
+ .order('started_at', { ascending: false })
49
+ .limit(100);
50
+
51
+ if (historyError) {
52
+ return c.json({ error: sanitizeError(historyError, 'Failed to fetch jobs') }, 500);
53
+ }
54
+
55
+ // Derive job list from history
56
+ const jobNames = [...new Set((history || []).map((h: { job_name: string }) => h.job_name))];
57
+ const jobSummaries = jobNames.map((name) => {
58
+ const jobHistory = (history || []).filter((h: { job_name: string }) => h.job_name === name);
59
+ const latest = jobHistory[0] as
60
+ | { status: string; started_at: string; result: string | null; error: string | null }
61
+ | undefined;
62
+ return {
63
+ name,
64
+ lastRun: latest?.started_at ?? null,
65
+ lastStatus: latest?.status ?? 'unknown',
66
+ lastResult: latest?.result ?? null,
67
+ lastError: latest?.error ?? null,
68
+ runCount: jobHistory.length,
69
+ };
70
+ });
71
+
72
+ return c.json({ jobs: jobSummaries, history: history || [] });
73
+ }
74
+
75
+ return c.json({ jobs: jobs || [], history: [] });
76
+ } catch (error) {
77
+ return c.json({ error: sanitizeError(error, 'Failed to fetch jobs') }, 500);
78
+ }
79
+ });
80
+
81
+ /**
82
+ * Get job execution history
83
+ */
84
+ jobsRoutes.get('/history', async (c) => {
85
+ const jobName = c.req.query('jobName');
86
+ const limit = Math.min(Number(c.req.query('limit') || '50'), 200);
87
+
88
+ try {
89
+ let query = adminDb
90
+ .from('cron_job_history')
91
+ .select('*')
92
+ .order('started_at', { ascending: false })
93
+ .limit(limit);
94
+
95
+ if (jobName) {
96
+ query = query.eq('job_name', jobName);
97
+ }
98
+
99
+ const { data, error } = await query;
100
+
101
+ if (error) {
102
+ return c.json({ error: sanitizeError(error, 'Failed to fetch job history') }, 500);
103
+ }
104
+
105
+ return c.json({ history: data || [] });
106
+ } catch (error) {
107
+ return c.json({ error: sanitizeError(error, 'Failed to fetch job history') }, 500);
108
+ }
109
+ });
110
+
111
+ /**
112
+ * Manually trigger a job (run the cleanup function immediately)
113
+ */
114
+ const triggerSchema = z.object({
115
+ jobName: z.string().min(1),
116
+ });
117
+
118
+ jobsRoutes.post('/trigger', async (c) => {
119
+ let body: z.infer<typeof triggerSchema>;
120
+ try {
121
+ const rawBody = await c.req.json();
122
+ const result = triggerSchema.safeParse(rawBody);
123
+ if (!result.success) {
124
+ return c.json({ error: result.error.issues.map((e) => e.message).join(', ') }, 400);
125
+ }
126
+ body = result.data;
127
+ } catch {
128
+ return c.json({ error: 'Invalid JSON body' }, 400);
129
+ }
130
+
131
+ try {
132
+ // Only allow triggering known safe jobs
133
+ const allowedJobs = [
134
+ 'cleanup-login-attempts',
135
+ 'cleanup-expired-notifications',
136
+ 'cleanup-job-history',
137
+ ];
138
+
139
+ if (!allowedJobs.includes(body.jobName)) {
140
+ return c.json({ error: 'Unknown job name' }, 400);
141
+ }
142
+
143
+ // Execute the job's SQL directly
144
+ if (body.jobName === 'cleanup-login-attempts') {
145
+ await adminDb.rpc('cleanup_old_login_attempts', { p_retention_hours: 24 });
146
+ } else if (body.jobName === 'cleanup-expired-notifications') {
147
+ await adminDb
148
+ .from('notifications')
149
+ .delete()
150
+ .not('expires_at', 'is', null)
151
+ .lt('expires_at', new Date().toISOString());
152
+ } else if (body.jobName === 'cleanup-job-history') {
153
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
154
+ await adminDb.from('cron_job_history').delete().lt('created_at', thirtyDaysAgo);
155
+ }
156
+
157
+ // Log the manual trigger
158
+ await adminDb.from('cron_job_history').insert({
159
+ job_name: body.jobName,
160
+ status: 'succeeded',
161
+ finished_at: new Date().toISOString(),
162
+ result: 'Manually triggered by admin',
163
+ });
164
+
165
+ return c.json({ success: true });
166
+ } catch (error) {
167
+ return c.json({ error: sanitizeError(error, 'Failed to trigger job') }, 500);
168
+ }
169
+ });
170
+
171
+ export { jobsRoutes };