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/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# hazo_env — Change Log
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-06-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `EnvRoleMap` type (`Record<string, HazoEnvRole>`) in `src/types/index.ts`
|
|
7
|
+
- `roles?: EnvRoleMap` optional field on `EnvDescription` interface
|
|
8
|
+
- `getRoleMap()` client + server: config-driven role map; merges `DEFAULT_ROLE_MAP` → `globalThis.__HAZO_ENV_ROLES__` (client) → `[env.roles]` INI section (server); invalid role values skipped silently
|
|
9
|
+
- `[env.roles]` INI section support: arbitrary env names mapped to the four fixed roles (`development`, `test`, `staging`, `production`)
|
|
10
|
+
- `describeEnv()` server version now includes `roles` field
|
|
11
|
+
- Filesystem progress store: `writeMigrationProgress`, `readMigrationProgress`, `clearMigrationProgress` (`src/migrate/progress.ts`)
|
|
12
|
+
- `MigrationRequest.jobId` + `MigrationRequest.progressDir` optional fields — when both present, `runMigration` writes each progress event to `<progressDir>/<jobId>.json`
|
|
13
|
+
- `loadRuleset`, `syncRulesetFromIni`, `parseIniRules` masking ruleset functions now exported from package index
|
|
14
|
+
- `MaskRule` type now exported from package index
|
|
15
|
+
- `normalize()` utility now exported from package index (trim + lowercase env name string)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `getEnvRole()` rewritten to be config-driven via role map; unknown env names now default to `'development'` instead of `'production'` — this is a safe behavior fix (unknown names no longer silently get production-level protection)
|
|
19
|
+
|
|
20
|
+
## 0.2.0 — 2026-06-09
|
|
21
|
+
|
|
22
|
+
- feat: M2 migration engine — `runMigration`, `verifyFiles`, `takeSnapshot`, `restoreSnapshot`
|
|
23
|
+
- feat: M3 PII masking — `registerMask`, built-in transforms (`mask_email`, `fake_name`, `mask_phone`, `jitter_date`, `hash`, `tokenize`, `drop`, `nullify`, `redact_pii`)
|
|
24
|
+
- feat: masking ruleset management — `loadRuleset`, `syncRulesetFromIni`, `parseIniRules`, `MaskRule` (not yet exported from index — fixed in 0.3.0)
|
|
25
|
+
- feat: `MaskTransform` type in `src/types/index.ts`
|
|
26
|
+
- feat: CLI commands `migrate`, `verify`, `restore`, `mask sync`, `mask list`
|
|
27
|
+
- chore: version bump to 0.2.0
|
|
28
|
+
|
|
3
29
|
## 0.1.1 — 2026-06-09
|
|
4
30
|
|
|
5
31
|
- fix: provision `sql-wasm.wasm` into test-app `public/` via seed script (fixes Connect page error when SQLite adapter loads via sql.js WASM)
|
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 `test` 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
|
|
|
@@ -62,18 +64,67 @@ const adapter = await createHazoConnect(resolveConnectConfig());
|
|
|
62
64
|
### Env resolution (client-safe — also in `hazo_env/client`)
|
|
63
65
|
|
|
64
66
|
```ts
|
|
65
|
-
import { getEnv, getEnvRole, getPattern, listEnvs, assertEnv, describeEnv,
|
|
67
|
+
import { getEnv, getEnvRole, getRoleMap, getPattern, listEnvs, assertEnv, describeEnv,
|
|
66
68
|
isDev, isTest, isStaging, isProd } from 'hazo_env';
|
|
67
69
|
|
|
68
70
|
getEnv() // 'dev' | 'test' | 'staging' | 'prod'
|
|
69
|
-
getEnvRole() // 'development' | '
|
|
71
|
+
getEnvRole() // 'development' | 'test' | 'staging' | 'production'
|
|
72
|
+
getRoleMap() // EnvRoleMap — full env→role mapping (config-driven; safe defaults)
|
|
70
73
|
getPattern() // e.g. 'dev_prod'
|
|
71
74
|
listEnvs() // ['dev', 'prod'] — valid envs for the declared pattern
|
|
72
75
|
assertEnv() // throws HazoError(ENV_INVALID) if HAZO_ENV is outside the pattern
|
|
73
|
-
describeEnv() // { env, role }
|
|
76
|
+
describeEnv() // { env, role, pattern, app, dataRoot, hostHint, roles? }
|
|
74
77
|
isDev() / isTest() / isStaging() / isProd()
|
|
75
78
|
```
|
|
76
79
|
|
|
80
|
+
`getEnvRole()` uses a config-driven role map so custom env names (e.g. `preview`, `qa`) resolve to the correct broad role. Unknown names default to `'development'` — never silently treated as production. Configure custom mappings in `[env.roles]`:
|
|
81
|
+
|
|
82
|
+
```ini
|
|
83
|
+
[env.roles]
|
|
84
|
+
preview = staging
|
|
85
|
+
qa = test
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Configuring environment roles
|
|
89
|
+
|
|
90
|
+
Canonical env names (`dev`, `test`, `staging`, `prod`) are pre-mapped and require no configuration. Non-canonical names (anything else) must declare their role in the `[env.roles]` INI section, or `doctor` will flag an error. Unknown names without a declaration fall back to `'development'` (safe: no masking, no prod-protection).
|
|
91
|
+
|
|
92
|
+
```ini
|
|
93
|
+
[env.roles]
|
|
94
|
+
; Map arbitrary env names to the four fixed roles:
|
|
95
|
+
; development | test | staging | production
|
|
96
|
+
qa = test
|
|
97
|
+
live = production
|
|
98
|
+
rc = staging
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Valid role values: `development`, `test`, `staging`, `production`.
|
|
102
|
+
|
|
103
|
+
#### Client-side global injection
|
|
104
|
+
|
|
105
|
+
On the client (browser), the INI file is not available. Inject the resolved values at render time using `globalThis` globals — the same pattern used for the env pattern:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// In your Next.js layout server component or _app (server-side):
|
|
109
|
+
// globalThis.__HAZO_ENV_PATTERN__ = 'dev,prod'; // comma-separated pattern
|
|
110
|
+
// globalThis.__HAZO_ENV_ROLES__ = { live: 'production', qa: 'test' };
|
|
111
|
+
|
|
112
|
+
// Client bundle reads these automatically:
|
|
113
|
+
import { getPattern, getRoleMap } from 'hazo_env/client';
|
|
114
|
+
getPattern() // reads __HAZO_ENV_PATTERN__
|
|
115
|
+
getRoleMap() // merges DEFAULT_ROLE_MAP → __HAZO_ENV_ROLES__
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Both globals are optional; omitting them makes the client fall back to canonical defaults.
|
|
119
|
+
|
|
120
|
+
### Utility
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { normalize } from 'hazo_env';
|
|
124
|
+
|
|
125
|
+
normalize(' Dev ') // → 'dev' (trim + lowercase)
|
|
126
|
+
```
|
|
127
|
+
|
|
77
128
|
### Resolvers (server-only)
|
|
78
129
|
|
|
79
130
|
```ts
|
|
@@ -101,12 +152,71 @@ const report = await doctor();
|
|
|
101
152
|
// report.checks DoctorCheck[] — { label, ok, message }
|
|
102
153
|
```
|
|
103
154
|
|
|
155
|
+
### Migration engine (server-only)
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { runMigration, verifyFiles, takeSnapshot, restoreSnapshot,
|
|
159
|
+
writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from 'hazo_env';
|
|
160
|
+
|
|
161
|
+
// Copy prod → staging (with PII masking applied automatically)
|
|
162
|
+
const result = await runMigration({
|
|
163
|
+
from: 'prod',
|
|
164
|
+
to: 'staging',
|
|
165
|
+
dryRun: false, // set true for a plan with counts, no writes
|
|
166
|
+
// allowProdTarget: true, // required when `to` is 'prod'
|
|
167
|
+
// confirmToken: '<token>', // required together with allowProdTarget
|
|
168
|
+
onProgress: (p) => console.log(p.phase, p.message),
|
|
169
|
+
// jobId: '<id>', // optional — persist progress to progressDir/<id>.json
|
|
170
|
+
// progressDir: '/tmp/hazo_jobs', // required when jobId is set
|
|
171
|
+
});
|
|
172
|
+
// result.ok, result.db, result.files, result.snapshotId, result.warnings, result.durationMs
|
|
173
|
+
|
|
174
|
+
// Stand-alone verification
|
|
175
|
+
await verifyFiles('staging', dataRoot, { hash: 'sample' });
|
|
176
|
+
|
|
177
|
+
// Manual snapshot / restore
|
|
178
|
+
const snap = takeSnapshot(connectConfig); // returns { snapshotId }
|
|
179
|
+
restoreSnapshot(connectConfig, snapshotId);
|
|
180
|
+
|
|
181
|
+
// Filesystem progress store (used by hazo_jobs integration)
|
|
182
|
+
writeMigrationProgress(dir, jobId, progress);
|
|
183
|
+
readMigrationProgress(dir, jobId); // → MigrationProgress | null
|
|
184
|
+
clearMigrationProgress(dir, jobId);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**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)`.
|
|
188
|
+
|
|
189
|
+
**Masking ruleset API** — load, parse, and sync rulesets programmatically:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { loadRuleset, syncRulesetFromIni, parseIniRules } from 'hazo_env';
|
|
193
|
+
import type { MaskRule } from 'hazo_env';
|
|
194
|
+
|
|
195
|
+
const rules: MaskRule[] = parseIniRules(iniText); // parse INI text → rules array
|
|
196
|
+
const loaded = await loadRuleset(adapter); // read from hazo_app_config DB
|
|
197
|
+
await syncRulesetFromIni(adapter, '/path/to/masking.ini'); // write parsed rules to DB
|
|
198
|
+
```
|
|
199
|
+
|
|
104
200
|
## CLI
|
|
105
201
|
|
|
106
202
|
```
|
|
107
203
|
hazo-env current # prints env, role, pattern, app, data_root
|
|
108
204
|
hazo-env doctor [--env <e>] [--all] # red/green validation table
|
|
109
|
-
hazo-env snapshot <env> #
|
|
205
|
+
hazo-env snapshot <env> # snapshot the target env DB
|
|
206
|
+
|
|
207
|
+
hazo-env migrate --from <env> --to <env> [options]
|
|
208
|
+
--transport auto|local|ssh|api # default auto
|
|
209
|
+
--no-db | --no-files # skip DB or file copy
|
|
210
|
+
--scrub auto|none # default auto (mask when target is test/staging)
|
|
211
|
+
--tables a,b,c # restrict to specific tables
|
|
212
|
+
--dry-run # plan + counts, no writes
|
|
213
|
+
--allow-prod-target --confirm <tok> # required when writing prod
|
|
214
|
+
|
|
215
|
+
hazo-env verify <env> [--hash none|sample|full] [--no-orphans]
|
|
216
|
+
hazo-env restore <env> --snapshot <snapshot-id>
|
|
217
|
+
|
|
218
|
+
hazo-env mask sync # sync hazo_env_masking.ini → hazo_app_config
|
|
219
|
+
hazo-env mask list # print active masking rules
|
|
110
220
|
```
|
|
111
221
|
|
|
112
222
|
## Config reference (`hazo_env_config.ini`)
|
|
@@ -121,12 +231,6 @@ Key sections:
|
|
|
121
231
|
|
|
122
232
|
Secret placeholders use `${ENV_VAR_NAME}` syntax — hazo_env substitutes them from `.env.local` at runtime.
|
|
123
233
|
|
|
124
|
-
## Tailwind v4 (`@source` required if consuming UI)
|
|
125
|
-
|
|
126
|
-
```css
|
|
127
|
-
@source "../node_modules/hazo_env/dist";
|
|
128
|
-
```
|
|
129
|
-
|
|
130
234
|
## License
|
|
131
235
|
|
|
132
236
|
MIT
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -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`
|
|
@@ -31,6 +33,16 @@ api_key = ${POSTGREST_API_KEY}
|
|
|
31
33
|
|
|
32
34
|
**Never put secret values directly in the ini file** — use `${ENV_VAR_NAME}` placeholders.
|
|
33
35
|
|
|
36
|
+
**Custom env names** — if your pattern uses names other than `dev`/`test`/`staging`/`prod` (e.g. `preview`, `qa`), add an `[env.roles]` section to map them to broad roles:
|
|
37
|
+
|
|
38
|
+
```ini
|
|
39
|
+
[env.roles]
|
|
40
|
+
preview = staging
|
|
41
|
+
qa = testing
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Without this, unknown env names default to `'development'` role (never silently production).
|
|
45
|
+
|
|
34
46
|
## 3. Create `.env.local` (gitignored)
|
|
35
47
|
|
|
36
48
|
One file per deployment (dev machine, staging host, prod host). Contains the secrets referenced by `${…}` placeholders in the ini:
|
|
@@ -90,3 +102,64 @@ const nextConfig = {
|
|
|
90
102
|
transpilePackages: ['hazo_env', 'hazo_core', /* … */],
|
|
91
103
|
};
|
|
92
104
|
```
|
|
105
|
+
|
|
106
|
+
## 9. (Optional) Set up the migration engine
|
|
107
|
+
|
|
108
|
+
Skip this section if you only need env resolution and resolvers.
|
|
109
|
+
|
|
110
|
+
### 9a. Create `hazo_env_masking.ini`
|
|
111
|
+
|
|
112
|
+
Copy `node_modules/hazo_env/config/hazo_env_masking.ini.sample` to your app's `config/` directory and declare per-table masking rules:
|
|
113
|
+
|
|
114
|
+
```ini
|
|
115
|
+
[hazo_users]
|
|
116
|
+
email = mask_email
|
|
117
|
+
full_name = fake_name
|
|
118
|
+
password_hash = drop
|
|
119
|
+
|
|
120
|
+
[my_app_clients]
|
|
121
|
+
notes = redact_pii
|
|
122
|
+
dob = jitter_date
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Sync to the DB so the runtime engine can read them:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx hazo-env mask sync
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 9b. Set `HAZO_ENV_MASK_KEY` in `.env.local`
|
|
132
|
+
|
|
133
|
+
Required for deterministic masking transforms (`hash`, `tokenize`). If absent, migration aborts when scrub mode is `auto`.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
HAZO_ENV_MASK_KEY=<random-32-char-hex>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 9c. Run a dry-run migration to verify the plan
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx hazo-env migrate --from prod --to staging --dry-run
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
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:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx hazo-env restore staging --snapshot <snapshot-id>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 10. (Required for non-canonical env names) Declare roles in `[env.roles]`
|
|
152
|
+
|
|
153
|
+
If your pattern uses any env name other than `dev`, `test`, `staging`, or `prod` (e.g. `qa`, `preview`, `live`, `rc`), add an `[env.roles]` section to `config/hazo_env_config.ini` to map each name to one of the four fixed roles:
|
|
154
|
+
|
|
155
|
+
```ini
|
|
156
|
+
[env.roles]
|
|
157
|
+
qa = test
|
|
158
|
+
preview = staging
|
|
159
|
+
live = production
|
|
160
|
+
rc = staging
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Valid role values: `development`, `test`, `staging`, `production`.
|
|
164
|
+
|
|
165
|
+
Without this declaration, undeclared non-canonical names default to `'development'` (safe — no masking, no prod-protection) **and `hazo-env doctor` will flag an error** until the mapping is added. Run `npx hazo-env doctor` to verify all env names have a declared role.
|
|
@@ -10,6 +10,22 @@ pattern = dev, test, staging, prod
|
|
|
10
10
|
; Application name (used in log output and doctor reports)
|
|
11
11
|
app = myapp
|
|
12
12
|
|
|
13
|
+
; ─── Environment role map ─────────────────────────────────────────────────────
|
|
14
|
+
; Map each environment NAME to one of the four fixed ROLES.
|
|
15
|
+
; Roles drive behavior: masking fires for test+staging roles;
|
|
16
|
+
; prod-protection fires for the production role.
|
|
17
|
+
; The four canonical names default automatically — list them only to be explicit.
|
|
18
|
+
; Non-canonical names MUST be declared here or doctor will flag an error
|
|
19
|
+
; and the role will default to 'development' (safe but unintended).
|
|
20
|
+
[env.roles]
|
|
21
|
+
; dev = development (default — no declaration needed)
|
|
22
|
+
; test = test (default)
|
|
23
|
+
; staging = staging (default)
|
|
24
|
+
; prod = production (default)
|
|
25
|
+
qa = test
|
|
26
|
+
preview = staging
|
|
27
|
+
; live = production (example: a production env with a different name)
|
|
28
|
+
|
|
13
29
|
[data]
|
|
14
30
|
; Base directory for all local file storage (relative to app root or absolute)
|
|
15
31
|
; Default: app_data
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
; hazo_env masking ruleset sample
|
|
2
|
-
; Copy to hazo_env_masking.ini and
|
|
3
|
-
;
|
|
4
|
-
;
|
|
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
|
-
[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
[
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,8 +1,8 @@
|
|
|
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
|
-
import { getEnv
|
|
5
|
-
import { describeEnv } from './env.server.js';
|
|
4
|
+
import { getEnv } from './index.client.js';
|
|
5
|
+
import { describeEnv, getEnvRole, getPattern } from './env.server.js';
|
|
6
6
|
import { doctor } from './doctor.js';
|
|
7
7
|
const [, , command, ...args] = process.argv;
|
|
8
8
|
function statusIcon(status) {
|
|
@@ -20,7 +20,7 @@ async function runCurrent() {
|
|
|
20
20
|
catch {
|
|
21
21
|
// Fallback if no config file
|
|
22
22
|
const env = getEnv();
|
|
23
|
-
desc = { env, role: getEnvRole(env), pattern:
|
|
23
|
+
desc = { env, role: getEnvRole(env), pattern: getPattern(), app: '', dataRoot: '(config not found)', hostHint: 'unknown' };
|
|
24
24
|
}
|
|
25
25
|
console.log(`\n${pc.bold('hazo-env current')}\n`);
|
|
26
26
|
console.log(` env: ${pc.bold(desc.env)}`);
|
|
@@ -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.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;GAGG;AACH,wBAAsB,MAAM,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2J5E"}
|
package/dist/doctor.js
CHANGED
|
@@ -4,9 +4,11 @@ import path from 'node:path';
|
|
|
4
4
|
import { HazoConfig } from 'hazo_config/server';
|
|
5
5
|
import { optional_import } from 'hazo_core';
|
|
6
6
|
import { getEnv } from './index.client.js';
|
|
7
|
-
import { listEnvs } from './env.server.js';
|
|
7
|
+
import { listEnvs, getEnvRole, getRoleMap } from './env.server.js';
|
|
8
8
|
import { resolveConnectConfig } from './resolve/connect.js';
|
|
9
9
|
import { resolveFilesConfig } from './resolve/files.js';
|
|
10
|
+
// Canonical env names that have built-in role defaults — no [env.roles] entry required
|
|
11
|
+
const CANONICAL_ENV_NAMES = new Set(['dev', 'test', 'staging', 'prod']);
|
|
10
12
|
function findConfigFile(pkg) {
|
|
11
13
|
return path.resolve(process.cwd(), 'config', `${pkg}_config.ini`);
|
|
12
14
|
}
|
|
@@ -110,8 +112,7 @@ export async function doctor(opts = {}) {
|
|
|
110
112
|
if (opts.probe) {
|
|
111
113
|
for (const env of envsToCheck) {
|
|
112
114
|
// Get role for this env to guard against touching prod unattended
|
|
113
|
-
const role = env
|
|
114
|
-
: env === 'staging' ? 'staging' : 'production';
|
|
115
|
+
const role = getEnvRole(env);
|
|
115
116
|
if (role === 'production' && !opts.all) {
|
|
116
117
|
checks.push({
|
|
117
118
|
label: `DB probe [db.${env}]`,
|
|
@@ -153,6 +154,18 @@ export async function doctor(opts = {}) {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
// 6. Check that every env in the pattern has an explicit or canonical role mapping
|
|
158
|
+
const roleMap = getRoleMap();
|
|
159
|
+
const allEnvs = listEnvs();
|
|
160
|
+
for (const env of allEnvs) {
|
|
161
|
+
if (!CANONICAL_ENV_NAMES.has(env) && !(env in roleMap)) {
|
|
162
|
+
checks.push({
|
|
163
|
+
label: `Role for env '${env}'`,
|
|
164
|
+
status: 'error',
|
|
165
|
+
detail: `env '${env}' in pattern has no declared role in [env.roles]; defaulting to development`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
156
169
|
const passed = checks.every((c) => c.status !== 'error');
|
|
157
170
|
return { env: targetEnv, checks, passed };
|
|
158
171
|
}
|
package/dist/env.server.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type { EnvPattern, EnvDescription } from './types/index.js';
|
|
1
|
+
import type { EnvPattern, EnvDescription, EnvRoleMap, HazoEnv, HazoEnvRole } from './types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get the role map, merging client defaults with any [env.roles] section from hazo_env_config.ini.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getRoleMap(): EnvRoleMap;
|
|
6
|
+
/** Get the broad role for the current environment (server-aware, reads INI) */
|
|
7
|
+
export declare function getEnvRole(env?: HazoEnv): HazoEnvRole;
|
|
2
8
|
/**
|
|
3
9
|
* Get the declared env pattern from [env] pattern in hazo_env_config.ini.
|
|
4
10
|
* Falls back to ['dev','test','staging','prod'] if the config is absent.
|
package/dist/env.server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.server.d.ts","sourceRoot":"","sources":["../src/env.server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAY,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"env.server.d.ts","sourceRoot":"","sources":["../src/env.server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAY,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAgB/G;;GAEG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAYvC;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,CAGrD;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAKvC;AAED,gEAAgE;AAChE,wBAAgB,QAAQ,IAAI,UAAU,CAErC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAUpD;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,cAAc,CAY5C"}
|