hazo_env 0.4.0 → 0.6.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/CHANGE_LOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # hazo_env — Change Log
2
2
 
3
+ ## 0.5.0 — 2026-06-25
4
+
5
+ ### Added
6
+ - `clearEnv()` — delete all non-preserved tables via PostgREST (reverse FK order) + optional file-dir wipe. Exported from `src/index.ts`. CLI: `hazo-env clear <env>` (prod-target guard: `--allow-prod-target --confirm=<token>`).
7
+ - `ClearRequest` / `ClearResult` types in `src/types/index.ts`; `'clear'` added to `MigrationProgress.phase` union.
8
+ - rsync-over-SSH file transport — `copyFiles()` auto-dispatches to rsync when `[transport.ssh.<fromEnv>]` is configured. `copyFilesRsync()` in `src/migrate/files.ts` spawns `rsync -az --partial --info=progress2`, parses `to-chk=N/TOTAL` for live percent. `onProgress` signature extended with optional `percent` arg.
9
+ - `resolveSshConfig(env)` in `src/resolve/ssh.ts` — reads `[transport.ssh.<env>]` section (host/user/key/path) with `${VAR}` expansion.
10
+ - `resolveTransport()` return type widened to `'local' | 'api' | 'rsync'`; `'ssh'` input now returns `'rsync'` instead of throwing.
11
+ - Per-table DB progress — `DbCopyOptions.onTableProgress` callback fires once per table during PostgREST copy pass; `run.ts` maps it to the 20→70% band with a `"table N/M: <name>"` message.
12
+ - `[transport.ssh.<env>]` section added to `config/hazo_env_config.ini.sample`.
13
+
14
+ ### Test-app
15
+ - `POST /api/clear` route added (mirrors `/api/migrate`).
16
+ - `'clear-env'` autotest scenario added (dry-run guard: verifies prod-target refuses without confirm).
17
+
3
18
  ## 0.4.0 — 2026-06-25
4
19
 
5
20
  ### Added
package/README.md CHANGED
@@ -235,6 +235,47 @@ Key sections:
235
235
 
236
236
  Secret placeholders use `${ENV_VAR_NAME}` syntax — hazo_env substitutes them from `.env.local` at runtime.
237
237
 
238
+ ## dump-restore transport
239
+
240
+ The `dump-restore` transport copies a database via SSH `pg_dump`/`pg_restore` (binary format). Use it when the source and target share the same Postgres server (e.g. prod→dev on the same VPS) and you want a fast binary copy without going through the PostgREST row-copy loop.
241
+
242
+ ### Config: `[backup.<env>]`
243
+
244
+ Add a section to `config/hazo_env_config.ini`:
245
+
246
+ ```ini
247
+ [backup.dev]
248
+ ssh_env = prod ; reuse [transport.ssh.prod] SSH connection
249
+ target_db = myapp_dev ; DB to drop/recreate
250
+ owner = adminuser ; createdb -O <owner>
251
+ dump_source = /var/backups/myapp-*.pgdump ; glob (newest used) or "fresh"
252
+ fresh_dump_db = myapp ; source DB for pg_dump when dump_source=fresh
253
+ pre_restore_cmd = sudo systemctl stop myapp-postgrest-dev
254
+ post_restore_cmd= sudo systemctl start myapp-postgrest-dev
255
+ ```
256
+
257
+ > **No PII scrubbing on this path.** The binary `pg_restore` copies raw rows; hazo_env's `scrubHook` only runs inside the PostgREST row-copy loop. If you need scrubbed data in dev, use the default `api` transport instead.
258
+
259
+ ### CLI
260
+
261
+ ```bash
262
+ npx hazo-env migrate --from prod --to dev --transport dump-restore
263
+ ```
264
+
265
+ ### Programmatic API
266
+
267
+ ```ts
268
+ import { resolveBackupConfig, restoreDbViaDump } from 'hazo_env';
269
+
270
+ const backup = resolveBackupConfig('dev'); // reads [backup.dev] from INI
271
+ if (backup) {
272
+ const result = await restoreDbViaDump(backup, {
273
+ onProgress: (msg) => console.log(msg),
274
+ });
275
+ // result.mode === 'dump-restore'
276
+ }
277
+ ```
278
+
238
279
  ## License
239
280
 
240
281
  MIT
@@ -62,3 +62,21 @@ location = remote
62
62
 
63
63
  [host.prod]
64
64
  location = remote
