@super-repo/envx 0.2.3-b.3 → 0.2.3-b.4
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 +498 -7
- package/dist/auto.js.map +1 -1
- package/dist/chunks/commands-D3eQPQO6.js +1502 -0
- package/dist/chunks/commands-D3eQPQO6.js.map +1 -0
- package/dist/chunks/{src-CwrtyfZE.js → src-D0n2wHDg.js} +0 -0
- package/dist/chunks/src-D0n2wHDg.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/commands/audit.d.ts +13 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/bake.d.ts +18 -0
- package/dist/commands/bake.d.ts.map +1 -0
- package/dist/commands/diff.d.ts +16 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/encrypt.d.ts.map +1 -1
- package/dist/commands/hook.d.ts +18 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/template.d.ts +13 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/types.d.ts +14 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/watch.d.ts +14 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunks/commands-DNf_gJRx.js +0 -538
- package/dist/chunks/commands-DNf_gJRx.js.map +0 -1
- package/dist/chunks/src-CwrtyfZE.js.map +0 -1
package/README.md
CHANGED
|
@@ -54,6 +54,21 @@ The CLI shape, the `encrypted:<base64>` ciphertext format, the `ENVX_PUBLIC_KEY*
|
|
|
54
54
|
| **Programmatic API** | `dotenvx.config()` / library functions | `envx()` (single call), `import "@super-repo/envx/auto"` side-effect entry |
|
|
55
55
|
| **Auto-environment detection** | Limited | Reads `VERCEL_ENV`, Netlify `CONTEXT`, `NODE_ENV` to pick `.env.<env>` when `--env` is omitted |
|
|
56
56
|
| **Distribution** | npm package | npm package + workspace-private helpers (`envx-libs`, `envx-common`) bundled via Vite |
|
|
57
|
+
| **Health check** | Not built in | `envx doctor` — workspace, config, keys file, decryptability, required, expand cycles |
|
|
58
|
+
| **`.env.example` sync** | Not built in | `envx template` (and `--check` for CI drift detection) |
|
|
59
|
+
| **Diff between env files** | Not built in | `envx diff` — keys-only-in-a/b + value mismatches (decrypts via `.env.keys`) |
|
|
60
|
+
| **Shell injection** | Not built in | `envx hook bash|zsh|fish|powershell` — eval-able exports for the parent shell |
|
|
61
|
+
| **Typed `process.env`** | Not built in | `envx types` — emits `env.d.ts` from loaded keys (Zod-shape-aware) |
|
|
62
|
+
| **Watch mode** | Not built in | `envx watch -- node app.js` — restart on `.env*` change |
|
|
63
|
+
| **Repo-wide secret audit** | Not built in | `envx audit` — AKIA, ghp_, sk_live_, JWT, OpenAI/Anthropic keys, PEM blocks, … |
|
|
64
|
+
| **Selective encryption** | Per-key flag (`-k`) | Per-key, glob patterns (`--pattern`), and `--secrets` preset |
|
|
65
|
+
| **Schema validation (Zod)** | Not built in | `schema:` field in config — duck-typed `safeParse()` with fail-fast exit |
|
|
66
|
+
| **Named profiles** | Not built in | `profiles: { staging: { ... } }` + `--profile staging` (per-field override) |
|
|
67
|
+
| **External secret refs** | Not built in | `${aws-secrets:my-id}` resolved by user-registered `resolvers:` callbacks |
|
|
68
|
+
| **Secret-provider plugins** | Not built in | `@super-repo/envx-plugins` ships AWS, GCP, Azure, Vault, 1Password, Doppler, Infisical |
|
|
69
|
+
| **Public-variable mirroring** | Not built in | `PUBLIC_*` vars auto-mirror under `VITE_`, `NEXT_PUBLIC_`, `REACT_APP_`, etc. |
|
|
70
|
+
| **Build-time secret bake** | Not built in | `envx bake` resolves all refs into `.env.resolved` for bundlers (Vite/Next/esbuild) |
|
|
71
|
+
| **Runtime / edge secrets** | Not built in | `createSecretRuntime()` — async, TTL-cached, V8-isolate-safe (CF Workers, Vercel Edge) |
|
|
57
72
|
| **License** | BSD-3-Clause | MIT |
|
|
58
73
|
|
|
59
74
|
In short: same crypto, same file formats, same CLI vocabulary — different path-resolution layer underneath.
|
|
@@ -81,6 +96,52 @@ The string form (`envx("prod")`) is shorthand for "scope this load to the named
|
|
|
81
96
|
|
|
82
97
|
`envx()` discovers `envx.config.{ts,js,json}` and `package.json#envx.config` automatically; explicit args override matching fields from the config.
|
|
83
98
|
|
|
99
|
+
#### Programmatic options — full surface
|
|
100
|
+
|
|
101
|
+
`envx()` accepts everything from `LoadEnvOptions` plus an optional `profile` string. The full shape (`EnvxProgrammaticOptions`):
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import envx, { type EnvxProgrammaticOptions } from "@super-repo/envx";
|
|
105
|
+
|
|
106
|
+
const opts: EnvxProgrammaticOptions = {
|
|
107
|
+
// env-file selection
|
|
108
|
+
envFiles: ["vault/.env.prod"],
|
|
109
|
+
envPath: "vault",
|
|
110
|
+
cascade: "prod", // string for explicit, true for auto-detected, false to disable
|
|
111
|
+
vault: false, // shortcut for envPath: "vault"
|
|
112
|
+
|
|
113
|
+
// load behavior
|
|
114
|
+
override: false,
|
|
115
|
+
quiet: true,
|
|
116
|
+
variables: ["DEBUG=1"], // post-load mutation; always wins
|
|
117
|
+
|
|
118
|
+
// auto-detection
|
|
119
|
+
autoDetect: true,
|
|
120
|
+
nodeEnvMap: { qa: "qa-prod" },
|
|
121
|
+
|
|
122
|
+
// post-load behavior
|
|
123
|
+
required: ["DATABASE_URL"], // exit 1 if any is unset after load
|
|
124
|
+
expand: true, // resolve ${VAR} references
|
|
125
|
+
defaults: { LOG_LEVEL: "info" }, // fill holes; never overwrite
|
|
126
|
+
|
|
127
|
+
// validation + plug-ins
|
|
128
|
+
schema: zodSchema, // any { safeParse(input) → { success, error?, data? } }
|
|
129
|
+
resolvers: { // ${provider:id} → resolver(id)
|
|
130
|
+
"aws-secrets": (id) => fetchSecret(id),
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// profile selection (programmatic equivalent of --profile)
|
|
134
|
+
profile: "staging", // throws if config.profiles[name] is missing
|
|
135
|
+
|
|
136
|
+
// advanced
|
|
137
|
+
workspaceRoot: "/abs/path", // skip findWorkspaceRoot() walk-up
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
envx(opts);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Caller-supplied fields override matching fields from the discovered config **per-field** — anything you omit falls through to config, then to defaults.
|
|
144
|
+
|
|
84
145
|
### Side-effect import
|
|
85
146
|
|
|
86
147
|
```ts
|
|
@@ -102,9 +163,19 @@ envx --dir vault -- pnpm test # load every .env* in vault/
|
|
|
102
163
|
envx print DATABASE_URL # print one variable
|
|
103
164
|
envx debug --cascade prod # show resolved paths
|
|
104
165
|
envx encrypt -e .env.prod # encrypt values in .env.prod (writes ./.env.keys)
|
|
166
|
+
envx encrypt --secrets # encrypt only conventional secret keys (*_SECRET, *_TOKEN, …)
|
|
105
167
|
envx decrypt -e .env.prod -k FOO # decrypt only FOO
|
|
106
168
|
envx rotate --dir vault # rotate keypair for every vault/.env* file
|
|
107
169
|
envx expand -e vault/.env.prod # decrypt + expand to stdout
|
|
170
|
+
envx doctor # health check (workspace, keys, decryptability, required, cycles)
|
|
171
|
+
envx template # write .env.example with values stripped
|
|
172
|
+
envx template --check # CI gate — exit 1 on .env.example drift
|
|
173
|
+
envx diff .env.dev .env.prod # show keys-only-in-a/b + value mismatches
|
|
174
|
+
eval "$(envx hook bash)" # inject loaded env into the parent shell
|
|
175
|
+
envx types # emit env.d.ts for typed process.env access
|
|
176
|
+
envx watch -- node app.js # restart on .env* change
|
|
177
|
+
envx audit # scan repo for plaintext secrets
|
|
178
|
+
envx --profile staging -- node app.js # apply named profile from envx.config.*
|
|
108
179
|
envx -c ./envx.config.json run -- node app.js
|
|
109
180
|
```
|
|
110
181
|
|
|
@@ -117,17 +188,32 @@ envx -c ./envx.config.json run -- node app.js
|
|
|
117
188
|
|
|
118
189
|
### Subcommands
|
|
119
190
|
|
|
191
|
+
#### Core
|
|
192
|
+
|
|
120
193
|
| command | what it does |
|
|
121
194
|
| --------- | -------------------------------------------------------------------------------------- |
|
|
122
195
|
| `run` | (default) load env files into `process.env` and execute a command |
|
|
123
196
|
| `print` | load env files and print a single variable's value |
|
|
124
197
|
| `debug` | show which files would be loaded without loading them |
|
|
125
|
-
| `encrypt` | encrypt values in one or more `.env*` files (writes `.env.keys` on first run)
|
|
198
|
+
| `encrypt` | encrypt values in one or more `.env*` files (writes `.env.keys` on first run). Selective: `--key`, `--pattern '*_SECRET,*_TOKEN'`, `--secrets` preset |
|
|
126
199
|
| `decrypt` | decrypt values back in place |
|
|
127
200
|
| `rotate` | generate a fresh keypair and re-encrypt every encrypted value in place |
|
|
128
201
|
| `info` | print resolved configuration, auto-detection details, NODE_ENV mappings, and the env files that would load |
|
|
129
202
|
| `expand` | decrypt (if needed) and expand `${VAR}` / `$VAR` / `${VAR:-default}` / `${VAR:?msg}` |
|
|
130
203
|
|
|
204
|
+
#### Workflow + DX
|
|
205
|
+
|
|
206
|
+
| command | what it does |
|
|
207
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
208
|
+
| `doctor` | run health checks: workspace + config visible, env files exist, keys file present, every encrypted value decrypts, `required` keys filled after a dry load, expansion has no cycles. Exits non-zero on hard failures. CI-friendly. |
|
|
209
|
+
| `template` | emit a value-stripped `.env.example` from your real env files. `--check` mode exits 1 on drift; `--stdout` prints to stdout; `--annotate` (default on) tags each key with the source file. |
|
|
210
|
+
| `diff <a> <b>` | compare two env files key-by-key. Reports keys-only-in-a, keys-only-in-b, and value mismatches. Decrypts via `.env.keys` so the comparison reflects logical values; values are masked unless `--show-values`. |
|
|
211
|
+
| `hook <shell>` | print eval-able shell exports for the loaded env. Supports `bash`, `zsh`, `fish`, `powershell`/`pwsh` with proper escaping. Use `eval "$(envx hook bash)"` to inject env into the parent shell. |
|
|
212
|
+
| `types` | emit an `env.d.ts` declaring every loaded key on `NodeJS.ProcessEnv`. Keys listed in `required` (or non-optional in a Zod schema) narrow to `string`; everything else is `string \| undefined`. |
|
|
213
|
+
| `watch` | spawn a command and restart it whenever any of the resolved env files change. Debounce + SIGTERM-then-SIGKILL grace for clean restarts. |
|
|
214
|
+
| `audit` | walk the repo looking for plaintext secrets — AWS keys, GitHub PATs, Stripe live/test keys, JWTs, OpenAI / Anthropic keys, Google API keys, Slack tokens, PEM private-key blocks. Findings are redacted; exit 1 on any hit. `--json` for downstream tools. |
|
|
215
|
+
| `bake` | resolve every `${{provider:id}}` ref + decrypt every encrypted value + run defaults / expand / schema / public-mirror, then write a sealed `.env.resolved` for build tools to consume. Output is plaintext — refuses to write under `.git/`; use `--public-only` for client bundles. `--json` for structured tools, `--out=-` for stdout. |
|
|
216
|
+
|
|
131
217
|
### Global options
|
|
132
218
|
|
|
133
219
|
| option | alias | default | description |
|
|
@@ -141,6 +227,7 @@ envx -c ./envx.config.json run -- node app.js
|
|
|
141
227
|
| `--cascade` | | _(off)_ | Cascade name; expands `.env`, `.env.<c>`, `.env.local`, `.env.<c>.local`. |
|
|
142
228
|
| `--override` | `-o` | `false` | Override existing `process.env` values. Conflicts with `--cascade`. |
|
|
143
229
|
| `--quiet` | `-q` | `true` | Suppress dotenv's own output. |
|
|
230
|
+
| `--profile` | `-P` | _(none)_ | Named profile from `profiles` in `envx.config.*` — fields override the base config per-field. |
|
|
144
231
|
|
|
145
232
|
### Defaults
|
|
146
233
|
|
|
@@ -464,6 +551,36 @@ export default {
|
|
|
464
551
|
PORT: "3000",
|
|
465
552
|
},
|
|
466
553
|
|
|
554
|
+
// ── validation ─────────────────────────────────────────
|
|
555
|
+
schema: z.object({ // any object exposing `safeParse(input) → { success, error?, data? }`
|
|
556
|
+
DATABASE_URL: z.string().url(), // Zod is the obvious choice but envx duck-types — bring your own
|
|
557
|
+
PORT: z.coerce.number(), // coerced values are written back to process.env
|
|
558
|
+
}),
|
|
559
|
+
|
|
560
|
+
// ── named profiles ─────────────────────────────────────
|
|
561
|
+
profiles: { // `--profile staging` (or programmatic `envx({ profile: "staging" })`)
|
|
562
|
+
staging: { // merges this object onto the base config per-field, profile wins
|
|
563
|
+
envFiles: [".env.staging"],
|
|
564
|
+
required: ["DB_URL"],
|
|
565
|
+
},
|
|
566
|
+
prod: {
|
|
567
|
+
envFiles: [".env.prod"],
|
|
568
|
+
cascade: true,
|
|
569
|
+
required: ["DB_URL", "API_KEY"],
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
// ── external secret refs ───────────────────────────────
|
|
574
|
+
resolvers: { // values like `${aws-secrets:my-id}` get passed to the resolver
|
|
575
|
+
"aws-secrets": (id: string) => { // and replaced by its return. Sync — wrap async SDK calls externally.
|
|
576
|
+
return cachedAwsSecret(id); // (e.g. pre-warm a cache before envx() runs in your bootstrap)
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
// ── public-variable mirroring (SSR / client-side bundlers) ─
|
|
581
|
+
publicPrefixes: ["VITE_", "NEXT_PUBLIC_"], // duplicate every PUBLIC_* under each prefix
|
|
582
|
+
publicSource: "PUBLIC_", // (default) the prefix that marks a var as public
|
|
583
|
+
|
|
467
584
|
// ── advanced ───────────────────────────────────────────
|
|
468
585
|
workspaceRoot: "/abs/path/to/repo", // escape hatch — skip findWorkspaceRoot() walk-up
|
|
469
586
|
}
|
|
@@ -471,6 +588,8 @@ export default {
|
|
|
471
588
|
|
|
472
589
|
All fields are optional. Anything you omit falls through to the built-in default for that field.
|
|
473
590
|
|
|
591
|
+
> **Note:** `schema`, `profiles`, and `resolvers` are TS/JS-only — JSON configs can't carry callable functions. A `package.json#envx.config` reference must point at a `.ts` / `.mts` / `.js` / `.mjs` / `.cjs` file to use them.
|
|
592
|
+
|
|
474
593
|
### Discovery order
|
|
475
594
|
|
|
476
595
|
When `--config <path>` is not passed, envx walks up looking for a config in this order:
|
|
@@ -509,6 +628,370 @@ Expanded explanations, worked examples, and recipes ship with the package in [`.
|
|
|
509
628
|
- **[Configuration](./docs/configuration.md)** — full schema, the per-field merge rules, where each field gets read from, and the discovery walk.
|
|
510
629
|
- **[Monorepo recipes](./docs/recipes.md)** — common layouts (single workspace `.env.keys`, vault subdir, per-app overrides, CI patterns) with the exact files and commands.
|
|
511
630
|
|
|
631
|
+
## Library API
|
|
632
|
+
|
|
633
|
+
Beyond `envx()`, the package re-exports the building blocks the CLI uses internally — useful when you need to script the same behavior from your own tool, plugin, or test harness. Everything below is importable from the default entry:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
import {
|
|
637
|
+
// Programmatic loader
|
|
638
|
+
envx,
|
|
639
|
+
type EnvxProgrammaticOptions,
|
|
640
|
+
type LoadEnvOptions,
|
|
641
|
+
|
|
642
|
+
// Audit (plaintext-secret scanner)
|
|
643
|
+
auditFiles,
|
|
644
|
+
BUILT_IN_PATTERNS,
|
|
645
|
+
type AuditFinding,
|
|
646
|
+
type AuditOptions,
|
|
647
|
+
type SecretPattern,
|
|
648
|
+
|
|
649
|
+
// Crypto primitives
|
|
650
|
+
ENCRYPTED_PREFIX,
|
|
651
|
+
generateKeyPair,
|
|
652
|
+
encryptValueAsymmetric,
|
|
653
|
+
decryptValueAsymmetric,
|
|
654
|
+
isEncrypted,
|
|
655
|
+
|
|
656
|
+
// Higher-level operations
|
|
657
|
+
encryptFiles,
|
|
658
|
+
decryptFiles,
|
|
659
|
+
rotateFiles,
|
|
660
|
+
|
|
661
|
+
// Env-file parser (line-preserving)
|
|
662
|
+
parseEnv,
|
|
663
|
+
serializeEnv,
|
|
664
|
+
toRecord,
|
|
665
|
+
|
|
666
|
+
// Variable expansion (cycle-safe)
|
|
667
|
+
expandRecord,
|
|
668
|
+
expandEnvSrc,
|
|
669
|
+
|
|
670
|
+
// Config + path resolution
|
|
671
|
+
loadDotenvxConfig,
|
|
672
|
+
findWorkspaceRoot,
|
|
673
|
+
resolveCwdOrWorkspace,
|
|
674
|
+
resolveEnvPaths,
|
|
675
|
+
detectEnvironment,
|
|
676
|
+
|
|
677
|
+
// Keys-file management
|
|
678
|
+
readKeysFile,
|
|
679
|
+
writeKeysFile,
|
|
680
|
+
defaultKeysPath,
|
|
681
|
+
} from "@super-repo/envx";
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Audit programmatically
|
|
685
|
+
|
|
686
|
+
`envx audit` is a thin wrapper around `auditFiles()`. Use it directly to plug envx's secret detection into a pre-commit hook, a CI script, or your own dashboard:
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { auditFiles, type SecretPattern } from "@super-repo/envx";
|
|
690
|
+
|
|
691
|
+
// Add custom patterns alongside the built-ins.
|
|
692
|
+
const orgPatterns: SecretPattern[] = [
|
|
693
|
+
{ id: "internal-token", label: "ACME internal token", regex: /\bacme_[A-Za-z0-9]{32}\b/ },
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
const { findings, filesScanned } = auditFiles({
|
|
697
|
+
roots: ["packages/", "apps/"],
|
|
698
|
+
ignore: ["fixtures", "snapshots"],
|
|
699
|
+
extra: orgPatterns,
|
|
700
|
+
max: 100, // 0 = unlimited
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
if (findings.length > 0) {
|
|
704
|
+
console.error(`audit: ${findings.length} finding(s) across ${filesScanned} files`);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Findings are redacted (`AKIA…MPLE [redacted 20 chars]`) so the report itself never leaks the secret payload.
|
|
710
|
+
|
|
711
|
+
### Custom profiles + resolvers in code
|
|
712
|
+
|
|
713
|
+
When you need conditional behavior in a programmatic call (without a config file):
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
import envx from "@super-repo/envx";
|
|
717
|
+
import { z } from "zod";
|
|
718
|
+
|
|
719
|
+
const Schema = z.object({
|
|
720
|
+
DATABASE_URL: z.string().url(),
|
|
721
|
+
PORT: z.coerce.number().int().min(1).max(65535),
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
await preloadAwsSecretsCache(); // resolvers are sync — warm caches up front
|
|
725
|
+
|
|
726
|
+
envx({
|
|
727
|
+
schema: Schema,
|
|
728
|
+
resolvers: {
|
|
729
|
+
"aws-secrets": (id) => awsSecretsCache.get(id),
|
|
730
|
+
"1password": (ref) => onePasswordCache.get(ref),
|
|
731
|
+
},
|
|
732
|
+
required: ["DATABASE_URL", "PORT"],
|
|
733
|
+
expand: true, // ${HOST}/api works after resolvers run
|
|
734
|
+
});
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
Order of operations inside `envx()` once a profile / resolvers / schema are in play:
|
|
738
|
+
|
|
739
|
+
```
|
|
740
|
+
1. resolve workspaceRoot
|
|
741
|
+
2. resolve env file paths
|
|
742
|
+
3. load each env file → mutates process.env (subject to `override`)
|
|
743
|
+
4. apply `variables` → unconditional overwrite
|
|
744
|
+
5. run `resolvers` on ${provider:id} refs
|
|
745
|
+
6. apply `defaults` → only for keys still unset
|
|
746
|
+
7. expand `${VAR}` references (when `expand: true`)
|
|
747
|
+
8. validate against `schema` (when configured) — exits 1 on failure
|
|
748
|
+
9. enforce `required` (any unset → log + exit 1)
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## Public variables (SSR + client bundlers)
|
|
752
|
+
|
|
753
|
+
Modern frameworks each demand their own prefix for variables that should reach client-side code:
|
|
754
|
+
|
|
755
|
+
| framework | prefix |
|
|
756
|
+
| ----------------- | --------------- |
|
|
757
|
+
| Vite | `VITE_` |
|
|
758
|
+
| Next.js | `NEXT_PUBLIC_` |
|
|
759
|
+
| Create React App | `REACT_APP_` |
|
|
760
|
+
| Nuxt 3 (public) | `NUXT_PUBLIC_` |
|
|
761
|
+
| SvelteKit (public)| `PUBLIC_` |
|
|
762
|
+
| Remix (loader) | _(none)_ |
|
|
763
|
+
|
|
764
|
+
Maintaining the same value under three different keys (`VITE_API_URL`, `NEXT_PUBLIC_API_URL`, `REACT_APP_API_URL`) is the kind of busywork envx exists to delete. Mark a variable "public" once with `PUBLIC_` (or any prefix you configure as `publicSource`), and envx mirrors it under every framework prefix at load time.
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
// envx.config.ts
|
|
768
|
+
export default {
|
|
769
|
+
publicPrefixes: ["VITE_", "NEXT_PUBLIC_"],
|
|
770
|
+
// publicSource: "PUBLIC_" // (default)
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
```env
|
|
775
|
+
# .env
|
|
776
|
+
PUBLIC_API_URL=https://api.example.com
|
|
777
|
+
PUBLIC_FEATURE_FLAG=true
|
|
778
|
+
DATABASE_URL=secret-stuff # not mirrored — no PUBLIC_ prefix
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
After `envx -- node app.js`, `process.env` looks like:
|
|
782
|
+
|
|
783
|
+
| key | value | source |
|
|
784
|
+
| ---------------------------- | --------------------------- | --------------- |
|
|
785
|
+
| `PUBLIC_API_URL` | `https://api.example.com` | original |
|
|
786
|
+
| `VITE_API_URL` | `https://api.example.com` | mirror |
|
|
787
|
+
| `NEXT_PUBLIC_API_URL` | `https://api.example.com` | mirror |
|
|
788
|
+
| `PUBLIC_FEATURE_FLAG` | `true` | original |
|
|
789
|
+
| `VITE_FEATURE_FLAG` | `true` | mirror |
|
|
790
|
+
| `NEXT_PUBLIC_FEATURE_FLAG` | `true` | mirror |
|
|
791
|
+
| `DATABASE_URL` | `secret-stuff` | original (kept private — no mirror) |
|
|
792
|
+
|
|
793
|
+
CLI flag for one-off use:
|
|
794
|
+
|
|
795
|
+
```sh
|
|
796
|
+
envx --public-prefix VITE_ --public-prefix NEXT_PUBLIC_ -- pnpm build
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Mirroring runs **after** `expand`, so `${VAR}` references inside `PUBLIC_*` values resolve before being mirrored. Existing target keys are never overwritten — a `VITE_API_URL` already set in the parent shell wins over the auto-mirror.
|
|
800
|
+
|
|
801
|
+
## Security models — when secrets are fetched
|
|
802
|
+
|
|
803
|
+
Pick one based on where you want plaintext to live:
|
|
804
|
+
|
|
805
|
+
| model | when secrets are fetched | plaintext on disk? | best for |
|
|
806
|
+
| ------------------------------ | --------------------------------------- | ------------------------------- | ---------------------------------------- |
|
|
807
|
+
| **Encrypted-at-rest** (default)| process startup — `envx run --` decrypts via `.env.keys` | ciphertext only, in committed `.env*` | most apps — single binary, fast cold start |
|
|
808
|
+
| **Load-time fetch** (resolvers)| process startup — plugins fetch via `resolvers:` | none — plaintext stays in process memory | server processes, long-running workers |
|
|
809
|
+
| **Build-time bake** | once, in CI — `envx bake` writes `.env.resolved` | yes (in the build artifact) ⚠️ | static SPA bundles, public-only vars |
|
|
810
|
+
| **Runtime fetch** (edge) | per cold start, lazily — `secrets.get()` on first read | none — secrets stay in the source-of-truth | edge functions, serverless, zero-trust |
|
|
811
|
+
|
|
812
|
+
The four models stack — most apps end up using two or more (e.g. encrypted-at-rest for non-secret config, runtime fetch for actual secrets).
|
|
813
|
+
|
|
814
|
+
### Build-time bake (`envx bake`)
|
|
815
|
+
|
|
816
|
+
```sh
|
|
817
|
+
# CI step — resolves every encrypted value + ${provider:id} ref + ${VAR}
|
|
818
|
+
# expansion + schema validation, then writes a sealed file the bundler picks up.
|
|
819
|
+
envx bake --out dist/.env.resolved
|
|
820
|
+
|
|
821
|
+
# Public-only mode for client bundles — only PUBLIC_* keys + their framework mirrors.
|
|
822
|
+
envx bake --public-only --out dist/.env.public
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
⚠️ The output file is **plaintext**. envx writes a `# DO NOT COMMIT` banner and refuses to write under `.git/`, but you must add the path to `.gitignore` yourself. Use `--public-only` whenever the artifact will ship to clients.
|
|
826
|
+
|
|
827
|
+
Build-tool integration:
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
// vite.config.ts
|
|
831
|
+
import { defineConfig, loadEnv } from "vite";
|
|
832
|
+
|
|
833
|
+
export default defineConfig(({ mode }) => {
|
|
834
|
+
// Vite's loadEnv reads .env.resolved like any other .env file.
|
|
835
|
+
const env = loadEnv(mode, process.cwd(), ".env.resolved");
|
|
836
|
+
return {
|
|
837
|
+
define: {
|
|
838
|
+
"import.meta.env.VITE_API_URL": JSON.stringify(env.VITE_API_URL),
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
```sh
|
|
845
|
+
# CI script
|
|
846
|
+
envx bake --public-only --out .env.resolved && pnpm vite build
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### Runtime fetch — edge / serverless
|
|
850
|
+
|
|
851
|
+
For the "secrets never leave the source-of-truth" model, use `createSecretRuntime` from `@super-repo/envx-plugins/runtime`. Async-first, TTL caching, no `fs` / `child_process` dependencies — works in Cloudflare Workers, Vercel Edge, Deno Deploy, AWS Lambda, Bun, Node.
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
// src/secrets.ts — module-level singleton, one per worker
|
|
855
|
+
import { createSecretRuntime } from "@super-repo/envx-plugins/runtime";
|
|
856
|
+
import { hcVault } from "@super-repo/envx-plugins/vault";
|
|
857
|
+
|
|
858
|
+
export const secrets = createSecretRuntime({
|
|
859
|
+
providers: [
|
|
860
|
+
hcVault({
|
|
861
|
+
endpoint: env.VAULT_ADDR,
|
|
862
|
+
token: env.VAULT_TOKEN,
|
|
863
|
+
}),
|
|
864
|
+
],
|
|
865
|
+
ttl: 300, // refresh every 5 min
|
|
866
|
+
onFailure: "cache-stale", // serve stale on transient backend failures
|
|
867
|
+
});
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
```ts
|
|
871
|
+
// src/handler.ts (Cloudflare Worker / Vercel Edge / Lambda — same shape)
|
|
872
|
+
import { secrets } from "./secrets";
|
|
873
|
+
|
|
874
|
+
export default async function handler(req: Request): Promise<Response> {
|
|
875
|
+
const dbUrl = await secrets.get("vault:prod/db");
|
|
876
|
+
const apiKey = await secrets.get("vault:prod/api");
|
|
877
|
+
// … handler logic …
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
What you get:
|
|
882
|
+
- **Lazy fetch** — first read triggers the network call. Subsequent reads within the TTL window hit memory.
|
|
883
|
+
- **Single-flight** — N concurrent reads of the same ref coalesce into one fetch. Edge functions handling a burst of requests don't fan out N parallel calls to your secret store.
|
|
884
|
+
- **TTL refresh** — values older than `ttl` seconds are refetched on next read. Set `ttl: Infinity` to cache forever; `ttl: 0` to fetch every read.
|
|
885
|
+
- **Failure modes** — `"throw"` (default), `"cache-stale"` (serve previous value, log warning), or `"cache-error"` (cache the failure for `errorTtl` seconds so you don't hammer a failing backend).
|
|
886
|
+
- **No fs / child_process imports** — the runtime entry point is V8-isolate-safe.
|
|
887
|
+
|
|
888
|
+
### Compatibility note
|
|
889
|
+
|
|
890
|
+
Plugin compatibility for the runtime entry depends on what each provider's transport needs:
|
|
891
|
+
|
|
892
|
+
| provider | edge runtime? | reason |
|
|
893
|
+
| ------------------ | --------------------------- | ----------------------------------- |
|
|
894
|
+
| HashiCorp Vault | ✓ | native `fetch` |
|
|
895
|
+
| Doppler | ✓ | native `fetch` |
|
|
896
|
+
| Infisical | ✓ | native `fetch` |
|
|
897
|
+
| AWS Secrets Manager| ⚠ Node-compat only | `@aws-sdk/client-secrets-manager` uses Node streams |
|
|
898
|
+
| GCP Secret Manager | ⚠ Node-compat only | `@google-cloud/secret-manager` uses gRPC |
|
|
899
|
+
| Azure Key Vault | ⚠ Node-compat only | `@azure/identity` token chain |
|
|
900
|
+
| 1Password (CLI) | ✗ | spawns the `op` binary |
|
|
901
|
+
| 1Password (SDK) | depends on `@1password/sdk` | check the SDK's runtime support |
|
|
902
|
+
|
|
903
|
+
For Cloudflare Workers / Vercel Edge / Deno: prefer Vault, Doppler, Infisical, or a custom HTTP `buildProvider`. AWS / GCP / Azure SDKs run fine in Lambda / Cloud Run / Cloud Functions but won't work in V8-only edges.
|
|
904
|
+
|
|
905
|
+
## Secret-provider plugins
|
|
906
|
+
|
|
907
|
+
Ship as a sibling package: [`@super-repo/envx-plugins`](https://www.npmjs.com/package/@super-repo/envx-plugins). Plugins return a `SecretProvider` you wire into envx's `resolvers:` config so values like `${aws-secrets:my-db}` and `${vault:prod/api}` fetch from the matching backend at load time.
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
import envx from "@super-repo/envx";
|
|
911
|
+
import { awsSecrets, autoPreload, asResolvers } from "@super-repo/envx-plugins";
|
|
912
|
+
|
|
913
|
+
const aws = awsSecrets({ region: "us-east-1" });
|
|
914
|
+
|
|
915
|
+
await autoPreload([aws], { envFiles: [".env", "vault/.env.prod"] });
|
|
916
|
+
|
|
917
|
+
envx({ resolvers: asResolvers([aws]) });
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
```env
|
|
921
|
+
DATABASE_URL=${aws-secrets:prod/db}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Built-in providers: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, 1Password (CLI + SDK), Doppler, Infisical. Each plugin lazy-loads its SDK — install only what you use. See the [`@super-repo/envx-plugins` README](../plugins/README.md) for per-provider setup.
|
|
925
|
+
|
|
926
|
+
## Pre-commit hooks
|
|
927
|
+
|
|
928
|
+
Three of the new commands (`audit`, `doctor`, `template --check`) are designed to compose into a single git pre-commit gate so secrets, broken configs, and stale `.env.example` files never reach the remote.
|
|
929
|
+
|
|
930
|
+
The repo ships a reference `.hooks/pre-commit` you can adopt verbatim or copy into your own setup:
|
|
931
|
+
|
|
932
|
+
```sh
|
|
933
|
+
# Activate the in-repo .hooks/ directory (one-time per clone)
|
|
934
|
+
git config core.hooksPath .hooks
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
That single config line tells git to look for hooks under `<repo>/.hooks/` instead of `.git/hooks/`. The directory is committed (everyone gets the same hooks), no third-party tool is required.
|
|
938
|
+
|
|
939
|
+
The hook itself:
|
|
940
|
+
|
|
941
|
+
```bash
|
|
942
|
+
#!/usr/bin/env bash
|
|
943
|
+
# .hooks/pre-commit
|
|
944
|
+
set -euo pipefail
|
|
945
|
+
[[ "${ENVX_SKIP_HOOK:-}" == "1" ]] && exit 0
|
|
946
|
+
|
|
947
|
+
# Use the workspace binary if available, else a global envx
|
|
948
|
+
if command -v pnpm >/dev/null && [[ -f package.json ]]; then
|
|
949
|
+
ENVX="pnpm exec envx"
|
|
950
|
+
else
|
|
951
|
+
ENVX="envx"
|
|
952
|
+
fi
|
|
953
|
+
|
|
954
|
+
failed=0
|
|
955
|
+
|
|
956
|
+
# 1. Audit staged files for plaintext secrets (AKIA, ghp_, sk_live_, JWTs, …)
|
|
957
|
+
staged=$(git diff --cached --name-only --diff-filter=ACMR | tr '\n' ' ')
|
|
958
|
+
[[ -n "$staged" ]] && ! $ENVX audit $staged && failed=1
|
|
959
|
+
|
|
960
|
+
# 2. Workspace health (skip required-key gate so dev machines don't fail)
|
|
961
|
+
$ENVX doctor --ignore-required || failed=1
|
|
962
|
+
|
|
963
|
+
# 3. .env.example drift gate
|
|
964
|
+
[[ -f .env ]] && ! $ENVX template --check && failed=1
|
|
965
|
+
|
|
966
|
+
exit $failed
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
Bypass options:
|
|
970
|
+
- `ENVX_SKIP_HOOK=1 git commit ...` — skip just the envx hook
|
|
971
|
+
- `git commit --no-verify` — skip every git hook (use sparingly)
|
|
972
|
+
|
|
973
|
+
The full annotated script lives at `.hooks/pre-commit` in this repo. CI should run the same three commands without the `--ignore-required` flag so missing required keys fail builds (not just commits).
|
|
974
|
+
|
|
975
|
+
### Pre-push variant
|
|
976
|
+
|
|
977
|
+
For a stricter gate that runs only on push (so commits stay fast but pushes are guarded):
|
|
978
|
+
|
|
979
|
+
```bash
|
|
980
|
+
#!/usr/bin/env bash
|
|
981
|
+
# .hooks/pre-push — same checks, run against the whole repo, no skip.
|
|
982
|
+
set -euo pipefail
|
|
983
|
+
envx audit && envx doctor && envx template --check
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
### CI-friendly invocations
|
|
987
|
+
|
|
988
|
+
```yaml
|
|
989
|
+
# .github/workflows/secrets.yml
|
|
990
|
+
- run: pnpm exec envx audit --json > audit.json
|
|
991
|
+
- run: pnpm exec envx doctor # fails the build on missing required keys
|
|
992
|
+
- run: pnpm exec envx template --check # fails on .env.example drift
|
|
993
|
+
```
|
|
994
|
+
|
|
512
995
|
## Embedding the CLI
|
|
513
996
|
|
|
514
997
|
The yargs factory is exported under the `./commands` subpath if you want to register envx's commands inside your own tool:
|
|
@@ -542,13 +1025,21 @@ packages/envx/core/
|
|
|
542
1025
|
│ │ └── dotenvx.ts # `dotenvx-proxy` direct passthrough to upstream
|
|
543
1026
|
│ └── commands/
|
|
544
1027
|
│ ├── index.ts # createCli — registers subcommands + global options
|
|
545
|
-
│ ├── run.ts # `run [command..]`
|
|
546
|
-
│ ├── print.ts # `print <variable>`
|
|
547
|
-
│ ├── debug.ts # `debug`
|
|
548
|
-
│ ├── encrypt.ts # `encrypt`
|
|
1028
|
+
│ ├── run.ts # `run [command..]` — load env + spawn
|
|
1029
|
+
│ ├── print.ts # `print <variable>` — print one value
|
|
1030
|
+
│ ├── debug.ts # `debug` — show resolved paths
|
|
1031
|
+
│ ├── encrypt.ts # `encrypt` — incl. --pattern / --secrets
|
|
549
1032
|
│ ├── decrypt.ts # `decrypt`
|
|
550
|
-
│ ├── rotate.ts # `rotate`
|
|
551
|
-
│
|
|
1033
|
+
│ ├── rotate.ts # `rotate` — refresh keypair across files
|
|
1034
|
+
│ ├── expand.ts # `expand` — resolve ${VAR} on stdout
|
|
1035
|
+
│ ├── info.ts # `info` — print resolved config
|
|
1036
|
+
│ ├── doctor.ts # `doctor` — health checks (CI gate)
|
|
1037
|
+
│ ├── template.ts # `template` — emit/check .env.example
|
|
1038
|
+
│ ├── diff.ts # `diff <a> <b>` — keys + value mismatches
|
|
1039
|
+
│ ├── hook.ts # `hook <shell>` — eval-able shell exports
|
|
1040
|
+
│ ├── types.ts # `types` — emit env.d.ts
|
|
1041
|
+
│ ├── watch.ts # `watch -- <cmd>` — restart on env change
|
|
1042
|
+
│ └── audit.ts # `audit` — plaintext-secret scanner
|
|
552
1043
|
├── tests/
|
|
553
1044
|
│ ├── *.test.ts # unit suites
|
|
554
1045
|
│ └── integration/
|
package/dist/auto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto.js","names":[],"sources":["../src/auto.ts"],"sourcesContent":["// #region -- Side-effect entry point ----------------------\n\n// `import \"@
|
|
1
|
+
{"version":3,"file":"auto.js","names":[],"sources":["../src/auto.ts"],"sourcesContent":["// #region -- Side-effect entry point ----------------------\n\n// `import \"@super-repo/envx/auto\"` (or \"/config\") loads .env into\n// process.env on import — same ergonomics as `import \"dotenv/config\"`.\n// Picks up envx.config.* and the package.json `envx.config` discovery\n// chain automatically. Use the named/default export from the package\n// root when you want an explicit handle.\n\nimport envx from \"./index.js\";\n\nenvx();\n\n// #endregion -----------------------------------------------\n"],"mappings":";;AAUA,MAAM"}
|