@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.
@@ -0,0 +1,121 @@
1
+ # Library API
2
+
3
+ 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:
4
+
5
+ ```ts
6
+ import {
7
+ // Programmatic loader
8
+ envx,
9
+ type EnvxProgrammaticOptions,
10
+ type LoadEnvOptions,
11
+
12
+ // Audit (plaintext-secret scanner)
13
+ auditFiles,
14
+ BUILT_IN_PATTERNS,
15
+ type AuditFinding,
16
+ type AuditOptions,
17
+ type SecretPattern,
18
+
19
+ // Crypto primitives
20
+ ENCRYPTED_PREFIX,
21
+ generateKeyPair,
22
+ encryptValueAsymmetric,
23
+ decryptValueAsymmetric,
24
+ isEncrypted,
25
+
26
+ // Higher-level operations
27
+ encryptFiles,
28
+ decryptFiles,
29
+ rotateFiles,
30
+
31
+ // Env-file parser (line-preserving)
32
+ parseEnv,
33
+ serializeEnv,
34
+ toRecord,
35
+
36
+ // Variable expansion (cycle-safe)
37
+ expandRecord,
38
+ expandEnvSrc,
39
+
40
+ // Config + path resolution
41
+ defineConfig,
42
+ loadDotenvxConfig,
43
+ findWorkspaceRoot,
44
+ resolveCwdOrWorkspace,
45
+ resolveEnvPaths,
46
+ detectEnvironment,
47
+
48
+ // Keys-file management
49
+ readKeysFile,
50
+ writeKeysFile,
51
+ defaultKeysPath,
52
+ } from "@super-repo/envx";
53
+ ```
54
+
55
+ ## Audit programmatically
56
+
57
+ `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:
58
+
59
+ ```ts
60
+ import { auditFiles, type SecretPattern } from "@super-repo/envx";
61
+
62
+ // Add custom patterns alongside the built-ins.
63
+ const orgPatterns: SecretPattern[] = [
64
+ { id: "internal-token", label: "ACME internal token", regex: /\bacme_[A-Za-z0-9]{32}\b/ },
65
+ ];
66
+
67
+ const { findings, filesScanned } = auditFiles({
68
+ roots: ["packages/", "apps/"],
69
+ ignore: ["fixtures", "snapshots"],
70
+ extra: orgPatterns,
71
+ max: 100, // 0 = unlimited
72
+ respectGitignore: true, // default — skip anything matched by .gitignore / .git/info/exclude
73
+ });
74
+
75
+ if (findings.length > 0) {
76
+ console.error(`audit: ${findings.length} finding(s) across ${filesScanned} files`);
77
+ process.exit(1);
78
+ }
79
+ ```
80
+
81
+ Findings are redacted (`AKIA…MPLE [redacted 20 chars]`) so the report itself never leaks the secret payload.
82
+
83
+ ## Custom profiles + resolvers in code
84
+
85
+ When you need conditional behavior in a programmatic call (without a config file):
86
+
87
+ ```ts
88
+ import envx from "@super-repo/envx";
89
+ import { z } from "zod";
90
+
91
+ const Schema = z.object({
92
+ DATABASE_URL: z.string().url(),
93
+ PORT: z.coerce.number().int().min(1).max(65535),
94
+ });
95
+
96
+ await preloadAwsSecretsCache(); // resolvers are sync — warm caches up front
97
+
98
+ envx({
99
+ schema: Schema,
100
+ resolvers: {
101
+ "aws-secrets": (id) => awsSecretsCache.get(id),
102
+ "1password": (ref) => onePasswordCache.get(ref),
103
+ },
104
+ required: ["DATABASE_URL", "PORT"],
105
+ expand: true, // ${HOST}/api works after resolvers run
106
+ });
107
+ ```
108
+
109
+ Order of operations inside `envx()` once a profile / resolvers / schema are in play:
110
+
111
+ ```
112
+ 1. resolve workspaceRoot
113
+ 2. resolve env file paths
114
+ 3. load each env file → mutates process.env (subject to `override`)
115
+ 4. apply `variables` → unconditional overwrite
116
+ 5. run `resolvers` on ${provider:id} refs
117
+ 6. apply `defaults` → only for keys still unset
118
+ 7. expand `${VAR}` references (when `expand: true`)
119
+ 8. validate against `schema` (when configured) — exits 1 on failure
120
+ 9. enforce `required` (any unset → log + exit 1)
121
+ ```
@@ -0,0 +1,49 @@
1
+ # Public variables (SSR + client bundlers)
2
+
3
+ Modern frameworks each demand their own prefix for variables that should reach client-side code:
4
+
5
+ | framework | prefix |
6
+ | ----------------- | --------------- |
7
+ | Vite | `VITE_` |
8
+ | Next.js | `NEXT_PUBLIC_` |
9
+ | Create React App | `REACT_APP_` |
10
+ | Nuxt 3 (public) | `NUXT_PUBLIC_` |
11
+ | SvelteKit (public)| `PUBLIC_` |
12
+ | Remix (loader) | _(none)_ |
13
+
14
+ 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.
15
+
16
+ ```ts
17
+ // envx.config.ts
18
+ export default {
19
+ publicPrefixes: ["VITE_", "NEXT_PUBLIC_"],
20
+ // publicSource: "PUBLIC_" // (default)
21
+ }
22
+ ```
23
+
24
+ ```env
25
+ # .env
26
+ PUBLIC_API_URL=https://api.example.com
27
+ PUBLIC_FEATURE_FLAG=true
28
+ DATABASE_URL=secret-stuff # not mirrored — no PUBLIC_ prefix
29
+ ```
30
+
31
+ After `envx -- node app.js`, `process.env` looks like:
32
+
33
+ | key | value | source |
34
+ | ---------------------------- | --------------------------- | --------------- |
35
+ | `PUBLIC_API_URL` | `https://api.example.com` | original |
36
+ | `VITE_API_URL` | `https://api.example.com` | mirror |
37
+ | `NEXT_PUBLIC_API_URL` | `https://api.example.com` | mirror |
38
+ | `PUBLIC_FEATURE_FLAG` | `true` | original |
39
+ | `VITE_FEATURE_FLAG` | `true` | mirror |
40
+ | `NEXT_PUBLIC_FEATURE_FLAG` | `true` | mirror |
41
+ | `DATABASE_URL` | `secret-stuff` | original (kept private — no mirror) |
42
+
43
+ CLI flag for one-off use:
44
+
45
+ ```sh
46
+ envx --public-prefix VITE_ --public-prefix NEXT_PUBLIC_ -- pnpm build
47
+ ```
48
+
49
+ 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.
@@ -0,0 +1,103 @@
1
+ # Security models — when secrets are fetched
2
+
3
+ Pick one based on where you want plaintext to live:
4
+
5
+ | model | when secrets are fetched | plaintext on disk? | best for |
6
+ | ------------------------------ | --------------------------------------- | ------------------------------- | ---------------------------------------- |
7
+ | **Encrypted-at-rest** (default)| process startup — `envx run --` decrypts via `.env.keys` | ciphertext only, in committed `.env*` | most apps — single binary, fast cold start |
8
+ | **Load-time fetch** (resolvers)| process startup — plugins fetch via `resolvers:` | none — plaintext stays in process memory | server processes, long-running workers |
9
+ | **Build-time bake** | once, in CI — `envx bake` writes `.env.resolved` | yes (in the build artifact) ⚠️ | static SPA bundles, public-only vars |
10
+ | **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 |
11
+
12
+ 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).
13
+
14
+ ## Build-time bake (`envx bake`)
15
+
16
+ ```sh
17
+ # CI step — resolves every encrypted value + ${provider:id} ref + ${VAR}
18
+ # expansion + schema validation, then writes a sealed file the bundler picks up.
19
+ envx bake --out dist/.env.resolved
20
+
21
+ # Public-only mode for client bundles — only PUBLIC_* keys + their framework mirrors.
22
+ envx bake --public-only --out dist/.env.public
23
+ ```
24
+
25
+ ⚠️ 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.
26
+
27
+ Build-tool integration:
28
+
29
+ ```ts
30
+ // vite.config.ts
31
+ import { defineConfig, loadEnv } from "vite";
32
+
33
+ export default defineConfig(({ mode }) => {
34
+ // Vite's loadEnv reads .env.resolved like any other .env file.
35
+ const env = loadEnv(mode, process.cwd(), ".env.resolved");
36
+ return {
37
+ define: {
38
+ "import.meta.env.VITE_API_URL": JSON.stringify(env.VITE_API_URL),
39
+ },
40
+ };
41
+ });
42
+ ```
43
+
44
+ ```sh
45
+ # CI script
46
+ envx bake --public-only --out .env.resolved && pnpm vite build
47
+ ```
48
+
49
+ ## Runtime fetch — edge / serverless
50
+
51
+ 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.
52
+
53
+ ```ts
54
+ // src/secrets.ts — module-level singleton, one per worker
55
+ import { createSecretRuntime } from "@super-repo/envx-plugins/runtime";
56
+ import { hcVault } from "@super-repo/envx-plugins/vault";
57
+
58
+ export const secrets = createSecretRuntime({
59
+ providers: [
60
+ hcVault({
61
+ endpoint: env.VAULT_ADDR,
62
+ token: env.VAULT_TOKEN,
63
+ }),
64
+ ],
65
+ ttl: 300, // refresh every 5 min
66
+ onFailure: "cache-stale", // serve stale on transient backend failures
67
+ });
68
+ ```
69
+
70
+ ```ts
71
+ // src/handler.ts (Cloudflare Worker / Vercel Edge / Lambda — same shape)
72
+ import { secrets } from "./secrets";
73
+
74
+ export default async function handler(req: Request): Promise<Response> {
75
+ const dbUrl = await secrets.get("vault:prod/db");
76
+ const apiKey = await secrets.get("vault:prod/api");
77
+ // … handler logic …
78
+ }
79
+ ```
80
+
81
+ What you get:
82
+ - **Lazy fetch** — first read triggers the network call. Subsequent reads within the TTL window hit memory.
83
+ - **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.
84
+ - **TTL refresh** — values older than `ttl` seconds are refetched on next read. Set `ttl: Infinity` to cache forever; `ttl: 0` to fetch every read.
85
+ - **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).
86
+ - **No fs / child_process imports** — the runtime entry point is V8-isolate-safe.
87
+
88
+ ## Compatibility note
89
+
90
+ Plugin compatibility for the runtime entry depends on what each provider's transport needs:
91
+
92
+ | provider | edge runtime? | reason |
93
+ | ------------------ | --------------------------- | ----------------------------------- |
94
+ | HashiCorp Vault | ✓ | native `fetch` |
95
+ | Doppler | ✓ | native `fetch` |
96
+ | Infisical | ✓ | native `fetch` |
97
+ | AWS Secrets Manager| ⚠ Node-compat only | `@aws-sdk/client-secrets-manager` uses Node streams |
98
+ | GCP Secret Manager | ⚠ Node-compat only | `@google-cloud/secret-manager` uses gRPC |
99
+ | Azure Key Vault | ⚠ Node-compat only | `@azure/identity` token chain |
100
+ | 1Password (CLI) | ✗ | spawns the `op` binary |
101
+ | 1Password (SDK) | depends on `@1password/sdk` | check the SDK's runtime support |
102
+
103
+ 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.
@@ -0,0 +1,87 @@
1
+ # `envx template`
2
+
3
+ Generate a `.env.example` from your real env files. The output mirrors the source's organization — comments, blank lines, and declaration order are preserved verbatim. Values are stripped (or replaced with AI-generated placeholders when `--ai` is set).
4
+
5
+ ## What gets dropped
6
+
7
+ - The encryption banner block — any line starting with `#/` (the four-line `#/-------------------[ENVX_PUBLIC_KEY]----------------------/` decoration emitted by `envx encrypt`).
8
+ - `ENVX_PUBLIC_KEY*` and `DOTENV_PUBLIC_KEY*` kv lines themselves. The example file is encryption-agnostic; consumers don't need a public key to hand-edit a copy.
9
+
10
+ Everything else passes through:
11
+
12
+ ```env
13
+ # === Database ===
14
+ DATABASE_URL=postgres://prod # primary connection
15
+ DATABASE_POOL=10
16
+
17
+ # === Auth ===
18
+ API_KEY=sk_live_abc123
19
+ ```
20
+
21
+ becomes:
22
+
23
+ ```env
24
+ # === Database ===
25
+ DATABASE_URL= # primary connection
26
+ DATABASE_POOL=
27
+
28
+ # === Auth ===
29
+ API_KEY=
30
+ ```
31
+
32
+ ## `--ai` — generate placeholder values
33
+
34
+ Pass `--ai` to populate each `KEY=` with a realistic-looking-but-obviously-fake placeholder. envx loads your env files first (decrypting via `.env.keys` automatically), then reads `ANTHROPIC_API_KEY` from `process.env` to call Claude. One API call covers the entire key set — keys + their trailing comments are sent as hints; Claude returns a JSON map; the renderer drops the values into the template.
35
+
36
+ ```sh
37
+ envx template --ai # default: anthropic + claude-haiku-4-5-20251001
38
+ envx template --ai --ai-model claude-sonnet-4-6 # bigger model
39
+ envx template --ai --ai-provider openai # use OPENAI_API_KEY instead
40
+ ```
41
+
42
+ ### API key lookup
43
+
44
+ Same chain as `@super-repo/czar`'s AI integration:
45
+
46
+ 1. `process.env[apiKeyEnv]` (default `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`)
47
+ 2. For Anthropic: `CLAUDE_CODE_OAUTH_TOKEN`, then `ANTHROPIC_AUTH_TOKEN` (subscription / Claude-Code-style bearer tokens are detected by their `sk-ant-oat` prefix and sent via `Authorization: Bearer` instead of `x-api-key`)
48
+
49
+ The whole point of envx loading the env first is so the API key can live in your `.env` (encrypted, even) rather than the shell environment. Anywhere envx normally finds a value, the AI client will find the key.
50
+
51
+ ### Determinism + `--check`
52
+
53
+ `--ai` is not deterministic — Claude returns slightly different placeholder values per run. Don't pair it with `--check` (the on-disk file would always drift). Typical workflow:
54
+
55
+ - Local dev: `envx template --ai` once, review, commit.
56
+ - CI: `envx template --check` (no `--ai`) — verifies the structure is up to date with the source `.env*` files. The committed AI placeholder values stay put.
57
+
58
+ If you want CI to also regenerate values, drop `--check` and rely on a separate "fix-up" PR.
59
+
60
+ ## Multi-file source
61
+
62
+ When the resolved env-file list has more than one entry (e.g. `--cascade prod` expands to `.env`, `.env.prod`, `.env.local`, `.env.prod.local`), each file gets its own section header:
63
+
64
+ ```env
65
+ # Generated by `envx template` — do not edit by hand.
66
+ # Run `envx template` to regenerate, or `envx template --check` in CI.
67
+
68
+ # ── .env ─────────────────────────────────
69
+ DATABASE_URL=
70
+ LOG_LEVEL=
71
+
72
+ # ── .env.prod ─────────────────────────────────
73
+ DATABASE_REPLICAS=
74
+ ```
75
+
76
+ Keys are deduplicated across files: the first occurrence wins. Subsequent files' comments and blank lines still pass through (so each section's structure stays visible), but duplicate kv lines are skipped.
77
+
78
+ ## Flags
79
+
80
+ | flag | default | what it does |
81
+ | ----------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------- |
82
+ | `--out <path>` | `.env.example` | Output path (relative to cwd). |
83
+ | `--stdout` | `false` | Print to stdout instead of writing. |
84
+ | `--check` | `false` | Compare on-disk template vs regenerated; exit 1 on drift. |
85
+ | `--ai` | `false` | Populate example values via Claude. Requires an API key (see lookup chain above). |
86
+ | `--ai-provider` | `anthropic` | `anthropic` or `openai`. |
87
+ | `--ai-model <model>` | `claude-haiku-4-5-20251001` / `gpt-4o-mini` | Override the model. Defaults pick the smallest sensible model per provider. |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@super-repo/envx",
3
3
  "description": "A global executable to run applications with the ENV variables witin a monorepo loaded by dotenvx",
4
- "version": "0.2.3-b.5",
4
+ "version": "0.2.3-b.6",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "envx": "./dist/cli.js",
@@ -48,6 +48,7 @@
48
48
  "dependencies": {
49
49
  "@dotenvx/dotenvx": "^1.49.1",
50
50
  "eciesjs": "^0.4.10",
51
+ "ignore": "^5.3.2",
51
52
  "yargs": "^17.7.0",
52
53
  "zod": "^3.25.0"
53
54
  },
@@ -60,8 +61,8 @@
60
61
  "vite": "^8.0.11",
61
62
  "vite-plugin-dts": "^5.0.0",
62
63
  "vitest": "^4.1.5",
63
- "@super-repo/cli": "0.2.3-b.5",
64
- "@super-repo/envx-libs": "0.0.1",
65
- "@super-repo/envx-common": "0.0.1"
64
+ "@super-repo/cli": "0.2.3-b.6",
65
+ "@super-repo/envx-common": "0.0.1",
66
+ "@super-repo/envx-libs": "0.0.1"
66
67
  }
67
68
  }