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,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulumi program for a single Hetzner k3s cluster.
|
|
3
|
+
*
|
|
4
|
+
* One call to `buildHetznerK8sProgram(config)` returns a Pulumi inline
|
|
5
|
+
* program (a function) that declares the cloud resources. The Automation
|
|
6
|
+
* API wrapper in `../index.js` runs it.
|
|
7
|
+
*
|
|
8
|
+
* Resources declared:
|
|
9
|
+
* - SshKey (optional — reuse existing by ID for HA)
|
|
10
|
+
* - Network + Subnet
|
|
11
|
+
* - Firewall
|
|
12
|
+
* - Floating IP
|
|
13
|
+
* - Master server (cloud-init: master-init.sh)
|
|
14
|
+
* - Supabase server (cloud-init: supabase-init.sh)
|
|
15
|
+
* - Worker servers × N (cloud-init: worker-init.sh)
|
|
16
|
+
* - Placement group for workers (spread)
|
|
17
|
+
*
|
|
18
|
+
* Outputs: masterIp, masterPrivateIp, supabaseIp, workerIps, floatingIp,
|
|
19
|
+
* networkId, sshKeyId, k3sToken.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { randomBytes } from 'node:crypto';
|
|
23
|
+
import * as hcloud from '@pulumi/hcloud';
|
|
24
|
+
import * as pulumi from '@pulumi/pulumi';
|
|
25
|
+
import { loadCloudInit, renderScript } from '../cloud-init.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} K8sStackConfig
|
|
29
|
+
* @property {string} projectName
|
|
30
|
+
* @property {string} environment Environment name (used for cluster name + labels)
|
|
31
|
+
* @property {string} sshPublicKey OpenSSH-format public key
|
|
32
|
+
* @property {string} [existingSshKeyId] If set, skip creating a new SSH key
|
|
33
|
+
* (HA case: shared key across clusters)
|
|
34
|
+
* @property {string} [k3sToken] Pre-shared k3s node-join token
|
|
35
|
+
* (auto-generated if omitted)
|
|
36
|
+
* @property {string} location Hetzner datacenter (nbg1/hel1/fsn1/hil/ash/sin)
|
|
37
|
+
* @property {string} networkZone eu-central / us-east / us-west / ap-southeast
|
|
38
|
+
* @property {string} masterServerType
|
|
39
|
+
* @property {string} supabaseServerType
|
|
40
|
+
* @property {string} workerServerType
|
|
41
|
+
* @property {number} minWorkers Static floor of worker servers (provisioned by Pulumi). Default 1.
|
|
42
|
+
* @property {number} maxWorkers Upper bound for cluster-autoscaler. Not consumed by Pulumi; flows to the CA Deployment in applyK3sManifests. Default 3.
|
|
43
|
+
* @property {string} k3sVersion
|
|
44
|
+
* @property {string} privateNetworkRange Default 10.0.0.0/8
|
|
45
|
+
* @property {string} subnetRange Default 10.0.1.0/24
|
|
46
|
+
* @property {Record<string,string>} labels
|
|
47
|
+
* @property {string[]} [allowedSshIps] CIDR list, defaults to 0.0.0.0/0
|
|
48
|
+
* @property {string[]} [allowedK8sApiIps] CIDR list, defaults to 0.0.0.0/0
|
|
49
|
+
* @property {string} apiToken Hetzner Cloud API token. Baked into
|
|
50
|
+
* the master cloud-init so the node
|
|
51
|
+
* can write the `hcloud` and
|
|
52
|
+
* `hcloud-csi` kube-system Secrets
|
|
53
|
+
* that hcloud-cloud-controller-manager
|
|
54
|
+
* and hcloud-csi-driver read on boot.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build an inline Pulumi program for the given cluster config.
|
|
59
|
+
* @param {K8sStackConfig} config
|
|
60
|
+
* @returns {() => Promise<Record<string, unknown>>}
|
|
61
|
+
*/
|
|
62
|
+
export function buildHetznerK8sProgram(config) {
|
|
63
|
+
const clusterName = `${config.projectName}-${config.environment}`;
|
|
64
|
+
const labels = { project: config.projectName, environment: config.environment, ...config.labels };
|
|
65
|
+
// Operator-IP firewall lock (H-2). The deploy flow populates these from
|
|
66
|
+
// projectConfig.operatorCidrs; an empty list = nothing can SSH in or hit
|
|
67
|
+
// the k8s API. We refuse to build a program that would do that — far
|
|
68
|
+
// better to fail loudly than to silently apply an open `0.0.0.0/0`.
|
|
69
|
+
if (!config.allowedSshIps?.length) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'allowedSshIps required (no default; pass operatorCidrs from .vibecarbon.json or set ALLOWED_SSH_IPS=...)',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!config.allowedK8sApiIps?.length) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'allowedK8sApiIps required (no default; pass operatorCidrs from .vibecarbon.json or set ALLOWED_SSH_IPS=...)',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const allowedSshIps = config.allowedSshIps;
|
|
80
|
+
const allowedK8sApiIps = config.allowedK8sApiIps;
|
|
81
|
+
const k3sToken = config.k3sToken ?? randomBytes(32).toString('hex');
|
|
82
|
+
const privateNetworkRange = config.privateNetworkRange ?? '10.0.0.0/8';
|
|
83
|
+
const subnetRange = config.subnetRange ?? '10.0.1.0/24';
|
|
84
|
+
const minWorkers = config.minWorkers ?? 1;
|
|
85
|
+
|
|
86
|
+
const masterInitTemplate = loadCloudInit('master-init.sh');
|
|
87
|
+
const supabaseInitTemplate = loadCloudInit('supabase-init.sh');
|
|
88
|
+
const workerInitTemplate = loadCloudInit('worker-init.sh');
|
|
89
|
+
|
|
90
|
+
return async () => {
|
|
91
|
+
// SSH key — either reuse an existing one (HA shared key) or create.
|
|
92
|
+
let sshKeyId;
|
|
93
|
+
if (config.existingSshKeyId) {
|
|
94
|
+
sshKeyId = config.existingSshKeyId;
|
|
95
|
+
} else {
|
|
96
|
+
const sshKey = new hcloud.SshKey('ssh-key', {
|
|
97
|
+
name: `${clusterName}-${config.location}-key`,
|
|
98
|
+
publicKey: config.sshPublicKey,
|
|
99
|
+
labels,
|
|
100
|
+
});
|
|
101
|
+
sshKeyId = sshKey.id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Private network + subnet
|
|
105
|
+
const network = new hcloud.Network('network', {
|
|
106
|
+
name: `${clusterName}-network`,
|
|
107
|
+
ipRange: privateNetworkRange,
|
|
108
|
+
labels,
|
|
109
|
+
});
|
|
110
|
+
const subnet = new hcloud.NetworkSubnet('subnet', {
|
|
111
|
+
networkId: network.id,
|
|
112
|
+
type: 'cloud',
|
|
113
|
+
networkZone: config.networkZone,
|
|
114
|
+
ipRange: subnetRange,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Firewall. `replaceOnChanges: ['*']` works around an upstream
|
|
118
|
+
// hcloud-go@1.32.1 bug: the Firewall `Update` code path nil-pointer
|
|
119
|
+
// panics when rules change on a live firewall. Forcing replace avoids
|
|
120
|
+
// the Update call site entirely; recreate is cheap.
|
|
121
|
+
const firewall = new hcloud.Firewall(
|
|
122
|
+
'firewall',
|
|
123
|
+
{
|
|
124
|
+
name: `${clusterName}-firewall`,
|
|
125
|
+
labels,
|
|
126
|
+
rules: [
|
|
127
|
+
{
|
|
128
|
+
direction: 'in',
|
|
129
|
+
protocol: 'tcp',
|
|
130
|
+
port: '22',
|
|
131
|
+
sourceIps: allowedSshIps,
|
|
132
|
+
description: 'SSH access',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
direction: 'in',
|
|
136
|
+
protocol: 'tcp',
|
|
137
|
+
port: '80',
|
|
138
|
+
sourceIps: ['0.0.0.0/0', '::/0'],
|
|
139
|
+
description: 'HTTP',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
direction: 'in',
|
|
143
|
+
protocol: 'tcp',
|
|
144
|
+
port: '443',
|
|
145
|
+
sourceIps: ['0.0.0.0/0', '::/0'],
|
|
146
|
+
description: 'HTTPS',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
direction: 'in',
|
|
150
|
+
protocol: 'tcp',
|
|
151
|
+
port: '6443',
|
|
152
|
+
sourceIps: allowedK8sApiIps,
|
|
153
|
+
description: 'Kubernetes API',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
direction: 'in',
|
|
157
|
+
protocol: 'tcp',
|
|
158
|
+
port: 'any',
|
|
159
|
+
sourceIps: [privateNetworkRange],
|
|
160
|
+
description: 'Internal cluster traffic (TCP)',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
direction: 'in',
|
|
164
|
+
protocol: 'udp',
|
|
165
|
+
port: 'any',
|
|
166
|
+
sourceIps: [privateNetworkRange],
|
|
167
|
+
description: 'Internal cluster traffic (UDP)',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{ replaceOnChanges: ['*'], deleteBeforeReplace: true },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Floating IP (stable ingress) — created before master so we can pass
|
|
175
|
+
// its address into the master's cloud-init script. Name includes the
|
|
176
|
+
// region (config.location) so parallel deploys across regions don't
|
|
177
|
+
// collide on the shared-across-project Hetzner namespace.
|
|
178
|
+
const floatingIp = new hcloud.FloatingIp('ingress', {
|
|
179
|
+
type: 'ipv4',
|
|
180
|
+
homeLocation: config.location,
|
|
181
|
+
name: `${clusterName}-${config.location}-ingress`,
|
|
182
|
+
labels,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Master server — cloud-init script is rendered with floating IP, network
|
|
186
|
+
// id, and the Hetzner API token. The token + network id flow into
|
|
187
|
+
// `hcloud` and `hcloud-csi` kube-system Secrets; hcloud-cloud-controller-
|
|
188
|
+
// manager and hcloud-csi-driver fail to authenticate without them,
|
|
189
|
+
// which leaves PVCs Pending and the whole Supabase install hanging on
|
|
190
|
+
// wait-for-db.
|
|
191
|
+
const masterUserData = pulumi.all([floatingIp.ipAddress, network.id]).apply(([ip, netId]) =>
|
|
192
|
+
renderScript(masterInitTemplate, {
|
|
193
|
+
k3s_version: config.k3sVersion,
|
|
194
|
+
k3s_token: k3sToken,
|
|
195
|
+
cluster_name: clusterName,
|
|
196
|
+
disable_traefik: 'true',
|
|
197
|
+
hcloud_token: config.apiToken ?? '',
|
|
198
|
+
network_id: netId,
|
|
199
|
+
floating_ip: ip,
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const master = new hcloud.Server(
|
|
204
|
+
'master',
|
|
205
|
+
{
|
|
206
|
+
name: `${clusterName}-master`,
|
|
207
|
+
serverType: config.masterServerType,
|
|
208
|
+
image: 'ubuntu-24.04',
|
|
209
|
+
location: config.location,
|
|
210
|
+
sshKeys: [sshKeyId],
|
|
211
|
+
firewallIds: [firewall.id.apply((id) => Number.parseInt(id, 10))],
|
|
212
|
+
labels: { ...labels, role: 'master' },
|
|
213
|
+
networks: [
|
|
214
|
+
{ networkId: network.id.apply((id) => Number.parseInt(id, 10)), ip: '10.0.1.1' },
|
|
215
|
+
],
|
|
216
|
+
userData: masterUserData,
|
|
217
|
+
},
|
|
218
|
+
{ dependsOn: [subnet] },
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Attach floating IP to master
|
|
222
|
+
new hcloud.FloatingIpAssignment('ingress-assignment', {
|
|
223
|
+
floatingIpId: floatingIp.id.apply((id) => Number.parseInt(id, 10)),
|
|
224
|
+
serverId: master.id.apply((id) => Number.parseInt(id, 10)),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Supabase server — joins master via k3s agent. master_ip MUST be the
|
|
228
|
+
// master's private IP: Hetzner firewall (above) restricts public 6443
|
|
229
|
+
// to operator IPs only, so the supabase node cannot reach the master's
|
|
230
|
+
// public IP on 6443 at all. Mirrors the worker-init pattern below.
|
|
231
|
+
// Hardcoding 10.0.1.1 also drops the master.ipv4Address.apply() wrapper,
|
|
232
|
+
// removing a needless cross-resource dep that delayed cloud-init render.
|
|
233
|
+
const supabaseUserData = renderScript(supabaseInitTemplate, {
|
|
234
|
+
k3s_version: config.k3sVersion,
|
|
235
|
+
k3s_token: k3sToken,
|
|
236
|
+
master_ip: '10.0.1.1',
|
|
237
|
+
});
|
|
238
|
+
// NOTE: previously `dependsOn: [master]` — we dropped that so Hetzner
|
|
239
|
+
// provisions master + supabase in parallel, saving ~30-60s on the
|
|
240
|
+
// critical path. The supabase cloud-init (supabase-init.sh) already
|
|
241
|
+
// polls the master k3s API with a 120-attempt retry loop, so it
|
|
242
|
+
// tolerates master coming up slightly later.
|
|
243
|
+
const supabase = new hcloud.Server(
|
|
244
|
+
'supabase',
|
|
245
|
+
{
|
|
246
|
+
name: `${clusterName}-supabase`,
|
|
247
|
+
serverType: config.supabaseServerType,
|
|
248
|
+
image: 'ubuntu-24.04',
|
|
249
|
+
location: config.location,
|
|
250
|
+
sshKeys: [sshKeyId],
|
|
251
|
+
firewallIds: [firewall.id.apply((id) => Number.parseInt(id, 10))],
|
|
252
|
+
labels: { ...labels, role: 'supabase', 'node-pool': 'supabase-pool' },
|
|
253
|
+
networks: [
|
|
254
|
+
{ networkId: network.id.apply((id) => Number.parseInt(id, 10)), ip: '10.0.1.2' },
|
|
255
|
+
],
|
|
256
|
+
userData: supabaseUserData,
|
|
257
|
+
},
|
|
258
|
+
{ dependsOn: [subnet] },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Workers (spread across placement group). Name includes region so
|
|
262
|
+
// re-runs / scale across regions don't collide in Hetzner's per-project
|
|
263
|
+
// placement-group namespace.
|
|
264
|
+
const placementGroup = new hcloud.PlacementGroup('workers-pg', {
|
|
265
|
+
name: `${clusterName}-${config.location}-workers-pg`,
|
|
266
|
+
type: 'spread',
|
|
267
|
+
labels,
|
|
268
|
+
});
|
|
269
|
+
const workerUserData = renderScript(workerInitTemplate, {
|
|
270
|
+
k3s_version: config.k3sVersion,
|
|
271
|
+
k3s_token: k3sToken,
|
|
272
|
+
master_ip: '10.0.1.1',
|
|
273
|
+
cluster_name: clusterName,
|
|
274
|
+
});
|
|
275
|
+
const workers = [];
|
|
276
|
+
for (let i = 0; i < minWorkers; i++) {
|
|
277
|
+
workers.push(
|
|
278
|
+
new hcloud.Server(
|
|
279
|
+
`worker-${i + 1}`,
|
|
280
|
+
{
|
|
281
|
+
name: `${clusterName}-worker-${i + 1}`,
|
|
282
|
+
serverType: config.workerServerType,
|
|
283
|
+
image: 'ubuntu-24.04',
|
|
284
|
+
location: config.location,
|
|
285
|
+
sshKeys: [sshKeyId],
|
|
286
|
+
firewallIds: [firewall.id.apply((id) => Number.parseInt(id, 10))],
|
|
287
|
+
placementGroupId: placementGroup.id.apply((id) => Number.parseInt(id, 10)),
|
|
288
|
+
labels: {
|
|
289
|
+
...labels,
|
|
290
|
+
role: 'worker',
|
|
291
|
+
'node-pool': 'worker-pool',
|
|
292
|
+
'cluster-autoscaler/node': 'static',
|
|
293
|
+
},
|
|
294
|
+
networks: [
|
|
295
|
+
{
|
|
296
|
+
networkId: network.id.apply((id) => Number.parseInt(id, 10)),
|
|
297
|
+
ip: `10.0.1.${10 + i}`,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
userData: workerUserData,
|
|
301
|
+
},
|
|
302
|
+
{ dependsOn: [subnet] },
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
masterIp: master.ipv4Address,
|
|
309
|
+
masterPrivateIp: '10.0.1.1',
|
|
310
|
+
supabaseIp: supabase.ipv4Address,
|
|
311
|
+
supabasePrivateIp: '10.0.1.2',
|
|
312
|
+
workerIps: workers.map((w) => w.ipv4Address),
|
|
313
|
+
floatingIp: floatingIp.ipAddress,
|
|
314
|
+
networkId: network.id,
|
|
315
|
+
sshKeyId: typeof sshKeyId === 'string' ? sshKeyId : sshKeyId.apply((id) => String(id)),
|
|
316
|
+
k3sToken,
|
|
317
|
+
clusterName,
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubectl helpers that avoid exposing secrets in argv.
|
|
3
|
+
*
|
|
4
|
+
* Any Kubernetes manifest containing secret data MUST use
|
|
5
|
+
* kubectlApplyManifest so the bytes flow through stdin instead of the
|
|
6
|
+
* shell command line.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Apply a Kubernetes manifest via `kubectl apply -f -` piped from stdin.
|
|
16
|
+
* The manifest body (including any secret data) never appears in argv.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} manifest - Kubernetes resource as a plain object
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {string} [options.namespace]
|
|
21
|
+
* @param {object} [options.env]
|
|
22
|
+
* @param {number} [options.timeout=60000]
|
|
23
|
+
* @returns {string} - kubectl stdout
|
|
24
|
+
*/
|
|
25
|
+
export function kubectlApplyManifest(manifest, options = {}) {
|
|
26
|
+
const yaml = JSON.stringify(manifest);
|
|
27
|
+
const args = ['apply', '-f', '-'];
|
|
28
|
+
if (options.namespace) args.push('-n', options.namespace);
|
|
29
|
+
const res = spawnSync('kubectl', args, {
|
|
30
|
+
input: yaml,
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
env: options.env ?? process.env,
|
|
33
|
+
timeout: options.timeout ?? 60_000,
|
|
34
|
+
});
|
|
35
|
+
if (res.status !== 0) {
|
|
36
|
+
const stderr = (res.stderr || '').trim();
|
|
37
|
+
const exitCode = res.status ?? '(null)';
|
|
38
|
+
// NEVER include res.stdout — kubectl may echo the submitted manifest on
|
|
39
|
+
// failure, and the manifest may contain secret data.
|
|
40
|
+
throw new Error(`kubectl apply failed (exit ${exitCode}): ${stderr || '(no stderr)'}`);
|
|
41
|
+
}
|
|
42
|
+
return res.stdout;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Apply a strategic-merge patch to a resource via a temp patch file.
|
|
47
|
+
* Use this instead of interpolating JSON into a shell command line.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} resourceRef - e.g. 'deployment/app' or 'serviceaccount/default'
|
|
50
|
+
* @param {string} namespace
|
|
51
|
+
* @param {object} patch - patch object
|
|
52
|
+
* @param {object} [options]
|
|
53
|
+
* @param {string} [options.type='strategic'] - 'strategic' | 'merge' | 'json'
|
|
54
|
+
* @param {object} [options.env]
|
|
55
|
+
* @param {boolean} [options.ignoreError]
|
|
56
|
+
* @param {number} [options.timeout=60000]
|
|
57
|
+
* @returns {string|null} - kubectl stdout, or null when ignoreError + failure
|
|
58
|
+
*/
|
|
59
|
+
export function kubectlPatch(resourceRef, namespace, patch, options = {}) {
|
|
60
|
+
const { type = 'strategic', env, ignoreError = false, timeout = 60_000 } = options;
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), 'vibecarbon-patch-'));
|
|
62
|
+
const patchFile = join(dir, 'patch.json');
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(patchFile, JSON.stringify(patch), { mode: 0o600 });
|
|
65
|
+
const args = ['patch', resourceRef, '-n', namespace, '--type', type, '--patch-file', patchFile];
|
|
66
|
+
const res = spawnSync('kubectl', args, {
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
env: env ?? process.env,
|
|
69
|
+
timeout,
|
|
70
|
+
});
|
|
71
|
+
if (res.status !== 0) {
|
|
72
|
+
if (ignoreError) return null;
|
|
73
|
+
const stderr = (res.stderr || '').trim();
|
|
74
|
+
const exitCode = res.status ?? '(null)';
|
|
75
|
+
throw new Error(`kubectl patch failed (exit ${exitCode}): ${stderr || '(no stderr)'}`);
|
|
76
|
+
}
|
|
77
|
+
return res.stdout;
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(dir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License management for Vibecarbon
|
|
3
|
+
*
|
|
4
|
+
* Handles license storage, retrieval, activation, and validation.
|
|
5
|
+
* Licenses are stored at ~/.vibecarbon/license
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { c } from '../colors.js';
|
|
12
|
+
import { allowsFlag as _allowsFlag, getTier, TIERS } from './tiers.js';
|
|
13
|
+
import { validateLicenseKey } from './validator.js';
|
|
14
|
+
|
|
15
|
+
// License storage location
|
|
16
|
+
const CONFIG_DIR = join(homedir(), '.vibecarbon');
|
|
17
|
+
const LICENSE_FILE = join(CONFIG_DIR, 'license');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensure the config directory exists
|
|
21
|
+
*/
|
|
22
|
+
function ensureConfigDir() {
|
|
23
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
24
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the stored license file
|
|
30
|
+
* @returns {object|null} License data or null if not found
|
|
31
|
+
*/
|
|
32
|
+
function _readLicenseFile() {
|
|
33
|
+
if (!existsSync(LICENSE_FILE)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(LICENSE_FILE, 'utf-8');
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write license data to file
|
|
47
|
+
* @param {object} data - License data to store
|
|
48
|
+
*/
|
|
49
|
+
function writeLicenseFile(data) {
|
|
50
|
+
ensureConfigDir();
|
|
51
|
+
writeFileSync(LICENSE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the current license status
|
|
56
|
+
* @returns {object} License information with tier and features
|
|
57
|
+
*/
|
|
58
|
+
export function getLicense() {
|
|
59
|
+
const stored = _readLicenseFile();
|
|
60
|
+
|
|
61
|
+
// No license file → Graphite tier (free)
|
|
62
|
+
if (!stored?.key) {
|
|
63
|
+
return {
|
|
64
|
+
tier: 'graphite',
|
|
65
|
+
...TIERS.graphite,
|
|
66
|
+
active: false,
|
|
67
|
+
message: 'No license activated. Using Graphite tier.',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Re-validate the stored key
|
|
72
|
+
const validation = validateLicenseKey(stored.key);
|
|
73
|
+
|
|
74
|
+
// If licensing is disabled in validator, return fullerene
|
|
75
|
+
if (validation.licensingDisabled) {
|
|
76
|
+
return {
|
|
77
|
+
tier: 'fullerene',
|
|
78
|
+
...TIERS.fullerene,
|
|
79
|
+
active: true,
|
|
80
|
+
licensingDisabled: true,
|
|
81
|
+
displayName: 'All Features Enabled',
|
|
82
|
+
message: 'Licensing disabled - all features available.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!validation.valid) {
|
|
87
|
+
// Stored key is no longer valid (corrupted, etc.)
|
|
88
|
+
return {
|
|
89
|
+
tier: 'graphite',
|
|
90
|
+
...TIERS.graphite,
|
|
91
|
+
active: false,
|
|
92
|
+
message: `License invalid: ${validation.error}. Reverted to Graphite tier.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tier = getTier(validation.tier);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
tier: validation.tier,
|
|
100
|
+
...tier,
|
|
101
|
+
active: true,
|
|
102
|
+
customerId: validation.customerId || stored.customerId,
|
|
103
|
+
activatedAt: stored.activatedAt,
|
|
104
|
+
isLifetime: true,
|
|
105
|
+
devMode: validation.devMode,
|
|
106
|
+
verified: validation.verified,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Activate a license key
|
|
112
|
+
* @param {string} key - The license key to activate
|
|
113
|
+
* @returns {object} Activation result
|
|
114
|
+
*/
|
|
115
|
+
export function activateLicense(key) {
|
|
116
|
+
if (!key || typeof key !== 'string') {
|
|
117
|
+
return { success: false, error: 'License key is required' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const validation = validateLicenseKey(key.trim());
|
|
121
|
+
|
|
122
|
+
if (!validation.valid) {
|
|
123
|
+
return { success: false, error: validation.error };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const licenseData = {
|
|
127
|
+
key: key.trim(),
|
|
128
|
+
tier: validation.tier,
|
|
129
|
+
customerId: validation.customerId,
|
|
130
|
+
activatedAt: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
writeLicenseFile(licenseData);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return { success: false, error: `Failed to save license: ${error.message}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tier = getTier(validation.tier);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
tier: validation.tier,
|
|
144
|
+
tierName: tier.displayName,
|
|
145
|
+
features: tier.features,
|
|
146
|
+
isLifetime: true,
|
|
147
|
+
devMode: validation.devMode,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Deactivate (remove) the current license
|
|
153
|
+
* @returns {object} Deactivation result
|
|
154
|
+
*/
|
|
155
|
+
export function deactivateLicense() {
|
|
156
|
+
if (!existsSync(LICENSE_FILE)) {
|
|
157
|
+
return { success: true, message: 'No license was activated' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
unlinkSync(LICENSE_FILE);
|
|
162
|
+
return { success: true, message: 'License deactivated. Using Graphite tier.' };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return { success: false, error: `Failed to remove license: ${error.message}` };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if the current license allows a specific deployment mode
|
|
170
|
+
* @param {string} deploymentFlag - The deployment flag (e.g., '--k8s', '--ha')
|
|
171
|
+
* @returns {object} Access check result
|
|
172
|
+
*/
|
|
173
|
+
export function checkDeploymentAccess(deploymentFlag) {
|
|
174
|
+
const license = getLicense();
|
|
175
|
+
|
|
176
|
+
// If licensing is disabled in the validator, allow everything
|
|
177
|
+
if (license.licensingDisabled) {
|
|
178
|
+
return {
|
|
179
|
+
allowed: true,
|
|
180
|
+
tier: license.tier,
|
|
181
|
+
licensingDisabled: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const allowed = _allowsFlag(license.tier, deploymentFlag);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
allowed,
|
|
189
|
+
tier: license.tier,
|
|
190
|
+
requiredTier: allowed ? license.tier : 'diamond',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Guard function: require any paid license or exit with upgrade message
|
|
196
|
+
* @param {string} commandName - The command that requires a license
|
|
197
|
+
*/
|
|
198
|
+
export function requireLicense(commandName) {
|
|
199
|
+
const license = getLicense();
|
|
200
|
+
|
|
201
|
+
if (license.licensingDisabled) return;
|
|
202
|
+
if (license.active) return;
|
|
203
|
+
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(` ${c.warning('License required')}`);
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(
|
|
208
|
+
` ${c.dim(`The ${c.bold(commandName)} command requires a license.`)} ${c.dim('(create, up, down, reset, status are always free)')}`,
|
|
209
|
+
);
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(
|
|
212
|
+
` ${c.bold('Vibecarbon Diamond')} — ${c.success('$149 lifetime')} ${c.dim('— deploy to production, indie & startup use')}`,
|
|
213
|
+
);
|
|
214
|
+
console.log(
|
|
215
|
+
` ${c.bold('Vibecarbon Fullerene')} — ${c.success('$499 lifetime')} ${c.dim('— deploy for clients & agencies, commercial use rights')}`,
|
|
216
|
+
);
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(` ${c.dim('Purchase:')} ${c.info('https://vibecarbon.dev/pricing')}`);
|
|
219
|
+
console.log(` ${c.dim('Activate:')} ${c.info('vibecarbon activate <key>')}`);
|
|
220
|
+
console.log(` ${c.dim('Terms:')} ${c.dim('TERMS.md or https://vibecarbon.dev/terms')}`);
|
|
221
|
+
console.log('');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get a formatted status message for the current license
|
|
227
|
+
* @returns {string} Status message
|
|
228
|
+
*/
|
|
229
|
+
export function getLicenseStatus() {
|
|
230
|
+
const license = getLicense();
|
|
231
|
+
|
|
232
|
+
if (!license.active) {
|
|
233
|
+
return `License: Graphite tier (free, no license activated)
|
|
234
|
+
Features: ${TIERS.graphite.features.join(', ')}
|
|
235
|
+
|
|
236
|
+
Upgrade to deploy:
|
|
237
|
+
Diamond ($149 lifetime) — deploy to production, indie & startup use
|
|
238
|
+
Fullerene ($499 lifetime) — deploy for clients & agencies, commercial use rights
|
|
239
|
+
|
|
240
|
+
Purchase at https://vibecarbon.dev/pricing`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let status = `License: ${license.displayName}
|
|
244
|
+
Customer ID: ${license.customerId}
|
|
245
|
+
Activated: ${license.activatedAt}
|
|
246
|
+
Features: ${license.features.join(', ')}
|
|
247
|
+
Max servers: ${license.maxServers === Infinity ? 'Unlimited' : license.maxServers}
|
|
248
|
+
Expires: Never (lifetime license)`;
|
|
249
|
+
|
|
250
|
+
if (license.devMode) {
|
|
251
|
+
status += '\n\n[Development Mode - Signature not verified]';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return status;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Re-export tier utilities
|
|
258
|
+
export { allowsFlag, compareTiers, getRequiredTier, getTier, TIERS } from './tiers.js';
|
|
259
|
+
export { generateDevLicenseKey, validateLicenseKey } from './validator.js';
|