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
package/src/lib/deploy/image.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { runCommandAsync } from '../command.js';
|
|
17
|
+
import { knownHostsPathForKey } from '../host-keys.js';
|
|
18
|
+
import { shEscape } from '../shell.js';
|
|
16
19
|
|
|
17
20
|
const LOCAL_REGISTRY_PREFIX = 'vibecarbon-local';
|
|
18
21
|
|
|
@@ -93,9 +96,9 @@ export function inspectGitState(cwd) {
|
|
|
93
96
|
*
|
|
94
97
|
* @param {string} projectDir - Project root with the Dockerfile.
|
|
95
98
|
* @param {{projectName: string, timestamp?: string, rebuild?: boolean, tagPrefix?: string, buildArgs?: Record<string,string>}} options
|
|
96
|
-
* @returns {{tag: string, gitSha: string, isDirty: boolean}}
|
|
99
|
+
* @returns {Promise<{tag: string, gitSha: string, isDirty: boolean}>}
|
|
97
100
|
*/
|
|
98
|
-
export function buildLocalImage(projectDir, options) {
|
|
101
|
+
export async function buildLocalImage(projectDir, options) {
|
|
99
102
|
const { projectName, rebuild = false, tagPrefix, buildArgs = {} } = options;
|
|
100
103
|
if (!projectName) {
|
|
101
104
|
throw new Error('buildLocalImage: projectName is required');
|
|
@@ -122,7 +125,15 @@ export function buildLocalImage(projectDir, options) {
|
|
|
122
125
|
}
|
|
123
126
|
args.push('-t', tag, projectDir);
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
// silent: false (the default) inherits stdio so the operator sees layer
|
|
129
|
+
// progress — but runCommandAsync only *rejects* on nonzero exit when
|
|
130
|
+
// silent:true; with inherited stdio it resolves `false` instead (see
|
|
131
|
+
// global constraints). The old execFileSync({stdio:'inherit'}) threw on
|
|
132
|
+
// its own, so we must replicate "fail loudly" by hand here.
|
|
133
|
+
const ok = await runCommandAsync(['docker', ...args], {});
|
|
134
|
+
if (ok === false) {
|
|
135
|
+
throw new Error(`buildLocalImage: docker build failed for ${tag}`);
|
|
136
|
+
}
|
|
126
137
|
return { tag, gitSha, isDirty };
|
|
127
138
|
}
|
|
128
139
|
|
|
@@ -146,50 +157,47 @@ function defaultTimestamp() {
|
|
|
146
157
|
* `gzip -1` is universally present and dominant cost is network, not CPU.
|
|
147
158
|
*
|
|
148
159
|
* `pipefail` so a docker-save / gzip failure isn't masked by ssh's exit code.
|
|
149
|
-
* `StrictHostKeyChecking=accept-new` matches the convention used elsewhere
|
|
150
|
-
* in vibecarbon's ssh paths: trust-on-first-use, error on host-key change.
|
|
151
160
|
*
|
|
152
161
|
* @param {{tag: string, sshTarget: string, sshKey?: string}} args
|
|
162
|
+
* @returns {Promise<void>}
|
|
153
163
|
*/
|
|
154
|
-
export function sideloadCompose({ tag, sshTarget, sshKey }) {
|
|
155
|
-
// SSH options mirror remote-build.js's wrapper
|
|
156
|
-
// *
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// update the entry). iter-perfwave4 hit exactly this on first run.
|
|
164
|
+
export async function sideloadCompose({ tag, sshTarget, sshKey }) {
|
|
165
|
+
// SSH options mirror remote-build.js's wrapper + the compose deploy path:
|
|
166
|
+
// * Host-key pinned per-env (knownHostsPathForKey), NOT /dev/null + no.
|
|
167
|
+
// accept-new TOFU's an ephemeral/recycled Hetzner IP but rejects a
|
|
168
|
+
// changed key for an already-pinned host (MITM on an established env
|
|
169
|
+
// fails). GlobalKnownHostsFile=/dev/null ignores the system file; the
|
|
170
|
+
// operator's ~/.ssh/known_hosts is never touched (a stale entry there
|
|
171
|
+
// would otherwise reject a recycled IP — iter-perfwave4 hit this).
|
|
163
172
|
// * BatchMode=yes per feedback_ssh_batchmode_required.md (PR 1T):
|
|
164
|
-
// fast-fail rather than hang on interactive password prompt
|
|
165
|
-
// key fails.
|
|
173
|
+
// fast-fail rather than hang on interactive password prompt.
|
|
166
174
|
// * ConnectTimeout=30: bound the network half of dial; sideload's own
|
|
167
175
|
// stream is unbounded (limited by docker save throughput).
|
|
168
|
-
const sshFlags = [
|
|
169
|
-
|
|
170
|
-
'
|
|
171
|
-
|
|
172
|
-
'
|
|
173
|
-
'-o',
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
];
|
|
178
|
-
if (sshKey) sshFlags.unshift('-i', sshKey);
|
|
176
|
+
const sshFlags = ['-o', 'StrictHostKeyChecking=accept-new'];
|
|
177
|
+
if (sshKey) {
|
|
178
|
+
// shEscape the known_hosts assignment so a project dir with spaces can't
|
|
179
|
+
// split the argument in the bash -c string below.
|
|
180
|
+
sshFlags.push('-o', shEscape(`UserKnownHostsFile=${knownHostsPathForKey(sshKey)}`));
|
|
181
|
+
sshFlags.push('-o', 'GlobalKnownHostsFile=/dev/null');
|
|
182
|
+
}
|
|
183
|
+
sshFlags.push('-o', 'BatchMode=yes', '-o', 'ConnectTimeout=30');
|
|
184
|
+
if (sshKey) sshFlags.unshift('-i', shEscape(sshKey));
|
|
179
185
|
const cmd = `set -o pipefail && docker save ${tag} | gzip -1 | ssh ${sshFlags.join(' ')} ${sshTarget} 'gunzip | docker load'`;
|
|
180
186
|
// Capture stdio rather than inheriting it: ssh's "Permanently added" host-
|
|
181
187
|
// key warning + `docker load`'s "Loaded image:" success line both bypass
|
|
182
188
|
// the clack gutter when inherited and print flush-left, breaking the
|
|
183
189
|
// visual flow. On success we don't need either; on failure we surface
|
|
184
|
-
// the captured stderr for debugging via the thrown Error.
|
|
190
|
+
// the captured stderr for debugging via the thrown Error. silent:true is
|
|
191
|
+
// required both to get the capture and because runCommandAsync only
|
|
192
|
+
// rejects (rather than resolving `false`) on nonzero exit in that mode.
|
|
185
193
|
try {
|
|
186
|
-
|
|
194
|
+
await runCommandAsync(['bash', '-c', cmd], { silent: true });
|
|
187
195
|
} catch (err) {
|
|
188
196
|
const stderr = err.stderr?.toString().trim() || '';
|
|
189
197
|
const stdout = err.stdout?.toString().trim() || '';
|
|
190
198
|
// Surface whatever signal the operator can act on: child stderr first
|
|
191
199
|
// (real ssh / docker errors), then stdout, then the wrapping error's
|
|
192
|
-
// message (preserves "Connection refused" when
|
|
200
|
+
// message (preserves "Connection refused" when runCommandAsync's own
|
|
193
201
|
// error string is the only thing we have).
|
|
194
202
|
const detail = [stderr, stdout, err.message].filter(Boolean).join('\n').trim();
|
|
195
203
|
const wrapped = new Error(`sideloadCompose failed (exit ${err.status ?? '?'}): ${detail}`);
|