hazo_env 0.1.1 → 0.2.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 (43) hide show
  1. package/README.md +46 -4
  2. package/SETUP_CHECKLIST.md +47 -0
  3. package/config/hazo_env_masking.ini.sample +16 -16
  4. package/dist/cli.js +167 -6
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +8 -0
  8. package/dist/lib/index.d.ts +1 -1
  9. package/dist/lib/index.d.ts.map +1 -1
  10. package/dist/lib/index.js +2 -1
  11. package/dist/lib/secret_columns.d.ts +3 -0
  12. package/dist/lib/secret_columns.d.ts.map +1 -0
  13. package/dist/lib/secret_columns.js +14 -0
  14. package/dist/mask/registry.d.ts +8 -0
  15. package/dist/mask/registry.d.ts.map +1 -0
  16. package/dist/mask/registry.js +29 -0
  17. package/dist/mask/ruleset.d.ts +15 -0
  18. package/dist/mask/ruleset.d.ts.map +1 -0
  19. package/dist/mask/ruleset.js +77 -0
  20. package/dist/migrate/audit.d.ts +21 -0
  21. package/dist/migrate/audit.d.ts.map +1 -0
  22. package/dist/migrate/audit.js +36 -0
  23. package/dist/migrate/db.d.ts +13 -0
  24. package/dist/migrate/db.d.ts.map +1 -0
  25. package/dist/migrate/db.js +122 -0
  26. package/dist/migrate/files.d.ts +14 -0
  27. package/dist/migrate/files.d.ts.map +1 -0
  28. package/dist/migrate/files.js +72 -0
  29. package/dist/migrate/run.d.ts +3 -0
  30. package/dist/migrate/run.d.ts.map +1 -0
  31. package/dist/migrate/run.js +195 -0
  32. package/dist/migrate/snapshot.d.ts +8 -0
  33. package/dist/migrate/snapshot.d.ts.map +1 -0
  34. package/dist/migrate/snapshot.js +48 -0
  35. package/dist/migrate/transport.d.ts +3 -0
  36. package/dist/migrate/transport.d.ts.map +1 -0
  37. package/dist/migrate/transport.js +17 -0
  38. package/dist/migrate/verify.d.ts +8 -0
  39. package/dist/migrate/verify.d.ts.map +1 -0
  40. package/dist/migrate/verify.js +51 -0
  41. package/dist/types/index.d.ts +42 -27
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/package.json +15 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # hazo_env
2
2
 
3
- Canonical environment resolver for hazo apps. Typed env names, per-env DB/file/secret config, a `doctor` diagnostic command, and a `hazo-env` CLI.
3
+ Canonical environment resolver for hazo apps. Typed env names, per-env DB/file/secret config, a DB+files migration engine with PII masking, a `doctor` diagnostic command, and a `hazo-env` CLI.
4
4
 
5
5
  ## What it does
6
6
 
@@ -8,8 +8,10 @@ Canonical environment resolver for hazo apps. Typed env names, per-env DB/file/s
8
8
  - **Per-env DB config** — `resolveConnectConfig()` maps the current env to its `hazo_connect` config (SQLite or PostgREST) with zero hardcoded connection strings in app code.
9
9
  - **Per-env file config** — `resolveFilesConfig()` maps the current env to a `hazo_files` config rooted at the declared `data_root`.
10
10
  - **Secrets** — `getSecret()` resolves from `.env.local` only; placeholders in `hazo_env_config.ini` are substituted at runtime without storing secrets.
11
+ - **Migration engine** — `runMigration({ from, to })` copies a DB + files between envs. Validates → snapshots target → copies tables (paged, schema-checked) → copies files → verifies → writes hazo_audit entry. Prod target refused without an explicit confirm token.
12
+ - **PII masking** — masking rules declared per table/column in `hazo_env_masking.ini` (seed) and synced to `hazo_app_config` at runtime. Built-in transforms: `mask_email`, `fake_name`, `mask_phone`, `jitter_date`, `hash`, `tokenize`, `drop`, `nullify`. Custom transforms via `registerMask()`. Applied automatically when the migration target role is `testing` or `staging`.
11
13
  - **Doctor** — `doctor()` / `hazo-env doctor` validates pattern, DB reachability, required secrets (no values printed), data_root writability, and schema level.
12
- - **CLI** — `hazo-env current | doctor | snapshot`.
14
+ - **CLI** — `hazo-env current | doctor | snapshot | migrate | verify | restore | mask`.
13
15
 