65
+
66
+ ; ─── SSH transport for cross-host file rsync ─────────────────────────────────
67
+ ; When [transport.ssh.<env>] is present, copyFiles will use rsync-over-SSH
68
+ ; to pull files from that environment's file server instead of local copy.
69
+ ; This enables prod→dev migration when both envs run on separate hosts.
70
+
71
+ [transport.ssh.prod]
72
+ ; SSH connection to the prod file server (used when pulling prod→dev).
73
+ ; The worker runs rsync FROM prod TO dev — prod stays read-only.
74
+ host = ${SSH_HOST_PROD}
75
+ user = ${SSH_USER_PROD}
76
+ key = ${SSH_KEY_PATH_PROD}
77
+ path = /home/pubs/kinstripe/var/uploads
78
+ ; Optional: pin the server's host key (output of `ssh-keyscan <host>` minus the
79
+ ; leading hostname, e.g. "ssh-ed25519 AAAA..."). When set, host-key checking is
80
+ ; strict and pinned — no trust-on-first-use. When omitted, the first connection
81
+ ; is accepted (accept-new) and persisted to config/hazo_env_known_hosts.
82
+ ; host_key = ${SSH_HOST_KEY_PROD}
package/dist/cli.js CHANGED
@@ -81,6 +81,8 @@ async function runMigrate() {
81
81
  const tablesIdx = args.findIndex((a) => a === '--tables');
82
82
  const tablesArg = tablesIdx >= 0 ? args[tablesIdx + 1] : undefined;
83
83
  const tables = tablesArg ? tablesArg.split(',').map((t) => t.trim()) : undefined;
84
+ const transportIdx = args.findIndex((a) => a === '--transport');
85
+ const transport = transportIdx >= 0 ? args[transportIdx + 1] : undefined;
84
86
  if (!from || !to) {
85
87
  console.error(pc.red('Error: migrate requires --from <env> and --to <env>'));
86
88
  process.exit(1);
@@ -96,6 +98,7 @@ async function runMigrate() {
96
98
  dryRun,
97
99
  allowProdTarget,
98
100
  confirmToken,
101
+ transport: transport,
99
102
  onProgress: (p) => {
100
103
  const pct = p.percent != null ? `${p.percent}%`.padStart(4) + ' ' : ' ';
101
104
  console.log(` ${pct}${pc.dim(p.phase.padEnd(12))} ${p.message}`);
@@ -167,6 +170,39 @@ async function runRestore() {
167
170
  restoreSnapshot(toConfig, snapshotId);
168
171
  console.log(` ${pc.green('✓')} Restored\n`);
169
172
  }
173
+ async function runClear() {
174
+ const targetEnv = args[0] ?? getEnv();
175
+ const allowProd = args.includes('--allow-prod-target');
176
+ const confirm = args.find((a) => a.startsWith('--confirm='))?.split('=')[1];
177
+ console.log(`\n${pc.bold('hazo-env clear')} ${pc.dim(targetEnv)}\n`);
178
+ const { clearEnv } = await import('./migrate/clear.js');
179
+ try {
180
+ let lastMsg = '';
181
+ const result = await clearEnv({
182
+ env: targetEnv,
183
+ allowProdTarget: allowProd,
184
+ confirmToken: confirm,
185
+ onProgress: (p) => {
186
+ if (p.message !== lastMsg) {
187
+ const pctStr = p.percent != null ? ` [${p.percent}%]` : '';
188
+ console.log(` ${pc.dim('·')} ${p.message}${pctStr}`);
189
+ lastMsg = p.message;
190
+ }
191
+ },
192
+ });
193
+ if (result.ok) {
194
+ console.log(` ${pc.green('✓')} Cleared ${result.tablesCleared} tables${result.filesCleared ? ' + files' : ''} in ${result.durationMs}ms\n`);
195
+ if (result.warnings.length)
196
+ result.warnings.forEach((w) => console.log(` ${pc.yellow('⚠')} ${w}`));
197
+ }
198
+ }
199
+ catch (e) {
200
+ const msg = e instanceof Error ? e.message : String(e);
201
+ console.error(`\n ${pc.red('✗')} ${msg}\n`);
202
+ process.exit(1);
203
+ }
204
+ console.log('');
205
+ }
170
206
  async function runMask() {
171
207
  const subCmd = args[0];
172
208
  if (subCmd === 'sync') {
@@ -239,6 +275,8 @@ Usage:
239
275
  hazo-env snapshot <env> Take a snapshot of an environment
240
276
  hazo-env mask sync Load masking ruleset from INI into DB
241
277
  hazo-env mask list List registered mask transforms
278
+ hazo-env clear <env> Wipe DB tables + files for an environment
279
+ --allow-prod-target --confirm=<token> Override prod-target safety
242
280
  `);
243
281
  }
244
282
  (async () => {
@@ -264,6 +302,9 @@ Usage:
264
302
  else if (command === 'mask') {
265
303
  await runMask();
266
304
  }
305
+ else if (command === 'clear') {
306
+ await runClear();
307
+ }
267
308
  else {
268
309
  printHelp();
269
310
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './resolve/connect.js';
6
6
  export * from './resolve/files.js';
7
7
  export * from './doctor.js';
8
8
  export { runMigration } from './migrate/run.js';
9
+ export { clearEnv } from './migrate/clear.js';
9
10
  export { verifyFiles } from './migrate/verify.js';
10
11
  export { takeSnapshot, restoreSnapshot } from './migrate/snapshot.js';
11
12
  export { writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from './migrate/progress.js';
@@ -14,4 +15,8 @@ export { loadRuleset, syncRulesetFromIni, parseIniRules } from './mask/ruleset.j
14
15
  export type { MaskRule } from './mask/ruleset.js';
15
16
  export type { MaskTransform } from './types/index.js';
16
17
  export * from './lib/index.js';
18
+ export { resolveBackupConfig } from './resolve/backup.js';
19
+ export type { BackupConfig } from './resolve/backup.js';
20
+ export { runSsh, assertSafeSshField } from './migrate/ssh-exec.js';
21
+ export { restoreDbViaDump } from './migrate/db-dump-restore.js';
17
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEvG,cAAc,kBAAkB,CAAC;AAEjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,aAAa,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE9G,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACnF,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEvG,cAAc,kBAAkB,CAAC;AAEjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,aAAa,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE9G,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACnF,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * from './resolve/files.js';
13
13
  export * from './doctor.js';
14
14
  // Migration engine (Phase 2)
15
15
  export { runMigration } from './migrate/run.js';
16
+ export { clearEnv } from './migrate/clear.js';
16
17
  export { verifyFiles } from './migrate/verify.js';
17
18
  export { takeSnapshot, restoreSnapshot } from './migrate/snapshot.js';
18
19
  export { writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from './migrate/progress.js';
@@ -22,3 +23,7 @@ export { registerMask } from './mask/registry.js';
22
23
  export { loadRuleset, syncRulesetFromIni, parseIniRules } from './mask/ruleset.js';
23
24
  // Lib
24
25
  export * from './lib/index.js';
26
+ // dump-restore transport
27
+ export { resolveBackupConfig } from './resolve/backup.js';
28
+ export { runSsh, assertSafeSshField } from './migrate/ssh-exec.js';
29
+ export { restoreDbViaDump } from './migrate/db-dump-restore.js';
@@ -0,0 +1,3 @@
1
+ import type { ClearRequest, ClearResult } from '../types/index.js';
2
+ export declare function clearEnv(req: ClearRequest): Promise<ClearResult>;
3
+ //# sourceMappingURL=clear.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clear.d.ts","sourceRoot":"","sources":["../../src/migrate/clear.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAqB,MAAM,mBAAmB,CAAC;AAYtF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAoHtE"}
@@ -0,0 +1,117 @@
1
+ // hazo_env/src/migrate/clear.ts — clearEnv: wipe DB tables + files for a target environment
2
+ import { HazoError, optional_import } from 'hazo_core';
3
+ import { getEnvRole } from '../env.server.js';
4
+ import { resolveMigrateConfig } from '../resolve/migrate.js';
5
+ import { resolveConnectConfig } from '../resolve/connect.js';
6
+ import { resolveFilesRoot } from '../resolve/files.js';
7
+ import { writeMigrationProgress } from './progress.js';
8
+ import fs from 'node:fs';
9
+ function emitProgress(req, p) {
10
+ req.onProgress?.(p);
11
+ if (req.jobId && req.progressDir) {
12
+ writeMigrationProgress(req.progressDir, req.jobId, p);
13
+ }
14
+ }
15
+ export async function clearEnv(req) {
16
+ const startMs = Date.now();
17
+ const warnings = [];
18
+ // Guard: refuse production target unless explicitly unlocked with a confirm token
19
+ const role = getEnvRole(req.env);
20
+ if (role === 'production' && !req.allowProdTarget) {
21
+ throw new HazoError({
22
+ code: 'HAZO_ENV_PROD_TARGET_REFUSED',
23
+ pkg: 'hazo_env',
24
+ message: `Refusing to clear production environment "${req.env}". Pass allowProdTarget:true and a confirmToken to override.`,
25
+ });
26
+ }
27
+ if (role === 'production' && req.allowProdTarget && !req.confirmToken) {
28
+ throw new HazoError({
29
+ code: 'HAZO_ENV_PROD_TARGET_REFUSED',
30
+ pkg: 'hazo_env',
31
+ message: 'Production target requires a confirmToken for safety.',
32
+ });
33
+ }
34
+ emitProgress(req, { phase: 'validate', message: 'Validating clear request', percent: 0 });
35
+ const migrateConfig = resolveMigrateConfig();
36
+ const { tables, preserve = [], pkOverrides = {} } = migrateConfig;
37
+ if (!tables || tables.length === 0) {
38
+ throw new HazoError({
39
+ code: 'HAZO_ENV_MISSING_TABLE_LIST',
40
+ pkg: 'hazo_env',
41
+ message: 'clearEnv requires an explicit table list. ' +
42
+ 'Set [migrate] tables = ... in hazo_env_config.ini.',
43
+ });
44
+ }
45
+ const tablesToClear = [...tables.filter((t) => !preserve.includes(t))].reverse();
46
+ const dbConfig = resolveConnectConfig({ env: req.env, allowOtherEnv: true });
47
+ if (dbConfig.type !== 'postgrest') {
48
+ throw new HazoError({
49
+ code: 'HAZO_ENV_NOT_IMPLEMENTED',
50
+ pkg: 'hazo_env',
51
+ message: `clearEnv only supports PostgREST connections (got "${dbConfig.type}").`,
52
+ });
53
+ }
54
+ if (!dbConfig.postgrest) {
55
+ throw new HazoError({
56
+ code: 'HAZO_ENV_NOT_IMPLEMENTED',
57
+ pkg: 'hazo_env',
58
+ message: 'PostgREST config missing base_url/api_key.',
59
+ });
60
+ }
61
+ const connect = await optional_import('hazo_connect/server');
62
+ if (!connect) {
63
+ throw new HazoError({
64
+ code: 'HAZO_ENV_MISSING_DEPENDENCY',
65
+ pkg: 'hazo_env',
66
+ message: 'hazo_connect is required for clearEnv. Install hazo_connect to continue.',
67
+ });
68
+ }
69
+ const tgtAdapter = connect.createHazoConnect({
70
+ type: 'postgrest',
71
+ postgrest: {
72
+ base_url: dbConfig.postgrest.base_url,
73
+ api_key: dbConfig.postgrest.api_key,
74
+ },
75
+ });
76
+ // DB clear (only if not explicitly disabled)
77
+ let tablesCleared = 0;
78
+ if (req.include?.db !== false) {
79
+ for (let idx = 0; idx < tablesToClear.length; idx++) {
80
+ const table = tablesToClear[idx];
81
+ const percent = Math.round(((idx + 1) / tablesToClear.length) * 90);
82
+ emitProgress(req, {
83
+ phase: 'clear',
84
+ message: `Clearing table ${idx + 1}/${tablesToClear.length}: ${table}`,
85
+ percent,
86
+ });
87
+ const pk = pkOverrides[table] ?? 'id';
88
+ try {
89
+ await tgtAdapter.rawQuery(`/${table}?${pk}=not.is.null`, { method: 'DELETE' });
90
+ tablesCleared++;
91
+ }
92
+ catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ warnings.push(`Warning: could not clear ${table}: ${msg}`);
95
+ }
96
+ }
97
+ }
98
+ // File clear
99
+ let filesCleared = false;
100
+ if (req.include?.files !== false) {
101
+ emitProgress(req, { phase: 'clear', message: 'Clearing files...', percent: 92 });
102
+ const filesRoot = resolveFilesRoot(req.env);
103
+ if (fs.existsSync(filesRoot)) {
104
+ fs.rmSync(filesRoot, { recursive: true, force: true });
105
+ }
106
+ fs.mkdirSync(filesRoot, { recursive: true });
107
+ filesCleared = true;
108
+ }
109
+ emitProgress(req, { phase: 'done', message: 'Clear complete', percent: 100 });
110
+ return {
111
+ ok: true,
112
+ tablesCleared,
113
+ filesCleared,
114
+ durationMs: Date.now() - startMs,
115
+ warnings,
116
+ };
117
+ }
@@ -0,0 +1,24 @@
1
+ import type { BackupConfig } from '../resolve/backup.js';
2
+ export interface DbDumpRestoreResult {
3
+ tables: number;
4
+ rows: number;
5
+ scrubbed: number;
6
+ mode: string;
7
+ }
8
+ /**
9
+ * Restore a Postgres database on a remote server via SSH pg_dump/pg_restore.
10
+ *
11
+ * Steps executed remotely:
12
+ * 1. Obtain a dump file (fresh pg_dump or locate an existing .pgdump)
13
+ * 2. Run pre_restore_cmd (if configured)
14
+ * 3. dropdb --if-exists + createdb -O <owner>
15
+ * 4. pg_restore -d <target_db>
16
+ * 5. Run post_restore_cmd (if configured)
17
+ *
18
+ * Returns a DbDumpRestoreResult with mode: 'dump-restore' and zero
19
+ * row/table counts (binary restore does not report these).
20
+ */
21
+ export declare function restoreDbViaDump(backup: BackupConfig, opts?: {
22
+ onProgress?: (msg: string) => void;
23
+ }): Promise<DbDumpRestoreResult>;
24
+ //# sourceMappingURL=db-dump-restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-dump-restore.d.ts","sourceRoot":"","sources":["../../src/migrate/db-dump-restore.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAC5C,OAAO,CAAC,mBAAmB,CAAC,CA6D9B"}
@@ -0,0 +1,72 @@
1
+ // hazo_env/src/migrate/db-dump-restore.ts — DB copy via SSH pg_dump/pg_restore
2
+ //
3
+ // Builds a single remote bash script and runs it via runSsh.
4
+ // ⚠️ No PII scrubbing on this path — pg_restore copies raw binary rows.
5
+ // Use the default api transport when scrubbed data is required in dev.
6
+ import { resolveSshConfig } from '../resolve/ssh.js';
7
+ import { runSsh } from './ssh-exec.js';
8
+ /**
9
+ * Restore a Postgres database on a remote server via SSH pg_dump/pg_restore.
10
+ *
11
+ * Steps executed remotely:
12
+ * 1. Obtain a dump file (fresh pg_dump or locate an existing .pgdump)
13
+ * 2. Run pre_restore_cmd (if configured)
14
+ * 3. dropdb --if-exists + createdb -O <owner>
15
+ * 4. pg_restore -d <target_db>
16
+ * 5. Run post_restore_cmd (if configured)
17
+ *
18
+ * Returns a DbDumpRestoreResult with mode: 'dump-restore' and zero
19
+ * row/table counts (binary restore does not report these).
20
+ */
21
+ export async function restoreDbViaDump(backup, opts) {
22
+ const ssh = resolveSshConfig(backup.ssh_env);
23
+ if (!ssh) {
24
+ throw new Error(`dump-restore: no [transport.ssh.${backup.ssh_env}] config found — ` +
25
+ `set ssh_env to a configured SSH transport in [backup.*]`);
26
+ }
27
+ // Build remote bash script lines
28
+ const lines = ['set -euo pipefail', ''];
29
+ // Step 1: Get or create dump
30
+ lines.push('# Step 1: Get or create dump');
31
+ if (backup.dump_source === 'fresh') {
32
+ lines.push('DUMP_FILE="/tmp/he-$(date +%s).pgdump"');
33
+ lines.push(`echo "Creating fresh dump of ${backup.fresh_dump_db}..."`);
34
+ lines.push(`pg_dump -Fc ${backup.fresh_dump_db} > "$DUMP_FILE"`);
35
+ }
36
+ else {
37
+ lines.push(`echo "Locating dump file matching ${backup.dump_source}..."`);
38
+ lines.push(`DUMP_FILE=$(ls -t ${backup.dump_source} 2>/dev/null | head -1)`);
39
+ lines.push(`if [ -z "$DUMP_FILE" ]; then echo "No dump file found matching ${backup.dump_source}"; exit 1; fi`);
40
+ lines.push('echo "Using dump: $DUMP_FILE"');
41
+ }
42
+ lines.push('');
43
+ // Step 2: Pre-restore command (optional)
44
+ if (backup.pre_restore_cmd) {
45
+ lines.push('# Step 2: Pre-restore command');
46
+ lines.push('echo "Running pre-restore..."');
47
+ lines.push(backup.pre_restore_cmd);
48
+ lines.push('');
49
+ }
50
+ // Step 3: Drop and recreate target DB
51
+ lines.push('# Step 3: Drop and recreate target DB');
52
+ lines.push(`echo "Dropping ${backup.target_db}..."`);
53
+ lines.push(`dropdb --if-exists ${backup.target_db}`);
54
+ lines.push(`echo "Creating ${backup.target_db}..."`);
55
+ lines.push(`createdb -O ${backup.owner} ${backup.target_db}`);
56
+ lines.push('');
57
+ // Step 4: Restore
58
+ lines.push('# Step 4: Restore');
59
+ lines.push(`echo "Restoring to ${backup.target_db}..."`);
60
+ lines.push(`pg_restore -d ${backup.target_db} "$DUMP_FILE"`);
61
+ lines.push('echo "Restore complete."');
62
+ // Step 5: Post-restore command (optional)
63
+ if (backup.post_restore_cmd) {
64
+ lines.push('');
65
+ lines.push('# Step 5: Post-restore command');
66
+ lines.push('echo "Running post-restore..."');
67
+ lines.push(backup.post_restore_cmd);
68
+ }
69
+ const script = lines.join('\n');
70
+ await runSsh(ssh, script, { onProgress: opts?.onProgress });
71
+ return { tables: 0, rows: 0, scrubbed: 0, mode: 'dump-restore' };
72
+ }
@@ -6,6 +6,7 @@ export interface DbCopyOptions {
6
6
  pkOverrides?: Record<string, string>;
7
7
  scrubHook?: (tableName: string, row: Record<string, unknown>) => Record<string, unknown>;
8
8
  onProgress?: (msg: string) => void;
9
+ onTableProgress?: (tableIdx: number, totalTables: number, tableName: string) => void;
9
10
  }
10
11
  export interface DbCopyResult {
11
12
  tables: number;
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/migrate/db.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC9B,MAAM,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6BD,wBAAsB,MAAM,CAC1B,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAqHvB"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/migrate/db.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC9B,MAAM,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACtF;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6BD,wBAAsB,MAAM,CAC1B,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAqHvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"db.postgrest.d.ts","sourceRoot":"","sources":["../../src/migrate/db.postgrest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM3D,wBAAsB,eAAe,CACnC,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA6GvB"}
1
+ {"version":3,"file":"db.postgrest.d.ts","sourceRoot":"","sources":["../../src/migrate/db.postgrest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM3D,wBAAsB,eAAe,CACnC,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA+GvB"}
@@ -43,7 +43,9 @@ export async function copyDbPostgrest(srcAdapter, tgtAdapter, opts = {}) {
43
43
  let totalScrubbed = 0;
44
44
  // --- Copy pass: forward dependency order ---
45
45
  for (const table of tablesToProcess) {
46
- opts.onProgress?.(`Copying table: ${table}`);
46
+ const tableIdx = tablesToProcess.indexOf(table);
47
+ opts.onProgress?.(`Copying table ${tableIdx + 1}/${tablesToProcess.length}: ${table}`);
48
+ opts.onTableProgress?.(tableIdx + 1, tablesToProcess.length, table);
47
49
  const pk = pkOverrides[table] ?? 'id';
48
50
  const srcService = connect.createCrudService(srcAdapter, table, { autoId: false });
49
51
  const tgtService = connect.createCrudService(tgtAdapter, table, { autoId: false });
@@ -2,8 +2,9 @@ import type { HazoEnv } from '../types/index.js';
2
2
  export interface FilesCopyOptions {
3
3
  fromEnv: HazoEnv;
4
4
  toEnv: HazoEnv;
5
+ transport?: 'local' | 'rsync';
5
6
  fileScrubHook?: (filePath: string) => Promise<void>;
6
- onProgress?: (msg: string) => void;
7
+ onProgress?: (msg: string, percent?: number) => void;
7
8
  }
8
9
  export interface FilesCopyResult {
9
10
  copied: number;
@@ -1 +1 @@
1
- {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/migrate/files.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AA0BD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4ChF"}
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/migrate/files.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AAoFD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkDhF"}
@@ -6,7 +6,10 @@
6
6
  // Failures are non-fatal warnings — the file copy still completes.
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
+ import { spawn } from 'node:child_process';
9
10
  import { resolveFilesRoot } from '../resolve/files.js';
11
+ import { resolveSshConfig } from '../resolve/ssh.js';
12
+ import { assertSafeSshField, setupKnownHosts } from './ssh-exec.js';
10
13
  function resolveEnvFilesRoot(env) {
11
14
  return resolveFilesRoot(env);
12
15
  }
@@ -29,7 +32,65 @@ async function scrubPdfFile(filePath) {
29
32
  // Non-fatal — PDF scrubbing failure is a warning, not an abort
30
33
  }
31
34
  }
35
+ async function copyFilesRsync(opts, ssh) {
36
+ const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
37
+ fs.mkdirSync(tgtRoot, { recursive: true });
38
+ assertSafeSshField('user', ssh.user);
39
+ assertSafeSshField('host', ssh.host);
40
+ assertSafeSshField('key', ssh.key);
41
+ assertSafeSshField('path', ssh.path);
42
+ const { knownHosts, strict } = setupKnownHosts(ssh);
43
+ const remote = `${ssh.user}@${ssh.host}:${ssh.path}/`;
44
+ const sshCmd = `ssh -i ${ssh.key} -o StrictHostKeyChecking=${strict} -o UserKnownHostsFile=${knownHosts}`;
45
+ const args = ['-az', '--partial', '--info=progress2', '-e', sshCmd, remote, tgtRoot + '/'];
46
+ return new Promise((resolve, reject) => {
47
+ const proc = spawn('rsync', args);
48
+ let totalFiles = 0;
49
+ let doneFiles = 0;
50
+ let totalBytes = 0;
51
+ proc.stdout.on('data', (chunk) => {
52
+ const text = chunk.toString();
53
+ // Parse --info=progress2: lines like " 117,504,614 100% 108.64MB/s 0:00:01 (xfr#123, to-chk=0/456)"
54
+ for (const line of text.split('\n')) {
55
+ // Extract to-chk=N/TOTAL
56
+ const chkMatch = line.match(/to-chk=(\d+)\/(\d+)/);
57
+ if (chkMatch) {
58
+ const remaining = parseInt(chkMatch[1], 10);
59
+ const total = parseInt(chkMatch[2], 10);
60
+ if (totalFiles === 0)
61
+ totalFiles = total;
62
+ doneFiles = total - remaining;
63
+ const pct = total > 0 ? Math.round((doneFiles / total) * 100) : 0;
64
+ opts.onProgress?.(`rsync: ${doneFiles}/${total} files (${pct}%)`, pct);
65
+ }
66
+ // Extract bytes: first number on lines with a %
67
+ const bytesMatch = line.match(/^\s+([\d,]+)\s+\d+%/);
68
+ if (bytesMatch) {
69
+ totalBytes = parseInt(bytesMatch[1].replace(/,/g, ''), 10);
70
+ }
71
+ }
72
+ });
73
+ proc.stderr.on('data', (chunk) => {
74
+ opts.onProgress?.(`rsync: ${chunk.toString().trim()}`, undefined);
75
+ });
76
+ proc.on('error', (err) => reject(err));
77
+ proc.on('close', (code) => {
78
+ if (code !== 0) {
79
+ reject(new Error(`rsync exited with code ${code}`));
80
+ }
81
+ else {
82
+ resolve({ copied: doneFiles || totalFiles, bytes: totalBytes, placeholdered: 0 });
83
+ }
84
+ });
85
+ });
86
+ }
32
87
  export async function copyFiles(opts) {
88
+ const ssh = resolveSshConfig(opts.fromEnv);
89
+ if (opts.transport === 'rsync' || (opts.transport !== 'local' && ssh !== null)) {
90
+ if (!ssh)
91
+ throw new Error(`rsync requested but no [transport.ssh.${opts.fromEnv}] config found`);
92
+ return copyFilesRsync(opts, ssh);
93
+ }
33
94
  const srcRoot = resolveEnvFilesRoot(opts.fromEnv);
34
95
  const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
35
96
  if (!fs.existsSync(srcRoot)) {
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/migrate/run.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAkC,MAAM,mBAAmB,CAAC;AA0D3G,wBAAsB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkLlF"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/migrate/run.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAkC,MAAM,mBAAmB,CAAC;AA0D3G,wBAAsB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyMlF"}
@@ -172,17 +172,40 @@ export async function runMigration(req) {
172
172
  // Step 5: DB copy
173
173
  if (includeDb) {
174
174
  emitProgress(req, { phase: 'db', message: 'Copying database tables', percent: 20 });
175
- const scrubHook = await buildScrubHook(req.to, scrubMode, tgtAdapter);
176
- const result = await copyDb(srcAdapter, tgtAdapter, {
177
- type: fromDbConfig.type,
178
- tables: req.tables ?? migrateConfig.tables,
179
- preserve: migrateConfig.preserve,
180
- pkOverrides: migrateConfig.pkOverrides,
181
- scrubHook,
182
- onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
183
- });
184
- dbResult = result;
185
- emitProgress(req, { phase: 'db', message: `DB copy complete: ${result.tables} tables, ${result.rows} rows`, percent: 70 });
175
+ if (req.transport === 'dump-restore') {
176
+ const { resolveBackupConfig } = await import('../resolve/backup.js');
177
+ const { restoreDbViaDump } = await import('./db-dump-restore.js');
178
+ const backup = resolveBackupConfig(req.to);
179
+ if (!backup) {
180
+ throw new HazoError({
181
+ code: 'HAZO_ENV_INVALID_REQUEST',
182
+ pkg: 'hazo_env',
183
+ message: `[backup.${req.to}] config section is required for dump-restore transport`,
184
+ });
185
+ }
186
+ dbResult = await restoreDbViaDump(backup, {
187
+ onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
188
+ });
189
+ emitProgress(req, { phase: 'db', message: 'DB restore via dump complete', percent: 70 });
190
+ }
191
+ else {
192
+ const scrubHook = await buildScrubHook(req.to, scrubMode, tgtAdapter);
193
+ const result = await copyDb(srcAdapter, tgtAdapter, {
194
+ type: fromDbConfig.type,
195
+ tables: req.tables ?? migrateConfig.tables,
196
+ preserve: migrateConfig.preserve,
197
+ pkOverrides: migrateConfig.pkOverrides,
198
+ scrubHook,
199
+ onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
200
+ onTableProgress: (idx, total, name) => emitProgress(req, {
201
+ phase: 'db',
202
+ message: `table ${idx}/${total}: ${name}`,
203
+ percent: 20 + Math.round((idx / total) * 50),
204
+ }),
205
+ });
206
+ dbResult = result;
207
+ emitProgress(req, { phase: 'db', message: `DB copy complete: ${result.tables} tables, ${result.rows} rows`, percent: 70 });
208
+ }
186
209
  }
187
210
  // Step 6: File copy
188
211
  if (includeFiles) {
@@ -0,0 +1,30 @@
1
+ import type { SshConfig } from '../resolve/ssh.js';
2
+ /**
3
+ * Reject SSH config values that could smuggle extra SSH options through
4
+ * shell argument expansion. Values starting with '-' (e.g. -oProxyCommand=...)
5
+ * or containing whitespace would be re-split by SSH and treated as flags.
6
+ */
7
+ export declare function assertSafeSshField(name: string, value: string): void;
8
+ /**
9
+ * Set up a persistent known_hosts file for SSH host-key pinning.
10
+ * When ssh.hostKey is set, the key is pinned and strict checking is enabled
11
+ * (no TOFU). Otherwise, accept-new is used — trust on first use, then persist.
12
+ *
13
+ * Returns { knownHosts: absolute path, strict: 'yes' | 'accept-new' }.
14
+ */
15
+ export declare function setupKnownHosts(ssh: SshConfig): {
16
+ knownHosts: string;
17
+ strict: string;
18
+ };
19
+ /**
20
+ * Run a bash script on a remote host via SSH, piping remoteScript to stdin.
21
+ *
22
+ * Spawns: ssh -i <key> -o StrictHostKeyChecking=<strict>
23
+ * -o UserKnownHostsFile=<knownHosts> <user>@<host> bash -s
24
+ *
25
+ * Streams stdout/stderr lines to opts.onProgress. Rejects on non-zero exit.
26
+ */
27
+ export declare function runSsh(ssh: SshConfig, remoteScript: string, opts?: {
28
+ onProgress?: (msg: string) => void;
29
+ }): Promise<void>;
30
+ //# sourceMappingURL=ssh-exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh-exec.d.ts","sourceRoot":"","sources":["../../src/migrate/ssh-exec.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMpE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAYtF;AAED;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,GAAG,EAAE,SAAS,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -0,0 +1,82 @@
1
+ // hazo_env/src/migrate/ssh-exec.ts — Shared SSH execution helpers
2
+ //
3
+ // Extracted from files.ts so that db-dump-restore.ts can reuse the same
4
+ // known_hosts pinning and safe-field validation without duplication.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { spawn } from 'node:child_process';
8
+ /**
9
+ * Reject SSH config values that could smuggle extra SSH options through
10
+ * shell argument expansion. Values starting with '-' (e.g. -oProxyCommand=...)
11
+ * or containing whitespace would be re-split by SSH and treated as flags.
12
+ */
13
+ export function assertSafeSshField(name, value) {
14
+ if (/^-/.test(value) || /\s/.test(value)) {
15
+ throw new Error(`Unsafe transport.ssh.${name} value: must not start with '-' or contain whitespace`);
16
+ }
17
+ }
18
+ /**
19
+ * Set up a persistent known_hosts file for SSH host-key pinning.
20
+ * When ssh.hostKey is set, the key is pinned and strict checking is enabled
21
+ * (no TOFU). Otherwise, accept-new is used — trust on first use, then persist.
22
+ *
23
+ * Returns { knownHosts: absolute path, strict: 'yes' | 'accept-new' }.
24
+ */
25
+ export function setupKnownHosts(ssh) {
26
+ const knownHosts = path.resolve(process.cwd(), 'config', 'hazo_env_known_hosts');
27
+ fs.mkdirSync(path.dirname(knownHosts), { recursive: true });
28
+ if (ssh.hostKey) {
29
+ // Pin the operator-provided host key; strict checking, no first-use prompt.
30
+ const line = `${ssh.host} ${ssh.hostKey}\n`;
31
+ const existing = fs.existsSync(knownHosts) ? fs.readFileSync(knownHosts, 'utf8') : '';
32
+ if (!existing.includes(line.trim()))
33
+ fs.appendFileSync(knownHosts, line);
34
+ }
35
+ // Pinned key → strict verify; otherwise accept-new — never disable host-key checking.
36
+ const strict = ssh.hostKey ? 'yes' : 'accept-new';
37
+ return { knownHosts, strict };
38
+ }
39
+ /**
40
+ * Run a bash script on a remote host via SSH, piping remoteScript to stdin.
41
+ *
42
+ * Spawns: ssh -i <key> -o StrictHostKeyChecking=<strict>
43
+ * -o UserKnownHostsFile=<knownHosts> <user>@<host> bash -s
44
+ *
45
+ * Streams stdout/stderr lines to opts.onProgress. Rejects on non-zero exit.
46
+ */
47
+ export async function runSsh(ssh, remoteScript, opts) {
48
+ const { knownHosts, strict } = setupKnownHosts(ssh);
49
+ const sshArgs = [
50
+ '-i', ssh.key,
51
+ '-o', `StrictHostKeyChecking=${strict}`,
52
+ '-o', `UserKnownHostsFile=${knownHosts}`,
53
+ `${ssh.user}@${ssh.host}`,
54
+ 'bash', '-s',
55
+ ];
56
+ return new Promise((resolve, reject) => {
57
+ const proc = spawn('ssh', sshArgs);
58
+ proc.stdin.write(remoteScript);
59
+ proc.stdin.end();
60
+ proc.stdout.on('data', (chunk) => {
61
+ for (const line of chunk.toString().split('\n')) {
62
+ if (line.trim())
63
+ opts?.onProgress?.(line);
64
+ }
65
+ });
66
+ proc.stderr.on('data', (chunk) => {
67
+ for (const line of chunk.toString().split('\n')) {
68
+ if (line.trim())
69
+ opts?.onProgress?.(line);
70
+ }
71
+ });
72
+ proc.on('error', (err) => reject(err));
73
+ proc.on('close', (code) => {
74
+ if (code !== 0) {
75
+ reject(new Error(`ssh exited with code ${code}`));
76
+ }
77
+ else {
78
+ resolve();
79
+ }
80
+ });
81
+ });
82
+ }
@@ -1,3 +1,3 @@
1
1
  import type { TransportMode, DbEnvConfig } from '../types/index.js';
2
- export declare function resolveTransport(fromConfig: DbEnvConfig, toConfig: DbEnvConfig, requested?: TransportMode): 'local' | 'api';
2
+ export declare function resolveTransport(fromConfig: DbEnvConfig, toConfig: DbEnvConfig, requested?: TransportMode): 'local' | 'api' | 'rsync' | 'dump-restore';
3
3
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/migrate/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,WAAW,EACvB,QAAQ,EAAE,WAAW,EACrB,SAAS,GAAE,aAAsB,GAChC,OAAO,GAAG,KAAK,CAYjB"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/migrate/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,WAAW,EACvB,QAAQ,EAAE,WAAW,EACrB,SAAS,GAAE,aAAsB,GAChC,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,cAAc,CAO5C"}
@@ -1,13 +1,8 @@
1
- // hazo_env/src/migrate/transport.ts — resolve transport mode for a migration
2
- import { HazoError } from 'hazo_core';
3
1
  export function resolveTransport(fromConfig, toConfig, requested = 'auto') {
4
- if (requested === 'ssh') {
5
- throw new HazoError({
6
- code: 'HAZO_ENV_NOT_IMPLEMENTED',
7
- pkg: 'hazo_env',
8
- message: 'SSH transport is not yet implemented. Use local or api transport.',
9
- });
10
- }
2
+ if (requested === 'dump-restore')
3
+ return 'dump-restore';
4
+ if (requested === 'ssh')
5
+ return 'rsync';
11
6
  if (requested === 'api')
12
7
  return 'api';
13
8
  // auto: both sqlite → local
@@ -0,0 +1,23 @@
1
+ export interface BackupConfig {
2
+ /** Which [transport.ssh.*] connection to reuse for the SSH session */
3
+ ssh_env: string;
4
+ /** The Postgres database to drop/recreate on the remote */
5
+ target_db: string;
6
+ /** Owner passed to createdb -O */
7
+ owner: string;
8
+ /** Glob matching an existing dump file, or the literal string "fresh" */
9
+ dump_source: string;
10
+ /** Source DB for pg_dump — required when dump_source === 'fresh' */
11
+ fresh_dump_db?: string;
12
+ /** Shell command to run on the remote before dropping/restoring the DB */
13
+ pre_restore_cmd?: string;
14
+ /** Shell command to run on the remote after a successful restore */
15
+ post_restore_cmd?: string;
16
+ }
17
+ /**
18
+ * Resolve the [backup.<env>] section from hazo_env_config.ini.
19
+ * Returns null when the section is missing (transport not configured).
20
+ * Throws when required fields are absent or ssh_env is unsafe.
21
+ */
22
+ export declare function resolveBackupConfig(env: string): BackupConfig | null;
23
+ //# sourceMappingURL=backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/resolve/backup.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAcD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA8BpE"}
@@ -0,0 +1,52 @@
1
+ // hazo_env/src/resolve/backup.ts — dump-restore backup transport config resolver
2
+ //
3
+ // Reads [backup.<env>] from config/hazo_env_config.ini and returns a BackupConfig,
4
+ // or null when the section is absent (transport not configured for that env).
5
+ import path from 'node:path';
6
+ import { HazoConfig } from 'hazo_config/server';
7
+ import { assertSafeSshField } from '../migrate/ssh-exec.js';
8
+ function tryLoadEnvConfig() {
9
+ try {
10
+ return new HazoConfig({ filePath: path.resolve(process.cwd(), 'config', 'hazo_env_config.ini') });
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function expandEnvVars(value) {
17
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '');
18
+ }
19
+ /**
20
+ * Resolve the [backup.<env>] section from hazo_env_config.ini.
21
+ * Returns null when the section is missing (transport not configured).
22
+ * Throws when required fields are absent or ssh_env is unsafe.
23
+ */
24
+ export function resolveBackupConfig(env) {
25
+ const config = tryLoadEnvConfig();
26
+ if (!config)
27
+ return null;
28
+ const section = config.getSection(`backup.${env}`);
29
+ if (!section)
30
+ return null;
31
+ const ssh_env_raw = section['ssh_env'];
32
+ const target_db_raw = section['target_db'];
33
+ const owner_raw = section['owner'];
34
+ const dump_source_raw = section['dump_source'];
35
+ const fresh_dump_db_raw = section['fresh_dump_db'];
36
+ const pre_restore_cmd_raw = section['pre_restore_cmd'];
37
+ const post_restore_cmd_raw = section['post_restore_cmd'];
38
+ if (!ssh_env_raw || !target_db_raw || !owner_raw || !dump_source_raw)
39
+ return null;
40
+ const ssh_env = expandEnvVars(ssh_env_raw);
41
+ // ssh_env is used as an SSH argument — must not contain shell-hostile chars
42
+ assertSafeSshField('ssh_env', ssh_env);
43
+ return {
44
+ ssh_env,
45
+ target_db: expandEnvVars(target_db_raw),
46
+ owner: expandEnvVars(owner_raw),
47
+ dump_source: expandEnvVars(dump_source_raw),
48
+ fresh_dump_db: fresh_dump_db_raw ? expandEnvVars(fresh_dump_db_raw) : undefined,
49
+ pre_restore_cmd: pre_restore_cmd_raw ? expandEnvVars(pre_restore_cmd_raw) : undefined,
50
+ post_restore_cmd: post_restore_cmd_raw ? expandEnvVars(post_restore_cmd_raw) : undefined,
51
+ };
52
+ }
@@ -0,0 +1,15 @@
1
+ export interface SshConfig {
2
+ host: string;
3
+ user: string;
4
+ key: string;
5
+ path: string;
6
+ /**
7
+ * Optional pinned host public key (the right-hand side of a known_hosts line,
8
+ * e.g. "ssh-ed25519 AAAA...."). When set, host-key checking is strict and the
9
+ * key is pinned — no TOFU. When absent, the transport falls back to
10
+ * accept-new against a persistent known_hosts file.
11
+ */
12
+ hostKey?: string;
13
+ }
14
+ export declare function resolveSshConfig(env: string): SshConfig | null;
15
+ //# sourceMappingURL=ssh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../../src/resolve/ssh.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAsB9D"}
@@ -0,0 +1,36 @@
1
+ // hazo_env/src/resolve/ssh.ts — SSH transport config resolver
2
+ import path from 'node:path';
3
+ import { HazoConfig } from 'hazo_config/server';
4
+ function tryLoadEnvConfig() {
5
+ try {
6
+ return new HazoConfig({ filePath: path.resolve(process.cwd(), 'config', 'hazo_env_config.ini') });
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ function expandEnvVars(value) {
13
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '');
14
+ }
15
+ export function resolveSshConfig(env) {
16
+ const config = tryLoadEnvConfig();
17
+ if (!config)
18
+ return null;
19
+ const section = config.getSection(`transport.ssh.${env}`);
20
+ if (!section)
21
+ return null;
22
+ const host = section['host'];
23
+ const user = section['user'];
24
+ const key = section['key'];
25
+ const remotePath = section['path'];
26
+ const hostKey = section['host_key'];
27
+ if (!host || !user || !key || !remotePath)
28
+ return null;
29
+ return {
30
+ host: expandEnvVars(host),
31
+ user: expandEnvVars(user),
32
+ key: expandEnvVars(key),
33
+ path: expandEnvVars(remotePath),
34
+ hostKey: hostKey ? expandEnvVars(hostKey) : undefined,
35
+ };
36
+ }
@@ -43,11 +43,11 @@ export interface ResolveConnectOptions {
43
43
  env?: HazoEnv;
44
44
  allowOtherEnv?: boolean;
45
45
  }
46
- export type TransportMode = 'auto' | 'local' | 'ssh' | 'api';
46
+ export type TransportMode = 'auto' | 'local' | 'ssh' | 'api' | 'dump-restore';
47
47
  export type ScrubMode = 'auto' | 'none';
48
48
  export type HashMode = 'none' | 'sample' | 'full';
49
49
  export interface MigrationProgress {
50
- phase: 'validate' | 'snapshot' | 'plan' | 'schema-check' | 'db' | 'files' | 'verify' | 'finalize' | 'done';
50
+ phase: 'validate' | 'snapshot' | 'plan' | 'schema-check' | 'db' | 'files' | 'verify' | 'finalize' | 'done' | 'clear';
51
51
  message: string;
52
52
  percent?: number;
53
53
  }
@@ -75,6 +75,7 @@ export interface MigrationResult {
75
75
  tables: number;
76
76
  rows: number;
77
77
  scrubbed: number;
78
+ mode?: string;
78
79
  };
79
80
  files?: {
80
81
  copied: number;
@@ -102,5 +103,24 @@ export interface MigrateConfig {
102
103
  preserve?: string[];
103
104
  pkOverrides?: Record<string, string>;
104
105
  }
106
+ export interface ClearRequest {
107
+ env: HazoEnv;
108
+ include?: {
109
+ db?: boolean;
110
+ files?: boolean;
111
+ };
112
+ allowProdTarget?: boolean;
113
+ confirmToken?: string;
114
+ jobId?: string;
115
+ progressDir?: string;
116
+ onProgress?: (p: MigrationProgress) => void;
117
+ }
118
+ export interface ClearResult {
119
+ ok: boolean;
120
+ tablesCleared: number;
121
+ filesCleared: boolean;
122
+ durationMs: number;
123
+ warnings: string[];
124
+ }
105
125
  export type MaskTransform = (value: unknown, key: string, maskKey: string) => unknown;
106
126
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnE,mCAAmC;AACnC,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAE5E,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC;AAElC,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAErD,2CAA2C;AAC3C,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEtD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC7B,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH;AAED,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,uCAAuC;AACvC,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAID,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AACxC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,cAAc,GAAG,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3G,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5C,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAGD,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_env",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Canonical environment resolver — typed env names, per-env DB/file/secret config, doctor and CLI for hazo apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",