hazo_env 0.1.1

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 ADDED
@@ -0,0 +1,22 @@
1
+ # hazo_env — Change Log
2
+
3
+ ## 0.1.1 — 2026-06-09
4
+
5
+ - fix: provision `sql-wasm.wasm` into test-app `public/` via seed script (fixes Connect page error when SQLite adapter loads via sql.js WASM)
6
+ - fix: test-app `tsconfig.json` jsx mode corrected to `react-jsx`
7
+ - chore: add test-app `.gitignore` for generated artifacts (`data/`, `public/sql-wasm.wasm`, `logs/`, `.next/`)
8
+ - docs: fill in README.md and SETUP_CHECKLIST.md with M1 API, quick-start, and config reference
9
+
10
+ ## 0.1.0 — 2026-06-08
11
+
12
+ Initial release — M1 Core.
13
+
14
+ - Env resolution: `getEnv`, `getEnvRole`, `getPattern`, `listEnvs`, `assertEnv`, `describeEnv`, `isDev`/`isTest`/`isStaging`/`isProd`
15
+ - Client-safe subset re-exported from `hazo_env/client`
16
+ - `resolveConnectConfig()` — maps current env to hazo_connect config (SQLite / PostgREST) via `hazo_env_config.ini`
17
+ - `resolveFilesConfig()` — maps current env to hazo_files config (data_root)
18
+ - `getSecret()` / `substitutePlaceholders()` — resolves secrets from `.env.local` only
19
+ - `doctor()` — validates pattern, DB reachability, secrets present, data_root writable, schema level
20
+ - `hazo-env` CLI: `current`, `doctor`, `snapshot` (stub)
21
+ - Test-app with sidebar, dev pages (Resolution, Connect, Files), autotest harness, seeded SQLite
22
+ - Unit tests via `hazo_testing`
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # hazo_env
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.
4
+
5
+ ## What it does
6
+
7
+ - **Env resolution** — typed `HazoEnv` (`dev | test | staging | prod`), role mapping, pattern declaration, and `assertEnv()` to fail fast at boot if `HAZO_ENV` is invalid.
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
+ - **Per-env file config** — `resolveFilesConfig()` maps the current env to a `hazo_files` config rooted at the declared `data_root`.
10
+ - **Secrets** — `getSecret()` resolves from `.env.local` only; placeholders in `hazo_env_config.ini` are substituted at runtime without storing secrets.
11
+ - **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`.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install hazo_env
18
+ ```
19
+
20
+ Peer deps (required): `hazo_core`, `hazo_config`.
21
+ Peer deps (optional): `hazo_connect` (for `resolveConnectConfig`), `hazo_files` (for `resolveFilesConfig`).
22
+
23
+ ## Quick start
24
+
25
+ ### 1. Create `hazo_env_config.ini` (copy from `config/hazo_env_config.ini.sample`)
26
+
27
+ ```ini
28
+ [env]
29
+ pattern = dev, prod
30
+ app = myapp
31
+
32
+ [db.dev]
33
+ type = sqlite
34
+ database_path = ${DATA_ROOT}/dev.sqlite
35
+
36
+ [db.prod]
37
+ type = postgrest
38
+ base_url = ${POSTGREST_PROD_URL}
39
+ api_key = ${POSTGREST_API_KEY}
40
+ ```
41
+
42
+ ### 2. Create `.env.local` (gitignored, one per deployment)
43
+
44
+ ```
45
+ POSTGREST_PROD_URL=https://db.myapp.com
46
+ POSTGREST_API_KEY=...
47
+ ```
48
+
49
+ ### 3. Boot
50
+
51
+ ```ts
52
+ import { assertEnv } from 'hazo_env';
53
+ import { resolveConnectConfig } from 'hazo_env';
54
+ import { createHazoConnect } from 'hazo_connect';
55
+
56
+ assertEnv(); // throws if HAZO_ENV not in pattern
57
+ const adapter = await createHazoConnect(resolveConnectConfig());
58
+ ```
59
+
60
+ ## API
61
+
62
+ ### Env resolution (client-safe — also in `hazo_env/client`)
63
+
64
+ ```ts
65
+ import { getEnv, getEnvRole, getPattern, listEnvs, assertEnv, describeEnv,
66
+ isDev, isTest, isStaging, isProd } from 'hazo_env';
67
+
68
+ getEnv() // 'dev' | 'test' | 'staging' | 'prod'
69
+ getEnvRole() // 'development' | 'testing' | 'staging' | 'production'
70
+ getPattern() // e.g. 'dev_prod'
71
+ listEnvs() // ['dev', 'prod'] — valid envs for the declared pattern
72
+ assertEnv() // throws HazoError(ENV_INVALID) if HAZO_ENV is outside the pattern
73
+ describeEnv() // { env, role }
74
+ isDev() / isTest() / isStaging() / isProd()
75
+ ```
76
+
77
+ ### Resolvers (server-only)
78
+
79
+ ```ts
80
+ import { resolveConnectConfig } from 'hazo_env';
81
+ import { resolveFilesConfig } from 'hazo_env';
82
+ import { getSecret } from 'hazo_env';
83
+
84
+ // Returns the hazo_connect config for the current env
85
+ resolveConnectConfig()
86
+
87
+ // Returns the hazo_files config for the current env (base_path from data_root)
88
+ resolveFilesConfig()
89
+
90
+ // Read a value from .env.local — throws HazoError(SECRET_MISSING) when required + absent
91
+ getSecret('POSTGREST_API_KEY', { required: true })
92
+ ```
93
+
94
+ ### Doctor
95
+
96
+ ```ts
97
+ import { doctor } from 'hazo_env';
98
+
99
+ const report = await doctor();
100
+ // report.ok boolean
101
+ // report.checks DoctorCheck[] — { label, ok, message }
102
+ ```
103
+
104
+ ## CLI
105
+
106
+ ```
107
+ hazo-env current # prints env, role, pattern, app, data_root
108
+ hazo-env doctor [--env <e>] [--all] # red/green validation table
109
+ hazo-env snapshot <env> # placeholder — full impl arrives in M2
110
+ ```
111
+
112
+ ## Config reference (`hazo_env_config.ini`)
113
+
114
+ See `config/hazo_env_config.ini.sample` for the full annotated template.
115
+
116
+ Key sections:
117
+ - `[env]` — `pattern` (comma-separated env names), `app` (app identifier)
118
+ - `[data]` — `root` (data_root; default `app_data`)
119
+ - `[db.<env>]` — `type` (`sqlite` | `postgrest`), `database_path` / `base_url` / `api_key`
120
+ - `[host.<env>]` — `location` (`local` | `remote`)
121
+
122
+ Secret placeholders use `${ENV_VAR_NAME}` syntax — hazo_env substitutes them from `.env.local` at runtime.
123
+
124
+ ## Tailwind v4 (`@source` required if consuming UI)
125
+
126
+ ```css
127
+ @source "../node_modules/hazo_env/dist";
128
+ ```
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,92 @@
1
+ # hazo_env — Setup Checklist
2
+
3
+ Follow these steps when adding `hazo_env` to a consuming application.
4
+
5
+ ## 1. Install
6
+
7
+ ```bash
8
+ npm install hazo_env hazo_core hazo_config
9
+ # optional — only if you use resolveConnectConfig / resolveFilesConfig:
10
+ npm install hazo_connect hazo_files
11
+ ```
12
+
13
+ ## 2. Create `hazo_env_config.ini`
14
+
15
+ Copy `node_modules/hazo_env/config/hazo_env_config.ini.sample` to your app's config directory and fill in the values for each env:
16
+
17
+ ```ini
18
+ [env]
19
+ pattern = dev, prod ; or: dev, test, prod / dev, test, staging, prod
20
+ app = myapp
21
+
22
+ [db.dev]
23
+ type = sqlite
24
+ database_path = ${DATA_ROOT}/dev.sqlite
25
+
26
+ [db.prod]
27
+ type = postgrest
28
+ base_url = ${POSTGREST_PROD_URL}
29
+ api_key = ${POSTGREST_API_KEY}
30
+ ```
31
+
32
+ **Never put secret values directly in the ini file** — use `${ENV_VAR_NAME}` placeholders.
33
+
34
+ ## 3. Create `.env.local` (gitignored)
35
+
36
+ One file per deployment (dev machine, staging host, prod host). Contains the secrets referenced by `${…}` placeholders in the ini:
37
+
38
+ ```
39
+ POSTGREST_PROD_URL=https://db.myapp.com
40
+ POSTGREST_API_KEY=...
41
+ ```
42
+
43
+ Add `.env.local` to `.gitignore`.
44
+
45
+ ## 4. Set `HAZO_ENV` in each deployment
46
+
47
+ Each deployment must have `HAZO_ENV` set to one of the values in `[env].pattern`:
48
+
49
+ - Local dev: `HAZO_ENV=dev` in `.env.local` or your shell profile
50
+ - Staging/prod: set in the process environment or platform config
51
+
52
+ ## 5. Call `assertEnv()` at boot
53
+
54
+ ```ts
55
+ import { assertEnv } from 'hazo_env';
56
+
57
+ assertEnv(); // throws if HAZO_ENV is missing or not in the declared pattern
58
+ ```
59
+
60
+ Add this as the first call in your app's startup (e.g. `instrumentation.ts` in Next.js).
61
+
62
+ ## 6. Replace hardcoded DB/file config with resolvers
63
+
64
+ ```ts
65
+ import { resolveConnectConfig } from 'hazo_env';
66
+ import { resolveFilesConfig } from 'hazo_env';
67
+ import { createHazoConnect } from 'hazo_connect';
68
+
69
+ const adapter = await createHazoConnect(resolveConnectConfig());
70
+ // resolveFilesConfig() → pass to createInitializedTrackedFileManager()
71
+ ```
72
+
73
+ ## 7. Run `hazo-env doctor` to verify
74
+
75
+ ```bash
76
+ npx hazo-env doctor
77
+ ```
78
+
79
+ All checks should be green before going to production. The doctor validates:
80
+ - `HAZO_ENV` is in the declared pattern
81
+ - DB config for the current env is readable and reachable
82
+ - All required secrets are present in `.env.local` (values are never printed)
83
+ - `data_root` exists and is writable
84
+
85
+ ## 8. (Next.js) Ensure `hazo_env` is in `transpilePackages`
86
+
87
+ ```js
88
+ // next.config.js
89
+ const nextConfig = {
90
+ transpilePackages: ['hazo_env', 'hazo_core', /* … */],
91
+ };
92
+ ```
@@ -0,0 +1,48 @@
1
+ ; hazo_env configuration sample
2
+ ; Copy to hazo_env_config.ini and fill in values.
3
+ ; Secret values should use ${ENV_VAR_NAME} placeholders, not literal values.
4
+
5
+ [env]
6
+ ; Declare the valid environment names for this app (comma-separated)
7
+ ; Standard values: dev, test, staging, prod
8
+ pattern = dev, test, staging, prod
9
+
10
+ ; Application name (used in log output and doctor reports)
11
+ app = myapp
12
+
13
+ [data]
14
+ ; Base directory for all local file storage (relative to app root or absolute)
15
+ ; Default: app_data
16
+ root = app_data
17
+
18
+ [db.dev]
19
+ ; SQLite database for local development
20
+ type = sqlite
21
+ database_path = ${DATA_ROOT}/dev.sqlite
22
+
23
+ [db.test]
24
+ ; SQLite database for automated tests
25
+ type = sqlite
26
+ database_path = ${DATA_ROOT}/test.sqlite
27
+
28
+ [db.staging]
29
+ ; PostgREST connection for staging environment
30
+ type = postgrest
31
+ base_url = ${POSTGREST_STAGING_URL}
32
+ api_key = ${POSTGREST_STAGING_API_KEY}
33
+
34
+ [db.prod]
35
+ ; PostgREST connection for production environment
36
+ type = postgrest
37
+ base_url = ${POSTGREST_PROD_URL}
38
+ api_key = ${POSTGREST_API_KEY}
39
+
40
+ [host.dev]
41
+ ; Hint about where the dev environment runs (local/remote)
42
+ location = local
43
+
44
+ [host.staging]
45
+ location = remote
46
+
47
+ [host.prod]
48
+ location = remote
@@ -0,0 +1,19 @@
1
+ ; hazo_env masking ruleset sample
2
+ ; Copy to hazo_env_masking.ini and customize.
3
+ ; This file seeds the masking engine with default rules for PII fields.
4
+ ; The runtime source of truth (once hazo_admin is available) is the DB-backed ruleset.
5
+
6
+ [rule.users_email]
7
+ table = hazo_auth_users
8
+ column = email
9
+ strategy = mask_email
10
+
11
+ [rule.users_name]
12
+ table = hazo_auth_users
13
+ column = full_name
14
+ strategy = fake_name
15
+
16
+ [rule.api_keys]
17
+ table = hazo_api_keys
18
+ column = key_hash
19
+ strategy = drop
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // hazo_env/src/cli.ts — CLI entry: hazo-env current | doctor | snapshot
3
+ import pc from 'picocolors';
4
+ import { getEnv, getEnvRole } from './index.client.js';
5
+ import { describeEnv } from './env.server.js';
6
+ import { doctor } from './doctor.js';
7
+ const [, , command, ...args] = process.argv;
8
+ function statusIcon(status) {
9
+ if (status === 'ok')
10
+ return pc.green('✓');
11
+ if (status === 'warn')
12
+ return pc.yellow('⚠');
13
+ return pc.red('✗');
14
+ }
15
+ async function runCurrent() {
16
+ let desc;
17
+ try {
18
+ desc = describeEnv();
19
+ }
20
+ catch {
21
+ // Fallback if no config file
22
+ const env = getEnv();
23
+ desc = { env, role: getEnvRole(env), pattern: ['dev', 'test', 'staging', 'prod'], app: '', dataRoot: '(config not found)', hostHint: 'unknown' };
24
+ }
25
+ console.log(`\n${pc.bold('hazo-env current')}\n`);
26
+ console.log(` env: ${pc.bold(desc.env)}`);
27
+ console.log(` role: ${desc.role}`);
28
+ console.log(` pattern: ${desc.pattern.join(', ')}`);
29
+ if (desc.app)
30
+ console.log(` app: ${desc.app}`);
31
+ console.log(` data root: ${desc.dataRoot}`);
32
+ console.log(` host hint: ${desc.hostHint}`);
33
+ console.log('');
34
+ }
35
+ async function runDoctor() {
36
+ const all = args.includes('--all');
37
+ const probe = args.includes('--probe');
38
+ // --env <e> (space-separated; also support --env=<e> for convenience)
39
+ const envFlagIdx = args.findIndex((a) => a === '--env');
40
+ const envFlag = envFlagIdx >= 0 ? args[envFlagIdx + 1]
41
+ : args.find((a) => a.startsWith('--env='))?.split('=')[1];
42
+ console.log(`\n${pc.bold('hazo-env doctor')}\n`);
43
+ const report = await doctor({ env: envFlag, all, probe });
44
+ console.log(` Checking env: ${pc.bold(report.env)}${all ? ' (all envs)' : ''}\n`);
45
+ const labelWidth = Math.max(...report.checks.map((c) => c.label.length)) + 2;
46
+ for (const check of report.checks) {
47
+ const icon = statusIcon(check.status);
48
+ const label = check.label.padEnd(labelWidth);
49
+ const detail = check.detail ? ` ${check.detail}` : '';
50
+ console.log(` ${icon} ${label}${detail}`);
51
+ }
52
+ console.log('');
53
+ if (report.passed) {
54
+ console.log(` ${pc.green('All checks passed.')}\n`);
55
+ }
56
+ else {
57
+ console.log(` ${pc.red('Some checks failed. Review the errors above.')}\n`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+ async function runSnapshot() {
62
+ const targetEnv = args[0] ?? getEnv();
63
+ console.log(`\n${pc.yellow('snapshot')} — not implemented until Phase 2 (migration engine)\n`);
64
+ console.log(` Requested snapshot of env: ${pc.bold(targetEnv)}`);
65
+ console.log(` Run "hazo-env snapshot" again after the migration engine is built.\n`);
66
+ process.exit(1);
67
+ }
68
+ function printHelp() {
69
+ console.log(`
70
+ ${pc.bold('hazo-env')} — environment resolver CLI
71
+
72
+ Usage:
73
+ hazo-env current Show current env, role, pattern, and data root
74
+ hazo-env doctor Run health checks for the current environment
75
+ hazo-env doctor --all Run health checks for all declared environments
76
+ hazo-env doctor --env <env> Run health checks for a specific environment
77
+ hazo-env doctor --probe Also probe live DB reachability
78
+ hazo-env snapshot <env> (Phase 2 — not yet implemented)
79
+ `);
80
+ }
81
+ (async () => {
82
+ try {
83
+ if (command === 'current') {
84
+ await runCurrent();
85
+ }
86
+ else if (command === 'doctor') {
87
+ await runDoctor();
88
+ }
89
+ else if (command === 'snapshot') {
90
+ await runSnapshot();
91
+ }
92
+ else {
93
+ printHelp();
94
+ }
95
+ }
96
+ catch (e) {
97
+ const msg = e instanceof Error ? e.message : String(e);
98
+ console.error(`\n${pc.red('Error:')} ${msg}\n`);
99
+ process.exit(1);
100
+ }
101
+ })();
@@ -0,0 +1,23 @@
1
+ export interface DoctorCheck {
2
+ label: string;
3
+ status: 'ok' | 'warn' | 'error';
4
+ detail?: string;
5
+ }
6
+ export interface DoctorReport {
7
+ env: string;
8
+ checks: DoctorCheck[];
9
+ passed: boolean;
10
+ }
11
+ export interface DoctorOptions {
12
+ env?: string;
13
+ /** Run checks for all envs in the pattern, not just the current one */
14
+ all?: boolean;
15
+ /** Run live reachability probes (sqlite open / postgrest ping) */
16
+ probe?: boolean;
17
+ }
18
+ /**
19
+ * Run environment health checks.
20
+ * Never prints secret values.
21
+ */
22
+ export declare function doctor(opts?: DoctorOptions): Promise<DoctorReport>;
23
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAWA,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,CA+I5E"}
package/dist/doctor.js ADDED
@@ -0,0 +1,158 @@
1
+ // hazo_env/src/doctor.ts — Environment health checker
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { HazoConfig } from 'hazo_config/server';
5
+ import { optional_import } from 'hazo_core';
6
+ import { getEnv } from './index.client.js';
7
+ import { listEnvs } from './env.server.js';
8
+ import { resolveConnectConfig } from './resolve/connect.js';
9
+ import { resolveFilesConfig } from './resolve/files.js';
10
+ function findConfigFile(pkg) {
11
+ return path.resolve(process.cwd(), 'config', `${pkg}_config.ini`);
12
+ }
13
+ /**
14
+ * Run environment health checks.
15
+ * Never prints secret values.
16
+ */
17
+ export async function doctor(opts = {}) {
18
+ const currentEnv = getEnv();
19
+ const targetEnv = opts.env ?? currentEnv;
20
+ const checks = [];
21
+ // 1. Check config file exists
22
+ const configPath = findConfigFile('hazo_env');
23
+ if (!fs.existsSync(configPath)) {
24
+ checks.push({
25
+ label: 'Config file (hazo_env_config.ini)',
26
+ status: 'error',
27
+ detail: `Not found at ${configPath}`,
28
+ });
29
+ return { env: targetEnv, checks, passed: false };
30
+ }
31
+ checks.push({ label: 'Config file (hazo_env_config.ini)', status: 'ok', detail: configPath });
32
+ // 2. Parse config file
33
+ let config;
34
+ try {
35
+ config = new HazoConfig({ filePath: configPath });
36
+ }
37
+ catch (e) {
38
+ checks.push({ label: 'Config file parse', status: 'error', detail: String(e) });
39
+ return { env: targetEnv, checks, passed: false };
40
+ }
41
+ // 3. Check env pattern declaration
42
+ const patternRaw = config.getSection('env')?.['"pattern"'] ??
43
+ config.getSection('env')?.['pattern'] ??
44
+ '';
45
+ const declaredPattern = patternRaw
46
+ ? patternRaw.split(',').map((s) => s.trim()).filter(Boolean)
47
+ : listEnvs();
48
+ if (patternRaw.length === 0) {
49
+ checks.push({
50
+ label: 'Env pattern ([env] pattern)',
51
+ status: 'warn',
52
+ detail: 'Not declared — using defaults',
53
+ });
54
+ }
55
+ else {
56
+ checks.push({
57
+ label: 'Env pattern ([env] pattern)',
58
+ status: 'ok',
59
+ detail: declaredPattern.join(', '),
60
+ });
61
+ }
62
+ // 4. Check DB config sections for the target env(s)
63
+ const envsToCheck = opts.all ? declaredPattern : [targetEnv];
64
+ for (const env of envsToCheck) {
65
+ try {
66
+ resolveConnectConfig({ env, allowOtherEnv: true });
67
+ checks.push({ label: `DB config [db.${env}]`, status: 'ok' });
68
+ }
69
+ catch (e) {
70
+ const msg = e instanceof Error ? e.message : String(e);
71
+ const isMissingSecret = msg.includes('SECRET_MISSING') || msg.includes('is not set') || msg.includes('Placeholder');
72
+ checks.push({
73
+ label: `DB config [db.${env}]`,
74
+ status: isMissingSecret ? 'warn' : 'error',
75
+ detail: isMissingSecret
76
+ ? `Secret placeholder unresolved (set in .env.local)`
77
+ : msg,
78
+ });
79
+ }
80
+ }
81
+ // 5. Check data root exists and is writable
82
+ try {
83
+ const filesConfig = resolveFilesConfig();
84
+ const dataRoot = filesConfig.local.basePath;
85
+ if (!fs.existsSync(dataRoot)) {
86
+ checks.push({
87
+ label: 'Data root',
88
+ status: 'warn',
89
+ detail: `${dataRoot} does not exist (will be created on first use)`,
90
+ });
91
+ }
92
+ else {
93
+ try {
94
+ fs.accessSync(dataRoot, fs.constants.W_OK);
95
+ checks.push({ label: 'Data root', status: 'ok', detail: dataRoot });
96
+ }
97
+ catch {
98
+ checks.push({ label: 'Data root', status: 'error', detail: `${dataRoot} is not writable` });
99
+ }
100
+ }
101
+ }
102
+ catch (e) {
103
+ checks.push({
104
+ label: 'Data root',
105
+ status: 'warn',
106
+ detail: `Could not resolve: ${e instanceof Error ? e.message : String(e)}`,
107
+ });
108
+ }
109
+ // 5b. Optional probe: actually open the DB
110
+ if (opts.probe) {
111
+ for (const env of envsToCheck) {
112
+ // Get role for this env to guard against touching prod unattended
113
+ const role = env === 'dev' ? 'development' : env === 'test' ? 'test'
114
+ : env === 'staging' ? 'staging' : 'production';
115
+ if (role === 'production' && !opts.all) {
116
+ checks.push({
117
+ label: `DB probe [db.${env}]`,
118
+ status: 'warn',
119
+ detail: 'Refusing to probe a prod target without --all. Pass all:true to override.',
120
+ });
121
+ continue;
122
+ }
123
+ let cfg;
124
+ try {
125
+ cfg = resolveConnectConfig({ env, allowOtherEnv: true });
126
+ }
127
+ catch {
128
+ continue; // already reported as error above
129
+ }
130
+ if (cfg.type === 'sqlite' && cfg.sqlite) {
131
+ const hazoConnect = await optional_import('hazo_connect');
132
+ if (!hazoConnect) {
133
+ checks.push({
134
+ label: `DB probe [db.${env}]`,
135
+ status: 'warn',
136
+ detail: 'install hazo_connect to enable probing',
137
+ });
138
+ }
139
+ else {
140
+ try {
141
+ const conn = hazoConnect.createHazoConnect({ type: 'sqlite', sqlite: cfg.sqlite });
142
+ await conn.query('SELECT 1');
143
+ checks.push({ label: `DB probe [db.${env}]`, status: 'ok', detail: cfg.sqlite.database_path });
144
+ }
145
+ catch (e) {
146
+ checks.push({
147
+ label: `DB probe [db.${env}]`,
148
+ status: 'error',
149
+ detail: e instanceof Error ? e.message : String(e),
150
+ });
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ const passed = checks.every((c) => c.status !== 'error');
157
+ return { env: targetEnv, checks, passed };
158
+ }
@@ -0,0 +1,19 @@
1
+ import type { EnvPattern, EnvDescription } from './types/index.js';
2
+ /**
3
+ * Get the declared env pattern from [env] pattern in hazo_env_config.ini.
4
+ * Falls back to ['dev','test','staging','prod'] if the config is absent.
5
+ */
6
+ export declare function getPattern(): EnvPattern;
7
+ /** List all valid environment names per the declared pattern */
8
+ export declare function listEnvs(): EnvPattern;
9
+ /**
10
+ * Assert that the current env is in the declared pattern.
11
+ * Throws HazoError(ENV_INVALID) if not.
12
+ */
13
+ export declare function assertEnv(pattern?: EnvPattern): void;
14
+ /**
15
+ * Describe the current environment (server-aware full version).
16
+ * Reads app, dataRoot, hostHint from hazo_env_config.ini.
17
+ */
18
+ export declare function describeEnv(): EnvDescription;
19
+ //# sourceMappingURL=env.server.d.ts.map
@@ -0,0 +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;AAc7E;;;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,CAW5C"}
@@ -0,0 +1,61 @@
1
+ // hazo_env/src/env.server.ts — Server-aware env resolution (reads hazo_env_config.ini)
2
+ import path from 'node:path';
3
+ import { HazoConfig } from 'hazo_config/server';
4
+ import { HazoError } from 'hazo_core';
5
+ import { getEnv, getEnvRole } from './index.client.js';
6
+ function findConfigFile(pkg) {
7
+ return path.resolve(process.cwd(), 'config', `${pkg}_config.ini`);
8
+ }
9
+ function tryLoadConfig() {
10
+ try {
11
+ return new HazoConfig({ filePath: findConfigFile('hazo_env') });
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ /**
18
+ * Get the declared env pattern from [env] pattern in hazo_env_config.ini.
19
+ * Falls back to ['dev','test','staging','prod'] if the config is absent.
20
+ */
21
+ export function getPattern() {
22
+ const config = tryLoadConfig();
23
+ const patternRaw = config?.getSection('env')?.['pattern'];
24
+ if (!patternRaw)
25
+ return ['dev', 'test', 'staging', 'prod'];
26
+ return patternRaw.split(',').map((s) => s.trim()).filter(Boolean);
27
+ }
28
+ /** List all valid environment names per the declared pattern */
29
+ export function listEnvs() {
30
+ return getPattern();
31
+ }
32
+ /**
33
+ * Assert that the current env is in the declared pattern.
34
+ * Throws HazoError(ENV_INVALID) if not.
35
+ */
36
+ export function assertEnv(pattern) {
37
+ const env = getEnv();
38
+ const validEnvs = pattern ?? getPattern();
39
+ if (!validEnvs.includes(env)) {
40
+ throw new HazoError({
41
+ code: 'ENV_INVALID',
42
+ pkg: 'hazo_env',
43
+ message: `Current env "${env}" is not in declared pattern [${validEnvs.join(', ')}]`,
44
+ });
45
+ }
46
+ }
47
+ /**
48
+ * Describe the current environment (server-aware full version).
49
+ * Reads app, dataRoot, hostHint from hazo_env_config.ini.
50
+ */
51
+ export function describeEnv() {
52
+ const env = getEnv();
53
+ const role = getEnvRole(env);
54
+ const config = tryLoadConfig();
55
+ const pattern = getPattern();
56
+ const app = config?.getSection('env')?.['app'] ?? '';
57
+ const dataRoot = config?.getSection('data')?.['root'] ?? 'app_data';
58
+ const locationRaw = config?.getSection(`host.${env}`)?.['location'] ?? 'unknown';
59
+ const hostHint = locationRaw === 'local' ? 'local' : locationRaw === 'remote' ? 'remote' : 'unknown';
60
+ return { env, role, pattern, app, dataRoot, hostHint };
61
+ }
@@ -0,0 +1,26 @@
1
+ import type { HazoEnv, HazoEnvRole, EnvPattern, EnvDescription } from './types/index.js';
2
+ export type { HazoEnv, HazoEnvRole, EnvPattern, EnvDescription };
3
+ /** Get the current environment name (normalized) */
4
+ export declare function getEnv(): HazoEnv;
5
+ /** Get the broad role for the current environment */
6
+ export declare function getEnvRole(env?: HazoEnv): HazoEnvRole;
7
+ export declare function isDev(env?: HazoEnv): boolean;
8
+ export declare function isTest(env?: HazoEnv): boolean;
9
+ export declare function isStaging(env?: HazoEnv): boolean;
10
+ export declare function isProd(env?: HazoEnv): boolean;
11
+ /**
12
+ * Get the declared env pattern (list of valid env names).
13
+ * On the client this is read from a build-time injected global or returns a default.
14
+ * The server-side index.ts provides a richer version that reads the INI.
15
+ */
16
+ export declare function getPattern(): EnvPattern;
17
+ /** List all valid environment names per the declared pattern */
18
+ export declare function listEnvs(): EnvPattern;
19
+ /**
20
+ * Assert that the current env is in the declared pattern.
21
+ * Throws HazoError(ENV_INVALID) if not.
22
+ */
23
+ export declare function assertEnv(pattern?: EnvPattern): void;
24
+ /** Describe the current environment (client-safe subset) */
25
+ export declare function describeEnv(): Pick<EnvDescription, 'env' | 'role'>;
26
+ //# sourceMappingURL=index.client.d.ts.map
@@ -0,0 +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;AACzF,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;AASjE,oDAAoD;AACpD,wBAAgB,MAAM,IAAI,OAAO,CAEhC;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,CAMrD;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"}
@@ -0,0 +1,65 @@
1
+ // hazo_env/src/index.client.ts — Client-safe entry point
2
+ import { getCurrentEnv, HazoError } from 'hazo_core/client';
3
+ /** Normalize raw env string to canonical form */
4
+ function normalize(raw) {
5
+ if (raw === 'development')
6
+ return 'dev';
7
+ if (raw === 'production')
8
+ return 'prod';
9
+ return raw;
10
+ }
11
+ /** Get the current environment name (normalized) */
12
+ export function getEnv() {
13
+ return normalize(getCurrentEnv());
14
+ }
15
+ /** Get the broad role for the current environment */
16
+ export function getEnvRole(env) {
17
+ const e = env ?? getEnv();
18
+ if (e === 'dev')
19
+ return 'development';
20
+ if (e === 'test')
21
+ return 'test';
22
+ if (e === 'staging')
23
+ return 'staging';
24
+ return 'production';
25
+ }
26
+ export function isDev(env) { return getEnvRole(env) === 'development'; }
27
+ export function isTest(env) { return getEnvRole(env) === 'test'; }
28
+ export function isStaging(env) { return getEnvRole(env) === 'staging'; }
29
+ export function isProd(env) { return getEnvRole(env) === 'production'; }
30
+ /**
31
+ * Get the declared env pattern (list of valid env names).
32
+ * On the client this is read from a build-time injected global or returns a default.
33
+ * The server-side index.ts provides a richer version that reads the INI.
34
+ */
35
+ export function getPattern() {
36
+ // Client-side: read from window.__HAZO_ENV_PATTERN__ if injected, else default
37
+ if (typeof globalThis !== 'undefined' && globalThis.__HAZO_ENV_PATTERN__) {
38
+ return globalThis.__HAZO_ENV_PATTERN__;
39
+ }
40
+ return ['dev', 'test', 'staging', 'prod'];
41
+ }
42
+ /** List all valid environment names per the declared pattern */
43
+ export function listEnvs() {
44
+ return getPattern();
45
+ }
46
+ /**
47
+ * Assert that the current env is in the declared pattern.
48
+ * Throws HazoError(ENV_INVALID) if not.
49
+ */
50
+ export function assertEnv(pattern) {
51
+ const env = getEnv();
52
+ const validEnvs = pattern ?? getPattern();
53
+ if (!validEnvs.includes(env)) {
54
+ throw new HazoError({
55
+ code: 'ENV_INVALID',
56
+ pkg: 'hazo_env',
57
+ message: `Current env "${env}" is not in declared pattern [${validEnvs.join(', ')}]`,
58
+ });
59
+ }
60
+ }
61
+ /** Describe the current environment (client-safe subset) */
62
+ export function describeEnv() {
63
+ const env = getEnv();
64
+ return { env, role: getEnvRole(env) };
65
+ }
@@ -0,0 +1,8 @@
1
+ export { getEnv, getEnvRole, isDev, isTest, isStaging, isProd } from './index.client.js';
2
+ export { getPattern, listEnvs, assertEnv, describeEnv } from './env.server.js';
3
+ export * from './types/index.js';
4
+ export * from './resolve/secrets.js';
5
+ export * from './resolve/connect.js';
6
+ export * from './resolve/files.js';
7
+ export * from './doctor.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEzF,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE/E,cAAc,kBAAkB,CAAC;AAEjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // hazo_env/src/index.ts — Server entry point
2
+ // Pure client helpers re-exported explicitly (getPattern/listEnvs/assertEnv/describeEnv are overridden below)
3
+ export { getEnv, getEnvRole, isDev, isTest, isStaging, isProd } from './index.client.js';
4
+ // Server-aware overrides
5
+ export { getPattern, listEnvs, assertEnv, describeEnv } from './env.server.js';
6
+ // Types
7
+ export * from './types/index.js';
8
+ // Resolver + secrets
9
+ export * from './resolve/secrets.js';
10
+ export * from './resolve/connect.js';
11
+ export * from './resolve/files.js';
12
+ // Doctor
13
+ export * from './doctor.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { DbEnvConfig, ResolveConnectOptions } from '../types/index.js';
2
+ /**
3
+ * Resolve the connect configuration for the given (or current) environment.
4
+ * Reads the [db.<env>] section from hazo_env_config.ini and substitutes ${VAR} placeholders.
5
+ * ${DATA_ROOT} is automatically resolved from the [data] root value.
6
+ *
7
+ * @param opts.env - Target env (defaults to current env)
8
+ * @param opts.allowOtherEnv - Allow resolving a non-current env (default: false)
9
+ */
10
+ export declare function resolveConnectConfig(opts?: ResolveConnectOptions): DbEnvConfig;
11
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/resolve/connect.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAmB5E;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,qBAA0B,GAAG,WAAW,CA4DlF"}
@@ -0,0 +1,85 @@
1
+ // hazo_env/src/resolve/connect.ts — Server-only connect config resolver
2
+ import path from 'node:path';
3
+ import { HazoConfig } from 'hazo_config/server';
4
+ import { HazoConfigError } from 'hazo_core';
5
+ import { getEnv } from '../index.client.js';
6
+ import { substitutePlaceholders } from './secrets.js';
7
+ import { resolveFilesConfig } from './files.js';
8
+ function findConfigFile(pkg) {
9
+ return path.resolve(process.cwd(), 'config', `${pkg}_config.ini`);
10
+ }
11
+ function loadConfig() {
12
+ const filePath = findConfigFile('hazo_env');
13
+ try {
14
+ return new HazoConfig({ filePath });
15
+ }
16
+ catch {
17
+ throw new HazoConfigError({
18
+ code: 'HAZO_ENV_CONFIG_FILE_MISSING',
19
+ pkg: 'hazo_env',
20
+ message: `Config file not found: ${filePath}. Create config/hazo_env_config.ini.`,
21
+ });
22
+ }
23
+ }
24
+ /**
25
+ * Resolve the connect configuration for the given (or current) environment.
26
+ * Reads the [db.<env>] section from hazo_env_config.ini and substitutes ${VAR} placeholders.
27
+ * ${DATA_ROOT} is automatically resolved from the [data] root value.
28
+ *
29
+ * @param opts.env - Target env (defaults to current env)
30
+ * @param opts.allowOtherEnv - Allow resolving a non-current env (default: false)
31
+ */
32
+ export function resolveConnectConfig(opts = {}) {
33
+ const currentEnv = getEnv();
34
+ const targetEnv = opts.env ?? currentEnv;
35
+ if (!opts.allowOtherEnv && targetEnv !== currentEnv) {
36
+ throw new HazoConfigError({
37
+ code: 'HAZO_ENV_CONFIG_ENV_MISMATCH',
38
+ pkg: 'hazo_env',
39
+ message: `resolveConnectConfig: refusing to resolve env "${targetEnv}" when running as "${currentEnv}". Pass allowOtherEnv:true to override.`,
40
+ });
41
+ }
42
+ const config = loadConfig();
43
+ const section = config.getSection(`db.${targetEnv}`);
44
+ if (!section) {
45
+ throw new HazoConfigError({
46
+ code: 'HAZO_ENV_CONFIG_MISSING_DB_SECTION',
47
+ pkg: 'hazo_env',
48
+ message: `No [db.${targetEnv}] section found in hazo_env_config.ini.`,
49
+ });
50
+ }
51
+ // Compute DATA_ROOT for ${DATA_ROOT} substitution in sqlite paths
52
+ let dataRoot = 'app_data';
53
+ try {
54
+ dataRoot = resolveFilesConfig().local.basePath;
55
+ }
56
+ catch {
57
+ // fallback: use relative default
58
+ }
59
+ const extraVars = { DATA_ROOT: dataRoot };
60
+ const type = section['type'];
61
+ if (type === 'sqlite') {
62
+ const rawPath = section['database_path'];
63
+ if (!rawPath) {
64
+ throw new HazoConfigError({
65
+ code: 'HAZO_ENV_CONFIG_MISSING_DB_PATH',
66
+ pkg: 'hazo_env',
67
+ message: `[db.${targetEnv}] is type=sqlite but has no database_path.`,
68
+ });
69
+ }
70
+ const database_path = substitutePlaceholders(rawPath, extraVars);
71
+ return { type: 'sqlite', sqlite: { database_path } };
72
+ }
73
+ if (type === 'postgrest') {
74
+ const rawUrl = section['base_url'] ?? '';
75
+ const rawKey = section['api_key'] ?? '';
76
+ const base_url = substitutePlaceholders(rawUrl, extraVars);
77
+ const api_key = substitutePlaceholders(rawKey, extraVars);
78
+ return { type: 'postgrest', postgrest: { base_url, api_key } };
79
+ }
80
+ throw new HazoConfigError({
81
+ code: 'HAZO_ENV_CONFIG_UNKNOWN_DB_TYPE',
82
+ pkg: 'hazo_env',
83
+ message: `[db.${targetEnv}] has unknown type "${type}". Expected "sqlite" or "postgrest".`,
84
+ });
85
+ }
@@ -0,0 +1,8 @@
1
+ import type { FilesEnvConfig } from '../types/index.js';
2
+ /**
3
+ * Resolve the local file storage configuration.
4
+ * Reads data_root from [data] section in hazo_env_config.ini.
5
+ * Falls back to app_data if not configured.
6
+ */
7
+ export declare function resolveFilesConfig(appConfigPkg?: string): FilesEnvConfig;
8
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/resolve/files.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAgBxD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,cAAc,CAoCxE"}
@@ -0,0 +1,53 @@
1
+ // hazo_env/src/resolve/files.ts — Server-only files config resolver
2
+ import path from 'node:path';
3
+ import { HazoConfig } from 'hazo_config/server';
4
+ const DEFAULT_DATA_ROOT = 'app_data';
5
+ function findConfigFile(pkg) {
6
+ return path.resolve(process.cwd(), 'config', `${pkg}_config.ini`);
7
+ }
8
+ function tryLoadConfig(pkg) {
9
+ try {
10
+ return new HazoConfig({ filePath: findConfigFile(pkg) });
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ /**
17
+ * Resolve the local file storage configuration.
18
+ * Reads data_root from [data] section in hazo_env_config.ini.
19
+ * Falls back to app_data if not configured.
20
+ */
21
+ export function resolveFilesConfig(appConfigPkg) {
22
+ const envConfig = tryLoadConfig('hazo_env');
23
+ const dataSection = envConfig?.getSection('data');
24
+ let dataRoot = dataSection?.['root'] ?? DEFAULT_DATA_ROOT;
25
+ // Also check the consuming app's config if a pkg name was provided
26
+ if (appConfigPkg) {
27
+ const appConfig = tryLoadConfig(appConfigPkg);
28
+ const appDataSection = appConfig?.getSection('data');
29
+ if (appDataSection?.['root']) {
30
+ dataRoot = appDataSection['root'];
31
+ }
32
+ }
33
+ // Resolve to absolute path if relative
34
+ if (!path.isAbsolute(dataRoot)) {
35
+ dataRoot = path.resolve(process.cwd(), dataRoot);
36
+ }
37
+ // Read optional limits from hazo_env_config.ini [data] section
38
+ const maxFileSizeStr = dataSection?.['max_file_size_mb'];
39
+ const allowedExtensionsStr = dataSection?.['allowed_extensions'];
40
+ const local = { basePath: dataRoot };
41
+ if (maxFileSizeStr) {
42
+ const mb = parseInt(maxFileSizeStr, 10);
43
+ if (!isNaN(mb))
44
+ local.maxFileSize = mb * 1024 * 1024;
45
+ }
46
+ if (allowedExtensionsStr) {
47
+ local.allowedExtensions = allowedExtensionsStr
48
+ .split(',')
49
+ .map((e) => e.trim())
50
+ .filter(Boolean);
51
+ }
52
+ return { provider: 'local', local };
53
+ }
@@ -0,0 +1,16 @@
1
+ export interface GetSecretOptions {
2
+ required?: boolean;
3
+ }
4
+ /**
5
+ * Get a secret value from environment variables.
6
+ * Loads .env.local on first call.
7
+ * Throws HazoConfigError with code HAZO_ENV_CONFIG_SECRET_MISSING if required and absent.
8
+ */
9
+ export declare function getSecret(name: string, options?: GetSecretOptions): string | undefined;
10
+ /**
11
+ * Replace ${VAR_NAME} placeholders in a string with values from extraVars or the environment.
12
+ * extraVars take precedence over process.env (used for e.g. DATA_ROOT injection).
13
+ * Never logs the resolved values.
14
+ */
15
+ export declare function substitutePlaceholders(value: string, extraVars?: Record<string, string>): string;
16
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/resolve/secrets.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,GAAG,SAAS,CAW1F;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAahG"}
@@ -0,0 +1,48 @@
1
+ // hazo_env/src/resolve/secrets.ts — Server-only secret layer
2
+ import dotenv from 'dotenv';
3
+ import path from 'node:path';
4
+ import { registerSingleton, getSingleton, HazoConfigError } from 'hazo_core';
5
+ const DOTENV_SINGLETON_KEY = 'hazo_env:dotenv_loaded';
6
+ /** Load .env.local once per process (idempotent via singleton registry) */
7
+ function ensureSecretsLoaded() {
8
+ if (getSingleton(DOTENV_SINGLETON_KEY))
9
+ return;
10
+ dotenv.config({ path: path.resolve(process.cwd(), '.env.local'), override: false });
11
+ registerSingleton(DOTENV_SINGLETON_KEY, true);
12
+ }
13
+ /**
14
+ * Get a secret value from environment variables.
15
+ * Loads .env.local on first call.
16
+ * Throws HazoConfigError with code HAZO_ENV_CONFIG_SECRET_MISSING if required and absent.
17
+ */
18
+ export function getSecret(name, options = {}) {
19
+ ensureSecretsLoaded();
20
+ const value = process.env[name];
21
+ if ((value === undefined || value === '') && options.required) {
22
+ throw new HazoConfigError({
23
+ code: 'HAZO_ENV_CONFIG_SECRET_MISSING',
24
+ pkg: 'hazo_env',
25
+ message: `Required secret "${name}" is not set. Add it to .env.local or the environment.`,
26
+ });
27
+ }
28
+ return value || undefined;
29
+ }
30
+ /**
31
+ * Replace ${VAR_NAME} placeholders in a string with values from extraVars or the environment.
32
+ * extraVars take precedence over process.env (used for e.g. DATA_ROOT injection).
33
+ * Never logs the resolved values.
34
+ */
35
+ export function substitutePlaceholders(value, extraVars) {
36
+ ensureSecretsLoaded();
37
+ return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
38
+ const resolved = extraVars?.[varName] ?? process.env[varName];
39
+ if (resolved === undefined) {
40
+ throw new HazoConfigError({
41
+ code: 'HAZO_ENV_CONFIG_SECRET_MISSING',
42
+ pkg: 'hazo_env',
43
+ message: `Placeholder \${${varName}} references "${varName}" which is not set in extraVars or environment.`,
44
+ });
45
+ }
46
+ return resolved;
47
+ });
48
+ }
@@ -0,0 +1,78 @@
1
+ /** Canonical environment names after normalization */
2
+ export type HazoEnv = 'dev' | 'test' | 'staging' | 'prod' | string;
3
+ /** Broad role of an environment */
4
+ export type HazoEnvRole = 'development' | 'test' | 'staging' | 'production';
5
+ /** The set of valid environment names for this app, from [env] pattern in INI */
6
+ export type EnvPattern = string[];
7
+ /** Hint about where an environment runs */
8
+ export type HostHint = 'local' | 'remote' | 'unknown';
9
+ /** DB configuration for a single environment (from [db.<env>] in INI) */
10
+ export interface DbEnvConfig {
11
+ type: 'sqlite' | 'postgrest';
12
+ sqlite?: {
13
+ database_path: string;
14
+ };
15
+ postgrest?: {
16
+ base_url: string;
17
+ api_key: string;
18
+ };
19
+ }
20
+ /** Files configuration returned by resolveFilesConfig */
21
+ export interface FilesEnvConfig {
22
+ provider: 'local';
23
+ local: {
24
+ basePath: string;
25
+ maxFileSize?: number;
26
+ allowedExtensions?: string[];
27
+ };
28
+ }
29
+ /** Result of describeEnv() */
30
+ export interface EnvDescription {
31
+ env: HazoEnv;
32
+ role: HazoEnvRole;
33
+ pattern: EnvPattern;
34
+ app: string;
35
+ dataRoot: string;
36
+ hostHint: HostHint;
37
+ }
38
+ /** Options for resolveConnectConfig */
39
+ export interface ResolveConnectOptions {
40
+ env?: HazoEnv;
41
+ allowOtherEnv?: boolean;
42
+ }
43
+ /** Describes a requested migration between two environments */
44
+ export interface MigrationRequest {
45
+ fromEnv: HazoEnv;
46
+ toEnv: HazoEnv;
47
+ includeFiles?: boolean;
48
+ dryRun?: boolean;
49
+ }
50
+ /** Result of a completed migration */
51
+ export interface MigrationResult {
52
+ success: boolean;
53
+ rowsMigrated?: number;
54
+ filesMigrated?: number;
55
+ error?: string;
56
+ }
57
+ /** Progress event emitted during a migration run */
58
+ export interface MigrationProgress {
59
+ phase: 'db' | 'files' | 'verify' | 'done';
60
+ message: string;
61
+ percent?: number;
62
+ }
63
+ /** Result of a post-migration file verification */
64
+ export interface VerifyReport {
65
+ passed: boolean;
66
+ missingFiles: string[];
67
+ extraFiles: string[];
68
+ checksumErrors: string[];
69
+ }
70
+ /** A transform rule for scrubbing/masking sensitive fields */
71
+ export interface ScrubTransform {
72
+ table: string;
73
+ column: string;
74
+ /** Transform strategy: replace with fixed value, hash, or nullify */
75
+ strategy: 'replace' | 'hash' | 'nullify';
76
+ replacement?: string;
77
+ }
78
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnE,mCAAmC;AACnC,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAE5E,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC;AAElC,2CAA2C;AAC3C,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEtD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC7B,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH;AAED,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,uCAAuC;AACvC,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAID,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAID,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,QAAQ,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
@@ -0,0 +1,2 @@
1
+ // hazo_env/src/types/index.ts
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "hazo_env",
3
+ "version": "0.1.1",
4
+ "description": "Canonical environment resolver — typed env names, per-env DB/file/secret config, doctor and CLI for hazo apps",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/index.client.d.ts",
15
+ "import": "./dist/index.client.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "bin": {
20
+ "hazo-env": "./dist/cli.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "config",
25
+ "README.md",
26
+ "SETUP_CHECKLIST.md",
27
+ "CHANGE_LOG.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.build.json",
31
+ "lint": "tsc --noEmit",
32
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
33
+ "dev:test-app": "npm run build && cd test-app && npm run dev",
34
+ "build:test-app": "npm run build && cd test-app && npm run build"
35
+ },
36
+ "dependencies": {
37
+ "dotenv": "^16.4.5",
38
+ "picocolors": "^1.1.1"
39
+ },
40
+ "peerDependencies": {
41
+ "hazo_core": "^1.1.0",
42
+ "hazo_config": "^2.1.10",
43
+ "hazo_connect": "^3.4.0",
44
+ "hazo_files": "^3.0.0",
45
+ "react": "^18.0.0 || ^19.0.0",
46
+ "react-dom": "^18.0.0 || ^19.0.0",
47
+ "next": "^14.0.0 || ^16.0.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "hazo_connect": {
51
+ "optional": true
52
+ },
53
+ "hazo_files": {
54
+ "optional": true
55
+ },
56
+ "react": {
57
+ "optional": true
58
+ },
59
+ "react-dom": {
60
+ "optional": true
61
+ },
62
+ "next": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "typescript": "^5.7.2",
68
+ "jest": "^30.2.0",
69
+ "ts-jest": "^29.4.5",
70
+ "@types/jest": "^30.0.0",
71
+ "jest-environment-node": "^30.2.0",
72
+ "@types/node": "^22.10.0",
73
+ "@types/react": "^19.0.0",
74
+ "@types/react-dom": "^19.0.0",
75
+ "hazo_core": "^1.1.0",
76
+ "hazo_config": "^2.1.10",
77
+ "hazo_connect": "^3.4.0",
78
+ "hazo_files": "^3.0.0",
79
+ "next": "^16.0.10",
80
+ "react": "^19.0.0",
81
+ "react-dom": "^19.0.0",
82
+ "lucide-react": "^0.553.0",
83
+ "tailwindcss": "^4.2.4",
84
+ "@tailwindcss/postcss": "^4.2.4",
85
+ "postcss": "^8.4.49"
86
+ },
87
+ "keywords": [
88
+ "hazo",
89
+ "env",
90
+ "environment",
91
+ "config",
92
+ "resolver",
93
+ "cli"
94
+ ],
95
+ "author": "Pubs Abayasiri",
96
+ "license": "MIT",
97
+ "repository": {
98
+ "type": "git",
99
+ "url": "git+https://github.com/pub12/hazo_env.git"
100
+ },
101
+ "bugs": {
102
+ "url": "https://github.com/pub12/hazo_env/issues"
103
+ },
104
+ "homepage": "https://github.com/pub12/hazo_env#readme"
105
+ }