vibecarbon 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +110 -663
  2. package/README.md +4 -4
  3. package/carbon/.env.example +11 -0
  4. package/carbon/backup/compose-backup.sh +8 -0
  5. package/carbon/docker-compose.yml +11 -0
  6. package/carbon/ha/primary-init.sql +10 -2
  7. package/carbon/k8s/base/backup/cronjob.yaml +9 -0
  8. package/carbon/k8s/base/repl-gateway/kustomization.yaml +11 -0
  9. package/carbon/k8s/base/repl-gateway/repl-gateway.yaml +92 -0
  10. package/carbon/k8s/values/supabase.values.yaml +6 -1
  11. package/carbon/src/client/lib/supabase.ts +41 -0
  12. package/carbon/src/client/locales/de.json +27 -12
  13. package/carbon/src/client/locales/en.json +30 -15
  14. package/carbon/src/client/locales/es.json +27 -12
  15. package/carbon/src/client/locales/fr.json +27 -12
  16. package/carbon/src/client/locales/pt.json +27 -12
  17. package/carbon/src/client/pages/Checkout.tsx +1 -1
  18. package/carbon/src/server/billing/providers/paddle.ts +21 -2
  19. package/carbon/src/server/billing/providers/polar.ts +14 -0
  20. package/carbon/src/server/index.ts +12 -9
  21. package/carbon/src/server/lib/client-ip.ts +68 -0
  22. package/carbon/src/server/lib/env.ts +20 -6
  23. package/carbon/src/server/lib/rate-limiter.ts +11 -65
  24. package/carbon/src/server/lib/supabase.ts +10 -4
  25. package/carbon/src/server/middleware/requireOrgRole.ts +76 -0
  26. package/carbon/src/server/middleware/requirePlan.ts +64 -34
  27. package/carbon/src/server/middleware/requireSuperAdmin.ts +26 -0
  28. package/carbon/src/server/routes/v1/admin/contact.ts +5 -20
  29. package/carbon/src/server/routes/v1/admin/jobs.ts +6 -20
  30. package/carbon/src/server/routes/v1/admin/newsletter.ts +26 -23
  31. package/carbon/src/server/routes/v1/auth.ts +5 -2
  32. package/carbon/src/server/routes/v1/billing.ts +24 -64
  33. package/carbon/src/server/routes/v1/index.ts +8 -1
  34. package/carbon/src/server/routes/v1/newsletter.ts +18 -12
  35. package/carbon/src/server/routes/v1/theme.ts +36 -15
  36. package/carbon/supabase/migrations/00003_pg_cron.sql +2 -2
  37. package/carbon/supabase/migrations/00004_contact_newsletter.sql +24 -14
  38. package/carbon/tests/component/supabase-env-guard.test.ts +54 -0
  39. package/carbon/tests/integration/server/middleware/rate-limit-ip-spoof.test.ts +48 -0
  40. package/carbon/tests/integration/server/middleware/require-plan.test.ts +101 -0
  41. package/carbon/tests/integration/server/routes/admin-newsletter.test.ts +93 -0
  42. package/carbon/tests/integration/server/routes/billing-subscription-idor.test.ts +92 -0
  43. package/carbon/tests/integration/server/routes/newsletter-unsubscribe.test.ts +65 -0
  44. package/carbon/tests/integration/server/routes/theme-validation.test.ts +66 -0
  45. package/carbon/tests/unit/server/client-ip.test.ts +76 -0
  46. package/carbon/tests/unit/server/env-bool.test.ts +63 -0
  47. package/carbon/tests/unit/server/paddle-webhook.test.ts +57 -0
  48. package/carbon/tests/unit/server/polar-webhook.test.ts +63 -0
  49. package/carbon/tests/unit/server/supabase-client.test.ts +30 -0
  50. package/carbon/volumes/db/wal-archive.sh +36 -0
  51. package/package.json +2 -2
  52. package/src/backup.js +0 -2
  53. package/src/cli.js +16 -0
  54. package/src/deploy.js +16 -5
  55. package/src/destroy.js +968 -886
  56. package/src/failover.js +118 -237
  57. package/src/lib/command.js +19 -1
  58. package/src/lib/deploy/compose/ha.js +180 -102
  59. package/src/lib/deploy/compose/index.js +106 -57
  60. package/src/lib/deploy/compose/single.js +421 -0
  61. package/src/lib/deploy/image.js +38 -30
  62. package/src/lib/deploy/k8s/ha/index.js +397 -150
  63. package/src/lib/deploy/k8s/k3s.js +411 -279
  64. package/src/lib/deploy/orchestrator.js +274 -453
  65. package/src/lib/deploy/remote-build.js +8 -1
  66. package/src/lib/deploy/replication.js +540 -0
  67. package/src/lib/deploy/state.js +62 -5
  68. package/src/lib/deploy/tier-registry.js +30 -0
  69. package/src/lib/deploy/utils.js +30 -2
  70. package/src/lib/deploy/wireguard.js +146 -0
  71. package/src/lib/host-keys.js +120 -5
  72. package/src/lib/iac/index.js +57 -23
  73. package/src/lib/licensing/gate.js +59 -0
  74. package/src/lib/licensing/index.js +1 -1
  75. package/src/lib/licensing/tiers.js +6 -5
  76. package/src/lib/prod-confirm.js +65 -0
  77. package/src/lib/providers/hetzner-s3.js +85 -0
  78. package/src/lib/retry.js +59 -0
  79. package/src/lib/scale-plan.js +159 -0
  80. package/src/lib/ssh.js +35 -35
  81. package/src/restore.js +102 -14
  82. package/src/scale.js +105 -133
  83. package/src/status.js +122 -0
  84. package/src/upgrade.js +0 -4
  85. package/carbon/ha/activate-standby.sh +0 -52
  86. package/carbon/ha/standby-init.sh +0 -74
  87. package/carbon/src/server/lib/request.ts +0 -34
  88. package/src/lib/pod-backups.js +0 -53