14
16
  ## Installation
15
17
 
@@ -18,7 +20,7 @@ npm install hazo_env
18
20
  ```
19
21
 
20
22
  Peer deps (required): `hazo_core`, `hazo_config`.
21
- Peer deps (optional): `hazo_connect` (for `resolveConnectConfig`), `hazo_files` (for `resolveFilesConfig`).
23
+ Peer deps (optional): `hazo_connect` (for `resolveConnectConfig`), `hazo_files` (for `resolveFilesConfig`), `hazo_secure` (for masking transforms), `hazo_pdf` (for `mask_pdf` transform), `hazo_audit` (for migration audit entries).
22
24
 
23
25
  ## Quick start
24
26
 
@@ -101,12 +103,52 @@ const report = await doctor();
101
103
  // report.checks DoctorCheck[] — { label, ok, message }
102
104
  ```
103
105
 
106
+ ### Migration engine (server-only)
107
+
108
+ ```ts
109
+ import { runMigration, verifyFiles, takeSnapshot, restoreSnapshot } from 'hazo_env';
110
+
111
+ // Copy prod → staging (with PII masking applied automatically)
112
+ const result = await runMigration({
113
+ from: 'prod',
114
+ to: 'staging',
115
+ dryRun: false, // set true for a plan with counts, no writes
116
+ // allowProdTarget: true, // required when `to` is 'prod'
117
+ // confirmToken: '<token>', // required together with allowProdTarget
118
+ onProgress: (p) => console.log(p.phase, p.message),
119
+ });
120
+ // result.ok, result.db, result.files, result.snapshotId, result.warnings, result.durationMs
121
+
122
+ // Stand-alone verification
123
+ await verifyFiles('staging', dataRoot, { hash: 'sample' });
124
+
125
+ // Manual snapshot / restore
126
+ const snap = takeSnapshot(connectConfig); // returns { snapshotId }
127
+ restoreSnapshot(connectConfig, snapshotId);
128
+ ```
129
+
130
+ **Masking** — customize per table/column in `config/hazo_env_masking.ini`. Sync to the DB with `hazo-env mask sync`. Register custom transforms with `registerMask(name, fn)`.
131
+
104
132
  ## CLI
105
133
 
106
134
  ```
107
135
  hazo-env current # prints env, role, pattern, app, data_root
108
136
  hazo-env doctor [--env <e>] [--all] # red/green validation table
109
- hazo-env snapshot <env> # placeholder full impl arrives in M2
137
+ hazo-env snapshot <env> # snapshot the target env DB
138
+
139
+ hazo-env migrate --from <env> --to <env> [options]
140
+ --transport auto|local|ssh|api # default auto
141
+ --no-db | --no-files # skip DB or file copy
142
+ --scrub auto|none # default auto (mask when target is test/staging)
143
+ --tables a,b,c # restrict to specific tables
144
+ --dry-run # plan + counts, no writes
145
+ --allow-prod-target --confirm <tok> # required when writing prod
146
+
147
+ hazo-env verify <env> [--hash none|sample|full] [--no-orphans]
148
+ hazo-env restore <env> --snapshot <snapshot-id>
149
+
150
+ hazo-env mask sync # sync hazo_env_masking.ini → hazo_app_config
151
+ hazo-env mask list # print active masking rules
110
152
  ```
111
153
 
112
154
  ## Config reference (`hazo_env_config.ini`)
@@ -8,6 +8,8 @@ Follow these steps when adding `hazo_env` to a consuming application.
8
8
  npm install hazo_env hazo_core hazo_config
9
9
  # optional — only if you use resolveConnectConfig / resolveFilesConfig:
10
10
  npm install hazo_connect hazo_files
11
+ # optional — only if you use the migration engine with masking + PDF scrub + audit trail:
12
+ npm install hazo_secure hazo_pdf hazo_audit
11
13
  ```
12
14
 
13
15
  ## 2. Create `hazo_env_config.ini`
@@ -90,3 +92,48 @@ const nextConfig = {
90
92
  transpilePackages: ['hazo_env', 'hazo_core', /* … */],
91
93
  };
92
94
  ```
