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.
- package/LICENSE +110 -663
- package/README.md +4 -4
- package/carbon/.env.example +11 -0
- package/carbon/backup/compose-backup.sh +8 -0
- package/carbon/docker-compose.yml +11 -0
- package/carbon/ha/primary-init.sql +10 -2
- package/carbon/k8s/base/backup/cronjob.yaml +9 -0
- package/carbon/k8s/base/repl-gateway/kustomization.yaml +11 -0
- package/carbon/k8s/base/repl-gateway/repl-gateway.yaml +92 -0
- package/carbon/k8s/values/supabase.values.yaml +6 -1
- package/carbon/src/client/lib/supabase.ts +41 -0
- package/carbon/src/client/locales/de.json +27 -12
- package/carbon/src/client/locales/en.json +30 -15
- package/carbon/src/client/locales/es.json +27 -12
- package/carbon/src/client/locales/fr.json +27 -12
- package/carbon/src/client/locales/pt.json +27 -12
- package/carbon/src/client/pages/Checkout.tsx +1 -1
- package/carbon/src/server/billing/providers/paddle.ts +21 -2
- package/carbon/src/server/billing/providers/polar.ts +14 -0
- package/carbon/src/server/index.ts +12 -9
- package/carbon/src/server/lib/client-ip.ts +68 -0
- package/carbon/src/server/lib/env.ts +20 -6
- package/carbon/src/server/lib/rate-limiter.ts +11 -65
- package/carbon/src/server/lib/supabase.ts +10 -4
- package/carbon/src/server/middleware/requireOrgRole.ts +76 -0
- package/carbon/src/server/middleware/requirePlan.ts +64 -34
- package/carbon/src/server/middleware/requireSuperAdmin.ts +26 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +5 -20
- package/carbon/src/server/routes/v1/admin/jobs.ts +6 -20
- package/carbon/src/server/routes/v1/admin/newsletter.ts +26 -23
- package/carbon/src/server/routes/v1/auth.ts +5 -2
- package/carbon/src/server/routes/v1/billing.ts +24 -64
- package/carbon/src/server/routes/v1/index.ts +8 -1
- package/carbon/src/server/routes/v1/newsletter.ts +18 -12
- package/carbon/src/server/routes/v1/theme.ts +36 -15
- package/carbon/supabase/migrations/00003_pg_cron.sql +2 -2
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +24 -14
- package/carbon/tests/component/supabase-env-guard.test.ts +54 -0
- package/carbon/tests/integration/server/middleware/rate-limit-ip-spoof.test.ts +48 -0
- package/carbon/tests/integration/server/middleware/require-plan.test.ts +101 -0
- package/carbon/tests/integration/server/routes/admin-newsletter.test.ts +93 -0
- package/carbon/tests/integration/server/routes/billing-subscription-idor.test.ts +92 -0
- package/carbon/tests/integration/server/routes/newsletter-unsubscribe.test.ts +65 -0
- package/carbon/tests/integration/server/routes/theme-validation.test.ts +66 -0
- package/carbon/tests/unit/server/client-ip.test.ts +76 -0
- package/carbon/tests/unit/server/env-bool.test.ts +63 -0
- package/carbon/tests/unit/server/paddle-webhook.test.ts +57 -0
- package/carbon/tests/unit/server/polar-webhook.test.ts +63 -0
- package/carbon/tests/unit/server/supabase-client.test.ts +30 -0
- package/carbon/volumes/db/wal-archive.sh +36 -0
- package/package.json +2 -2
- package/src/backup.js +0 -2
- package/src/cli.js +16 -0
- package/src/deploy.js +16 -5
- package/src/destroy.js +968 -886
- package/src/failover.js +118 -237
- package/src/lib/command.js +19 -1
- package/src/lib/deploy/compose/ha.js +180 -102
- package/src/lib/deploy/compose/index.js +106 -57
- package/src/lib/deploy/compose/single.js +421 -0
- package/src/lib/deploy/image.js +38 -30
- package/src/lib/deploy/k8s/ha/index.js +397 -150
- package/src/lib/deploy/k8s/k3s.js +411 -279
- package/src/lib/deploy/orchestrator.js +274 -453
- package/src/lib/deploy/remote-build.js +8 -1
- package/src/lib/deploy/replication.js +540 -0
- package/src/lib/deploy/state.js +62 -5
- package/src/lib/deploy/tier-registry.js +30 -0
- package/src/lib/deploy/utils.js +30 -2
- package/src/lib/deploy/wireguard.js +146 -0
- package/src/lib/host-keys.js +120 -5
- package/src/lib/iac/index.js +57 -23
- package/src/lib/licensing/gate.js +59 -0
- package/src/lib/licensing/index.js +1 -1
- package/src/lib/licensing/tiers.js +6 -5
- package/src/lib/prod-confirm.js +65 -0
- package/src/lib/providers/hetzner-s3.js +85 -0
- package/src/lib/retry.js +59 -0
- package/src/lib/scale-plan.js +159 -0
- package/src/lib/ssh.js +35 -35
- package/src/restore.js +102 -14
- package/src/scale.js +105 -133
- package/src/status.js +122 -0
- package/src/upgrade.js +0 -4
- package/carbon/ha/activate-standby.sh +0 -52
- package/carbon/ha/standby-init.sh +0 -74
- package/carbon/src/server/lib/request.ts +0 -34
- 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
|
-
}
|
package/src/lib/pod-backups.js
DELETED
|
@@ -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
|
-
}
|