hazo_env 0.2.0 → 0.4.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 +47 -0
- package/README.md +83 -17
- package/SETUP_CHECKLIST.md +32 -0
- package/config/hazo_env_config.ini.sample +16 -0
- package/dist/cli.js +21 -7
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +168 -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 +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/migrate/db.d.ts +3 -0
- package/dist/migrate/db.d.ts.map +1 -1
- package/dist/migrate/db.js +6 -1
- package/dist/migrate/db.postgrest.d.ts +4 -0
- package/dist/migrate/db.postgrest.d.ts.map +1 -0
- package/dist/migrate/db.postgrest.js +93 -0
- package/dist/migrate/files.d.ts.map +1 -1
- package/dist/migrate/files.js +2 -4
- 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.map +1 -1
- package/dist/migrate/run.js +60 -25
- package/dist/migrate/snapshot.d.ts.map +1 -1
- package/dist/migrate/snapshot.js +3 -0
- package/dist/migrate/verify.d.ts +1 -0
- package/dist/migrate/verify.d.ts.map +1 -1
- package/dist/migrate/verify.js +120 -30
- package/dist/resolve/files.d.ts +6 -0
- package/dist/resolve/files.d.ts.map +1 -1
- package/dist/resolve/files.js +23 -0
- package/dist/resolve/migrate.d.ts +3 -0
- package/dist/resolve/migrate.d.ts.map +1 -0
- package/dist/resolve/migrate.js +29 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -9
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# hazo_env — Change Log
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `verifyFiles()` L2–L5 — DB-backed file existence (L2), size check (L3), hash check L4 (`full|sample|none`; sample = 1% + top-10 by size + top-10 by changed_at), orphan detection L5. Previously all layers were stubs.
|
|
7
|
+
- `VerifyOptions.adapter` — pass a `HazoConnectAdapter` to enable DB-backed checks; without it verify falls back to sentinel-only (disk) and sets `report.skippedReason`.
|
|
8
|
+
- `VerifyReport` — new fields: `hashed?: number`, `skippedReason?: string`.
|
|
9
|
+
- `runMigration` — step 7 verify now passes `tgtAdapter`; emits detailed warning on `!ok` (counts + snapshotId) but continues (G2: warns-not-aborts).
|
|
10
|
+
- CLI `hazo-env verify` — new `--hash full|sample|none` flag (default `sample`); exits 1 on failure (CI-safe).
|
|
11
|
+
- `doctor()` — section 7a: `_migrations` parity check across all SQLite envs (`--probe --all`); section 7b: masking ruleset column validation against live schema.
|
|
12
|
+
- `DoctorOptions.probe` + `DoctorOptions.all` — enable live DB reachability probes + multi-env checks.
|
|
13
|
+
- `hazo_files` added as optional peer dependency (`^3.5.0`).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `DoctorReport.passed` replaces `DoctorReport.ok` (previously typo in docs).
|
|
17
|
+
- `DoctorCheck.status` is `'ok'|'warn'|'error'` (not `boolean`).
|
|
18
|
+
|
|
19
|
+
### Test-app
|
|
20
|
+
- Seed script creates 7 `hazo_files` fixture rows (3 clean, 1 size-mismatch, 1 hash-mismatch, 1 missing, 1 orphan) + physical files under `data/test/files/`.
|
|
21
|
+
- `/api/verify` route + `/verify` report page added.
|
|
22
|
+
- `'verify-fixtures'` autotest scenario added (5 cases).
|
|
23
|
+
|
|
24
|
+
## 0.3.0 — 2026-06-10
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `EnvRoleMap` type (`Record<string, HazoEnvRole>`) in `src/types/index.ts`
|
|
28
|
+
- `roles?: EnvRoleMap` optional field on `EnvDescription` interface
|
|
29
|
+
- `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
|
|
30
|
+
- `[env.roles]` INI section support: arbitrary env names mapped to the four fixed roles (`development`, `test`, `staging`, `production`)
|
|
31
|
+
- `describeEnv()` server version now includes `roles` field
|
|
32
|
+
- Filesystem progress store: `writeMigrationProgress`, `readMigrationProgress`, `clearMigrationProgress` (`src/migrate/progress.ts`)
|
|
33
|
+
- `MigrationRequest.jobId` + `MigrationRequest.progressDir` optional fields — when both present, `runMigration` writes each progress event to `<progressDir>/<jobId>.json`
|
|
34
|
+
- `loadRuleset`, `syncRulesetFromIni`, `parseIniRules` masking ruleset functions now exported from package index
|
|
35
|
+
- `MaskRule` type now exported from package index
|
|
36
|
+
- `normalize()` utility now exported from package index (trim + lowercase env name string)
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- `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)
|
|
40
|
+
|
|
41
|
+
## 0.2.0 — 2026-06-09
|
|
42
|
+
|
|
43
|
+
- feat: M2 migration engine — `runMigration`, `verifyFiles`, `takeSnapshot`, `restoreSnapshot`
|
|
44
|
+
- feat: M3 PII masking — `registerMask`, built-in transforms (`mask_email`, `fake_name`, `mask_phone`, `jitter_date`, `hash`, `tokenize`, `drop`, `nullify`, `redact_pii`)
|
|
45
|
+
- feat: masking ruleset management — `loadRuleset`, `syncRulesetFromIni`, `parseIniRules`, `MaskRule` (not yet exported from index — fixed in 0.3.0)
|
|
46
|
+
- feat: `MaskTransform` type in `src/types/index.ts`
|
|
47
|
+
- feat: CLI commands `migrate`, `verify`, `restore`, `mask sync`, `mask list`
|
|
48
|
+
- chore: version bump to 0.2.0
|
|
49
|
+
|
|
3
50
|
## 0.1.1 — 2026-06-09
|
|
4
51
|
|
|
5
52
|
- 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
|
@@ -9,8 +9,8 @@ Canonical environment resolver for hazo apps. Typed env names, per-env DB/file/s
|
|
|
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
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 `
|
|
13
|
-
- **Doctor** — `doctor()` / `hazo-env doctor` validates pattern, DB reachability, required secrets (no values printed), data_root writability, and
|
|
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`.
|
|
13
|
+
- **Doctor** — `doctor()` / `hazo-env doctor` validates pattern, DB reachability, required secrets (no values printed), data_root writability, schema level, and (with `--probe --all`) migration parity across envs + masking ruleset column validation.
|
|
14
14
|
- **CLI** — `hazo-env current | doctor | snapshot | migrate | verify | restore | mask`.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
@@ -64,18 +64,67 @@ const adapter = await createHazoConnect(resolveConnectConfig());
|
|
|
64
64
|
### Env resolution (client-safe — also in `hazo_env/client`)
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
|
-
import { getEnv, getEnvRole, getPattern, listEnvs, assertEnv, describeEnv,
|
|
67
|
+
import { getEnv, getEnvRole, getRoleMap, getPattern, listEnvs, assertEnv, describeEnv,
|
|
68
68
|
isDev, isTest, isStaging, isProd } from 'hazo_env';
|
|
69
69
|
|
|
70
70
|
getEnv() // 'dev' | 'test' | 'staging' | 'prod'
|
|
71
|
-
getEnvRole() // 'development' | '
|
|
71
|
+
getEnvRole() // 'development' | 'test' | 'staging' | 'production'
|
|
72
|
+
getRoleMap() // EnvRoleMap — full env→role mapping (config-driven; safe defaults)
|
|
72
73
|
getPattern() // e.g. 'dev_prod'
|
|
73
74
|
listEnvs() // ['dev', 'prod'] — valid envs for the declared pattern
|
|
74
75
|
assertEnv() // throws HazoError(ENV_INVALID) if HAZO_ENV is outside the pattern
|
|
75
|
-
describeEnv() // { env, role }
|
|
76
|
+
describeEnv() // { env, role, pattern, app, dataRoot, hostHint, roles? }
|
|
76
77
|
isDev() / isTest() / isStaging() / isProd()
|
|
77
78
|
```
|
|
78
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
|
+
|
|
79
128
|
### Resolvers (server-only)
|
|
80
129
|
|
|
81
130
|
```ts
|
|
@@ -98,15 +147,18 @@ getSecret('POSTGREST_API_KEY', { required: true })
|
|
|
98
147
|
```ts
|
|
99
148
|
import { doctor } from 'hazo_env';
|
|
100
149
|
|
|
101
|
-
const report = await doctor();
|
|
102
|
-
// report.
|
|
103
|
-
// report.checks DoctorCheck[] — { label, ok,
|
|
150
|
+
const report = await doctor({ probe: true, all: true });
|
|
151
|
+
// report.passed boolean
|
|
152
|
+
// report.checks DoctorCheck[] — { label, status: 'ok'|'warn'|'error', detail? }
|
|
153
|
+
// Migration-readiness checks (--probe --all): _migrations parity across envs,
|
|
154
|
+
// masking ruleset column validation against live schema.
|
|
104
155
|
```
|
|
105
156
|
|
|
106
157
|
### Migration engine (server-only)
|
|
107
158
|
|
|
108
159
|
```ts
|
|
109
|
-
import { runMigration, verifyFiles, takeSnapshot, restoreSnapshot
|
|
160
|
+
import { runMigration, verifyFiles, takeSnapshot, restoreSnapshot,
|
|
161
|
+
writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from 'hazo_env';
|
|
110
162
|
|
|
111
163
|
// Copy prod → staging (with PII masking applied automatically)
|
|
112
164
|
const result = await runMigration({
|
|
@@ -116,19 +168,39 @@ const result = await runMigration({
|
|
|
116
168
|
// allowProdTarget: true, // required when `to` is 'prod'
|
|
117
169
|
// confirmToken: '<token>', // required together with allowProdTarget
|
|
118
170
|
onProgress: (p) => console.log(p.phase, p.message),
|
|
171
|
+
// jobId: '<id>', // optional — persist progress to progressDir/<id>.json
|
|
172
|
+
// progressDir: '/tmp/hazo_jobs', // required when jobId is set
|
|
119
173
|
});
|
|
120
174
|
// result.ok, result.db, result.files, result.snapshotId, result.warnings, result.durationMs
|
|
121
175
|
|
|
122
|
-
// Stand-alone verification
|
|
123
|
-
await verifyFiles('staging', dataRoot, { hash: 'sample' });
|
|
176
|
+
// Stand-alone verification (with optional hazo_connect adapter for L2–L5 checks)
|
|
177
|
+
await verifyFiles('staging', dataRoot, { hash: 'sample', checkOrphans: true, adapter });
|
|
178
|
+
// report.ok, report.checked, report.hashed, report.missing[], report.sizeMismatch[],
|
|
179
|
+
// report.hashMismatch[], report.orphans[], report.skippedReason?
|
|
124
180
|
|
|
125
181
|
// Manual snapshot / restore
|
|
126
182
|
const snap = takeSnapshot(connectConfig); // returns { snapshotId }
|
|
127
183
|
restoreSnapshot(connectConfig, snapshotId);
|
|
184
|
+
|
|
185
|
+
// Filesystem progress store (used by hazo_jobs integration)
|
|
186
|
+
writeMigrationProgress(dir, jobId, progress);
|
|
187
|
+
readMigrationProgress(dir, jobId); // → MigrationProgress | null
|
|
188
|
+
clearMigrationProgress(dir, jobId);
|
|
128
189
|
```
|
|
129
190
|
|
|
130
191
|
**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
192
|
|
|
193
|
+
**Masking ruleset API** — load, parse, and sync rulesets programmatically:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { loadRuleset, syncRulesetFromIni, parseIniRules } from 'hazo_env';
|
|
197
|
+
import type { MaskRule } from 'hazo_env';
|
|
198
|
+
|
|
199
|
+
const rules: MaskRule[] = parseIniRules(iniText); // parse INI text → rules array
|
|
200
|
+
const loaded = await loadRuleset(adapter); // read from hazo_app_config DB
|
|
201
|
+
await syncRulesetFromIni(adapter, '/path/to/masking.ini'); // write parsed rules to DB
|
|
202
|
+
```
|
|
203
|
+
|
|
132
204
|
## CLI
|
|
133
205
|
|
|
134
206
|
```
|
|
@@ -163,12 +235,6 @@ Key sections:
|
|
|
163
235
|
|
|
164
236
|
Secret placeholders use `${ENV_VAR_NAME}` syntax — hazo_env substitutes them from `.env.local` at runtime.
|
|
165
237
|
|
|
166
|
-
## Tailwind v4 (`@source` required if consuming UI)
|
|
167
|
-
|
|
168
|
-
```css
|
|
169
|
-
@source "../node_modules/hazo_env/dist";
|
|
170
|
-
```
|
|
171
|
-
|
|
172
238
|
## License
|
|
173
239
|
|
|
174
240
|
MIT
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -33,6 +33,16 @@ api_key = ${POSTGREST_API_KEY}
|
|
|
33
33
|
|
|
34
34
|
**Never put secret values directly in the ini file** — use `${ENV_VAR_NAME}` placeholders.
|
|
35
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
|
+
|
|
36
46
|
## 3. Create `.env.local` (gitignored)
|
|
37
47
|
|
|
38
48
|
One file per deployment (dev machine, staging host, prod host). Contains the secrets referenced by `${…}` placeholders in the ini:
|
|
@@ -84,6 +94,12 @@ All checks should be green before going to production. The doctor validates:
|
|
|
84
94
|
- All required secrets are present in `.env.local` (values are never printed)
|
|
85
95
|
- `data_root` exists and is writable
|
|
86
96
|
|
|
97
|
+
Add `--probe --all` for migration-readiness checks:
|
|
98
|
+
```bash
|
|
99
|
+
npx hazo-env doctor --probe --all
|
|
100
|
+
```
|
|
101
|
+
This additionally checks: live DB reachability for all envs, `_migrations` table parity across envs, masking ruleset column validation against live schema.
|
|
102
|
+
|
|
87
103
|
## 8. (Next.js) Ensure `hazo_env` is in `transpilePackages`
|
|
88
104
|
|
|
89
105
|
```js
|
|
@@ -137,3 +153,19 @@ Review row counts and scrub counts before running without `--dry-run`. The migra
|
|
|
137
153
|
```bash
|
|
138
154
|
npx hazo-env restore staging --snapshot <snapshot-id>
|
|
139
155
|
```
|
|
156
|
+
|
|
157
|
+
## 10. (Required for non-canonical env names) Declare roles in `[env.roles]`
|
|
158
|
+
|
|
159
|
+
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:
|
|
160
|
+
|
|
161
|
+
```ini
|
|
162
|
+
[env.roles]
|
|
163
|
+
qa = test
|
|
164
|
+
preview = staging
|
|
165
|
+
live = production
|
|
166
|
+
rc = staging
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Valid role values: `development`, `test`, `staging`, `production`.
|
|
170
|
+
|
|
171
|
+
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
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
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)}`);
|
|
@@ -122,12 +122,22 @@ async function runMigrate() {
|
|
|
122
122
|
}
|
|
123
123
|
async function runVerify() {
|
|
124
124
|
const targetEnv = args[0] ?? getEnv();
|
|
125
|
-
|
|
125
|
+
// Parse --hash full|sample|none (default: sample) and --no-orphans
|
|
126
|
+
const hashIdx = args.indexOf('--hash');
|
|
127
|
+
const hashArg = hashIdx >= 0 ? args[hashIdx + 1] : 'sample';
|
|
128
|
+
const hash = (hashArg === 'full' || hashArg === 'none' || hashArg === 'sample') ? hashArg : 'sample';
|
|
129
|
+
const checkOrphans = !args.includes('--no-orphans');
|
|
130
|
+
console.log(`\n${pc.bold('hazo-env verify')} ${pc.dim(targetEnv)} ${pc.dim(`--hash ${hash}`)}\n`);
|
|
126
131
|
const { verifyFiles } = await import('./migrate/verify.js');
|
|
127
132
|
const { resolveFilesConfig } = await import('./resolve/files.js');
|
|
128
133
|
const dataRoot = resolveFilesConfig().local.basePath;
|
|
129
|
-
const report = await verifyFiles(targetEnv, dataRoot, { hash
|
|
130
|
-
|
|
134
|
+
const report = await verifyFiles(targetEnv, dataRoot, { hash, checkOrphans });
|
|
135
|
+
if (report.skippedReason) {
|
|
136
|
+
console.log(` ${pc.yellow('⚠')} ${report.skippedReason}`);
|
|
137
|
+
}
|
|
138
|
+
console.log(` Checked: ${report.checked} files${report.hashed != null ? `, hashed: ${report.hashed}` : ''}`);
|
|
139
|
+
if (report.orphans.length)
|
|
140
|
+
console.log(` ${pc.dim('ℹ')} Orphans (no DB row): ${report.orphans.join(', ')}`);
|
|
131
141
|
if (report.ok) {
|
|
132
142
|
console.log(` ${pc.green('✓')} All files verified\n`);
|
|
133
143
|
}
|
|
@@ -136,8 +146,10 @@ async function runVerify() {
|
|
|
136
146
|
console.log(` ${pc.red('✗')} Missing: ${report.missing.join(', ')}`);
|
|
137
147
|
if (report.sizeMismatch.length)
|
|
138
148
|
console.log(` ${pc.yellow('⚠')} Size mismatch: ${report.sizeMismatch.join(', ')}`);
|
|
149
|
+
if (report.hashMismatch.length)
|
|
150
|
+
console.log(` ${pc.yellow('⚠')} Hash mismatch: ${report.hashMismatch.join(', ')}`);
|
|
139
151
|
console.log('');
|
|
140
|
-
process.exit(1);
|
|
152
|
+
process.exit(1); // S4: standalone verify exits 1 on failure (CI-friendly)
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
async function runRestore() {
|
|
@@ -221,6 +233,8 @@ Usage:
|
|
|
221
233
|
--no-db / --no-files Skip DB or file copy
|
|
222
234
|
--allow-prod-target --confirm <token> Override prod-target safety
|
|
223
235
|
hazo-env verify <env> Verify files for an environment
|
|
236
|
+
--hash full|sample|none Hash-check all / sampled / none (default: sample)
|
|
237
|
+
--no-orphans Skip orphan detection
|
|
224
238
|
hazo-env restore <env> --snapshot <id> Restore a snapshot
|
|
225
239
|
hazo-env snapshot <env> Take a snapshot of an environment
|
|
226
240
|
hazo-env mask sync Load masking ruleset from INI into DB
|
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,CA8R5E"}
|
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,170 @@ 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
|
+
}
|
|
169
|
+
// 7. Migration readiness checks
|
|
170
|
+
// 7a. _migrations parity (probe mode, --all): open each SQLite env and compare name-sets
|
|
171
|
+
if (opts.probe && opts.all) {
|
|
172
|
+
const migSets = new Map();
|
|
173
|
+
for (const env of envsToCheck) {
|
|
174
|
+
let cfg;
|
|
175
|
+
try {
|
|
176
|
+
cfg = resolveConnectConfig({ env, allowOtherEnv: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (cfg.type !== 'sqlite' || !cfg.sqlite)
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
const Database = require('better-sqlite3');
|
|
185
|
+
const db = Database(cfg.sqlite.database_path);
|
|
186
|
+
let names;
|
|
187
|
+
try {
|
|
188
|
+
names = db.prepare('SELECT name FROM _migrations').all().map((r) => r.name);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
names = [];
|
|
192
|
+
}
|
|
193
|
+
db.close();
|
|
194
|
+
migSets.set(env, new Set(names));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// DB not openable — skip (probe check already reported error)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (migSets.size >= 2) {
|
|
201
|
+
const envList = [...migSets.keys()];
|
|
202
|
+
const referenceSet = migSets.get(envList[0]);
|
|
203
|
+
const referenceEnv = envList[0];
|
|
204
|
+
for (const env of envList.slice(1)) {
|
|
205
|
+
const s = migSets.get(env);
|
|
206
|
+
const onlyInRef = [...referenceSet].filter((n) => !s.has(n));
|
|
207
|
+
const onlyInEnv = [...s].filter((n) => !referenceSet.has(n));
|
|
208
|
+
if (onlyInRef.length === 0 && onlyInEnv.length === 0) {
|
|
209
|
+
checks.push({ label: `Migration parity (${referenceEnv} ↔ ${env})`, status: 'ok', detail: `${s.size} applied` });
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const detail = [
|
|
213
|
+
onlyInRef.length ? `only in ${referenceEnv}: ${onlyInRef.join(', ')}` : '',
|
|
214
|
+
onlyInEnv.length ? `only in ${env}: ${onlyInEnv.join(', ')}` : '',
|
|
215
|
+
].filter(Boolean).join('; ');
|
|
216
|
+
checks.push({ label: `Migration parity (${referenceEnv} ↔ ${env})`, status: 'warn', detail });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 7b. Masking ruleset column validation: every (table, column) in the masking INI must
|
|
222
|
+
// exist in at least one SQLite env's schema. Skip silently for PostgREST envs.
|
|
223
|
+
const maskingConfigPath = path.resolve(process.cwd(), 'config', 'hazo_env_masking.ini');
|
|
224
|
+
if (fs.existsSync(maskingConfigPath)) {
|
|
225
|
+
try {
|
|
226
|
+
// Parse the masking INI directly — HazoConfig doesn't expose section enumeration.
|
|
227
|
+
// Format: each [section] is a table name; keys are column names.
|
|
228
|
+
const iniContent = fs.readFileSync(maskingConfigPath, 'utf-8');
|
|
229
|
+
const rules = [];
|
|
230
|
+
let currentTable = '';
|
|
231
|
+
for (const rawLine of iniContent.split('\n')) {
|
|
232
|
+
const line = rawLine.trim();
|
|
233
|
+
if (!line || line.startsWith(';') || line.startsWith('#'))
|
|
234
|
+
continue;
|
|
235
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
236
|
+
if (sectionMatch) {
|
|
237
|
+
currentTable = sectionMatch[1].trim();
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!currentTable)
|
|
241
|
+
continue;
|
|
242
|
+
const eqIdx = line.indexOf('=');
|
|
243
|
+
if (eqIdx < 0)
|
|
244
|
+
continue;
|
|
245
|
+
const column = line.slice(0, eqIdx).trim();
|
|
246
|
+
if (column)
|
|
247
|
+
rules.push({ table: currentTable, column });
|
|
248
|
+
}
|
|
249
|
+
if (rules.length > 0) {
|
|
250
|
+
// Collect SQLite schemas for checked envs
|
|
251
|
+
const schemas = new Map(); // "table.column" → present
|
|
252
|
+
for (const env of envsToCheck) {
|
|
253
|
+
let cfg;
|
|
254
|
+
try {
|
|
255
|
+
cfg = resolveConnectConfig({ env, allowOtherEnv: true });
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (cfg.type !== 'sqlite' || !cfg.sqlite)
|
|
261
|
+
continue;
|
|
262
|
+
try {
|
|
263
|
+
const Database = require('better-sqlite3');
|
|
264
|
+
const db = Database(cfg.sqlite.database_path);
|
|
265
|
+
try {
|
|
266
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
|
|
267
|
+
for (const tbl of tables) {
|
|
268
|
+
const cols = db.prepare(`PRAGMA table_info(${tbl})`).all();
|
|
269
|
+
for (const col of cols) {
|
|
270
|
+
schemas.set(`${tbl}.${col.name}`, new Set());
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch { /* DB empty or unreadable */ }
|
|
275
|
+
db.close();
|
|
276
|
+
}
|
|
277
|
+
catch { /* DB not openable */ }
|
|
278
|
+
}
|
|
279
|
+
if (schemas.size > 0) {
|
|
280
|
+
const badRules = [];
|
|
281
|
+
for (const { table, column } of rules) {
|
|
282
|
+
if (!schemas.has(`${table}.${column}`)) {
|
|
283
|
+
badRules.push(`${table}.${column}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (badRules.length === 0) {
|
|
287
|
+
checks.push({ label: 'Masking ruleset columns', status: 'ok', detail: `${rules.length} rule(s) validated` });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
checks.push({
|
|
291
|
+
label: 'Masking ruleset columns',
|
|
292
|
+
status: 'warn',
|
|
293
|
+
detail: `Column(s) not found in schema: ${badRules.join(', ')} (PostgREST envs skipped)`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// No SQLite envs probed — can't validate columns; skip silently
|
|
299
|
+
const hasPostgrest = envsToCheck.some((env) => {
|
|
300
|
+
try {
|
|
301
|
+
return resolveConnectConfig({ env, allowOtherEnv: true }).type === 'postgrest';
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
if (hasPostgrest) {
|
|
308
|
+
checks.push({
|
|
309
|
+
label: 'Masking ruleset columns',
|
|
310
|
+
status: 'ok',
|
|
311
|
+
detail: 'PostgREST env — column validation skipped',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Masking config parse failure — non-fatal, skip
|
|
319
|
+
}
|
|
320
|
+
}
|
|
156
321
|
const passed = checks.every((c) => c.status !== 'error');
|
|
157
322
|
return { env: targetEnv, checks, passed };
|
|
158
323
|
}
|
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"}
|
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"}
|