95
+
96
+ ## 9. (Optional) Set up the migration engine
97
+
98
+ Skip this section if you only need env resolution and resolvers.
99
+
100
+ ### 9a. Create `hazo_env_masking.ini`
101
+
102
+ Copy `node_modules/hazo_env/config/hazo_env_masking.ini.sample` to your app's `config/` directory and declare per-table masking rules:
103
+
104
+ ```ini
105
+ [hazo_users]
106
+ email = mask_email
107
+ full_name = fake_name
108
+ password_hash = drop
109
+
110
+ [my_app_clients]
111
+ notes = redact_pii
112
+ dob = jitter_date
113
+ ```
114
+
115
+ Sync to the DB so the runtime engine can read them:
116
+
117
+ ```bash
118
+ npx hazo-env mask sync
119
+ ```
120
+
121
+ ### 9b. Set `HAZO_ENV_MASK_KEY` in `.env.local`
122
+
123
+ Required for deterministic masking transforms (`hash`, `tokenize`). If absent, migration aborts when scrub mode is `auto`.
124
+
125
+ ```
126
+ HAZO_ENV_MASK_KEY=<random-32-char-hex>
127
+ ```
128
+
129
+ ### 9c. Run a dry-run migration to verify the plan
130
+
131
+ ```bash
132
+ npx hazo-env migrate --from prod --to staging --dry-run
133
+ ```
134
+
135
+ Review row counts and scrub counts before running without `--dry-run`. The migration will snapshot the target env before writing so you can restore if needed:
136
+
137
+ ```bash
138
+ npx hazo-env restore staging --snapshot <snapshot-id>
139
+ ```
@@ -1,19 +1,19 @@
1
1
  ; hazo_env masking ruleset sample
2
- ; Copy to hazo_env_masking.ini and customize.
3
- ; This file seeds the masking engine with default rules for PII fields.
4
- ; The runtime source of truth (once hazo_admin is available) is the DB-backed ruleset.
2
+ ; Copy to config/hazo_env_masking.ini and customise.
3
+ ; Load into hazo_app_config with: hazo-env mask sync
4
+ ;
5
+ ; Format: [table_name] then column = transform_name
6
+ ; Transforms: mask_email, mask_phone, fake_name, jitter_date, hash, tokenize, drop, nullify
7
+ ; Custom transforms can be registered via registerMask() in your app.
5
8
 
6
- [rule.users_email]
7
- table = hazo_auth_users
8
- column = email
9
- strategy = mask_email
9
+ [hazo_users]
10
+ email = mask_email
11
+ full_name = fake_name
12
+ phone = mask_phone
13
+ dob = jitter_date
14
+ password_hash = drop
10
15
 
11
- [rule.users_name]
12
- table = hazo_auth_users
13
- column = full_name
14
- strategy = fake_name
15
-
16
- [rule.api_keys]
17
- table = hazo_api_keys
18
- column = key_hash
19
- strategy = drop
16
+ [hazo_auth_users]
17
+ email = mask_email
18
+ full_name = fake_name
19
+ password_hash = drop
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // hazo_env/src/cli.ts — CLI entry: hazo-env current | doctor | snapshot
2
+ // hazo_env/src/cli.ts — CLI entry: hazo-env current | doctor | snapshot | migrate | verify | restore
3
3
  import pc from 'picocolors';
4
4
  import { getEnv, getEnvRole } from './index.client.js';
5
5
  import { describeEnv } from './env.server.js';
@@ -60,10 +60,151 @@ async function runDoctor() {
60
60
  }
