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.
- package/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- package/src/upgrade.js +721 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare API operations
|
|
3
|
+
* Shared across deploy.js and destroy.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import { fetchWithRetry } from './fetch-retry.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// ZONE AND ACCOUNT OPERATIONS
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get all zones in the Cloudflare account
|
|
15
|
+
*
|
|
16
|
+
* @param {string} apiToken - Cloudflare API token
|
|
17
|
+
* @returns {Promise<Array>} - Array of zone objects
|
|
18
|
+
*/
|
|
19
|
+
export async function getZones(apiToken) {
|
|
20
|
+
const response = await fetchWithRetry('https://api.cloudflare.com/client/v4/zones', {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${apiToken}`,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const error = await response.json();
|
|
29
|
+
throw new Error(`Cloudflare API error: ${JSON.stringify(error.errors)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
return data.result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the Cloudflare account ID
|
|
38
|
+
*
|
|
39
|
+
* @param {string} apiToken - Cloudflare API token
|
|
40
|
+
* @returns {Promise<string|null>} - Account ID or null if not found
|
|
41
|
+
*/
|
|
42
|
+
export async function getAccountId(apiToken) {
|
|
43
|
+
const response = await fetchWithRetry('https://api.cloudflare.com/client/v4/accounts', {
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${apiToken}`,
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
if (data.result && data.result.length > 0) {
|
|
52
|
+
return data.result[0].id;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// DNS RECORD OPERATIONS
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create or update a DNS record
|
|
63
|
+
*
|
|
64
|
+
* @param {string} apiToken - Cloudflare API token
|
|
65
|
+
* @param {string} zoneId - Zone ID
|
|
66
|
+
* @param {object} config - Record configuration
|
|
67
|
+
* @param {string} config.type - Record type (A, CNAME, etc.)
|
|
68
|
+
* @param {string} config.name - Record name
|
|
69
|
+
* @param {string} config.content - Record content (IP address, etc.)
|
|
70
|
+
* @param {boolean} [config.proxied=true] - Whether to proxy through Cloudflare
|
|
71
|
+
* @param {number} [config.ttl=1] - TTL (1 = auto)
|
|
72
|
+
* @returns {Promise<object>} - API response
|
|
73
|
+
*/
|
|
74
|
+
export async function createDNSRecord(apiToken, zoneId, config) {
|
|
75
|
+
const { type, name, content, proxied = true, ttl = 1 } = config;
|
|
76
|
+
|
|
77
|
+
// Check if record already exists
|
|
78
|
+
const listResponse = await fetchWithRetry(
|
|
79
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=${type}&name=${name}`,
|
|
80
|
+
{
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${apiToken}`,
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const listData = await listResponse.json();
|
|
89
|
+
const existing = listData.result?.find((r) => r.name === name && r.type === type);
|
|
90
|
+
|
|
91
|
+
if (existing) {
|
|
92
|
+
// Update existing record
|
|
93
|
+
const updateResponse = await fetchWithRetry(
|
|
94
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${existing.id}`,
|
|
95
|
+
{
|
|
96
|
+
method: 'PATCH',
|
|
97
|
+
headers: {
|
|
98
|
+
Authorization: `Bearer ${apiToken}`,
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({ content, proxied, ttl }),
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
return updateResponse.json();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create new record
|
|
108
|
+
const response = await fetchWithRetry(
|
|
109
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: `Bearer ${apiToken}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({ type, name, content, proxied, ttl }),
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return response.json();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Delete a DNS record, but only if its target IP belongs to this caller.
|
|
125
|
+
*
|
|
126
|
+
* Cloudflare zones can hold multiple A records for the same name (e.g. when
|
|
127
|
+
* two e2e scenarios share a root domain and each owns a subdomain).
|
|
128
|
+
* Deleting unconditionally wipes neighbours' records too — the blast-radius
|
|
129
|
+
* bug observed in the 2026-05-16 matrix where compose-ha's destroy zeroed
|
|
130
|
+
* compose's `e1.carbonstack.dev` mid-verify.
|
|
131
|
+
*
|
|
132
|
+
* Ownership rule: a record is "ours" iff its `content` (the target IP) is
|
|
133
|
+
* in `ownedIps`. Records pointing elsewhere are preserved with a `skipped`
|
|
134
|
+
* count. Pass an empty array to refuse all deletes (the safer default when
|
|
135
|
+
* envConfig has no server IPs to compare against).
|
|
136
|
+
*
|
|
137
|
+
* @param {string} apiToken - Cloudflare API token
|
|
138
|
+
* @param {string} zoneId - Zone ID
|
|
139
|
+
* @param {string} name - Record name (FQDN)
|
|
140
|
+
* @param {string[]} ownedIps - IPs that belong to this caller's stack
|
|
141
|
+
* @param {string} [type='A'] - Record type
|
|
142
|
+
* @returns {Promise<{deleted: number, skipped: number, total: number, skippedTargets: string[]}>}
|
|
143
|
+
*/
|
|
144
|
+
export async function deleteDNSRecord(apiToken, zoneId, name, ownedIps, type = 'A') {
|
|
145
|
+
const listResponse = await fetchWithRetry(
|
|
146
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=${type}&name=${name}`,
|
|
147
|
+
{
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${apiToken}`,
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const listData = await listResponse.json();
|
|
156
|
+
const records = (listData.result || []).filter((r) => r.name === name && r.type === type);
|
|
157
|
+
const total = records.length;
|
|
158
|
+
|
|
159
|
+
if (total === 0) {
|
|
160
|
+
return { deleted: 0, skipped: 0, total: 0, skippedTargets: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const owned = new Set(ownedIps || []);
|
|
164
|
+
let deleted = 0;
|
|
165
|
+
const skippedTargets = [];
|
|
166
|
+
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
if (!owned.has(record.content)) {
|
|
169
|
+
skippedTargets.push(record.content);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const response = await fetchWithRetry(
|
|
173
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${record.id}`,
|
|
174
|
+
{
|
|
175
|
+
method: 'DELETE',
|
|
176
|
+
headers: {
|
|
177
|
+
Authorization: `Bearer ${apiToken}`,
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
if (response.ok) deleted++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { deleted, skipped: skippedTargets.length, total, skippedTargets };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// HEALTH CHECK OPERATIONS
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a health check
|
|
194
|
+
*
|
|
195
|
+
* @param {string} apiToken - Cloudflare API token
|
|
196
|
+
* @param {string} zoneId - Zone ID
|
|
197
|
+
* @param {object} config - Health check configuration
|
|
198
|
+
* @returns {Promise<object>} - API response
|
|
199
|
+
*/
|
|
200
|
+
export async function createHealthCheck(apiToken, zoneId, config) {
|
|
201
|
+
const { name, address, path = '/api/health', port = 443 } = config;
|
|
202
|
+
|
|
203
|
+
const response = await fetchWithRetry(
|
|
204
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/healthchecks`,
|
|
205
|
+
{
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: {
|
|
208
|
+
Authorization: `Bearer ${apiToken}`,
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
name,
|
|
213
|
+
address,
|
|
214
|
+
type: 'HTTPS',
|
|
215
|
+
port,
|
|
216
|
+
method: 'GET',
|
|
217
|
+
path,
|
|
218
|
+
interval: 60,
|
|
219
|
+
timeout: 5,
|
|
220
|
+
retries: 2,
|
|
221
|
+
consecutive_fails: 2,
|
|
222
|
+
consecutive_successes: 1,
|
|
223
|
+
}),
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return response.json();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Delete a health check
|
|
232
|
+
*
|
|
233
|
+
* @param {string} apiToken - Cloudflare API token
|
|
234
|
+
* @param {string} zoneId - Zone ID
|
|
235
|
+
* @param {string} name - Health check name
|
|
236
|
+
* @returns {Promise<boolean>} - True if deleted
|
|
237
|
+
*/
|
|
238
|
+
export async function deleteHealthCheck(apiToken, zoneId, name) {
|
|
239
|
+
// List health checks
|
|
240
|
+
const listResponse = await fetchWithRetry(
|
|
241
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/healthchecks`,
|
|
242
|
+
{
|
|
243
|
+
headers: {
|
|
244
|
+
Authorization: `Bearer ${apiToken}`,
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const listData = await listResponse.json();
|
|
251
|
+
const healthCheck = listData.result?.find((h) => h.name === name);
|
|
252
|
+
|
|
253
|
+
if (!healthCheck) return false;
|
|
254
|
+
|
|
255
|
+
const response = await fetchWithRetry(
|
|
256
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/healthchecks/${healthCheck.id}`,
|
|
257
|
+
{
|
|
258
|
+
method: 'DELETE',
|
|
259
|
+
headers: {
|
|
260
|
+
Authorization: `Bearer ${apiToken}`,
|
|
261
|
+
'Content-Type': 'application/json',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return response.ok;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// HIGH-LEVEL SETUP FUNCTIONS
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Set up Cloudflare for HA deployment with DNS records and health checks
|
|
275
|
+
*
|
|
276
|
+
* @param {string} apiToken - Cloudflare API token
|
|
277
|
+
* @param {string} zoneId - Zone ID
|
|
278
|
+
* @param {string} domain - Domain name
|
|
279
|
+
* @param {Array} servers - Array of server objects with name and ip
|
|
280
|
+
* @returns {Promise<object>} - Setup result
|
|
281
|
+
*/
|
|
282
|
+
export async function setupHA(apiToken, zoneId, domain, servers) {
|
|
283
|
+
const s = p.spinner();
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Create A record pointing to primary server.
|
|
287
|
+
//
|
|
288
|
+
// proxied: false (gray cloud, DNS-only). Was `true` (orange cloud)
|
|
289
|
+
// back when we used Cloudflare Load Balancers for automatic
|
|
290
|
+
// failover — the proxy was load-bearing because LB rules ran at
|
|
291
|
+
// CF's edge. Cloudflare LBs were removed 2026-03-26 in favor of
|
|
292
|
+
// manual failover (rewriting this A record on demand), so the
|
|
293
|
+
// proxy is no longer needed and is actively harmful: with orange
|
|
294
|
+
// cloud, CF intercepts the apex and serves a 404 from its edge
|
|
295
|
+
// until it can verify the origin's cert and a route exists. The
|
|
296
|
+
// wildcard below was already gray-cloud — apex matches it now so
|
|
297
|
+
// both are direct-to-origin via Traefik + Let's Encrypt.
|
|
298
|
+
// RCA: k8s-ha 2026-04-30 deploy probe got 404 for 1200s — DNS
|
|
299
|
+
// resolved fine, but CF proxy returned 404 instead of forwarding.
|
|
300
|
+
s.start(`Creating DNS record for ${domain}`);
|
|
301
|
+
await createDNSRecord(apiToken, zoneId, {
|
|
302
|
+
type: 'A',
|
|
303
|
+
name: domain,
|
|
304
|
+
content: servers[0].ip,
|
|
305
|
+
proxied: false,
|
|
306
|
+
});
|
|
307
|
+
s.stop('Primary DNS record created');
|
|
308
|
+
|
|
309
|
+
// Create wildcard A record for subdomains (registry, api, etc.)
|
|
310
|
+
// DNS-only (not proxied) — Traefik handles TLS via Let's Encrypt
|
|
311
|
+
await createDNSRecord(apiToken, zoneId, {
|
|
312
|
+
type: 'A',
|
|
313
|
+
name: `*.${domain}`,
|
|
314
|
+
content: servers[0].ip,
|
|
315
|
+
proxied: false,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Create health checks for monitoring both regions
|
|
319
|
+
for (const server of servers) {
|
|
320
|
+
s.start(`Creating health check for ${server.name}`);
|
|
321
|
+
await createHealthCheck(apiToken, zoneId, {
|
|
322
|
+
name: `${server.name}-health`,
|
|
323
|
+
address: server.ip,
|
|
324
|
+
path: '/api/health',
|
|
325
|
+
});
|
|
326
|
+
s.stop(`Health check created for ${server.name}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
success: true,
|
|
331
|
+
mode: 'dns',
|
|
332
|
+
primaryIp: servers[0].ip,
|
|
333
|
+
standbyIp: servers[1]?.ip,
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
s.stop(`Cloudflare DNS setup failed: ${error.message}`);
|
|
337
|
+
return { success: false, error: error.message };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Set up Cloudflare for simple single-server deployment
|
|
343
|
+
*
|
|
344
|
+
* @param {string} apiToken - Cloudflare API token
|
|
345
|
+
* @param {string} zoneId - Zone ID
|
|
346
|
+
* @param {string} domain - Domain name
|
|
347
|
+
* @param {string} serverIp - Server IP address
|
|
348
|
+
* @returns {Promise<object>} - Setup result
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* Set the zone-level SSL mode on Cloudflare. Used when we want Cloudflare's
|
|
352
|
+
* edge to terminate TLS (so the origin can skip ACME / cert-manager).
|
|
353
|
+
*
|
|
354
|
+
* - 'off' — no encryption (don't use in prod)
|
|
355
|
+
* - 'flexible' — CF↔browser HTTPS, CF↔origin HTTP (allows origin to run
|
|
356
|
+
* without any TLS; not recommended — redirect loops with origin-side
|
|
357
|
+
* HTTPS)
|
|
358
|
+
* - 'full' — CF↔browser HTTPS, CF↔origin HTTPS with ANY cert (self-signed
|
|
359
|
+
* OK). Our sweet spot when the user opts into the TLS-proxy path.
|
|
360
|
+
* - 'strict' — CF↔origin HTTPS with a valid cert. Requires real ACME,
|
|
361
|
+
* defeats the purpose of TLS proxy.
|
|
362
|
+
*
|
|
363
|
+
* @param {string} apiToken
|
|
364
|
+
* @param {string} zoneId
|
|
365
|
+
* @param {'off'|'flexible'|'full'|'strict'} mode
|
|
366
|
+
*/
|
|
367
|
+
export async function setZoneSslMode(apiToken, zoneId, mode) {
|
|
368
|
+
const response = await fetchWithRetry(
|
|
369
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/settings/ssl`,
|
|
370
|
+
{
|
|
371
|
+
method: 'PATCH',
|
|
372
|
+
headers: {
|
|
373
|
+
Authorization: `Bearer ${apiToken}`,
|
|
374
|
+
'Content-Type': 'application/json',
|
|
375
|
+
},
|
|
376
|
+
body: JSON.stringify({ value: mode }),
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
const data = await response.json();
|
|
380
|
+
if (!data.success) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Cloudflare zone SSL mode update failed: ${JSON.stringify(data.errors ?? data)}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return data.result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function setupSimple(apiToken, zoneId, domain, serverIp, options = {}) {
|
|
389
|
+
// proxied default flipped to false (DNS-only / gray-cloud) on
|
|
390
|
+
// 2026-04-26 after e2e run #3 surfaced the orange-cloud
|
|
391
|
+
// chicken-and-egg: with proxied=true, dig resolves to CF edge IPs
|
|
392
|
+
// (172.67.x / 104.21.x), not the origin floating IP. CF then
|
|
393
|
+
// intercepts HTTPS, terminates with its Universal SSL cert, and
|
|
394
|
+
// tries to talk to origin per the zone's SSL mode (default Full
|
|
395
|
+
// (strict) for new zones — requires a valid origin cert). Origin's
|
|
396
|
+
// cert-manager can't issue via HTTP-01 because LE follows CF's
|
|
397
|
+
// Always-Use-HTTPS redirect and gets rejected at origin since cert
|
|
398
|
+
// isn't issued yet. Loop. With proxied=false, dig resolves to the
|
|
399
|
+
// origin IP directly, LE HTTP-01 hits origin on :80, traefik serves
|
|
400
|
+
// the challenge, cert issues, HTTPS works. Callers that want CF's
|
|
401
|
+
// edge TLS can opt in via options.proxied=true (they're then
|
|
402
|
+
// responsible for setting the zone SSL mode + ensuring origin
|
|
403
|
+
// accepts CF's challenge path).
|
|
404
|
+
const { proxied = false, onProgress } = options;
|
|
405
|
+
// When caller provides onProgress, skip internal spinners (caller manages UI)
|
|
406
|
+
const s = onProgress ? null : p.spinner();
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
// Create A record for main domain
|
|
410
|
+
s?.start(`Creating DNS records for ${domain}`);
|
|
411
|
+
onProgress?.(`Creating DNS records for ${domain}`);
|
|
412
|
+
const result = await createDNSRecord(apiToken, zoneId, {
|
|
413
|
+
type: 'A',
|
|
414
|
+
name: domain,
|
|
415
|
+
content: serverIp,
|
|
416
|
+
proxied,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Create wildcard A record for all subdomains (api, registry, etc.)
|
|
420
|
+
// DNS-only (not proxied) — Cloudflare free/pro plans don't proxy wildcards,
|
|
421
|
+
// and Traefik handles TLS via Let's Encrypt anyway.
|
|
422
|
+
await createDNSRecord(apiToken, zoneId, {
|
|
423
|
+
type: 'A',
|
|
424
|
+
name: `*.${domain}`,
|
|
425
|
+
content: serverIp,
|
|
426
|
+
proxied: false,
|
|
427
|
+
});
|
|
428
|
+
s?.stop('DNS records created (root + wildcard)');
|
|
429
|
+
onProgress?.('DNS records created (root + wildcard)');
|
|
430
|
+
|
|
431
|
+
// Create health check (informational, doesn't affect routing without LB)
|
|
432
|
+
s?.start('Creating health check');
|
|
433
|
+
onProgress?.('Creating health check');
|
|
434
|
+
await createHealthCheck(apiToken, zoneId, {
|
|
435
|
+
name: `${domain}-health`,
|
|
436
|
+
address: serverIp,
|
|
437
|
+
path: '/api/health',
|
|
438
|
+
});
|
|
439
|
+
s?.stop('Health check created');
|
|
440
|
+
onProgress?.('Health check created');
|
|
441
|
+
|
|
442
|
+
return { success: true, mode: 'simple', record: result.result };
|
|
443
|
+
} catch (error) {
|
|
444
|
+
s?.stop(`Cloudflare setup failed: ${error.message}`);
|
|
445
|
+
return { success: false, error: error.message };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ANSI color codes and formatting utilities for CLI output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ANSI escape codes
|
|
6
|
+
export const colors = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
brightRed: '\x1b[91m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Color formatting helpers
|
|
18
|
+
export const c = {
|
|
19
|
+
error: (s) => `${colors.red}${s}${colors.reset}`,
|
|
20
|
+
success: (s) => `${colors.green}${s}${colors.reset}`,
|
|
21
|
+
warning: (s) => `${colors.yellow}${s}${colors.reset}`,
|
|
22
|
+
info: (s) => `${colors.cyan}${s}${colors.reset}`,
|
|
23
|
+
bold: (s) => `${colors.bold}${s}${colors.reset}`,
|
|
24
|
+
dim: (s) => `${colors.dim}${s}${colors.reset}`,
|
|
25
|
+
// Bright red + bold — for prominent destruction-warning headlines
|
|
26
|
+
// (destroy preview banner, "WARNING: data will be lost" footer). More
|
|
27
|
+
// vibrant than `c.error` (which stays as the muted bullet color).
|
|
28
|
+
danger: (s) => `${colors.bold}${colors.brightRed}${s}${colors.reset}`,
|
|
29
|
+
// Composite styles
|
|
30
|
+
boldCyan: (s) => `${colors.bold}${colors.cyan}${s}${colors.reset}`,
|
|
31
|
+
boldYellow: (s) => `${colors.bold}${colors.yellow}${s}${colors.reset}`,
|
|
32
|
+
boldCyanUnderline: (s) => `${colors.bold}${colors.cyan}\x1b[4m${s}${colors.reset}`,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Branded banner for CLI commands
|
|
36
|
+
export function printBanner() {
|
|
37
|
+
const bgCyan = '\x1b[46m';
|
|
38
|
+
const border = 2;
|
|
39
|
+
const inner = 44;
|
|
40
|
+
const width = inner + border * 2;
|
|
41
|
+
const full = `${bgCyan}${' '.repeat(width)}${colors.reset}`;
|
|
42
|
+
const side = `${bgCyan}${' '.repeat(border)}${colors.reset}`;
|
|
43
|
+
const empty = `${side}${' '.repeat(inner)}${side}`;
|
|
44
|
+
const text = ' v i b e c a r b o n ';
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(full);
|
|
47
|
+
console.log(empty);
|
|
48
|
+
console.log(`${side}${colors.bold}${colors.cyan}${text}${colors.reset}${side}`);
|
|
49
|
+
console.log(empty);
|
|
50
|
+
console.log(full);
|
|
51
|
+
console.log();
|
|
52
|
+
}
|