@super-repo/envx 0.2.3-b.5 → 0.2.3-b.6
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/README.md +82 -1006
- package/dist/auto.js +1 -1
- package/dist/chunks/{commands-Cg8YMJHj.js → commands-B70xh15E.js} +368 -25
- package/dist/chunks/commands-B70xh15E.js.map +1 -0
- package/dist/chunks/src-B_rA7_sV.js +64 -0
- package/dist/chunks/src-B_rA7_sV.js.map +1 -0
- package/dist/chunks/{src-jaqb5pGP.js → src-BeMTu_ms.js} +0 -0
- package/dist/chunks/{src-jaqb5pGP.js.map → src-BeMTu_ms.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/audit.d.ts +12 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/template-ai.d.ts +39 -0
- package/dist/commands/template-ai.d.ts.map +1 -0
- package/dist/commands/template.d.ts +6 -2
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/index.js +2 -63
- package/docs/defaults.md +130 -0
- package/docs/encryption.md +57 -0
- package/docs/hooks.md +267 -0
- package/docs/library-api.md +121 -0
- package/docs/public-variables.md +49 -0
- package/docs/security-models.md +103 -0
- package/docs/template.md +87 -0
- package/package.json +5 -4
- package/dist/chunks/commands-Cg8YMJHj.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as createCli } from "./chunks/commands-
|
|
2
|
+
import { t as createCli } from "./chunks/commands-B70xh15E.js";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
//#region src/cli.ts
|
|
5
|
-
createCli(hideBin(process.argv)).
|
|
5
|
+
await createCli(hideBin(process.argv)).parseAsync();
|
|
6
6
|
//#endregion
|
|
7
7
|
|
|
8
8
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["// The `#!/usr/bin/env node` shebang is added at bundle time by the vite\n// build (configs/vite.config.ts → output.banner) — keeping it out of source\n// avoids a vite/esbuild quirk that drops the rest of the file body when a\n// `.ts` file starts with a shebang.\nimport { hideBin } from \"yargs/helpers\";\n\nimport { createCli } from \"./commands/index.js\";\n\
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["// The `#!/usr/bin/env node` shebang is added at bundle time by the vite\n// build (configs/vite.config.ts → output.banner) — keeping it out of source\n// avoids a vite/esbuild quirk that drops the rest of the file body when a\n// `.ts` file starts with a shebang.\nimport { hideBin } from \"yargs/helpers\";\n\nimport { createCli } from \"./commands/index.js\";\n\n// `parseAsync` is required for any async command handler (template --ai\n// makes a network call). Sync handlers stay sync — yargs awaits a\n// non-promise value as a no-op, so this is a strict superset of parseSync.\nawait createCli(hideBin(process.argv)).parseAsync();\n"],"mappings":";;;;AAWA,MAAM,UAAU,QAAQ,QAAQ,KAAK,CAAC,CAAC,YAAY"}
|
package/dist/commands/audit.d.ts
CHANGED
|
@@ -10,4 +10,16 @@ import { CommandModule } from 'yargs';
|
|
|
10
10
|
* only the first/last 4 chars). Exit 1 when any finding is reported.
|
|
11
11
|
*/
|
|
12
12
|
export declare const auditCommand: CommandModule;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the list of files staged for the next commit (added, copied,
|
|
15
|
+
* modified, or renamed — `--diff-filter=ACMR` excludes deletions). NUL-
|
|
16
|
+
* delimited (`-z`) so paths with spaces or newlines round-trip safely.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` when not inside a git repo (so the caller can render a
|
|
19
|
+
* nicer error than git's stderr). Empty array means "in a repo, but
|
|
20
|
+
* nothing staged" — distinct from the failure case.
|
|
21
|
+
*
|
|
22
|
+
* Exported for unit testing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function listStagedFiles(): string[] | null;
|
|
13
25
|
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAO3C;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,aA0H1B,CAAC;AAkBF;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,GAAG,IAAI,CAejD"}
|
package/dist/commands/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createCli } from "../chunks/commands-
|
|
1
|
+
import { t as createCli } from "../chunks/commands-B70xh15E.js";
|
|
2
2
|
export { createCli };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface AiOptions {
|
|
2
|
+
readonly provider?: "anthropic" | "openai";
|
|
3
|
+
readonly model?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Env var holding the API key. Defaults to `ANTHROPIC_API_KEY` for
|
|
6
|
+
* Anthropic and `OPENAI_API_KEY` for OpenAI.
|
|
7
|
+
*/
|
|
8
|
+
readonly apiKeyEnv?: string;
|
|
9
|
+
readonly baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class AiError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export interface AiAuth {
|
|
15
|
+
readonly token: string;
|
|
16
|
+
readonly kind: "api-key" | "oauth";
|
|
17
|
+
readonly sourceEnv: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve an API key from `process.env`. Looks at `apiKeyEnv` first
|
|
21
|
+
* (defaults to provider-appropriate name), then known OAuth fallbacks
|
|
22
|
+
* for Anthropic. Returns `null` when nothing is set — caller decides
|
|
23
|
+
* how to surface the failure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveAuth(provider: "anthropic" | "openai", apiKeyEnv: string): AiAuth | null;
|
|
26
|
+
/**
|
|
27
|
+
* Generate an example value per key. Returns a Map keyed by env-var
|
|
28
|
+
* name. Keys without a usable AI response fall back to an empty string
|
|
29
|
+
* so the template still serializes cleanly.
|
|
30
|
+
*
|
|
31
|
+
* `context` is an optional per-key hint the renderer can supply (e.g.
|
|
32
|
+
* the trailing comment from the source line, or the comment block
|
|
33
|
+
* directly above) to anchor the generation.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateExamples(keys: ReadonlyArray<{
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly hint?: string;
|
|
38
|
+
}>, opts?: AiOptions): Promise<Map<string, string>>;
|
|
39
|
+
//# sourceMappingURL=template-ai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-ai.d.ts","sourceRoot":"","sources":["../../src/commands/template-ai.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,OAAQ,SAAQ,KAAK;gBACpB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,WAAW,GAAG,QAAQ,EAChC,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAyBf;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,aAAa,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACtE,IAAI,GAAE,SAAc,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA+B9B"}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { CommandModule } from 'yargs';
|
|
2
2
|
/**
|
|
3
3
|
* Generate (or check) a stripped `.env.example` from one or more real
|
|
4
|
-
* env files.
|
|
5
|
-
*
|
|
4
|
+
* env files. The output mirrors the source files' organization —
|
|
5
|
+
* comments, blank lines, and declaration order are preserved verbatim.
|
|
6
|
+
* Values are stripped (or replaced with AI-generated placeholders when
|
|
7
|
+
* `--ai` is set). The encryption banner block (`#/-----[ENVX_PUBLIC_KEY]…`)
|
|
8
|
+
* and the `*PUBLIC_KEY*` kv lines themselves are dropped — the example
|
|
9
|
+
* file should be encryption-agnostic.
|
|
6
10
|
*
|
|
7
11
|
* Three modes:
|
|
8
12
|
* - default: write the rendered template to --out (default `.env.example`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/commands/template.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/commands/template.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAgB3C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,EAAE,aA2H7B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,64 +1,3 @@
|
|
|
1
|
-
import { A as isEncrypted, C as parseEnv, D as decryptValueAsymmetric, E as ENCRYPTED_PREFIX, O as encryptValueAsymmetric, T as toRecord, _ as resolveCwdOrWorkspace, a as auditFiles, b as expandEnvSrc, c as encryptFiles, d as writeKeysFile,
|
|
2
|
-
|
|
3
|
-
function envx(arg) {
|
|
4
|
-
const { config: baseConfig } = loadDotenvxConfig();
|
|
5
|
-
const userOpts = arg === void 0 ? {} : typeof arg === "string" ? { cascade: arg } : arg;
|
|
6
|
-
let cfg = baseConfig;
|
|
7
|
-
if (userOpts.profile) {
|
|
8
|
-
const profile = cfg.profiles?.[userOpts.profile];
|
|
9
|
-
if (!profile) throw new Error(`[ENVX_UNKNOWN_PROFILE] '${userOpts.profile}' not found (available: ${Object.keys(cfg.profiles ?? {}).join(", ") || "none"})`);
|
|
10
|
-
cfg = {
|
|
11
|
-
...cfg,
|
|
12
|
-
...profile
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
const { profile: _drop, ...rest } = userOpts;
|
|
16
|
-
loadEnv(mergeOpts(cfg, rest));
|
|
17
|
-
return process.env;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Per-field merge: caller-supplied option wins, falling through to the
|
|
21
|
-
* matching config field, falling through to whatever default `loadEnv`
|
|
22
|
-
* has built in. Fields the caller doesn't pass and the config doesn't
|
|
23
|
-
* set are simply omitted from the merged options object.
|
|
24
|
-
*/
|
|
25
|
-
function mergeOpts(config, user) {
|
|
26
|
-
const out = {};
|
|
27
|
-
if (user.envFiles !== void 0) out.envFiles = user.envFiles;
|
|
28
|
-
else if (config.envFiles !== void 0) out.envFiles = [...config.envFiles];
|
|
29
|
-
if (user.envPath !== void 0) out.envPath = user.envPath;
|
|
30
|
-
else if (config.envPath !== void 0) out.envPath = config.envPath;
|
|
31
|
-
if (user.cascade !== void 0) out.cascade = user.cascade;
|
|
32
|
-
else if (config.cascade !== void 0) out.cascade = config.cascade;
|
|
33
|
-
if (user.vault !== void 0) out.vault = user.vault;
|
|
34
|
-
if (user.variables !== void 0) out.variables = user.variables;
|
|
35
|
-
if (user.override !== void 0) out.override = user.override;
|
|
36
|
-
else if (config.override !== void 0) out.override = config.override;
|
|
37
|
-
if (user.quiet !== void 0) out.quiet = user.quiet;
|
|
38
|
-
else if (config.quiet !== void 0) out.quiet = config.quiet;
|
|
39
|
-
if (user.autoDetect !== void 0) out.autoDetect = user.autoDetect;
|
|
40
|
-
else if (config.autoDetect !== void 0) out.autoDetect = config.autoDetect;
|
|
41
|
-
if (user.nodeEnvMap !== void 0) out.nodeEnvMap = user.nodeEnvMap;
|
|
42
|
-
else if (config.nodeEnvMap !== void 0) out.nodeEnvMap = config.nodeEnvMap;
|
|
43
|
-
if (user.required !== void 0) out.required = user.required;
|
|
44
|
-
else if (config.required !== void 0) out.required = config.required;
|
|
45
|
-
if (user.expand !== void 0) out.expand = user.expand;
|
|
46
|
-
else if (config.expand !== void 0) out.expand = config.expand;
|
|
47
|
-
if (user.defaults !== void 0) out.defaults = user.defaults;
|
|
48
|
-
else if (config.defaults !== void 0) out.defaults = config.defaults;
|
|
49
|
-
if (user.workspaceRoot !== void 0) out.workspaceRoot = user.workspaceRoot;
|
|
50
|
-
else if (config.workspaceRoot !== void 0) out.workspaceRoot = config.workspaceRoot;
|
|
51
|
-
if (user.schema !== void 0) out.schema = user.schema;
|
|
52
|
-
else if (config.schema !== void 0) out.schema = config.schema;
|
|
53
|
-
if (user.resolvers !== void 0) out.resolvers = user.resolvers;
|
|
54
|
-
else if (config.resolvers !== void 0) out.resolvers = config.resolvers;
|
|
55
|
-
if (user.publicPrefixes !== void 0) out.publicPrefixes = user.publicPrefixes;
|
|
56
|
-
else if (config.publicPrefixes !== void 0) out.publicPrefixes = config.publicPrefixes;
|
|
57
|
-
if (user.publicSource !== void 0) out.publicSource = user.publicSource;
|
|
58
|
-
else if (config.publicSource !== void 0) out.publicSource = config.publicSource;
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
1
|
+
import { A as isEncrypted, C as parseEnv, D as decryptValueAsymmetric, E as ENCRYPTED_PREFIX, O as encryptValueAsymmetric, T as toRecord, _ as resolveCwdOrWorkspace, a as auditFiles, b as expandEnvSrc, c as encryptFiles, d as writeKeysFile, i as BUILT_IN_PATTERNS, k as generateKeyPair, l as defaultKeysPath, m as findWorkspaceRoot, n as defineConfig, o as rotateFiles, p as detectEnvironment, r as loadDotenvxConfig, s as decryptFiles, u as readKeysFile, v as resolveEnvPaths, w as serializeEnv, x as expandRecord } from "./chunks/src-BeMTu_ms.js";
|
|
2
|
+
import { t as envx } from "./chunks/src-B_rA7_sV.js";
|
|
62
3
|
export { BUILT_IN_PATTERNS, ENCRYPTED_PREFIX, auditFiles, decryptFiles, decryptValueAsymmetric, envx as default, envx, defaultKeysPath, defineConfig, detectEnvironment, encryptFiles, encryptValueAsymmetric, expandEnvSrc, expandRecord, findWorkspaceRoot, generateKeyPair, isEncrypted, loadDotenvxConfig, parseEnv, readKeysFile, resolveCwdOrWorkspace, resolveEnvPaths, rotateFiles, serializeEnv, toRecord, writeKeysFile };
|
|
63
|
-
|
|
64
|
-
//# sourceMappingURL=index.js.map
|
package/docs/defaults.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Defaults
|
|
2
|
+
|
|
3
|
+
Six resolution rules drive every envx invocation. Each is overridable — see [Configuration](./configuration.md) for the layered customization map.
|
|
4
|
+
|
|
5
|
+
## 1. Environment auto-detection
|
|
6
|
+
|
|
7
|
+
When `--env` is left at its default (`[".env"]`), envx auto-detects the deployment environment from well-known platform signals and rewrites the env-file suffix accordingly. Source-of-truth: `detectEnvironment()`.
|
|
8
|
+
|
|
9
|
+
Order of precedence: **Vercel → Netlify → `NODE_ENV` → unsuffixed**.
|
|
10
|
+
|
|
11
|
+
| signal | detected | env file picked |
|
|
12
|
+
| ------------------------------------------------------- | ---------- | --------------- |
|
|
13
|
+
| `VERCEL` set + `VERCEL_ENV=production` | `prod` | `.env.prod` |
|
|
14
|
+
| `VERCEL` set + any other `VERCEL_ENV` (or unset) | `dev` | `.env.dev` |
|
|
15
|
+
| `NETLIFY` set + `CONTEXT=production` | `prod` | `.env.prod` |
|
|
16
|
+
| `NETLIFY` set + `CONTEXT=deploy-preview` / `branch-deploy` | `dev` | `.env.dev` |
|
|
17
|
+
| `NETLIFY` set + any other `CONTEXT` | `dev` | `.env.dev` |
|
|
18
|
+
| `NODE_ENV=production` | `prod` | `.env.prod` |
|
|
19
|
+
| `NODE_ENV=development` | `dev` | `.env.dev` |
|
|
20
|
+
| `NODE_ENV=local` | `local` | `.env.local` |
|
|
21
|
+
| `NODE_ENV=<other>` (e.g. `staging`, `qa`, `preview`) | `<other>` | `.env.<other>` |
|
|
22
|
+
| _no signals_ | `root` | `.env` |
|
|
23
|
+
|
|
24
|
+
`NODE_ENV` values not in the built-in map (`production`, `development`, `local`) **pass through lowercased** as the suffix. So `NODE_ENV=staging` → `.env.staging`, `NODE_ENV=QA` → `.env.qa`, etc. — no config required.
|
|
25
|
+
|
|
26
|
+
**Customize:**
|
|
27
|
+
- Pass `--env <name>` (or `-e <name>`) to bypass detection entirely; bare names get the `.env.` prefix automatically (`--env staging` → `.env.staging`).
|
|
28
|
+
- Disable detection: set `autoDetect: false` in `envx.config.{ts,js,json}`.
|
|
29
|
+
- Override the `NODE_ENV → suffix` mapping in config:
|
|
30
|
+
```ts
|
|
31
|
+
// envx.config.ts
|
|
32
|
+
export default {
|
|
33
|
+
nodeEnvMap: {
|
|
34
|
+
// built-ins keep working unless you override them:
|
|
35
|
+
production: "prod",
|
|
36
|
+
development: "dev",
|
|
37
|
+
local: "local",
|
|
38
|
+
// your additions / remappings:
|
|
39
|
+
qa: "qa-prod", // NODE_ENV=qa → .env.qa-prod
|
|
40
|
+
preview: "", // NODE_ENV=preview → .env (no suffix)
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
- Programmatic: `envx({ envFiles: [...] })`, `envx("staging")` (sets `cascade`), or `envx({ autoDetect: false, nodeEnvMap: { ... } })`.
|
|
45
|
+
|
|
46
|
+
Run **`envx info`** to print the resolved settings, the detected environment, the active mapping, and which files would be loaded.
|
|
47
|
+
|
|
48
|
+
## 2. Cascade expansion order
|
|
49
|
+
|
|
50
|
+
`--cascade <name>` expands the base files into a layered set, **least- to most-specific**, so that the most specific one wins. Source-of-truth: `expandCascadePaths()`.
|
|
51
|
+
|
|
52
|
+
For a base of `.env` and `--cascade prod`:
|
|
53
|
+
|
|
54
|
+
| order | path | role |
|
|
55
|
+
| :---: | ----------------- | --------------------------------- |
|
|
56
|
+
| 1 | `.env.prod.local` | per-developer prod override (gitignored) |
|
|
57
|
+
| 2 | `.env.local` | per-developer override (gitignored) |
|
|
58
|
+
| 3 | `.env.prod` | shared prod values (committable, encryptable) |
|
|
59
|
+
| 4 | `.env` | shared baseline |
|
|
60
|
+
|
|
61
|
+
Without `--cascade`: `.env.local` → `.env` (still least-specific-last).
|
|
62
|
+
|
|
63
|
+
**Customize:**
|
|
64
|
+
- **CLI**: pass `--cascade <name>` to cascade with an explicit env name (e.g. `--cascade prod`).
|
|
65
|
+
- **Config**: set `cascade: true` to cascade using the auto-detected env name. `cascade: false` (or omit) disables.
|
|
66
|
+
- **Programmatic**: `envx({ cascade: true })` for auto-detected, `envx({ cascade: "prod" })` for explicit, `envx("prod")` (string shorthand) is equivalent to the explicit form.
|
|
67
|
+
- `--override` conflicts with cascade — envx exits with an error if both are set.
|
|
68
|
+
|
|
69
|
+
## 3. Path resolution (cwd-first → workspace-root fallback)
|
|
70
|
+
|
|
71
|
+
Every relative path passed to `--dir`, `--env-path`, `--env-keys-file`, or set in config goes through the same resolver. Source-of-truth: `resolveCwdOrWorkspace()`.
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
absolute path? → use verbatim
|
|
75
|
+
<cwd>/<rel> exists? → use that
|
|
76
|
+
<workspaceRoot>/<rel>? → use that
|
|
77
|
+
neither? → return <cwd>/<rel> so callers (encrypt, etc.) can create it
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The "default `.env.keys`" lookup runs the same resolver against the literal `".env.keys"` — so a single `.env.keys` at the workspace root serves every package by default.
|
|
81
|
+
|
|
82
|
+
**Customize:**
|
|
83
|
+
- Pass an absolute path to `-fk` / `--dir` to skip the walk-up entirely.
|
|
84
|
+
- Set `envKeysFile` / `envPath` in `envx.config.{ts,js,json}`.
|
|
85
|
+
- Programmatic: `envx({ envPath: "vault", envKeysFile: "/abs/path/.env.keys" })`.
|
|
86
|
+
|
|
87
|
+
## 4. Workspace root detection
|
|
88
|
+
|
|
89
|
+
`findWorkspaceRoot(startDir)` walks up from `startDir` and returns the first directory that matches one of these signals, in order:
|
|
90
|
+
|
|
91
|
+
| pass | signal |
|
|
92
|
+
| :--: | ------------------------------------------------------------ |
|
|
93
|
+
| 1 | `package.json` with a `workspaces` field (npm/yarn) |
|
|
94
|
+
| 1 | `package.json` with a `pnpm.workspaces` field |
|
|
95
|
+
| 2 | `pnpm-workspace.yaml` |
|
|
96
|
+
| 2 | `lerna.json` |
|
|
97
|
+
| 2 | `nx.json` |
|
|
98
|
+
| 2 | `rush.json` |
|
|
99
|
+
| 2 | `yarn.lock` |
|
|
100
|
+
| 2 | `pnpm-lock.yaml` |
|
|
101
|
+
|
|
102
|
+
If nothing matches, `startDir` is returned (so envx degrades to single-package behavior — the cwd-first / fallback resolver still works, the fallback is just a no-op).
|
|
103
|
+
|
|
104
|
+
**Customize:**
|
|
105
|
+
- The detection is currently hardcoded. If you have a non-standard layout, pass absolute paths to `--dir` / `-fk` / `--env` and skip the walk entirely.
|
|
106
|
+
|
|
107
|
+
## 5. Default `--env` / `--env-path` behavior
|
|
108
|
+
|
|
109
|
+
| condition | resolved env files |
|
|
110
|
+
| ------------------------------------------------------ | --------------------------------------------------------------- |
|
|
111
|
+
| `--env` explicitly passed | exactly the files passed (with `.env.` prefix added to bare names) |
|
|
112
|
+
| `envFiles` set in config | the config's list |
|
|
113
|
+
| `--env-path <dir>` set, no `--env` | every `.env*` file inside `<dir>` (sorted, excluding `.env.keys`) |
|
|
114
|
+
| neither set | `[".env"]` (with auto-detection from rule #1) |
|
|
115
|
+
|
|
116
|
+
`--vault` is exact shorthand for `--env-path vault`. The `.env.keys` file is **never** discovered by `listEnvFiles`, so `--dir vault` won't accidentally try to "encrypt" your private keys.
|
|
117
|
+
|
|
118
|
+
**Customize:**
|
|
119
|
+
- `--env <file>` (repeatable) — explicit list.
|
|
120
|
+
- `--env-path <dir>` (or `--dir`, `-d`) — pull every `.env*` from a directory.
|
|
121
|
+
- `envFiles` / `envPath` in config.
|
|
122
|
+
- Programmatic: `envx({ envFiles: [...], envPath: "..." })`.
|
|
123
|
+
|
|
124
|
+
## 6. CLI variable overrides
|
|
125
|
+
|
|
126
|
+
`-v KEY=VALUE` (repeatable) sets values on `process.env` *after* env files load. Format is `^(\w+)=([\s\S]+)$` — multi-line values are supported, but the key must be a valid bash identifier. Source-of-truth: `validateCmdVariable()`.
|
|
127
|
+
|
|
128
|
+
**Customize:**
|
|
129
|
+
- `-v KEY=VALUE` per-call.
|
|
130
|
+
- Programmatic: `envx({ variables: ["KEY=VALUE", ...] })`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Encryption
|
|
2
|
+
|
|
3
|
+
envx uses **asymmetric (ECIES) encryption by default** — exactly the same scheme as `dotenvx`. On first encrypt of a fresh `.env*` file:
|
|
4
|
+
|
|
5
|
+
1. A fresh secp256k1 keypair is generated.
|
|
6
|
+
2. The public key gets prepended to the env file as a banner + `ENVX_PUBLIC_KEY*` header (safe to commit).
|
|
7
|
+
3. The matching private key gets written to `.env.keys` (gitignore this) under `ENVX_PRIVATE_KEY*` with a section banner per env file.
|
|
8
|
+
4. Every selected value is encrypted with the public key. Since the public key is committed alongside the env file, anyone with read access can encrypt new values; only the private-key holder can decrypt.
|
|
9
|
+
|
|
10
|
+
A typical encrypted `.env.dev`:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
#/-------------------[ENVX_PUBLIC_KEY]----------------------/
|
|
14
|
+
#/ public-key encryption for .env files /
|
|
15
|
+
#/ [how it works](https://dotenvx.com/encryption) /
|
|
16
|
+
#/----------------------------------------------------------/
|
|
17
|
+
ENVX_PUBLIC_KEY_DEV="0266287313a6f4d107115d5963640830fcca378ae34a5e3dbf775122fd121f7084"
|
|
18
|
+
|
|
19
|
+
DATABASE_URL=encrypted:Bf3p…
|
|
20
|
+
API_KEY=encrypted:7nQ2…
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
…and the matching `.env.keys` (gitignored):
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
#/------------------!ENVX_PRIVATE_KEYS!---------------------/
|
|
27
|
+
#/ private decryption keys. DO NOT commit to source control /
|
|
28
|
+
#/ [how it works](https://dotenvx.com/encryption) /
|
|
29
|
+
#/----------------------------------------------------------/
|
|
30
|
+
|
|
31
|
+
# .env.dev
|
|
32
|
+
ENVX_PRIVATE_KEY_DEV="…64 hex chars…"
|
|
33
|
+
|
|
34
|
+
# .env.prod
|
|
35
|
+
ENVX_PRIVATE_KEY_PROD="…64 hex chars…"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Rotating keys
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
envx rotate # rotate keypair for ./.env
|
|
42
|
+
envx rotate --dir vault # rotate every vault/.env* file
|
|
43
|
+
envx rotate -k API_KEY # rotate one variable in place
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Rotation generates a fresh keypair, decrypts every value with the old private key, re-encrypts with the new public key, and updates both the env file and `.env.keys` atomically. Files without a public-key header surface an error — run `envx encrypt` against them first to set up the keypair.
|
|
47
|
+
|
|
48
|
+
## dotenvx-compatible naming
|
|
49
|
+
|
|
50
|
+
envx reads keys under either prefix and writes new ones under the canonical names:
|
|
51
|
+
|
|
52
|
+
| concept | canonical (envx writes this) | dotenvx-compat (envx reads this) |
|
|
53
|
+
| ----------- | ------------------------------- | -------------------------------- |
|
|
54
|
+
| public key | `ENVX_PUBLIC_KEY*` | `DOTENV_PUBLIC_KEY*` |
|
|
55
|
+
| private key | `ENVX_PRIVATE_KEY*` | `DOTENV_PRIVATE_KEY*` |
|
|
56
|
+
|
|
57
|
+
A `.env.keys` produced by upstream dotenvx will Just Work — envx decrypts and re-encrypts in place without renaming the entry, so the diff doesn't ripple.
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Pre-commit hooks & CI
|
|
2
|
+
|
|
3
|
+
Three commands compose into the envx git gate, but they belong at different layers:
|
|
4
|
+
|
|
5
|
+
| layer | command | scope |
|
|
6
|
+
| -------------- | ---------------------------------------------------------------------------------------- | ------------------------------ |
|
|
7
|
+
| **pre-commit** | `envx audit --staged` | only the files git is about to commit |
|
|
8
|
+
| **pre-push** | `envx audit && envx doctor && envx template --check` | the whole working tree |
|
|
9
|
+
| **CI** | same as pre-push, without `--ignore-required` so missing required keys fail builds | the whole repo at the merge target |
|
|
10
|
+
|
|
11
|
+
> **Note:** `envx audit` honors `.gitignore` by default (anything matched by `.gitignore` or `.git/info/exclude` is skipped, including `.env*` files). Pass `--no-respect-gitignore` to scan everything.
|
|
12
|
+
|
|
13
|
+
## Choosing the runner
|
|
14
|
+
|
|
15
|
+
Every recipe below uses the bare `envx` command. If the package is installed locally as a devDependency, prefix it with whichever runner your project uses — they're all interchangeable:
|
|
16
|
+
|
|
17
|
+
| project setup | invocation |
|
|
18
|
+
| ------------------- | -------------------------------- |
|
|
19
|
+
| npm / unknown | `npx envx audit --staged` |
|
|
20
|
+
| pnpm | `pnpm exec envx audit --staged` |
|
|
21
|
+
| Yarn (PnP or v1+) | `yarn envx audit --staged` |
|
|
22
|
+
| Bun | `bunx envx audit --staged` |
|
|
23
|
+
| globally installed | `envx audit --staged` |
|
|
24
|
+
|
|
25
|
+
`npx envx` is the most portable and works in any project that has `@super-repo/envx` on the install graph (root or nested). The recipes below show `npx`; swap for your runner if you prefer.
|
|
26
|
+
|
|
27
|
+
## Why this split
|
|
28
|
+
|
|
29
|
+
A pre-commit hook should only inspect what the commit is actually changing. Three reasons:
|
|
30
|
+
|
|
31
|
+
1. **Speed** — pre-commit runs on every `git commit`. The developer notices anything over a second. Whole-repo checks (`doctor`, `template --check`) cross that line; staged-set scans don't.
|
|
32
|
+
2. **Signal** — a failing `template --check` on a commit that didn't touch any `.env*` file is noise, not a real defect. Push the heavier checks one layer out where they make sense.
|
|
33
|
+
3. **Honesty** — the gate should reflect the diff git is about to record, not the rest of the working tree.
|
|
34
|
+
|
|
35
|
+
`envx audit --staged` runs `git diff --cached --name-only --diff-filter=ACMR` internally to find the staged set, then scans only those files. That's the right shape for pre-commit. Everything else is pre-push (or CI).
|
|
36
|
+
|
|
37
|
+
## Pre-commit recipes
|
|
38
|
+
|
|
39
|
+
All four recipes do the same one thing: scan the staged file set for plaintext secrets. Fast, scoped, fails closed when a secret slips into the diff.
|
|
40
|
+
|
|
41
|
+
### Husky
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm i -D husky && npx husky init
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# .husky/pre-commit
|
|
49
|
+
[ "${ENVX_SKIP_HOOK:-}" = "1" ] && exit 0
|
|
50
|
+
npx envx audit --staged
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### lefthook
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm i -D lefthook && npx lefthook install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
# lefthook.yml
|
|
61
|
+
pre-commit:
|
|
62
|
+
commands:
|
|
63
|
+
audit:
|
|
64
|
+
run: npx envx audit --staged
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### simple-git-hooks
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm i -D simple-git-hooks
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"simple-git-hooks": {
|
|
76
|
+
"pre-commit": "npx envx audit --staged"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Native git (no third-party tool)
|
|
82
|
+
|
|
83
|
+
Commit hooks under `.hooks/` and activate the directory once per clone — everyone gets the same hooks, no install step.
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
# One-time per clone
|
|
87
|
+
git config core.hooksPath .hooks
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
#!/usr/bin/env bash
|
|
92
|
+
# .hooks/pre-commit
|
|
93
|
+
set -euo pipefail
|
|
94
|
+
[ "${ENVX_SKIP_HOOK:-}" = "1" ] && exit 0
|
|
95
|
+
npx envx audit --staged
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Pre-push recipes
|
|
99
|
+
|
|
100
|
+
Pre-push is where the whole-repo checks run. The push has a much bigger blast radius than a commit, and it happens far less often, so the latency cost is acceptable. This is the right place for `doctor` (config / keys-file / decryptability sanity) and `template --check` (drift detection between source `.env*` and the committed `.env.example`).
|
|
101
|
+
|
|
102
|
+
### Husky
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# .husky/pre-push
|
|
106
|
+
[ "${ENVX_SKIP_HOOK:-}" = "1" ] && exit 0
|
|
107
|
+
npx envx audit
|
|
108
|
+
npx envx doctor --ignore-required
|
|
109
|
+
[ -f .env ] && npx envx template --check
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### lefthook
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
# lefthook.yml
|
|
116
|
+
pre-push:
|
|
117
|
+
parallel: true
|
|
118
|
+
commands:
|
|
119
|
+
audit:
|
|
120
|
+
run: npx envx audit
|
|
121
|
+
doctor:
|
|
122
|
+
run: npx envx doctor --ignore-required
|
|
123
|
+
template-check:
|
|
124
|
+
run: '[ -f .env ] && npx envx template --check || true'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### simple-git-hooks
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"simple-git-hooks": {
|
|
132
|
+
"pre-push": "npx envx audit && npx envx doctor --ignore-required && ([ ! -f .env ] || npx envx template --check)"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Native git
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
#!/usr/bin/env bash
|
|
141
|
+
# .hooks/pre-push
|
|
142
|
+
set -euo pipefail
|
|
143
|
+
[ "${ENVX_SKIP_HOOK:-}" = "1" ] && exit 0
|
|
144
|
+
npx envx audit
|
|
145
|
+
npx envx doctor --ignore-required
|
|
146
|
+
[ -f .env ] && npx envx template --check
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Bypass
|
|
150
|
+
|
|
151
|
+
- `ENVX_SKIP_HOOK=1 git commit …` (or `git push …`) — skip the envx hook only.
|
|
152
|
+
- `git commit --no-verify` / `git push --no-verify` — skip every git hook (use sparingly).
|
|
153
|
+
|
|
154
|
+
## Want both layers in one hook?
|
|
155
|
+
|
|
156
|
+
If you don't run pre-push hooks at all (some teams disable them entirely), you can still scope the heavier checks to commits that actually touch env-related files. Use the staged file list as a trigger:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
#!/usr/bin/env bash
|
|
160
|
+
# .hooks/pre-commit — staged audit always; doctor + template only when env files change
|
|
161
|
+
set -euo pipefail
|
|
162
|
+
[ "${ENVX_SKIP_HOOK:-}" = "1" ] && exit 0
|
|
163
|
+
|
|
164
|
+
# Always: scan the commit set for plaintext secrets.
|
|
165
|
+
npx envx audit --staged
|
|
166
|
+
|
|
167
|
+
# Conditionally: re-validate config / template when env-related files
|
|
168
|
+
# are part of this commit. The trigger matches `.env*`, envx config
|
|
169
|
+
# files, and package.json (because `envx.config` can live there).
|
|
170
|
+
ENV_TRIGGER='(^|/)\.env(\.|$)|(^|/)envx\.config\.|(^|/)package\.json$'
|
|
171
|
+
if git diff --cached --name-only --diff-filter=ACMR | grep -qE "$ENV_TRIGGER"; then
|
|
172
|
+
npx envx doctor --ignore-required
|
|
173
|
+
[ -f .env ] && npx envx template --check
|
|
174
|
+
fi
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This keeps the common-case commit (no env files touched) at the speed of just `audit --staged`, while still gating commits that change env-relevant files.
|
|
178
|
+
|
|
179
|
+
## CI
|
|
180
|
+
|
|
181
|
+
CI is the canonical place for the full battery — no `--ignore-required` so missing required keys fail builds, not just commits. Examples for the three most common npm clients — pick one:
|
|
182
|
+
|
|
183
|
+
### npm
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
# .github/workflows/secrets.yml
|
|
187
|
+
name: secrets-gate
|
|
188
|
+
on: [pull_request, push]
|
|
189
|
+
jobs:
|
|
190
|
+
envx:
|
|
191
|
+
runs-on: ubuntu-latest
|
|
192
|
+
steps:
|
|
193
|
+
- uses: actions/checkout@v4
|
|
194
|
+
- uses: actions/setup-node@v4
|
|
195
|
+
with: { node-version: 20, cache: npm }
|
|
196
|
+
- run: npm ci
|
|
197
|
+
- run: npx envx audit --json > audit.json # fails on plaintext secrets
|
|
198
|
+
- run: npx envx doctor # fails on missing required keys
|
|
199
|
+
- run: npx envx template --check # fails on .env.example drift
|
|
200
|
+
- if: failure()
|
|
201
|
+
uses: actions/upload-artifact@v4
|
|
202
|
+
with: { name: envx-audit, path: audit.json }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### pnpm
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
# .github/workflows/secrets.yml
|
|
209
|
+
name: secrets-gate
|
|
210
|
+
on: [pull_request, push]
|
|
211
|
+
jobs:
|
|
212
|
+
envx:
|
|
213
|
+
runs-on: ubuntu-latest
|
|
214
|
+
steps:
|
|
215
|
+
- uses: actions/checkout@v4
|
|
216
|
+
- uses: pnpm/action-setup@v4
|
|
217
|
+
- uses: actions/setup-node@v4
|
|
218
|
+
with: { node-version: 20, cache: pnpm }
|
|
219
|
+
- run: pnpm install --frozen-lockfile
|
|
220
|
+
- run: pnpm exec envx audit --json > audit.json
|
|
221
|
+
- run: pnpm exec envx doctor
|
|
222
|
+
- run: pnpm exec envx template --check
|
|
223
|
+
- if: failure()
|
|
224
|
+
uses: actions/upload-artifact@v4
|
|
225
|
+
with: { name: envx-audit, path: audit.json }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### yarn
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# .github/workflows/secrets.yml
|
|
232
|
+
name: secrets-gate
|
|
233
|
+
on: [pull_request, push]
|
|
234
|
+
jobs:
|
|
235
|
+
envx:
|
|
236
|
+
runs-on: ubuntu-latest
|
|
237
|
+
steps:
|
|
238
|
+
- uses: actions/checkout@v4
|
|
239
|
+
- uses: actions/setup-node@v4
|
|
240
|
+
with: { node-version: 20, cache: yarn }
|
|
241
|
+
- run: yarn install --frozen-lockfile
|
|
242
|
+
- run: yarn envx audit --json > audit.json
|
|
243
|
+
- run: yarn envx doctor
|
|
244
|
+
- run: yarn envx template --check
|
|
245
|
+
- if: failure()
|
|
246
|
+
uses: actions/upload-artifact@v4
|
|
247
|
+
with: { name: envx-audit, path: audit.json }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Matrix CI
|
|
251
|
+
|
|
252
|
+
Point `--profile` at a profile defined in `envx.config.ts`:
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
strategy:
|
|
256
|
+
matrix: { profile: [staging, prod] }
|
|
257
|
+
steps:
|
|
258
|
+
- run: npx envx --profile ${{ matrix.profile }} doctor
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Without installing — `npx -p`
|
|
262
|
+
|
|
263
|
+
`npx -p @super-repo/envx envx audit --staged` runs the latest published version without adding it to the install graph. Useful for one-off audits or environments where you don't want the devDependency:
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
- run: npx -p @super-repo/envx envx audit --json > audit.json
|
|
267
|
+
```
|