61
61
  async function runSnapshot() {
62
62
  const targetEnv = args[0] ?? getEnv();
63
- console.log(`\n${pc.yellow('snapshot')} — not implemented until Phase 2 (migration engine)\n`);
64
- console.log(` Requested snapshot of env: ${pc.bold(targetEnv)}`);
65
- console.log(` Run "hazo-env snapshot" again after the migration engine is built.\n`);
66
- process.exit(1);
63
+ console.log(`\n${pc.bold('hazo-env snapshot')} ${pc.dim(targetEnv)}\n`);
64
+ const { takeSnapshot } = await import('./migrate/snapshot.js');
65
+ const { resolveConnectConfig } = await import('./resolve/connect.js');
66
+ const toConfig = resolveConnectConfig({ env: targetEnv, allowOtherEnv: true });
67
+ const result = takeSnapshot(toConfig);
68
+ console.log(` ${pc.green('✓')} Snapshot: ${result.snapshotId}\n`);
69
+ }
70
+ async function runMigrate() {
71
+ const fromIdx = args.findIndex((a) => a === '--from');
72
+ const toIdx = args.findIndex((a) => a === '--to');
73
+ const from = fromIdx >= 0 ? args[fromIdx + 1] : undefined;
74
+ const to = toIdx >= 0 ? args[toIdx + 1] : undefined;
75
+ const dryRun = args.includes('--dry-run');
76
+ const noFiles = args.includes('--no-files');
77
+ const noDb = args.includes('--no-db');
78
+ const allowProdTarget = args.includes('--allow-prod-target');
79
+ const confirmIdx = args.findIndex((a) => a === '--confirm');
80
+ const confirmToken = confirmIdx >= 0 ? args[confirmIdx + 1] : undefined;
81
+ const tablesIdx = args.findIndex((a) => a === '--tables');
82
+ const tablesArg = tablesIdx >= 0 ? args[tablesIdx + 1] : undefined;
83
+ const tables = tablesArg ? tablesArg.split(',').map((t) => t.trim()) : undefined;
84
+ if (!from || !to) {
85
+ console.error(pc.red('Error: migrate requires --from <env> and --to <env>'));
86
+ process.exit(1);
87
+ }
88
+ console.log(`\n${pc.bold('hazo-env migrate')} ${pc.dim(`${from} → ${to}${dryRun ? ' (dry-run)' : ''}`)}\n`);
89
+ const { runMigration } = await import('./migrate/run.js');
90
+ try {
91
+ const result = await runMigration({
92
+ from,
93
+ to,
94
+ include: { db: !noDb, files: !noFiles },
95
+ tables: tables ?? '*',
96
+ dryRun,
97
+ allowProdTarget,
98
+ confirmToken,
99
+ onProgress: (p) => {
100
+ const pct = p.percent != null ? `${p.percent}%`.padStart(4) + ' ' : ' ';
101
+ console.log(` ${pct}${pc.dim(p.phase.padEnd(12))} ${p.message}`);
102
+ },
103
+ });
104
+ if (result.ok) {
105
+ console.log(`\n ${pc.green('✓')} Migration ${dryRun ? 'plan built (dry-run)' : 'complete'}`);
106
+ if (result.db)
107
+ console.log(` ${pc.dim('DB:')} ${result.db.tables} tables, ${result.db.rows} rows${result.db.scrubbed ? `, ${result.db.scrubbed} scrubbed` : ''}`);
108
+ if (result.files)
109
+ console.log(` ${pc.dim('Files:')} ${result.files.copied} copied`);
110
+ if (result.snapshotId && !result.snapshotId.startsWith('no-db:'))
111
+ console.log(` ${pc.dim('Snapshot:')} ${result.snapshotId}`);
112
+ if (result.warnings.length)
113
+ result.warnings.forEach((w) => console.log(` ${pc.yellow('⚠')} ${w}`));
114
+ }
115
+ }
116
+ catch (e) {
117
+ const msg = e instanceof Error ? e.message : String(e);
118
+ console.error(`\n ${pc.red('✗')} ${msg}\n`);
119
+ process.exit(1);
120
+ }
121
+ console.log('');
122
+ }
123
+ async function runVerify() {
124
+ const targetEnv = args[0] ?? getEnv();
125
+ console.log(`\n${pc.bold('hazo-env verify')} ${pc.dim(targetEnv)}\n`);
126
+ const { verifyFiles } = await import('./migrate/verify.js');
127
+ const { resolveFilesConfig } = await import('./resolve/files.js');
128
+ const dataRoot = resolveFilesConfig().local.basePath;
129
+ const report = await verifyFiles(targetEnv, dataRoot, { hash: 'sample', checkOrphans: true });
130
+ console.log(` Checked: ${report.checked} files`);
131
+ if (report.ok) {
132
+ console.log(` ${pc.green('✓')} All files verified\n`);
133
+ }
134
+ else {
135
+ if (report.missing.length)
136
+ console.log(` ${pc.red('✗')} Missing: ${report.missing.join(', ')}`);
137
+ if (report.sizeMismatch.length)
138
+ console.log(` ${pc.yellow('⚠')} Size mismatch: ${report.sizeMismatch.join(', ')}`);
139
+ console.log('');
140
+ process.exit(1);
141
+ }
142
+ }
143
+ async function runRestore() {
144
+ const targetEnv = args[0];
145
+ const snapshotIdx = args.findIndex((a) => a === '--snapshot');
146
+ const snapshotId = snapshotIdx >= 0 ? args[snapshotIdx + 1] : undefined;
147
+ if (!targetEnv || !snapshotId) {
148
+ console.error(pc.red('Error: restore requires <env> --snapshot <snapshot-id>'));
149
+ process.exit(1);
150
+ }
151
+ console.log(`\n${pc.bold('hazo-env restore')} ${pc.dim(`${targetEnv} ← ${snapshotId}`)}\n`);
152
+ const { restoreSnapshot } = await import('./migrate/snapshot.js');
153
+ const { resolveConnectConfig } = await import('./resolve/connect.js');
154
+ const toConfig = resolveConnectConfig({ env: targetEnv, allowOtherEnv: true });
155
+ restoreSnapshot(toConfig, snapshotId);
156
+ console.log(` ${pc.green('✓')} Restored\n`);
157
+ }
158
+ async function runMask() {
159
+ const subCmd = args[0];
160
+ if (subCmd === 'sync') {
161
+ console.log(`\n${pc.bold('hazo-env mask sync')}\n`);
162
+ try {
163
+ const connectMod = await import('hazo_connect/server').catch(() => null);
164
+ if (!connectMod) {
165
+ console.error(pc.red('Error: hazo_connect is required for mask sync.'));
166
+ process.exit(1);
167
+ }
168
+ const { resolveConnectConfig } = await import('./resolve/connect.js');
169
+ const { syncRulesetFromIni } = await import('./mask/ruleset.js');
170
+ const dbConfig = resolveConnectConfig({ allowOtherEnv: false });
171
+ if (dbConfig.type !== 'sqlite' || !dbConfig.sqlite) {
172
+ console.error(pc.red('Error: mask sync only supports SQLite databases.'));
173
+ process.exit(1);
174
+ }
175
+ const adapter = connectMod.createHazoConnect({
176
+ type: 'sqlite',
177
+ sqlite: { database_path: dbConfig.sqlite.database_path },
178
+ });
179
+ const count = await syncRulesetFromIni(adapter);
180
+ console.log(` ${pc.green('✓')} Synced ${count} rules into hazo_app_config\n`);
181
+ }
182
+ catch (e) {
183
+ console.error(`\n ${pc.red('✗')} ${e instanceof Error ? e.message : String(e)}\n`);
184
+ process.exit(1);
185
+ }
186
+ return;
187
+ }
188
+ if (subCmd === 'list') {
189
+ console.log(`\n${pc.bold('hazo-env mask list')}\n`);
190
+ const { listTransforms } = await import('./mask/registry.js');
191
+ const transforms = await listTransforms();
192
+ if (transforms.length === 0) {
193
+ console.log(' (no transforms registered)\n');
194
+ }
195
+ else {
196
+ for (const name of transforms)
197
+ console.log(` ${pc.dim('•')} ${name}`);
198
+ console.log('');
199
+ }
200
+ return;
201
+ }
202
+ console.log(`
203
+ ${pc.bold('hazo-env mask')} — masking commands
204
+
205
+ hazo-env mask sync Load hazo_env_masking.ini into hazo_app_config
206
+ hazo-env mask list List registered transform functions
207
+ `);
67
208
  }
