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,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret-leak detector for vibecarbon-managed projects.
|
|
3
|
+
*
|
|
4
|
+
* Two layers of defense feed off this module:
|
|
5
|
+
* 1. The pre-commit hook scaffolded into every `vibecarbon create` project
|
|
6
|
+
* (.git/hooks/pre-commit calls scripts/secret-scan.mjs over staged files).
|
|
7
|
+
* Catches secrets before they enter git history.
|
|
8
|
+
* 2. The CLI deploy/add path. Runs against the working tree (filtered
|
|
9
|
+
* through `git ls-files`, so .gitignore is respected and the scan
|
|
10
|
+
* mirrors what `git push` would actually upload) and refuses the
|
|
11
|
+
* operation on any finding. Belt-and-suspenders against
|
|
12
|
+
* `git commit --no-verify`.
|
|
13
|
+
*
|
|
14
|
+
* No `--allow-secrets` bypass exists by design. If a finding is a true
|
|
15
|
+
* false positive, add the substring (or `regex:<pat>`) to .vibecarbonignore
|
|
16
|
+
* at the repo root. If the finding is real, rotate the credential and
|
|
17
|
+
* remove it from history before retrying.
|
|
18
|
+
*
|
|
19
|
+
* The patterns are intentionally biased toward LOW false-positive rates:
|
|
20
|
+
* - Each rule includes both a primary regex and (where useful) a context
|
|
21
|
+
* regex that the surrounding 256 chars must also match. This prevents
|
|
22
|
+
* a stray 40-char alphanumeric in a docs file from tripping the
|
|
23
|
+
* "Hetzner token" rule.
|
|
24
|
+
* - JWT detection is gated on the *decoded* payload claim — a JWT is
|
|
25
|
+
* only flagged if its base64url-decoded payload contains
|
|
26
|
+
* `"role":"service_role"` or similar. The previous JWT-shaped
|
|
27
|
+
* placeholders in carbon/k8s/overlays/local/secrets.yaml tripped
|
|
28
|
+
* GitHub's scanner because they had `service_role` in the payload —
|
|
29
|
+
* we now ship plain non-JWT placeholders, so this rule fires only on
|
|
30
|
+
* real Supabase keys.
|
|
31
|
+
* - Allowlist support: a `.vibecarbonignore` file at the repo root can
|
|
32
|
+
* contain literal substrings or `regex:<pattern>` lines that suppress
|
|
33
|
+
* matches. Same syntax as our docs.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { execFileSync } from 'node:child_process';
|
|
37
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
38
|
+
import { extname, join, relative, sep } from 'node:path';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} SecretRule
|
|
42
|
+
* @property {string} id - stable identifier (used in allowlist + reports)
|
|
43
|
+
* @property {string} description - human-readable name shown to operators
|
|
44
|
+
* @property {RegExp} pattern - primary detection regex
|
|
45
|
+
* @property {RegExp} [context] - optional context regex (must also match
|
|
46
|
+
* the surrounding ±128 chars). Use to dampen false positives on rules
|
|
47
|
+
* that match generic shapes (e.g. 40-char alphanum strings).
|
|
48
|
+
* @property {(match: string, fullMatch: RegExpExecArray) => boolean} [predicate]
|
|
49
|
+
* - optional extra filter. Receives the matched substring; return false
|
|
50
|
+
* to discard.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** @type {SecretRule[]} */
|
|
54
|
+
export const SECRET_RULES = [
|
|
55
|
+
{
|
|
56
|
+
id: 'aws-access-key',
|
|
57
|
+
description: 'AWS access key ID',
|
|
58
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'aws-secret-key',
|
|
62
|
+
description: 'AWS secret access key',
|
|
63
|
+
pattern:
|
|
64
|
+
/(?:aws[_-]?secret[_-]?access[_-]?key|secretAccessKey)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'github-pat',
|
|
68
|
+
description: 'GitHub personal access token',
|
|
69
|
+
pattern: /\bgh[posru]_[A-Za-z0-9]{36,}\b/g,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'stripe-live-key',
|
|
73
|
+
description: 'Stripe live secret key',
|
|
74
|
+
pattern: /\bsk_live_[A-Za-z0-9]{24,}\b/g,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'stripe-test-key',
|
|
78
|
+
description: 'Stripe test secret key (still sensitive — can drain test ledger)',
|
|
79
|
+
pattern: /\bsk_test_[A-Za-z0-9]{24,}\b/g,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'slack-token',
|
|
83
|
+
description: 'Slack token',
|
|
84
|
+
pattern: /\bxox[abprs]-[0-9A-Za-z-]{10,}\b/g,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'openai-key',
|
|
88
|
+
description: 'OpenAI API key',
|
|
89
|
+
pattern: /\bsk-[A-Za-z0-9]{20,}\b/g,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'anthropic-key',
|
|
93
|
+
description: 'Anthropic API key',
|
|
94
|
+
pattern: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/g,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'google-service-account-json',
|
|
98
|
+
description: 'Google Cloud service account JSON',
|
|
99
|
+
pattern: /"type"\s*:\s*"service_account"/g,
|
|
100
|
+
context: /"private_key"\s*:\s*"-----BEGIN[ A-Z]+PRIVATE KEY-----/,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'google-api-key',
|
|
104
|
+
description: 'Google API key',
|
|
105
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'private-key-block',
|
|
109
|
+
description: 'PEM-encoded private key',
|
|
110
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'hetzner-token',
|
|
114
|
+
description: 'Hetzner Cloud API token',
|
|
115
|
+
// Hetzner tokens are 64 alphanumeric chars. Gate on a nearby
|
|
116
|
+
// `hetzner` / `hcloud` identifier so we don't flag every 64-char
|
|
117
|
+
// string in test fixtures or hashes.
|
|
118
|
+
pattern: /['"]([A-Za-z0-9]{64})['"]/g,
|
|
119
|
+
context: /(?:hetzner|hcloud|HCLOUD_TOKEN|HETZNER_API_TOKEN)/i,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'cloudflare-api-token',
|
|
123
|
+
description: 'Cloudflare scoped API token',
|
|
124
|
+
pattern: /\b[A-Za-z0-9_-]{40}\b/g,
|
|
125
|
+
context: /(?:cloudflare|cf[_-]?api[_-]?token|CLOUDFLARE_API_TOKEN)/i,
|
|
126
|
+
predicate: (m) => {
|
|
127
|
+
// Exclude hex (commit shas, hashes).
|
|
128
|
+
if (/^[a-f0-9]+$/i.test(m)) return false;
|
|
129
|
+
// Real Cloudflare API tokens are high-entropy random base62/64;
|
|
130
|
+
// P(all-lowercase) ≈ 10^-12 for a true 40-char token. All-lowercase
|
|
131
|
+
// 40-char strings with hyphen separators are the signature of
|
|
132
|
+
// hyphenated identifiers — kubernetes resource names, project
|
|
133
|
+
// identifiers, and the `${projectName}-${env}-${role}` server names
|
|
134
|
+
// vibecarbon writes to .vibecarbon.json (which collided with the
|
|
135
|
+
// `dnsProvider: 'cloudflare'` context match and false-flagged
|
|
136
|
+
// compose-ha warm-deploy in 2026-05-19 matrix run bq5c4h9l5).
|
|
137
|
+
return /[A-Z]/.test(m);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'supabase-service-role-jwt',
|
|
142
|
+
description: 'Supabase service-role JWT (privileged)',
|
|
143
|
+
pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
|
|
144
|
+
predicate: (m) => {
|
|
145
|
+
const payload = decodeJwtPayload(m);
|
|
146
|
+
if (!payload) return false;
|
|
147
|
+
// Only fire on truly privileged role claims. `anon` keys are
|
|
148
|
+
// intentionally public.
|
|
149
|
+
if (payload.role === 'service_role') return true;
|
|
150
|
+
if (payload.role === 'admin') return true;
|
|
151
|
+
return false;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'generic-secret-assignment',
|
|
156
|
+
description: 'High-entropy value assigned to a secret-named variable',
|
|
157
|
+
// Only fires when the surrounding identifier is suggestive AND the
|
|
158
|
+
// value has high entropy. This is the noisiest rule; the allowlist
|
|
159
|
+
// is the escape hatch.
|
|
160
|
+
pattern:
|
|
161
|
+
/(?:password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|credentials?)\s*[:=]\s*['"]([^'"\s]{20,})['"]/gi,
|
|
162
|
+
predicate: (_m, captured) => {
|
|
163
|
+
const value = captured?.[1];
|
|
164
|
+
if (!value) return false;
|
|
165
|
+
// Variable interpolations are not values. Both $VAR (shell, GH
|
|
166
|
+
// Actions) and ${expr} (Bash, JS template literals) refer to a
|
|
167
|
+
// value defined elsewhere — flagging them just trains operators
|
|
168
|
+
// to ignore the scanner.
|
|
169
|
+
if (/^\$/.test(value)) return false;
|
|
170
|
+
if (/^(?:REPLACE|TODO|CHANGE|YOUR|EXAMPLE|PLACEHOLDER|FIXME)/i.test(value)) return false;
|
|
171
|
+
if (/^(?:local|dev|test|sample|fake|demo)[-_]/i.test(value)) return false;
|
|
172
|
+
return shannonEntropy(value) >= 3.5;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Decode a JWT's base64url-encoded payload to a JS object.
|
|
179
|
+
* @param {string} jwt
|
|
180
|
+
* @returns {Record<string, unknown> | null}
|
|
181
|
+
*/
|
|
182
|
+
function decodeJwtPayload(jwt) {
|
|
183
|
+
const parts = jwt.split('.');
|
|
184
|
+
if (parts.length !== 3) return null;
|
|
185
|
+
const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
186
|
+
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
|
|
187
|
+
try {
|
|
188
|
+
const json = Buffer.from(padded, 'base64').toString('utf-8');
|
|
189
|
+
return JSON.parse(json);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Shannon entropy (bits per symbol) over an ASCII-ish string. Random
|
|
197
|
+
* hex lives ~4.0, random base64 ~5.0; English prose hovers around
|
|
198
|
+
* 3.0-3.5.
|
|
199
|
+
* @param {string} s
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
function shannonEntropy(s) {
|
|
203
|
+
if (!s) return 0;
|
|
204
|
+
const freq = new Map();
|
|
205
|
+
for (const c of s) freq.set(c, (freq.get(c) ?? 0) + 1);
|
|
206
|
+
let h = 0;
|
|
207
|
+
for (const n of freq.values()) {
|
|
208
|
+
const p = n / s.length;
|
|
209
|
+
h -= p * Math.log2(p);
|
|
210
|
+
}
|
|
211
|
+
return h;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @typedef {object} SecretFinding
|
|
216
|
+
* @property {string} ruleId - which SecretRule fired
|
|
217
|
+
* @property {string} description - human-readable name
|
|
218
|
+
* @property {string} match - the matched substring (truncated)
|
|
219
|
+
* @property {number} index - byte offset within the scanned content
|
|
220
|
+
* @property {number} line - 1-indexed line number
|
|
221
|
+
* @property {number} column - 1-indexed column number
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Scan a string buffer (typically a single file's contents) for secret
|
|
226
|
+
* patterns.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} content
|
|
229
|
+
* @param {{allowlist?: string[]}} [options]
|
|
230
|
+
* @returns {SecretFinding[]}
|
|
231
|
+
*/
|
|
232
|
+
export function scanContent(content, options = {}) {
|
|
233
|
+
const { allowlist = [] } = options;
|
|
234
|
+
if (!content || typeof content !== 'string') return [];
|
|
235
|
+
/** @type {SecretFinding[]} */
|
|
236
|
+
const findings = [];
|
|
237
|
+
|
|
238
|
+
for (const rule of SECRET_RULES) {
|
|
239
|
+
// Reset .lastIndex on the global regex between rules so multi-rule
|
|
240
|
+
// scans of the same content don't skip into mid-stream state.
|
|
241
|
+
rule.pattern.lastIndex = 0;
|
|
242
|
+
/** @type {RegExpExecArray | null} */
|
|
243
|
+
let m = rule.pattern.exec(content);
|
|
244
|
+
while (m !== null) {
|
|
245
|
+
const [whole] = m;
|
|
246
|
+
const { index } = m;
|
|
247
|
+
|
|
248
|
+
if (rule.context) {
|
|
249
|
+
const start = Math.max(0, index - 128);
|
|
250
|
+
const end = Math.min(content.length, index + whole.length + 128);
|
|
251
|
+
if (!rule.context.test(content.slice(start, end))) {
|
|
252
|
+
m = rule.pattern.exec(content);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (rule.predicate && !rule.predicate(whole, m)) {
|
|
258
|
+
m = rule.pattern.exec(content);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (matchesAllowlist(whole, allowlist)) {
|
|
263
|
+
m = rule.pattern.exec(content);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { line, column } = locateOffset(content, index);
|
|
268
|
+
findings.push({
|
|
269
|
+
ruleId: rule.id,
|
|
270
|
+
description: rule.description,
|
|
271
|
+
match: whole.length > 64 ? `${whole.slice(0, 32)}…${whole.slice(-16)}` : whole,
|
|
272
|
+
index,
|
|
273
|
+
line,
|
|
274
|
+
column,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
m = rule.pattern.exec(content);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return findings;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {string} value
|
|
286
|
+
* @param {string[]} allowlist
|
|
287
|
+
* @returns {boolean}
|
|
288
|
+
*/
|
|
289
|
+
function matchesAllowlist(value, allowlist) {
|
|
290
|
+
for (const entry of allowlist) {
|
|
291
|
+
if (!entry || entry.startsWith('#')) continue;
|
|
292
|
+
if (entry.startsWith('regex:')) {
|
|
293
|
+
try {
|
|
294
|
+
const re = new RegExp(entry.slice('regex:'.length));
|
|
295
|
+
if (re.test(value)) return true;
|
|
296
|
+
} catch {
|
|
297
|
+
// ignore malformed regex entries
|
|
298
|
+
}
|
|
299
|
+
} else if (value.includes(entry)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Convert a byte offset within `content` to (line, column).
|
|
308
|
+
* @param {string} content
|
|
309
|
+
* @param {number} offset
|
|
310
|
+
*/
|
|
311
|
+
function locateOffset(content, offset) {
|
|
312
|
+
let line = 1;
|
|
313
|
+
let lineStart = 0;
|
|
314
|
+
for (let i = 0; i < offset; i++) {
|
|
315
|
+
if (content.charCodeAt(i) === 10) {
|
|
316
|
+
line++;
|
|
317
|
+
lineStart = i + 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { line, column: offset - lineStart + 1 };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Load the allowlist from `.vibecarbonignore` at the given repo root.
|
|
325
|
+
* Returns an empty array if the file is missing.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} cwd
|
|
328
|
+
* @returns {string[]}
|
|
329
|
+
*/
|
|
330
|
+
export function loadAllowlist(cwd = process.cwd()) {
|
|
331
|
+
const path = join(cwd, '.vibecarbonignore');
|
|
332
|
+
try {
|
|
333
|
+
const raw = readFileSync(path, 'utf-8');
|
|
334
|
+
return raw
|
|
335
|
+
.split('\n')
|
|
336
|
+
.map((line) => line.trim())
|
|
337
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err && /** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return [];
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Directories we never want to descend into during a tree-walk scan.
|
|
345
|
+
// node_modules / build outputs / vendor stuff would slow us down (many
|
|
346
|
+
// of these contain test fixtures or minified blobs that trip the
|
|
347
|
+
// generic-secret rule). Each entry matches a basename.
|
|
348
|
+
const SKIP_DIRS = new Set([
|
|
349
|
+
'node_modules',
|
|
350
|
+
'.git',
|
|
351
|
+
'.next',
|
|
352
|
+
'.nuxt',
|
|
353
|
+
'.cache',
|
|
354
|
+
'.parcel-cache',
|
|
355
|
+
'.vercel',
|
|
356
|
+
'.turbo',
|
|
357
|
+
'.pnpm-store',
|
|
358
|
+
'dist',
|
|
359
|
+
'build',
|
|
360
|
+
'out',
|
|
361
|
+
'coverage',
|
|
362
|
+
'.nyc_output',
|
|
363
|
+
'.pulumi',
|
|
364
|
+
'.terraform',
|
|
365
|
+
'.vibecarbon', // local CLI state — generated, not source
|
|
366
|
+
'.venv',
|
|
367
|
+
'venv',
|
|
368
|
+
'__pycache__',
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
// File extensions we won't even open. Binary blobs that secrets shouldn't
|
|
372
|
+
// appear inside; opening them costs IO + risks regex misfires on bytes
|
|
373
|
+
// that happen to look like ASCII secrets.
|
|
374
|
+
const SKIP_EXTS = new Set([
|
|
375
|
+
'.png',
|
|
376
|
+
'.jpg',
|
|
377
|
+
'.jpeg',
|
|
378
|
+
'.gif',
|
|
379
|
+
'.webp',
|
|
380
|
+
'.ico',
|
|
381
|
+
'.pdf',
|
|
382
|
+
'.zip',
|
|
383
|
+
'.gz',
|
|
384
|
+
'.tar',
|
|
385
|
+
'.tgz',
|
|
386
|
+
'.7z',
|
|
387
|
+
'.rar',
|
|
388
|
+
'.mp3',
|
|
389
|
+
'.mp4',
|
|
390
|
+
'.mov',
|
|
391
|
+
'.avi',
|
|
392
|
+
'.webm',
|
|
393
|
+
'.woff',
|
|
394
|
+
'.woff2',
|
|
395
|
+
'.ttf',
|
|
396
|
+
'.otf',
|
|
397
|
+
'.eot',
|
|
398
|
+
'.so',
|
|
399
|
+
'.dll',
|
|
400
|
+
'.dylib',
|
|
401
|
+
'.bin',
|
|
402
|
+
'.wasm',
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
// Skip any individual file > 1 MiB. Real source files stay well under
|
|
406
|
+
// this; minified bundles can be large and noisy without containing
|
|
407
|
+
// real secrets.
|
|
408
|
+
const MAX_FILE_BYTES = 1_048_576;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Basenames that are universally gitignored across vibecarbon-generated
|
|
412
|
+
* projects (and ~all node/12-factor apps): runtime secret holders that
|
|
413
|
+
* are NEVER intended to be pushed. Only honored in the no-git fallback
|
|
414
|
+
* walk — when we have a real git tree, we trust `git ls-files
|
|
415
|
+
* --exclude-standard`, which already filters via the project's actual
|
|
416
|
+
* .gitignore (and would correctly fire if the user ever made the mistake
|
|
417
|
+
* of `git add .env`).
|
|
418
|
+
*
|
|
419
|
+
* Mirrors the .env stanza scaffolded by `vibecarbon create` into
|
|
420
|
+
* carbon/.gitignore: `.env`, `.env.local`, `.env.*.local`, `!.env.example`.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} basename
|
|
423
|
+
* @returns {boolean}
|
|
424
|
+
*/
|
|
425
|
+
function isUniversalLocalEnv(basename) {
|
|
426
|
+
if (basename === '.env' || basename === '.env.local') return true;
|
|
427
|
+
// .env.<anything>.local — production.local, development.local, etc.
|
|
428
|
+
if (/^\.env\..+\.local$/.test(basename)) return true;
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @typedef {object} FileFinding
|
|
434
|
+
* @property {string} file - relative path from cwd
|
|
435
|
+
* @property {string} ruleId
|
|
436
|
+
* @property {string} description
|
|
437
|
+
* @property {string} match - truncated for display
|
|
438
|
+
* @property {number} line
|
|
439
|
+
* @property {number} column
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Walk a directory tree and scan every text-like file under `cwd`.
|
|
444
|
+
*
|
|
445
|
+
* Mirrors what `git push` would actually upload by preferring
|
|
446
|
+
* `git ls-files --cached --others --exclude-standard` when `cwd` is a
|
|
447
|
+
* git working tree. This both (a) skips gitignored files like
|
|
448
|
+
* `.env.local` that may legitimately contain secrets and (b) matches
|
|
449
|
+
* exactly what the deploy step would push to GitHub. Falls back to a
|
|
450
|
+
* plain directory walk (with a built-in skip list) when git isn't
|
|
451
|
+
* available — useful for the standalone `carbon/` template, scanning
|
|
452
|
+
* unrelated directories, or pre-init projects.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} cwd
|
|
455
|
+
* @param {{allowlist?: string[], extraSkipDirs?: Set<string>}} [options]
|
|
456
|
+
* @returns {FileFinding[]}
|
|
457
|
+
*/
|
|
458
|
+
export function scanTree(cwd, options = {}) {
|
|
459
|
+
const allowlist = options.allowlist ?? loadAllowlist(cwd);
|
|
460
|
+
const skipDirs = new Set([...SKIP_DIRS, ...(options.extraSkipDirs ?? [])]);
|
|
461
|
+
/** @type {FileFinding[]} */
|
|
462
|
+
const findings = [];
|
|
463
|
+
|
|
464
|
+
/** @param {string} relPath */
|
|
465
|
+
const scanFile = (relPath) => {
|
|
466
|
+
const full = join(cwd, relPath);
|
|
467
|
+
if (SKIP_EXTS.has(extname(relPath).toLowerCase())) return;
|
|
468
|
+
const base = relPath.split('/').pop() || '';
|
|
469
|
+
if (base === 'package-lock.json' || base === 'pnpm-lock.yaml') return;
|
|
470
|
+
let content;
|
|
471
|
+
try {
|
|
472
|
+
const stat = statSync(full);
|
|
473
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
|
|
474
|
+
content = readFileSync(full, 'utf-8');
|
|
475
|
+
} catch {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const fileFindings = scanContent(content, { allowlist });
|
|
479
|
+
if (fileFindings.length === 0) return;
|
|
480
|
+
const display = relPath.split(sep).join('/');
|
|
481
|
+
for (const f of fileFindings) {
|
|
482
|
+
findings.push({
|
|
483
|
+
file: display,
|
|
484
|
+
ruleId: f.ruleId,
|
|
485
|
+
description: f.description,
|
|
486
|
+
match: f.match,
|
|
487
|
+
line: f.line,
|
|
488
|
+
column: f.column,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Prefer git's view of the tree when available — the most accurate
|
|
494
|
+
// proxy for "what `git push` would actually upload". `--cached`
|
|
495
|
+
// covers tracked files; `--others --exclude-standard` adds untracked
|
|
496
|
+
// files that aren't gitignored. Hidden behind a feature check so
|
|
497
|
+
// tests + non-git directories still work.
|
|
498
|
+
const gitFiles = listGitTrackedFiles(cwd);
|
|
499
|
+
if (gitFiles) {
|
|
500
|
+
for (const rel of gitFiles) scanFile(rel);
|
|
501
|
+
return findings;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** @param {string} dir */
|
|
505
|
+
function walk(dir) {
|
|
506
|
+
let entries;
|
|
507
|
+
try {
|
|
508
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
509
|
+
} catch {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
for (const ent of entries) {
|
|
513
|
+
const full = join(dir, ent.name);
|
|
514
|
+
if (ent.isDirectory()) {
|
|
515
|
+
if (skipDirs.has(ent.name)) continue;
|
|
516
|
+
walk(full);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!ent.isFile()) continue;
|
|
520
|
+
// Universal local-only env files: the project may have no
|
|
521
|
+
// .gitignore (e.g. created with -no-git), but these files would
|
|
522
|
+
// never reach a remote. Scanning them only produces guaranteed
|
|
523
|
+
// false positives that block legitimate `vibecarbon add`/`deploy`
|
|
524
|
+
// operations. The git-tracked path above (which we prefer when
|
|
525
|
+
// available) still surfaces the genuine "user accidentally
|
|
526
|
+
// committed .env" mistake.
|
|
527
|
+
if (isUniversalLocalEnv(ent.name)) continue;
|
|
528
|
+
const rel = relative(cwd, full);
|
|
529
|
+
scanFile(rel);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
walk(cwd);
|
|
533
|
+
return findings;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Ask git for the list of files it would push. Returns null if `cwd`
|
|
538
|
+
* isn't a git working tree, git isn't on PATH, or the call fails for
|
|
539
|
+
* any other reason — the caller should fall back to a plain directory
|
|
540
|
+
* walk in that case.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} cwd
|
|
543
|
+
* @returns {string[] | null}
|
|
544
|
+
*/
|
|
545
|
+
function listGitTrackedFiles(cwd) {
|
|
546
|
+
if (!existsSync(join(cwd, '.git'))) return null;
|
|
547
|
+
try {
|
|
548
|
+
const stdout = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
|
|
549
|
+
cwd,
|
|
550
|
+
encoding: 'utf-8',
|
|
551
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
552
|
+
});
|
|
553
|
+
return stdout
|
|
554
|
+
.split('\n')
|
|
555
|
+
.map((s) => s.trim())
|
|
556
|
+
.filter(Boolean);
|
|
557
|
+
} catch {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Format an array of findings for terminal output. Returns a multi-line
|
|
564
|
+
* human-readable string ending in a newline.
|
|
565
|
+
*
|
|
566
|
+
* @param {FileFinding[]} findings
|
|
567
|
+
* @returns {string}
|
|
568
|
+
*/
|
|
569
|
+
export function formatFindings(findings) {
|
|
570
|
+
if (!findings || findings.length === 0) return '';
|
|
571
|
+
const lines = [];
|
|
572
|
+
lines.push(`Detected ${findings.length} potential secret${findings.length === 1 ? '' : 's'}:`);
|
|
573
|
+
for (const f of findings) {
|
|
574
|
+
lines.push(` ${f.file}:${f.line}:${f.column} [${f.ruleId}] ${f.description}`);
|
|
575
|
+
lines.push(` → ${f.match}`);
|
|
576
|
+
}
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push('If a finding is a false positive, add it to .vibecarbonignore at the repo root:');
|
|
579
|
+
lines.push(' - one entry per line');
|
|
580
|
+
lines.push(' - literal substring match, OR `regex:<pattern>` for a regex');
|
|
581
|
+
lines.push(' - `#` comments are ignored');
|
|
582
|
+
return `${lines.join('\n')}\n`;
|
|
583
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret generation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
6
|
+
import bcrypt from 'bcryptjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a secure random password
|
|
10
|
+
*
|
|
11
|
+
* @param {number} length - Password length (default: 32)
|
|
12
|
+
* @returns {string} - Generated password
|
|
13
|
+
*/
|
|
14
|
+
export function generatePassword(length = 32) {
|
|
15
|
+
return randomBytes(length).toString('base64').slice(0, length).replace(/[+/=]/g, 'x');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a JWT token with HS256 signing
|
|
20
|
+
*
|
|
21
|
+
* @param {string} secret - JWT signing secret
|
|
22
|
+
* @param {object} payload - Token payload
|
|
23
|
+
* @param {number} exp - Expiration time in seconds (default: 10 years)
|
|
24
|
+
* @returns {string} - Signed JWT token
|
|
25
|
+
*/
|
|
26
|
+
export function generateJWT(secret, payload, exp = 315360000) {
|
|
27
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
28
|
+
const now = Math.floor(Date.now() / 1000);
|
|
29
|
+
|
|
30
|
+
const fullPayload = {
|
|
31
|
+
...payload,
|
|
32
|
+
iat: now,
|
|
33
|
+
exp: now + exp,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
37
|
+
const base64Payload = Buffer.from(JSON.stringify(fullPayload)).toString('base64url');
|
|
38
|
+
|
|
39
|
+
const signature = createHmac('sha256', secret)
|
|
40
|
+
.update(`${base64Header}.${base64Payload}`)
|
|
41
|
+
.digest('base64url');
|
|
42
|
+
|
|
43
|
+
return `${base64Header}.${base64Payload}.${signature}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a replication password for HA Postgres deployments.
|
|
48
|
+
* 24 random bytes → 32 base64url chars. URL/shell safe (no + / =).
|
|
49
|
+
*/
|
|
50
|
+
export function generateReplPassword() {
|
|
51
|
+
return randomBytes(24).toString('base64url');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate a bcrypt hash of a password
|
|
56
|
+
* Uses the same algorithm as Supabase GoTrue (bcrypt with cost factor 10)
|
|
57
|
+
*
|
|
58
|
+
* @param {string} password - Plain text password
|
|
59
|
+
* @returns {string} - Bcrypt hash
|
|
60
|
+
*/
|
|
61
|
+
export function hashPassword(password) {
|
|
62
|
+
return bcrypt.hashSync(password, 10);
|
|
63
|
+
}
|