hazo_env 0.1.1 → 0.3.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 +26 -0
- package/README.md +117 -13
- package/SETUP_CHECKLIST.md +73 -0
- package/config/hazo_env_config.ini.sample +16 -0
- package/config/hazo_env_masking.ini.sample +16 -16
- package/dist/cli.js +170 -9
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +16 -3
- package/dist/env.server.d.ts +7 -1
- package/dist/env.server.d.ts.map +1 -1
- package/dist/env.server.js +25 -2
- package/dist/index.client.d.ts +6 -2
- package/dist/index.client.d.ts.map +1 -1
- package/dist/index.client.js +15 -9
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -1
- package/dist/lib/secret_columns.d.ts +3 -0
- package/dist/lib/secret_columns.d.ts.map +1 -0
- package/dist/lib/secret_columns.js +14 -0
- package/dist/mask/registry.d.ts +8 -0
- package/dist/mask/registry.d.ts.map +1 -0
- package/dist/mask/registry.js +29 -0
- package/dist/mask/ruleset.d.ts +15 -0
- package/dist/mask/ruleset.d.ts.map +1 -0
- package/dist/mask/ruleset.js +77 -0
- package/dist/migrate/audit.d.ts +21 -0
- package/dist/migrate/audit.d.ts.map +1 -0
- package/dist/migrate/audit.js +36 -0
- package/dist/migrate/db.d.ts +13 -0
- package/dist/migrate/db.d.ts.map +1 -0
- package/dist/migrate/db.js +122 -0
- package/dist/migrate/files.d.ts +14 -0
- package/dist/migrate/files.d.ts.map +1 -0
- package/dist/migrate/files.js +72 -0
- package/dist/migrate/progress.d.ts +5 -0
- package/dist/migrate/progress.d.ts.map +1 -0
- package/dist/migrate/progress.js +24 -0
- package/dist/migrate/run.d.ts +3 -0
- package/dist/migrate/run.d.ts.map +1 -0
- package/dist/migrate/run.js +195 -0
- package/dist/migrate/snapshot.d.ts +8 -0
- package/dist/migrate/snapshot.d.ts.map +1 -0
- package/dist/migrate/snapshot.js +48 -0
- package/dist/migrate/transport.d.ts +3 -0
- package/dist/migrate/transport.d.ts.map +1 -0
- package/dist/migrate/transport.js +17 -0
- package/dist/migrate/verify.d.ts +8 -0
- package/dist/migrate/verify.d.ts.map +1 -0
- package/dist/migrate/verify.js +51 -0
- package/dist/types/index.d.ts +47 -27
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +15 -3
package/dist/env.server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { HazoConfig } from 'hazo_config/server';
|
|
4
4
|
import { HazoError } from 'hazo_core';
|
|
5
|
-
import { getEnv,
|
|
5
|
+
import { getEnv, getRoleMap as clientGetRoleMap, normalize } from './index.client.js';
|
|
6
6
|
function findConfigFile(pkg) {
|
|
7
7
|
return path.resolve(process.cwd(), 'config', `${pkg}_config.ini`);
|
|
8
8
|
}
|
|
@@ -14,6 +14,28 @@ function tryLoadConfig() {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
const VALID_ROLES = new Set(['development', 'test', 'staging', 'production']);
|
|
18
|
+
/**
|
|
19
|
+
* Get the role map, merging client defaults with any [env.roles] section from hazo_env_config.ini.
|
|
20
|
+
*/
|
|
21
|
+
export function getRoleMap() {
|
|
22
|
+
const config = tryLoadConfig();
|
|
23
|
+
const section = config?.getSection?.('env.roles') ?? {};
|
|
24
|
+
const out = { ...clientGetRoleMap() };
|
|
25
|
+
for (const [name, role] of Object.entries(section)) {
|
|
26
|
+
const r = String(role).trim();
|
|
27
|
+
if (VALID_ROLES.has(r)) {
|
|
28
|
+
out[name.trim()] = r;
|
|
29
|
+
}
|
|
30
|
+
// invalid values: skip silently (doctor will flag them)
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** Get the broad role for the current environment (server-aware, reads INI) */
|
|
35
|
+
export function getEnvRole(env) {
|
|
36
|
+
const e = normalize(env ?? getEnv());
|
|
37
|
+
return getRoleMap()[e] ?? 'development';
|
|
38
|
+
}
|
|
17
39
|
/**
|
|
18
40
|
* Get the declared env pattern from [env] pattern in hazo_env_config.ini.
|
|
19
41
|
* Falls back to ['dev','test','staging','prod'] if the config is absent.
|
|
@@ -57,5 +79,6 @@ export function describeEnv() {
|
|
|
57
79
|
const dataRoot = config?.getSection('data')?.['root'] ?? 'app_data';
|
|
58
80
|
const locationRaw = config?.getSection(`host.${env}`)?.['location'] ?? 'unknown';
|
|
59
81
|
const hostHint = locationRaw === 'local' ? 'local' : locationRaw === 'remote' ? 'remote' : 'unknown';
|
|
60
|
-
|
|
82
|
+
const roles = getRoleMap();
|
|
83
|
+
return { env, role, pattern, app, dataRoot, hostHint, roles };
|
|
61
84
|
}
|
package/dist/index.client.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type { HazoEnv, HazoEnvRole, EnvPattern, EnvDescription } from './types/index.js';
|
|
2
|
-
export type { HazoEnv, HazoEnvRole, EnvPattern, EnvDescription };
|
|
1
|
+
import type { HazoEnv, HazoEnvRole, EnvPattern, EnvDescription, EnvRoleMap } from './types/index.js';
|
|
2
|
+
export type { HazoEnv, HazoEnvRole, EnvPattern, EnvDescription, EnvRoleMap };
|
|
3
|
+
/** Normalize raw env string to canonical form */
|
|
4
|
+
export declare function normalize(raw: string): HazoEnv;
|
|
3
5
|
/** Get the current environment name (normalized) */
|
|
4
6
|
export declare function getEnv(): HazoEnv;
|
|
7
|
+
/** Get the role map, merging any build-time injected overrides from __HAZO_ENV_ROLES__ */
|
|
8
|
+
export declare function getRoleMap(): EnvRoleMap;
|
|
5
9
|
/** Get the broad role for the current environment */
|
|
6
10
|
export declare function getEnvRole(env?: HazoEnv): HazoEnvRole;
|
|
7
11
|
export declare function isDev(env?: HazoEnv): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACrG,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC;AAE7E,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAI9C;AAED,oDAAoD;AACpD,wBAAgB,MAAM,IAAI,OAAO,CAEhC;AASD,0FAA0F;AAC1F,wBAAgB,UAAU,IAAI,UAAU,CAIvC;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,CAGrD;AAED,wBAAgB,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAA8C;AAC3F,wBAAgB,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAuC;AACrF,wBAAgB,SAAS,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAA0C;AAC3F,wBAAgB,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAA6C;AAE3F;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAMvC;AAED,gEAAgE;AAChE,wBAAgB,QAAQ,IAAI,UAAU,CAErC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAUpD;AAED,4DAA4D;AAC5D,wBAAgB,WAAW,IAAI,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,MAAM,CAAC,CAGlE"}
|
package/dist/index.client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// hazo_env/src/index.client.ts — Client-safe entry point
|
|
2
2
|
import { getCurrentEnv, HazoError } from 'hazo_core/client';
|
|
3
3
|
/** Normalize raw env string to canonical form */
|
|
4
|
-
function normalize(raw) {
|
|
4
|
+
export function normalize(raw) {
|
|
5
5
|
if (raw === 'development')
|
|
6
6
|
return 'dev';
|
|
7
7
|
if (raw === 'production')
|
|
@@ -12,16 +12,22 @@ function normalize(raw) {
|
|
|
12
12
|
export function getEnv() {
|
|
13
13
|
return normalize(getCurrentEnv());
|
|
14
14
|
}
|
|
15
|
+
const DEFAULT_ROLE_MAP = {
|
|
16
|
+
dev: 'development',
|
|
17
|
+
test: 'test',
|
|
18
|
+
staging: 'staging',
|
|
19
|
+
prod: 'production',
|
|
20
|
+
};
|
|
21
|
+
/** Get the role map, merging any build-time injected overrides from __HAZO_ENV_ROLES__ */
|
|
22
|
+
export function getRoleMap() {
|
|
23
|
+
const g = globalThis;
|
|
24
|
+
const injected = g['__HAZO_ENV_ROLES__'];
|
|
25
|
+
return { ...DEFAULT_ROLE_MAP, ...(injected ?? {}) };
|
|
26
|
+
}
|
|
15
27
|
/** Get the broad role for the current environment */
|
|
16
28
|
export function getEnvRole(env) {
|
|
17
|
-
const e = env ?? getEnv();
|
|
18
|
-
|
|
19
|
-
return 'development';
|
|
20
|
-
if (e === 'test')
|
|
21
|
-
return 'test';
|
|
22
|
-
if (e === 'staging')
|
|
23
|
-
return 'staging';
|
|
24
|
-
return 'production';
|
|
29
|
+
const e = normalize(env ?? getEnv());
|
|
30
|
+
return getRoleMap()[e] ?? 'development';
|
|
25
31
|
}
|
|
26
32
|
export function isDev(env) { return getEnvRole(env) === 'development'; }
|
|
27
33
|
export function isTest(env) { return getEnvRole(env) === 'test'; }
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
export { getEnv,
|
|
2
|
-
export { getPattern, listEnvs, assertEnv, describeEnv } from './env.server.js';
|
|
1
|
+
export { getEnv, isDev, isTest, isStaging, isProd } from './index.client.js';
|
|
2
|
+
export { getPattern, listEnvs, assertEnv, describeEnv, getEnvRole, getRoleMap } from './env.server.js';
|
|
3
3
|
export * from './types/index.js';
|
|
4
4
|
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 { writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from './migrate/progress.js';
|
|
12
|
+
export { registerMask } from './mask/registry.js';
|
|
13
|
+
export { loadRuleset, syncRulesetFromIni, parseIniRules } from './mask/ruleset.js';
|
|
14
|
+
export type { MaskRule } from './mask/ruleset.js';
|
|
15
|
+
export type { MaskTransform } from './types/index.js';
|
|
16
|
+
export * from './lib/index.js';
|
|
8
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// hazo_env/src/index.ts — Server entry point
|
|
2
2
|
// Pure client helpers re-exported explicitly (getPattern/listEnvs/assertEnv/describeEnv are overridden below)
|
|
3
|
-
export { getEnv,
|
|
3
|
+
export { getEnv, isDev, isTest, isStaging, isProd } from './index.client.js';
|
|
4
4
|
// Server-aware overrides
|
|
5
|
-
export { getPattern, listEnvs, assertEnv, describeEnv } from './env.server.js';
|
|
5
|
+
export { getPattern, listEnvs, assertEnv, describeEnv, getEnvRole, getRoleMap } from './env.server.js';
|
|
6
6
|
// Types
|
|
7
7
|
export * from './types/index.js';
|
|
8
8
|
// Resolver + secrets
|
|
@@ -11,3 +11,14 @@ 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
|
+
export { writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from './migrate/progress.js';
|
|
19
|
+
// Masking registry
|
|
20
|
+
export { registerMask } from './mask/registry.js';
|
|
21
|
+
// Masking ruleset
|
|
22
|
+
export { loadRuleset, syncRulesetFromIni, parseIniRules } from './mask/ruleset.js';
|
|
23
|
+
// Lib
|
|
24
|
+
export * from './lib/index.js';
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './secret_columns.js';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
1
|
+
// hazo_env/src/lib/index.ts — Core library exports
|
|
2
|
+
export * from './secret_columns.js';
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HazoConnectAdapter } from 'hazo_connect';
|
|
2
|
+
export interface DbCopyOptions {
|
|
3
|
+
tables?: '*' | string[];
|
|
4
|
+
scrubHook?: (tableName: string, row: Record<string, unknown>) => Record<string, unknown>;
|
|
5
|
+
onProgress?: (msg: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface DbCopyResult {
|
|
8
|
+
tables: number;
|
|
9
|
+
rows: number;
|
|
10
|
+
scrubbed: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function copyDb(srcAdapter: HazoConnectAdapter, tgtAdapter: HazoConnectAdapter, opts?: DbCopyOptions): Promise<DbCopyResult>;
|
|
13
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +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,MAAM,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;IACxB,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,CA+GvB"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// hazo_env/src/migrate/db.ts — per-table DB copy with schema parity check
|
|
2
|
+
import { HazoError, optional_import } from 'hazo_core';
|
|
3
|
+
import { isSecretColumn } from '../lib/secret_columns.js';
|
|
4
|
+
const PAGE_SIZE = 500;
|
|
5
|
+
// The HazoConnectAdapter interface types rawQuery as (sql, RequestInit?) but the
|
|
6
|
+
// SQLite adapter extends it with { params?: unknown[] }. Cast through unknown to
|
|
7
|
+
// satisfy both the interface type and the runtime expectation.
|
|
8
|
+
function rawQuery(adapter, sql, params = []) {
|
|
9
|
+
return adapter.rawQuery(sql, { params });
|
|
10
|
+
}
|
|
11
|
+
async function getRawTables(adapter) {
|
|
12
|
+
const result = await rawQuery(adapter, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
|
13
|
+
return result.map((r) => r.name);
|
|
14
|
+
}
|
|
15
|
+
async function getMigrationNames(adapter) {
|
|
16
|
+
// Check if _migrations table exists
|
|
17
|
+
const tables = await getRawTables(adapter);
|
|
18
|
+
if (!tables.includes('_migrations'))
|
|
19
|
+
return null;
|
|
20
|
+
const rows = await rawQuery(adapter, 'SELECT name FROM _migrations ORDER BY name');
|
|
21
|
+
return rows.map((r) => r.name);
|
|
22
|
+
}
|
|
23
|
+
export async function copyDb(srcAdapter, tgtAdapter, opts = {}) {
|
|
24
|
+
const connect = await optional_import('hazo_connect/server');
|
|
25
|
+
if (!connect) {
|
|
26
|
+
throw new HazoError({
|
|
27
|
+
code: 'HAZO_ENV_MISSING_DEPENDENCY',
|
|
28
|
+
pkg: 'hazo_env',
|
|
29
|
+
message: 'hazo_connect is required for DB migration. Install hazo_connect to continue.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Schema parity check via _migrations
|
|
33
|
+
const srcMigrations = await getMigrationNames(srcAdapter);
|
|
34
|
+
const tgtMigrations = await getMigrationNames(tgtAdapter);
|
|
35
|
+
if (srcMigrations === null || tgtMigrations === null) {
|
|
36
|
+
throw new HazoError({
|
|
37
|
+
code: 'HAZO_ENV_SCHEMA_MISMATCH',
|
|
38
|
+
pkg: 'hazo_env',
|
|
39
|
+
message: `Schema parity check failed: _migrations table is missing on ${srcMigrations === null ? 'source' : 'target'}. Both databases must have matching _migrations tables.`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const srcSet = new Set(srcMigrations);
|
|
43
|
+
const tgtSet = new Set(tgtMigrations);
|
|
44
|
+
const missingInTgt = [...srcSet].filter((n) => !tgtSet.has(n));
|
|
45
|
+
const missingInSrc = [...tgtSet].filter((n) => !srcSet.has(n));
|
|
46
|
+
if (missingInTgt.length > 0 || missingInSrc.length > 0) {
|
|
47
|
+
throw new HazoError({
|
|
48
|
+
code: 'HAZO_ENV_SCHEMA_MISMATCH',
|
|
49
|
+
pkg: 'hazo_env',
|
|
50
|
+
message: `Schema parity check failed: migration name-sets differ.\n Missing in target: ${missingInTgt.join(', ') || 'none'}\n Missing in source: ${missingInSrc.join(', ') || 'none'}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Get tables to copy
|
|
54
|
+
const allTables = await getRawTables(srcAdapter);
|
|
55
|
+
// Exclude system/internal tables
|
|
56
|
+
const SKIP = new Set(['_migrations', 'sqlite_sequence', 'hazo_audit_events', 'hazo_audit_intent']);
|
|
57
|
+
let tablesToCopy = allTables.filter((t) => !SKIP.has(t));
|
|
58
|
+
if (opts.tables && opts.tables !== '*') {
|
|
59
|
+
tablesToCopy = tablesToCopy.filter((t) => opts.tables.includes(t));
|
|
60
|
+
}
|
|
61
|
+
let totalRows = 0;
|
|
62
|
+
let totalScrubbed = 0;
|
|
63
|
+
for (const table of tablesToCopy) {
|
|
64
|
+
opts.onProgress?.(`Copying table: ${table}`);
|
|
65
|
+
// Delete all rows in target
|
|
66
|
+
await rawQuery(tgtAdapter, `DELETE FROM "${table}"`);
|
|
67
|
+
// Get schema to find PK column and all columns
|
|
68
|
+
const schema = await rawQuery(srcAdapter, `PRAGMA table_info("${table}")`);
|
|
69
|
+
const pkCol = schema.find((c) => c.pk === 1)?.name ?? 'id';
|
|
70
|
+
const allCols = schema.map((c) => c.name);
|
|
71
|
+
// Page through source
|
|
72
|
+
const srcService = connect.createCrudService(srcAdapter, table, { autoId: false });
|
|
73
|
+
let offset = 0;
|
|
74
|
+
let pageRows = [];
|
|
75
|
+
do {
|
|
76
|
+
pageRows = await srcService.list((qb) => qb.order(pkCol, 'asc').limit(PAGE_SIZE).offset(offset));
|
|
77
|
+
if (pageRows.length === 0)
|
|
78
|
+
break;
|
|
79
|
+
for (let row of pageRows) {
|
|
80
|
+
// Drop secret columns
|
|
81
|
+
const cleaned = {};
|
|
82
|
+
let didScrub = false;
|
|
83
|
+
for (const col of allCols) {
|
|
84
|
+
if (isSecretColumn(col)) {
|
|
85
|
+
didScrub = true;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
cleaned[col] = row[col];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Apply scrub hook if provided (masking, Phase B)
|
|
92
|
+
if (opts.scrubHook) {
|
|
93
|
+
const hooked = opts.scrubHook(table, cleaned);
|
|
94
|
+
if (hooked !== cleaned)
|
|
95
|
+
didScrub = true;
|
|
96
|
+
row = hooked;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
row = cleaned;
|
|
100
|
+
}
|
|
101
|
+
if (didScrub)
|
|
102
|
+
totalScrubbed++;
|
|
103
|
+
// Only insert columns that exist in schema
|
|
104
|
+
const insertRow = {};
|
|
105
|
+
for (const col of Object.keys(row)) {
|
|
106
|
+
if (allCols.includes(col))
|
|
107
|
+
insertRow[col] = row[col];
|
|
108
|
+
}
|
|
109
|
+
// Use raw INSERT to preserve PKs
|
|
110
|
+
const cols = Object.keys(insertRow);
|
|
111
|
+
if (cols.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
const placeholders = cols.map(() => '?').join(', ');
|
|
114
|
+
const values = cols.map((c) => insertRow[c]);
|
|
115
|
+
await rawQuery(tgtAdapter, `INSERT OR REPLACE INTO "${table}" (${cols.map((c) => `"${c}"`).join(', ')}) VALUES (${placeholders})`, values);
|
|
116
|
+
totalRows++;
|
|
117
|
+
}
|
|
118
|
+
offset += PAGE_SIZE;
|
|
119
|
+
} while (pageRows.length === PAGE_SIZE);
|
|
120
|
+
}
|
|
121
|
+
return { tables: tablesToCopy.length, rows: totalRows, scrubbed: totalScrubbed };
|
|
122
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HazoEnv } from '../types/index.js';
|
|
2
|
+
export interface FilesCopyOptions {
|
|
3
|
+
fromEnv: HazoEnv;
|
|
4
|
+
toEnv: HazoEnv;
|
|
5
|
+
fileScrubHook?: (filePath: string) => Promise<void>;
|
|
6
|
+
onProgress?: (msg: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface FilesCopyResult {
|
|
9
|
+
copied: number;
|
|
10
|
+
bytes: number;
|
|
11
|
+
placeholdered: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function copyFiles(opts: FilesCopyOptions): Promise<FilesCopyResult>;
|
|
14
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +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;AA4BD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4ChF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// hazo_env/src/migrate/files.ts — local file-tree copy with PDF scrubbing (Phase C)
|
|
2
|
+
//
|
|
3
|
+
// PDF scrubbing via hazo_pdf/server (optional peer dep). When HAZO_ENV_MASK_KEY
|
|
4
|
+
// is set and a copied file has a .pdf extension, mask_pdf is called with an
|
|
5
|
+
// empty targets list (structure-only scrub / reserved for future target lists).
|
|
6
|
+
// Failures are non-fatal warnings — the file copy still completes.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { resolveFilesConfig } from '../resolve/files.js';
|
|
10
|
+
function resolveEnvFilesRoot(env) {
|
|
11
|
+
// resolveFilesConfig returns the data root; env-specific files are at <root>/<env>/files
|
|
12
|
+
const base = resolveFilesConfig().local.basePath;
|
|
13
|
+
return path.join(base, env, 'files');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attempt to scrub a PDF file in-place using hazo_pdf/mask_pdf.
|
|
17
|
+
* Non-fatal: any failure is silently swallowed — the copy always completes.
|
|
18
|
+
* Called only when HAZO_ENV_MASK_KEY is set.
|
|
19
|
+
*/
|
|
20
|
+
async function scrubPdfFile(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const pdfMod = await import('hazo_pdf/server').catch(() => null);
|
|
23
|
+
if (!pdfMod?.mask_pdf)
|
|
24
|
+
return; // hazo_pdf not installed or mask_pdf not exported
|
|
25
|
+
const { readFileSync, writeFileSync } = await import('node:fs');
|
|
26
|
+
const bytes = readFileSync(filePath);
|
|
27
|
+
const scrubbed = await pdfMod.mask_pdf(new Uint8Array(bytes), []);
|
|
28
|
+
writeFileSync(filePath, Buffer.from(scrubbed));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Non-fatal — PDF scrubbing failure is a warning, not an abort
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function copyFiles(opts) {
|
|
35
|
+
const srcRoot = resolveEnvFilesRoot(opts.fromEnv);
|
|
36
|
+
const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
|
|
37
|
+
if (!fs.existsSync(srcRoot)) {
|
|
38
|
+
opts.onProgress?.(`No files directory at ${srcRoot} — skipping file copy`);
|
|
39
|
+
return { copied: 0, bytes: 0, placeholdered: 0 };
|
|
40
|
+
}
|
|
41
|
+
fs.mkdirSync(tgtRoot, { recursive: true });
|
|
42
|
+
let copied = 0;
|
|
43
|
+
let bytes = 0;
|
|
44
|
+
async function copyDir(src, tgt) {
|
|
45
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const srcPath = path.join(src, entry.name);
|
|
48
|
+
const tgtPath = path.join(tgt, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
fs.mkdirSync(tgtPath, { recursive: true });
|
|
51
|
+
await copyDir(srcPath, tgtPath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
fs.copyFileSync(srcPath, tgtPath);
|
|
55
|
+
const stat = fs.statSync(srcPath);
|
|
56
|
+
bytes += stat.size;
|
|
57
|
+
copied++;
|
|
58
|
+
opts.onProgress?.(`Copied: ${entry.name}`);
|
|
59
|
+
// Phase C: PDF scrubbing when HAZO_ENV_MASK_KEY is set
|
|
60
|
+
if (entry.name.toLowerCase().endsWith('.pdf') && process.env['HAZO_ENV_MASK_KEY']) {
|
|
61
|
+
await scrubPdfFile(tgtPath);
|
|
62
|
+
}
|
|
63
|
+
// Legacy sync hook (still supported for callers that pass one)
|
|
64
|
+
if (opts.fileScrubHook) {
|
|
65
|
+
await opts.fileScrubHook(tgtPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await copyDir(srcRoot, tgtRoot);
|
|
71
|
+
return { copied, bytes, placeholdered: 0 };
|
|
72
|
+
}
|