68
209
  function printHelp() {
69
210
  console.log(`
@@ -75,7 +216,15 @@ Usage:
75
216
  hazo-env doctor --all Run health checks for all declared environments
76
217
  hazo-env doctor --env <env> Run health checks for a specific environment
77
218
  hazo-env doctor --probe Also probe live DB reachability
78
- hazo-env snapshot <env> (Phase 2 not yet implemented)
219
+ hazo-env migrate --from <env> --to <env> Migrate DB + files between environments
220
+ --dry-run Show plan without executing
221
+ --no-db / --no-files Skip DB or file copy
222
+ --allow-prod-target --confirm <token> Override prod-target safety
223
+ hazo-env verify <env> Verify files for an environment
224
+ hazo-env restore <env> --snapshot <id> Restore a snapshot
225
+ hazo-env snapshot <env> Take a snapshot of an environment
226
+ hazo-env mask sync Load masking ruleset from INI into DB
227
+ hazo-env mask list List registered mask transforms
79
228
  `);
80
229
  }
81
230
  (async () => {
@@ -89,6 +238,18 @@ Usage:
89
238
  else if (command === 'snapshot') {
90
239
  await runSnapshot();
91
240
  }
241
+ else if (command === 'migrate') {
242
+ await runMigrate();
243
+ }
244
+ else if (command === 'verify') {
245
+ await runVerify();
246
+ }
247
+ else if (command === 'restore') {
248
+ await runRestore();
249
+ }
250
+ else if (command === 'mask') {
251
+ await runMask();
252
+ }
92
253
  else {
93
254
  printHelp();
94
255
  }
package/dist/index.d.ts CHANGED
@@ -5,4 +5,10 @@ export * from './resolve/secrets.js';
5
5
  export * from './resolve/connect.js';
6
6
  export * from './resolve/files.js';
7
7
  export * from './doctor.js';
8
+ export { runMigration } from './migrate/run.js';
9
+ export { verifyFiles } from './migrate/verify.js';
10
+ export { takeSnapshot, restoreSnapshot } from './migrate/snapshot.js';
11
+ export { registerMask } from './mask/registry.js';
12
+ export type { MaskTransform } from './types/index.js';
13
+ export * from './lib/index.js';
8
14
  //# 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,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEzF,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE/E,cAAc,kBAAkB,CAAC;AAEjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEzF,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE/E,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;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,cAAc,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -11,3 +11,11 @@ export * from './resolve/connect.js';
11
11
  export * from './resolve/files.js';
12
12
  // Doctor
13
13
  export * from './doctor.js';
14
+ // Migration engine (Phase 2)
15
+ export { runMigration } from './migrate/run.js';
16
+ export { verifyFiles } from './migrate/verify.js';
17
+ export { takeSnapshot, restoreSnapshot } from './migrate/snapshot.js';
18
+ // Masking registry
19
+ export { registerMask } from './mask/registry.js';
20
+ // Lib
21
+ export * from './lib/index.js';
@@ -1,2 +1,2 @@
1
- export {};
1
+ export * from './secret_columns.js';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AACA,cAAc,qBAAqB,CAAC"}
package/dist/lib/index.js CHANGED
@@ -1 +1,2 @@
1
- export {};
1
+ // hazo_env/src/lib/index.ts — Core library exports
2
+ export * from './secret_columns.js';
@@ -0,0 +1,3 @@
1
+ export declare function isSecretColumn(columnName: string): boolean;
2
+ export declare const SECRET_COLUMNS_LIST: string[];
3
+ //# sourceMappingURL=secret_columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret_columns.d.ts","sourceRoot":"","sources":["../../src/lib/secret_columns.ts"],"names":[],"mappings":"AAYA,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,eAAO,MAAM,mBAAmB,UAAsB,CAAC"}
@@ -0,0 +1,14 @@
1
+ // hazo_env/src/lib/secret_columns.ts — columns that must never be copied as data
2
+ const SECRET_COLUMNS = new Set([
3
+ 'password', 'password_hash', 'hashed_password',
4
+ 'api_key', 'api_secret', 'secret', 'secret_key',
5
+ 'token', 'access_token', 'refresh_token', 'auth_token',
6
+ 'private_key', 'session_token', 'session_secret',
7
+ 'otp_secret', 'totp_secret', 'mfa_secret',
8
+ 'client_secret', 'signing_key', 'encryption_key',
9
+ 'webhook_secret', 'stripe_key', 'twilio_auth_token',
10
+ ]);
11
+ export function isSecretColumn(columnName) {
12
+ return SECRET_COLUMNS.has(columnName.toLowerCase());
13
+ }
14
+ export const SECRET_COLUMNS_LIST = [...SECRET_COLUMNS];
@@ -0,0 +1,8 @@
1
+ import type { MaskTransform } from '../types/index.js';
2
+ /** Register a custom transform (or override a built-in). */
3
+ export declare function registerMask(name: string, fn: MaskTransform): void;
4
+ /** Get the transform function for a given name. Returns undefined if not registered. */
5
+ export declare function getTransform(name: string): Promise<MaskTransform | undefined>;
6
+ /** List all registered transform names. */
7
+ export declare function listTransforms(): Promise<string[]>;
8
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/mask/registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAiBvD,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,IAAI,CAElE;AAED,wFAAwF;AACxF,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAGnF;AAED,2CAA2C;AAC3C,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAGxD"}
@@ -0,0 +1,29 @@
1
+ // hazo_env/src/mask/registry.ts — mask transform registry
2
+ import { optional_import } from 'hazo_core';
3
+ const _registry = new Map();
4
+ let _seeded = false;
5
+ async function ensureSeeded() {
6
+ if (_seeded)
7
+ return;
8
+ _seeded = true;
9
+ const maskModule = await optional_import('hazo_secure/mask');
10
+ if (maskModule?.BUILTIN_TRANSFORMS) {
11
+ for (const [name, fn] of Object.entries(maskModule.BUILTIN_TRANSFORMS)) {
12
+ _registry.set(name, fn);
13
+ }
14
+ }
15
+ }
16
+ /** Register a custom transform (or override a built-in). */
17
+ export function registerMask(name, fn) {
18
+ _registry.set(name, fn);
19
+ }
20
+ /** Get the transform function for a given name. Returns undefined if not registered. */
21
+ export async function getTransform(name) {
22
+ await ensureSeeded();
23
+ return _registry.get(name);
24
+ }
25
+ /** List all registered transform names. */
26
+ export async function listTransforms() {
27
+ await ensureSeeded();
28
+ return [..._registry.keys()].sort();
29
+ }
@@ -0,0 +1,15 @@
1
+ import type { HazoConnectAdapter } from 'hazo_connect';
2
+ export interface MaskRule {
3
+ table: string;
4
+ column: string;
5
+ transform: string;
6
+ params?: Record<string, unknown>;
7
+ }
8
+ export declare function ensureAppConfigTable(adapter: HazoConnectAdapter): Promise<void>;
9
+ /** Load rules from hazo_env_masking.ini into hazo_app_config. */
10
+ export declare function syncRulesetFromIni(adapter: HazoConnectAdapter, iniPath?: string): Promise<number>;
11
+ /** Read the effective ruleset from hazo_app_config. */
12
+ export declare function loadRuleset(adapter: HazoConnectAdapter): Promise<MaskRule[]>;
13
+ /** Parse INI-format masking rules. Format: [table_name] / column = transform */
14
+ export declare function parseIniRules(content: string): MaskRule[];
15
+ //# sourceMappingURL=ruleset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruleset.d.ts","sourceRoot":"","sources":["../../src/mask/ruleset.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AASD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYrF;AAED,iEAAiE;AACjE,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED,uDAAuD;AACvD,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAYlF;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,EAAE,CAkBzD"}
@@ -0,0 +1,77 @@
1
+ // hazo_env/src/mask/ruleset.ts — ruleset persistence (hazo_app_config)
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ const CONFIG_SECTION = 'hazo_env_masking';
5
+ // Ensure hazo_app_config table exists in the given SQLite adapter
6
+ function rawQuery(adapter, sql, params = []) {
7
+ return adapter.rawQuery(sql, { params });
8
+ }
9
+ export async function ensureAppConfigTable(adapter) {
10
+ await rawQuery(adapter, `
11
+ CREATE TABLE IF NOT EXISTS hazo_app_config (
12
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
13
+ config_section TEXT NOT NULL,
14
+ config_name TEXT NOT NULL,
15
+ config_value_json TEXT,
16
+ created_at TEXT DEFAULT (datetime('now')),
17
+ updated_at TEXT DEFAULT (datetime('now')),
18
+ UNIQUE(config_section, config_name)
19
+ )
20
+ `);
21
+ }
22
+ /** Load rules from hazo_env_masking.ini into hazo_app_config. */
23
+ export async function syncRulesetFromIni(adapter, iniPath) {
24
+ const resolvedPath = iniPath ?? path.resolve(process.cwd(), 'config', 'hazo_env_masking.ini');
25
+ if (!fs.existsSync(resolvedPath)) {
26
+ throw new Error(`hazo_env_masking.ini not found at ${resolvedPath}`);
27
+ }
28
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
29
+ const rules = parseIniRules(content);
30
+ await ensureAppConfigTable(adapter);
31
+ let count = 0;
32
+ for (const rule of rules) {
33
+ const name = `${rule.table}.${rule.column}`;
34
+ const valueJson = JSON.stringify({ transform: rule.transform, params: rule.params ?? {} });
35
+ await rawQuery(adapter, `
36
+ INSERT INTO hazo_app_config (config_section, config_name, config_value_json)
37
+ VALUES (?, ?, ?)
38
+ ON CONFLICT(config_section, config_name) DO UPDATE SET
39
+ config_value_json = excluded.config_value_json,
40
+ updated_at = datetime('now')
41
+ `, [CONFIG_SECTION, name, valueJson]);
42
+ count++;
43
+ }
44
+ return count;
45
+ }
46
+ /** Read the effective ruleset from hazo_app_config. */
47
+ export async function loadRuleset(adapter) {
48
+ await ensureAppConfigTable(adapter);
49
+ const rows = await rawQuery(adapter, `SELECT config_name, config_value_json FROM hazo_app_config WHERE config_section = ? ORDER BY config_name`, [CONFIG_SECTION]);
50
+ return rows.map((row) => {
51
+ const [table, column] = row.config_name.split('.');
52
+ const parsed = JSON.parse(row.config_value_json ?? '{}');
53
+ return { table, column, transform: parsed.transform, params: parsed.params };
54
+ });
55
+ }
56
+ /** Parse INI-format masking rules. Format: [table_name] / column = transform */
57
+ export function parseIniRules(content) {
58
+ const rules = [];
59
+ let currentTable = '';
60
+ for (const rawLine of content.split('\n')) {
61
+ const line = rawLine.trim();
62
+ if (!line || line.startsWith(';') || line.startsWith('#'))
63
+ continue;
64
+ if (line.startsWith('[') && line.endsWith(']')) {
65
+ currentTable = line.slice(1, -1).trim();
66
+ }
67
+ else if (currentTable && line.includes('=')) {
68
+ const eqIdx = line.indexOf('=');
69
+ const column = line.slice(0, eqIdx).trim();
70
+ const transform = line.slice(eqIdx + 1).trim();
71
+ if (column && transform) {
72
+ rules.push({ table: currentTable, column, transform });
73
+ }
74
+ }
75
+ }
76
+ return rules;
77
+ }
@@ -0,0 +1,21 @@
1
+ import type { HazoConnectAdapter } from 'hazo_connect';
2
+ export interface AuditMigrationPayload {
3
+ from: string;
4
+ to: string;
5
+ scrub: string;
6
+ db?: {
7
+ tables: number;
8
+ rows: number;
9
+ scrubbed: number;
10
+ };
11
+ files?: {
12
+ copied: number;
13
+ bytes: number;
14
+ };
15
+ verify?: {
16
+ ok: boolean;
17
+ checked: number;
18
+ };
19
+ }
20
+ export declare function auditMigration(adapter: HazoConnectAdapter, payload: AuditMigrationPayload): Promise<void>;
21
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/migrate/audit.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,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,CAAA;KAAE,CAAC;IAC1C,MAAM,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAOD,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAgCf"}
@@ -0,0 +1,36 @@
1
+ // hazo_env/src/migrate/audit.ts — emit audit intent event (wrapped, never fails migration)
2
+ import { optional_import } from 'hazo_core';
3
+ // Cast helper for SQLite rawQuery params extension
4
+ function rawQuery(adapter, sql, params = []) {
5
+ return adapter.rawQuery(sql, { params });
6
+ }
7
+ export async function auditMigration(adapter, payload) {
8
+ try {
9
+ // Ensure hazo_audit_intent table exists (CREATE TABLE IF NOT EXISTS)
10
+ await rawQuery(adapter, `
11
+ CREATE TABLE IF NOT EXISTS hazo_audit_intent (
12
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
13
+ event_name TEXT NOT NULL,
14
+ payload TEXT,
15
+ subject_kind TEXT,
16
+ subject_id TEXT,
17
+ created_at TEXT DEFAULT (datetime('now'))
18
+ )
19
+ `).catch(() => { });
20
+ const auditModule = await optional_import('hazo_audit/server');
21
+ if (auditModule?.emitIntentEvent) {
22
+ await auditModule.emitIntentEvent(adapter, {
23
+ event_name: 'hazo_env.migration',
24
+ payload: payload,
25
+ });
26
+ }
27
+ else {
28
+ // Fallback: insert directly
29
+ await rawQuery(adapter, `INSERT INTO hazo_audit_intent (event_name, payload, subject_kind) VALUES (?, ?, ?)`, ['hazo_env.migration', JSON.stringify(payload), 'migration']);
30
+ }
31
+ }
32
+ catch (e) {
33
+ // Audit write failure never fails the migration
34
+ console.warn('[hazo_env] audit write failed (non-fatal):', e instanceof Error ? e.message : String(e));
35
+ }
36
+ }