@@ -1,74 +0,0 @@
1
- #!/bin/bash
2
- # Initialize standby PostgreSQL as a streaming replica of the primary.
3
- #
4
- # This script runs ON the standby master node and uses kubectl to
5
- # exec into the postgres pod. It:
6
- # 1. Stops the standby database
7
- # 2. Wipes data and runs pg_basebackup from the primary
8
- # 3. Configures standby.signal and primary_conninfo
9
- # 4. Restarts as a hot standby
10
- #
11
- # Required env vars:
12
- # PRIMARY_HOST — IP address of the primary cluster (master node)
13
- #
14
- # Optional env vars:
15
- # PRIMARY_PORT — PostgreSQL port on the primary (default: 5432, use 30432 for NodePort)
16
- # REPL_PASSWORD — Replication user password (required; generated by vibecarbon create in .env.local)
17
- # NAMESPACE — Kubernetes namespace (default: vibecarbon)
18
-
19
- set -euo pipefail
20
-
21
- : "${PRIMARY_HOST:?PRIMARY_HOST is required}"
22
- PRIMARY_PORT="${PRIMARY_PORT:-5432}"
23
- : "${REPL_PASSWORD:?REPL_PASSWORD is required — generate one in .env.local and export it before running standby-init.sh}"
24
- NAMESPACE="${NAMESPACE:-vibecarbon}"
25
- PGDATA="/var/lib/postgresql/data"
26
-
27
- # Find the postgres pod
28
- POD=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=supabase-db -o jsonpath='{.items[0].metadata.name}')
29
- if [ -z "$POD" ]; then
30
- echo "ERROR: No postgres pod found in namespace $NAMESPACE"
31
- exit 1
32
- fi
33
-
34
- echo "Standby init: pod=$POD, primary=$PRIMARY_HOST:$PRIMARY_PORT"
35
-
36
- # 1. Stop postgres on standby
37
- echo "Stopping standby PostgreSQL..."
38
- kubectl exec -n "$NAMESPACE" "$POD" -- su postgres -c "pg_ctl stop -D $PGDATA -m fast" 2>/dev/null || true
39
- sleep 5
40
-
41
- # 2. Wipe data and run pg_basebackup from primary
42
- echo "Running pg_basebackup from primary ($PRIMARY_HOST:$PRIMARY_PORT)..."
43
- kubectl exec -n "$NAMESPACE" "$POD" -- bash -c "
44
- rm -rf ${PGDATA}/*
45
- PGPASSWORD='${REPL_PASSWORD}' pg_basebackup -h ${PRIMARY_HOST} -p ${PRIMARY_PORT} -U replicator -D ${PGDATA} -Fp -Xs -P -R
46
- "
47
-
48
- # 3. Configure standby signal and primary connection info
49
- echo "Configuring standby.signal and primary_conninfo..."
50
- kubectl exec -n "$NAMESPACE" "$POD" -- bash -c "
51
- touch ${PGDATA}/standby.signal
52
- echo \"primary_conninfo = 'host=${PRIMARY_HOST} port=${PRIMARY_PORT} user=replicator password=${REPL_PASSWORD}'\" >> ${PGDATA}/postgresql.auto.conf
53
- chown -R postgres:postgres ${PGDATA}
54
- "
55
-
56
- # 4. Start postgres as hot standby
57
- echo "Starting standby PostgreSQL..."
58
- kubectl exec -n "$NAMESPACE" "$POD" -- su postgres -c "pg_ctl start -D $PGDATA -l /var/log/postgresql/postgresql.log"
59
-
60
- # 5. Verify recovery mode
61
- echo "Verifying standby is in recovery mode..."
62
- sleep 5
63
- for i in $(seq 1 15); do
64
- result=$(kubectl exec -n "$NAMESPACE" "$POD" -- psql -U supabase_admin -tAc 'SELECT pg_is_in_recovery()' 2>/dev/null || echo "")
65
- if [ "$result" = "t" ]; then
66
- echo "Standby is in recovery mode — streaming replication active"
67
- exit 0
68
- fi
69
- echo " Waiting for standby to enter recovery mode (attempt $i/15)..."
70
- sleep 2
71
- done
72
-
73
- echo "WARNING: Could not confirm standby is in recovery mode"
74
- exit 1
@@ -1,34 +0,0 @@
1
- import { logger } from './logger';
2
-
3
- /**
4
- * Extract client IP address from request headers.
5
- * Checks common proxy headers (x-forwarded-for, x-real-ip, cf-connecting-ip).
6
- */
7
- export function getClientIp(c: { req: { header: (name: string) => string | undefined } }): string {
8
- // Cloudflare
9
- const cfIp = c.req.header('cf-connecting-ip');
10
- if (cfIp && isValidIp(cfIp)) return cfIp;
11
-
12
- // Standard proxy headers
13
- const forwarded = c.req.header('x-forwarded-for');
14
- if (forwarded) {
15
- const first = forwarded.split(',')[0].trim();
16
- if (isValidIp(first)) return first;
17
- }
18
-
19
- const realIp = c.req.header('x-real-ip');
20
- if (realIp && isValidIp(realIp)) return realIp;
21
-
22
- logger.warn(
23
- 'Could not extract client IP from request headers (x-forwarded-for, x-real-ip, cf-connecting-ip). ' +
24
- 'Using fallback. Check proxy configuration if this happens frequently.'
25
- );
26
- return '127.0.0.1';
27
- }
28
-
29
- const ipv4Regex = /^(?:\d{1,3}\.){3}\d{1,3}$/;
30
- const ipv6Regex = /^[a-fA-F0-9:]+$/;
31
-
32
- function isValidIp(ip: string): boolean {
33
- return ipv4Regex.test(ip) || ipv6Regex.test(ip);
34
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * List Supabase database backups stored inside the Postgres pod's /backups dir
3
- * (k8s mode, used when an env has no S3 configured). Shared by `backup` and
4
- * `restore` so the parsing of `ls` output lives in exactly one place.
5
- */
6
-
7
- import { getPostgresPod, sshKubectl } from './ssh.js';
8
-
9
- /**
10
- * @param {string} ip - master node IP
11
- * @param {string} sshKeyPath - path to the deploy SSH key
12
- * @param {{ sort?: 'recent' | 'largest' }} [options]
13
- * - 'recent' (default) → newest first (`ls -lt`); restore lists by recency.
14
- * - 'largest' → biggest first with human-readable sizes (`ls -lhS`);
15
- * backup lists by size.
16
- * @returns {Array<{ size: string, date: string, name: string }>} parsed entries
17
- * (empty array if the pod is unreachable or holds no backups)
18
- */
19
- export async function listPodBackups(ip, sshKeyPath, { sort = 'recent' } = {}) {
20
- const pod = await getPostgresPod(ip, sshKeyPath);
21
- const lsFlags = sort === 'largest' ? '-lhS' : '-lt';
22
-
23
- try {
24
- const output = await sshKubectl(ip, sshKeyPath, [
25
- 'exec',
26
- '-n',
27
- 'vibecarbon',
28
- pod,
29
- '--',
30
- 'sh',
31
- '-c',
32
- `ls ${lsFlags} /backups/*_full.tar.gz /backups/*.sql.gz 2>/dev/null || echo NO_BACKUPS`,
33
- ]);
34
-
35
- if (output === 'NO_BACKUPS' || !output) {
36
- return [];
37
- }
38
-
39
- return output
40
- .split('\n')
41
- .filter((line) => line.includes('.tar.gz') || line.includes('.sql.gz'))
42
- .map((line) => {
43
- const parts = line.split(/\s+/);
44
- return {
45
- size: parts[4],
46
- date: `${parts[5]} ${parts[6]} ${parts[7]}`,
47
- name: parts[parts.length - 1].replace('/backups/', ''),
48
- };
49
- });
50
- } catch {
51
- return [];
52
- }
53
- }