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
@@ -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
- execFileSync('docker', args, { stdio: 'inherit' });
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 script same tradeoffs:
156
- // * StrictHostKeyChecking=no + UserKnownHostsFile=/dev/null: ephemeral
157
- // VPS IPs get recycled across e2e runs (and across customer
158
- // redeploys after destroy). accept-new + the operator's real
159
- // ~/.ssh/known_hosts would fail with "host key verification failed"
160
- // whenever a recycled IP shows up with a new host key, even with
161
- // BatchMode=yes (in fact especially then, since ssh can't prompt to
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 if the
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
- '-o',
170
- 'StrictHostKeyChecking=no',
171
- '-o',
172
- 'UserKnownHostsFile=/dev/null',
173
- '-o',
174
- 'BatchMode=yes',
175
- '-o',
176
- 'ConnectTimeout=30',
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
- execFileSync('bash', ['-c', cmd], { stdio: ['ignore', 'pipe', 'pipe'] });
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 execFileSync's own
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}`);