envpkt 0.12.0 → 0.13.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/README.md +50 -1
- package/dist/cli.js +319 -69
- package/dist/index.d.ts +36 -2
- package/dist/index.js +85 -11
- package/package.json +1 -1
- package/schemas/envpkt.schema.json +13 -0
package/README.md
CHANGED
|
@@ -210,6 +210,32 @@ const result = boot() // decrypts sealed values, injects into process.env
|
|
|
210
210
|
|
|
211
211
|
Mixed mode is supported — sealed values take priority, with fnox as fallback for keys without `encrypted_value`.
|
|
212
212
|
|
|
213
|
+
## GitHub Actions
|
|
214
|
+
|
|
215
|
+
A composite action resolves the credentials in `envpkt.toml` into the CI job — with secret values masked in the log — so later steps just see them as environment variables:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
- uses: actions/checkout@v5
|
|
219
|
+
|
|
220
|
+
# Sealed packets are decrypted with the `age` CLI (not preinstalled on runners).
|
|
221
|
+
- run: sudo apt-get update && sudo apt-get install -y age
|
|
222
|
+
|
|
223
|
+
- uses: jordanburke/envpkt@v0.12.0
|
|
224
|
+
with:
|
|
225
|
+
config: ./envpkt.toml
|
|
226
|
+
strict: "true" # fail the build if a credential is expired/unhealthy
|
|
227
|
+
env:
|
|
228
|
+
ENVPKT_AGE_KEY: ${{ secrets.ENVPKT_AGE_KEY }}
|
|
229
|
+
|
|
230
|
+
- run: ./deploy.sh # sees the resolved vars; secret values redacted in the log
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**How it works.** Commit sealed (`encrypted_value`) packets to the repo and supply the age private key as the `ENVPKT_AGE_KEY` secret. `boot()` materializes the inline key to a `0600` temp file to decrypt, then [`env github`](#envpkt-env-github) masks each secret (`::add-mask::`) and writes it to `$GITHUB_ENV`. Identity precedence: `identity.key_file` → `ENVPKT_AGE_KEY_FILE` → `ENVPKT_AGE_KEY` (inline) → `~/.envpkt/age-key.txt`.
|
|
234
|
+
|
|
235
|
+
**Inputs:** `config`, `version` (npm version to run, default `latest`), `strict`, `profile`.
|
|
236
|
+
|
|
237
|
+
> Decrypting sealed packets requires the [`age`](https://github.com/FiloSottile/age) CLI on the runner (install it first, as above) — not needed if you only inject plaintext `[env.*]` defaults or resolve via fnox. Pin to a released tag (e.g. `@v0.12.0`); no moving major tag (`@v1`) is published yet. Node is assumed present; add `actions/setup-node` first to pin a version.
|
|
238
|
+
|
|
213
239
|
## Fleet Management
|
|
214
240
|
|
|
215
241
|
When you're running multiple agents, `envpkt fleet` scans a directory tree for `envpkt.toml` files and aggregates credential health across your entire fleet.
|
|
@@ -431,12 +457,35 @@ eval "$(envpkt env export --profile staging)"
|
|
|
431
457
|
eval "$(envpkt env export -c path/to/envpkt.toml)"
|
|
432
458
|
```
|
|
433
459
|
|
|
434
|
-
Add to your shell startup (e.g. `~/.zshrc`
|
|
460
|
+
Add to your shell startup (e.g. `~/.zshrc`) to load a global package once at login:
|
|
435
461
|
|
|
436
462
|
```bash
|
|
437
463
|
eval "$(envpkt env export 2>/dev/null)"
|
|
438
464
|
```
|
|
439
465
|
|
|
466
|
+
Secret values are emitted **only when the package sets top-level `scope = "shell"`** — the default `scope = "exec"` withholds them (use `envpkt exec`). For **per-project** credentials that load on `cd`, use [`envpkt shell-hook`](#envpkt-shell-hook) rather than a one-time startup eval.
|
|
467
|
+
|
|
468
|
+
### `envpkt shell-hook`
|
|
469
|
+
|
|
470
|
+
Generate a `cd` hook (zsh/bash) that loads a project's credentials when you enter its directory tree and restores your environment when you leave:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
eval "$(envpkt shell-hook zsh)" # add to ~/.zshrc (or: shell-hook bash)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
On each directory change it resolves the **nearest `envpkt.toml`, walking up from the current directory** (like `git`/`direnv` — so it works from any subdirectory, not just the project root), injects that package via `env export --track`, and restores the previous package on leave (prior values, not a blind unset). Env defaults always load; secret values load only for `scope = "shell"` packages. Backed by `envpkt config-path` — a resolve-only command that prints the active config path (no decryption).
|
|
477
|
+
|
|
478
|
+
> **Upward-walk discovery**: config resolution now walks up the directory tree to the nearest `envpkt.toml` before falling back to the global package. This also applies to `exec`, `env export`, and `audit` — running any of them from a subdirectory finds the enclosing project.
|
|
479
|
+
|
|
480
|
+
### `envpkt env github`
|
|
481
|
+
|
|
482
|
+
Inject resolved secrets into a GitHub Actions job. Emits `::add-mask::` for each secret value (redacting it from the log) and appends assignments to `$GITHUB_ENV` — under their namespaced wire names — so later steps in the job inherit them. Env defaults are written but not masked. `--strict` exits non-zero if the pre-flight audit is unhealthy. This is the engine behind the [GitHub Action](#github-actions).
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
# Run as a step; later steps in the job see the resolved vars
|
|
486
|
+
npx envpkt env github --strict
|
|
487
|
+
```
|
|
488
|
+
|
|
440
489
|
### `envpkt shell-hook`
|
|
441
490
|
|
|
442
491
|
Output a shell function that runs `envpkt audit --format minimal` whenever you `cd` into a directory. envpkt's config discovery chain automatically finds config files beyond CWD (see [Config Resolution](#config-resolution)), so the hook works even in directories without a local `envpkt.toml`.
|
package/dist/cli.js
CHANGED
|
@@ -303,6 +303,7 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
303
303
|
default: 1
|
|
304
304
|
}),
|
|
305
305
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
306
|
+
scope: Type.Optional(Type.Union([Type.Literal("shell"), Type.Literal("exec")], { description: "Whether `env export` emits this package's secrets for ambient/shell loading (`shell`) or withholds them so they are only available via `envpkt exec` (`exec`). Default `exec`. Never affects `envpkt exec`, which always injects everything." })),
|
|
306
307
|
namespace: Type.Optional(NamespaceSchema),
|
|
307
308
|
identity: Type.Optional(IdentitySchema),
|
|
308
309
|
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
@@ -369,11 +370,24 @@ const buildSearchPaths = () => {
|
|
|
369
370
|
const cloudPaths = Platform.cloudStorageDirs().toArray().map((cloud) => join(cloud.path, ".envpkt", CONFIG_FILENAME$2));
|
|
370
371
|
return [...homePaths, ...cloudPaths];
|
|
371
372
|
};
|
|
372
|
-
/**
|
|
373
|
+
/**
|
|
374
|
+
* Walk up from `dir` to the filesystem root, returning the nearest `envpkt.toml`.
|
|
375
|
+
* Makes a project's config apply throughout its subtree (like git/.env/direnv finding
|
|
376
|
+
* their root file), not only in the directory that literally contains the file.
|
|
377
|
+
* The global package lives in `~/.envpkt/` (a subdir), so it is not matched by this
|
|
378
|
+
* walk — it is resolved by the search-path/Platform fallback below.
|
|
379
|
+
*/
|
|
380
|
+
const walkUpForConfig = (dir) => {
|
|
381
|
+
const candidate = join(dir, CONFIG_FILENAME$2);
|
|
382
|
+
if (Fs.existsSync(candidate)) return Option(candidate);
|
|
383
|
+
const parent = dirname(dir);
|
|
384
|
+
return parent === dir ? Option(void 0) : walkUpForConfig(parent);
|
|
385
|
+
};
|
|
386
|
+
/** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
373
387
|
const discoverConfig = (cwd) => {
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
path:
|
|
388
|
+
const walked = walkUpForConfig(cwd ?? process.cwd()).orUndefined();
|
|
389
|
+
if (walked) return Option({
|
|
390
|
+
path: walked,
|
|
377
391
|
source: "cwd"
|
|
378
392
|
});
|
|
379
393
|
const customMatch = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean)).map((template) => ({
|
|
@@ -648,6 +662,17 @@ const formatError = (error) => {
|
|
|
648
662
|
case "AgeNotFound": return `${RED}Error:${RESET} age CLI not found: ${error.message}`;
|
|
649
663
|
case "DecryptFailed": return `${RED}Error:${RESET} Decrypt failed: ${error.message}`;
|
|
650
664
|
case "IdentityNotFound": return `${RED}Error:${RESET} Identity file not found: ${error.path}`;
|
|
665
|
+
case "SealKeyUnavailable": {
|
|
666
|
+
const e = error;
|
|
667
|
+
return [
|
|
668
|
+
`${RED}Error:${RESET} ${e.sealedKeys.length} sealed secret(s) can't be decrypted — no age key found.`,
|
|
669
|
+
`${DIM}Searched (in order):${RESET}`,
|
|
670
|
+
e.searched.map((l) => ` • ${l}`).join("\n"),
|
|
671
|
+
`${DIM}Fix one:${RESET}`,
|
|
672
|
+
` • Restore your key to ~/.envpkt/age-key.txt (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)`,
|
|
673
|
+
` • Re-provision from source: envpkt seal --edit <KEY>`
|
|
674
|
+
].join("\n");
|
|
675
|
+
}
|
|
651
676
|
case "AuditFailed": return `${RED}Error:${RESET} Audit failed: ${error.message}`;
|
|
652
677
|
case "CatalogNotFound": return `${RED}Error:${RESET} Catalog not found: ${error.path}`;
|
|
653
678
|
case "CatalogLoadError": return `${RED}Error:${RESET} Catalog load error: ${error.message}`;
|
|
@@ -835,6 +860,19 @@ const runAuditOnConfig = (config, options) => {
|
|
|
835
860
|
process.exit(code);
|
|
836
861
|
};
|
|
837
862
|
//#endregion
|
|
863
|
+
//#region src/cli/commands/config-path.ts
|
|
864
|
+
/**
|
|
865
|
+
* Print the `envpkt.toml` path resolved for the current directory, or nothing if none
|
|
866
|
+
* is found. Resolve-only: no config load, no boot, no decryption — cheap enough for a
|
|
867
|
+
* per-`cd` shell hook to gate on. Always exits 0; a missing config means "no package
|
|
868
|
+
* here", not an error.
|
|
869
|
+
*/
|
|
870
|
+
const runConfigPath = (options) => {
|
|
871
|
+
resolveConfigPath(options.config).fold(() => {}, ({ path }) => {
|
|
872
|
+
console.log(path);
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
//#endregion
|
|
838
876
|
//#region src/fnox/cli.ts
|
|
839
877
|
/** Export all secrets from fnox as key=value pairs for a given profile */
|
|
840
878
|
const fnoxExport = (profile, agentKey) => {
|
|
@@ -1326,10 +1364,13 @@ const materializeInlineKey = (key) => {
|
|
|
1326
1364
|
* the caller must dispose().
|
|
1327
1365
|
*/
|
|
1328
1366
|
const resolveSealIdentity = (config, configDir) => {
|
|
1329
|
-
if (config.identity?.key_file)
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1367
|
+
if (config.identity?.key_file) {
|
|
1368
|
+
const keyFilePath = resolve(configDir, expandPath(config.identity.key_file));
|
|
1369
|
+
if (existsSync(keyFilePath)) return Option({
|
|
1370
|
+
path: keyFilePath,
|
|
1371
|
+
dispose: noop
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1333
1374
|
const envFile = process.env["ENVPKT_AGE_KEY_FILE"];
|
|
1334
1375
|
if (envFile && existsSync(envFile)) return Option({
|
|
1335
1376
|
path: envFile,
|
|
@@ -1344,6 +1385,21 @@ const resolveSealIdentity = (config, configDir) => {
|
|
|
1344
1385
|
});
|
|
1345
1386
|
return Option(void 0);
|
|
1346
1387
|
};
|
|
1388
|
+
/** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
|
|
1389
|
+
const describeSealKeySearch = (config, configDir) => {
|
|
1390
|
+
const keyFile = config.identity?.key_file;
|
|
1391
|
+
const keyFileLine = keyFile ? `identity.key_file → ${resolve(configDir, expandPath(keyFile))} (${existsSync(resolve(configDir, expandPath(keyFile))) ? "found" : "missing"})` : `identity.key_file (not set)`;
|
|
1392
|
+
const envFile = process.env["ENVPKT_AGE_KEY_FILE"];
|
|
1393
|
+
const envFileLine = envFile ? `ENVPKT_AGE_KEY_FILE → ${envFile} (${existsSync(envFile) ? "found" : "missing"})` : `ENVPKT_AGE_KEY_FILE (unset)`;
|
|
1394
|
+
const inlineLine = resolveInlineKey().isEmpty ? `ENVPKT_AGE_KEY (unset)` : `ENVPKT_AGE_KEY (set, inline)`;
|
|
1395
|
+
const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
|
|
1396
|
+
return [
|
|
1397
|
+
keyFileLine,
|
|
1398
|
+
envFileLine,
|
|
1399
|
+
inlineLine,
|
|
1400
|
+
`${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
|
|
1401
|
+
];
|
|
1402
|
+
};
|
|
1347
1403
|
const resolveIdentityKey = (config, configDir) => {
|
|
1348
1404
|
return resolveIdentityFilePath(config, configDir, false).fold(() => Right(Option(void 0)), (path) => unwrapAgentKey(path).fold((err) => Left(err), (key) => Right(Option(key))));
|
|
1349
1405
|
};
|
|
@@ -1413,6 +1469,12 @@ const bootSafe = (options) => {
|
|
|
1413
1469
|
message: "unexpected"
|
|
1414
1470
|
}));
|
|
1415
1471
|
const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
|
|
1472
|
+
const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
|
|
1473
|
+
if (hasSealedValues && sealIdentity.isEmpty) return Left({
|
|
1474
|
+
_tag: "SealKeyUnavailable",
|
|
1475
|
+
sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
|
|
1476
|
+
searched: describeSealKeySearch(config, configDir)
|
|
1477
|
+
});
|
|
1416
1478
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
1417
1479
|
const secrets = {};
|
|
1418
1480
|
const injected = [];
|
|
@@ -1426,9 +1488,8 @@ const bootSafe = (options) => {
|
|
|
1426
1488
|
process.env[envEnv(key)] = value;
|
|
1427
1489
|
});
|
|
1428
1490
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
1429
|
-
if (hasSealedValues)
|
|
1491
|
+
if (hasSealedValues) sealIdentity.fold(() => {
|
|
1430
1492
|
log.warn("phase.sealed.no_identity_file", { sealed_keys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value).length });
|
|
1431
|
-
warnings.push("Sealed values found but no identity file available for decryption");
|
|
1432
1493
|
}, ({ path: idPath, dispose }) => {
|
|
1433
1494
|
try {
|
|
1434
1495
|
unsealSecrets(nonAliasSecretEntries, idPath).fold((err) => {
|
|
@@ -1533,6 +1594,30 @@ const bootSafe = (options) => {
|
|
|
1533
1594
|
}));
|
|
1534
1595
|
};
|
|
1535
1596
|
//#endregion
|
|
1597
|
+
//#region src/core/dotenv.ts
|
|
1598
|
+
const BARE_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
1599
|
+
/**
|
|
1600
|
+
* Quote a single value for dotenv output. Returns the value bare when safe,
|
|
1601
|
+
* otherwise double-quoted with POSIX-shell-quote escaping (`\`, `"`, `$`) and
|
|
1602
|
+
* whitespace collapsed to single-line escapes (`\n`, `\r`, `\t`) for portability.
|
|
1603
|
+
*/
|
|
1604
|
+
const quoteDotenvValue = (value) => {
|
|
1605
|
+
if (value === "") return "";
|
|
1606
|
+
if (BARE_SAFE.test(value)) return value;
|
|
1607
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
1608
|
+
};
|
|
1609
|
+
const formatEntry = (entry, includeSecrets) => {
|
|
1610
|
+
if (entry.secret && !includeSecrets) return `# (secret value omitted — re-run without --no-secrets to include)\n${entry.name}=`;
|
|
1611
|
+
return `${entry.name}=${quoteDotenvValue(entry.value)}`;
|
|
1612
|
+
};
|
|
1613
|
+
/** Serialize entries to dotenv text (no trailing newline). */
|
|
1614
|
+
const formatDotenv = (entries, options) => {
|
|
1615
|
+
const includeSecrets = options?.includeSecrets ?? true;
|
|
1616
|
+
const body = entries.map((e) => formatEntry(e, includeSecrets)).join("\n");
|
|
1617
|
+
const header = options?.header;
|
|
1618
|
+
return header ? `${header}\n\n${body}` : body;
|
|
1619
|
+
};
|
|
1620
|
+
//#endregion
|
|
1536
1621
|
//#region src/core/patterns.ts
|
|
1537
1622
|
const EXCLUDED_VARS = Set$1([
|
|
1538
1623
|
"PATH",
|
|
@@ -2665,6 +2750,20 @@ const writeIfValid = (configPath, updated, successMsg) => {
|
|
|
2665
2750
|
writeFileSync(configPath, updated, "utf-8");
|
|
2666
2751
|
console.log(successMsg);
|
|
2667
2752
|
};
|
|
2753
|
+
/**
|
|
2754
|
+
* Validate then preview (no write) — the `--dry-run` counterpart of `writeIfValid`.
|
|
2755
|
+
* Runs the same structural validation the real write would, so a dry-run can never
|
|
2756
|
+
* show a result that the actual write would reject. On invalid output it prints the
|
|
2757
|
+
* same error and exits 1, exactly as the write path does.
|
|
2758
|
+
*
|
|
2759
|
+
* `display` lets callers preview a focused slice (e.g. just the new block for `add`)
|
|
2760
|
+
* while still validating the full resulting config.
|
|
2761
|
+
*/
|
|
2762
|
+
const previewIfValid = (updated, display) => {
|
|
2763
|
+
validateOrExit(updated);
|
|
2764
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
2765
|
+
console.log(display ?? updated);
|
|
2766
|
+
};
|
|
2668
2767
|
//#endregion
|
|
2669
2768
|
//#region src/cli/commands/env.ts
|
|
2670
2769
|
const printPostWriteGuidance = () => {
|
|
@@ -2798,10 +2897,15 @@ const runEnvExport = (options) => {
|
|
|
2798
2897
|
console.error(formatError(err));
|
|
2799
2898
|
process.exit(2);
|
|
2800
2899
|
}, (boot) => {
|
|
2801
|
-
emitWarnings(boot);
|
|
2802
|
-
|
|
2803
|
-
|
|
2900
|
+
if (!options.track) emitWarnings(boot);
|
|
2901
|
+
const scope = loadConfig(boot.configPath).fold(() => "exec", (config) => config.scope ?? "exec");
|
|
2902
|
+
const gateSecrets = options.track === true && scope !== "shell";
|
|
2903
|
+
const entries = collectEmitEntries(boot);
|
|
2904
|
+
const emit = gateSecrets ? entries.filter((e) => !e.secret) : entries;
|
|
2905
|
+
emit.forEach(({ name, value }) => {
|
|
2906
|
+
console.log(options.track ? `_ENVPKT_HAD_${name}=\${${name}+1}; _ENVPKT_PREV_${name}="\${${name}-}"; export ${name}='${shellEscape(value)}'` : `export ${name}='${shellEscape(value)}'`);
|
|
2804
2907
|
});
|
|
2908
|
+
if (options.track) console.log(`_ENVPKT_INJECTED='${emit.map((e) => e.name).join(" ")}'`);
|
|
2805
2909
|
});
|
|
2806
2910
|
};
|
|
2807
2911
|
const runEnvGithub = (options) => {
|
|
@@ -2825,6 +2929,33 @@ const runEnvGithub = (options) => {
|
|
|
2825
2929
|
if (options.strict) process.exit(exitCodeForAudit(boot.audit));
|
|
2826
2930
|
});
|
|
2827
2931
|
};
|
|
2932
|
+
const buildDotenvHeader = (configPath) => `# Generated by envpkt — regenerate with: envpkt env dotenv\n# Source: ${configPath}. Do not edit by hand.`;
|
|
2933
|
+
const runEnvDotenv = (options) => {
|
|
2934
|
+
resolveForEmit(options).fold((err) => {
|
|
2935
|
+
console.error(formatError(err));
|
|
2936
|
+
process.exit(2);
|
|
2937
|
+
}, (boot) => {
|
|
2938
|
+
emitWarnings(boot);
|
|
2939
|
+
const entries = collectEmitEntries(boot);
|
|
2940
|
+
const includeSecrets = options.secrets !== false;
|
|
2941
|
+
const text = formatDotenv(entries, {
|
|
2942
|
+
includeSecrets,
|
|
2943
|
+
header: buildDotenvHeader(boot.configPath)
|
|
2944
|
+
});
|
|
2945
|
+
if (!options.output) {
|
|
2946
|
+
console.log(text);
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
const outPath = resolve(options.output);
|
|
2950
|
+
Try(() => writeFileSync(outPath, `${text}\n`, "utf-8")).fold((writeErr) => {
|
|
2951
|
+
console.error(`${RED}Error:${RESET} Failed to write ${outPath}: ${writeErr}`);
|
|
2952
|
+
process.exit(1);
|
|
2953
|
+
}, () => {
|
|
2954
|
+
console.error(`${GREEN}✓${RESET} Wrote ${CYAN}${outPath}${RESET}`);
|
|
2955
|
+
if (includeSecrets && entries.some((e) => e.secret)) console.error(`${YELLOW}Note:${RESET} ${outPath} contains secret values — ensure it is .gitignored.`);
|
|
2956
|
+
});
|
|
2957
|
+
});
|
|
2958
|
+
};
|
|
2828
2959
|
const buildEnvBlock = (name, value, options) => {
|
|
2829
2960
|
const lines = [`[env.${name}]`, `value = "${value}"`];
|
|
2830
2961
|
if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
|
|
@@ -3017,12 +3148,15 @@ const registerEnvCommands = (program) => {
|
|
|
3017
3148
|
env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
|
|
3018
3149
|
runEnvCheck(options);
|
|
3019
3150
|
});
|
|
3020
|
-
env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").action((options) => {
|
|
3151
|
+
env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").option("--track", "Emit prior-value snapshots + an _ENVPKT_INJECTED list (for the shell hook to restore on cd)").action((options) => {
|
|
3021
3152
|
runEnvExport(options);
|
|
3022
3153
|
});
|
|
3023
3154
|
env.command("github").description("Inject resolved secrets into $GITHUB_ENV for GitHub Actions, masking secret values in the log (::add-mask::)").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--strict", "Exit non-zero if the pre-flight audit is not healthy").action((options) => {
|
|
3024
3155
|
runEnvGithub(options);
|
|
3025
3156
|
});
|
|
3157
|
+
env.command("dotenv").description("Output resolved credentials in .env format (for Wrangler, Docker --env-file, Vite, etc.)").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("-o, --output <file>", "Write to a file instead of stdout").option("--no-secrets", "Omit secret values (emit KEY= with a note) — values are included by default").action((options) => {
|
|
3158
|
+
runEnvDotenv(options);
|
|
3159
|
+
});
|
|
3026
3160
|
env.command("add").description("Add a new environment default entry to envpkt.toml").argument("<name>", "Environment variable name").argument("<value>", "Default value").option("-c, --config <path>", "Path to envpkt.toml").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, value, options) => {
|
|
3027
3161
|
runEnvAdd(name, value, options);
|
|
3028
3162
|
});
|
|
@@ -3167,6 +3301,8 @@ const runFleet = (options) => {
|
|
|
3167
3301
|
//#endregion
|
|
3168
3302
|
//#region src/cli/commands/init.ts
|
|
3169
3303
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
3304
|
+
/** Resolve the top-level `scope` to scaffold: explicit --scope wins; --global implies "shell". */
|
|
3305
|
+
const resolveInitScope = (options) => options.scope ?? (options.global ? "shell" : void 0);
|
|
3170
3306
|
const todayIso = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3171
3307
|
const generateSecretBlock = (key, service) => {
|
|
3172
3308
|
return `[secret.${key}]
|
|
@@ -3191,6 +3327,8 @@ const generateTemplate = (options, fnoxKeys) => {
|
|
|
3191
3327
|
lines.push(`#:schema https://raw.githubusercontent.com/jordanburke/envpkt/main/schemas/envpkt.schema.json`);
|
|
3192
3328
|
lines.push(``);
|
|
3193
3329
|
lines.push(`version = 1`);
|
|
3330
|
+
const scope = resolveInitScope(options);
|
|
3331
|
+
if (scope) lines.push(`scope = "${scope}" # shell = secrets load ambiently on cd/eval; exec = only via envpkt exec`);
|
|
3194
3332
|
lines.push(``);
|
|
3195
3333
|
if (options.catalog) {
|
|
3196
3334
|
lines.push(`catalog = "${options.catalog}"`);
|
|
@@ -3249,6 +3387,10 @@ const formatConfigError = (err) => {
|
|
|
3249
3387
|
};
|
|
3250
3388
|
const runInit = (dir, options) => {
|
|
3251
3389
|
const outPath = join(dir, CONFIG_FILENAME);
|
|
3390
|
+
if (options.scope !== void 0 && options.scope !== "shell" && options.scope !== "exec") {
|
|
3391
|
+
console.error(`${RED}Error:${RESET} --scope must be "shell" or "exec" (got "${options.scope}")`);
|
|
3392
|
+
process.exit(1);
|
|
3393
|
+
}
|
|
3252
3394
|
if (existsSync(outPath) && !options.force) {
|
|
3253
3395
|
console.error(`${RED}Error:${RESET} ${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
|
|
3254
3396
|
process.exit(1);
|
|
@@ -3958,6 +4100,34 @@ const applySealedToml = (raw, sealedMeta) => {
|
|
|
3958
4100
|
};
|
|
3959
4101
|
return flushPending(List(lines).zipWithIndex().foldLeft(initial)((state, entry) => step(state, entry[0], entry[1]))).output.join("\n");
|
|
3960
4102
|
};
|
|
4103
|
+
/** Affirmative answer to a `[y/N]` confirm prompt. */
|
|
4104
|
+
const isAffirmative = (answer) => /^y(es)?$/i.test(answer.trim());
|
|
4105
|
+
/**
|
|
4106
|
+
* Collect new plaintext values for the `--edit`ed keys via the injected `prompt`. Before
|
|
4107
|
+
* overwriting an entry that is **already sealed** (`encrypted_value` present), require an
|
|
4108
|
+
* explicit confirmation — replacing it discards the only ciphertext, and the prior value
|
|
4109
|
+
* can't be recovered without the original key. Declined or empty entries are skipped (reported
|
|
4110
|
+
* via `onSkip`). I/O flows only through `prompt`/`onSkip`, so the confirm/skip logic is
|
|
4111
|
+
* unit-testable without a TTY.
|
|
4112
|
+
*/
|
|
4113
|
+
const collectEditedValues = async (editKeys, entries, prompt, onSkip) => {
|
|
4114
|
+
const values = {};
|
|
4115
|
+
for (const key of editKeys) {
|
|
4116
|
+
if (entries[key]?.encrypted_value) {
|
|
4117
|
+
if (!isAffirmative(await prompt(`Replace the sealed value for ${key}? The previous value can't be recovered without the original key. [y/N] `))) {
|
|
4118
|
+
onSkip?.(key, "declined");
|
|
4119
|
+
continue;
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
const value = await prompt(`Enter new value for ${key}: `);
|
|
4123
|
+
if (value === "") {
|
|
4124
|
+
onSkip?.(key, "empty");
|
|
4125
|
+
continue;
|
|
4126
|
+
}
|
|
4127
|
+
values[key] = value;
|
|
4128
|
+
}
|
|
4129
|
+
return values;
|
|
4130
|
+
};
|
|
3961
4131
|
/** Write sealed values back into the TOML file, preserving structure. */
|
|
3962
4132
|
const writeSealedToml = (configPath, sealedMeta) => {
|
|
3963
4133
|
const finalContent = applySealedToml(readFileSync(configPath, "utf-8"), sealedMeta);
|
|
@@ -4034,15 +4204,9 @@ const runSeal = async (options) => {
|
|
|
4034
4204
|
const prompt = (question) => new Promise((resolve) => {
|
|
4035
4205
|
rl.question(question, (answer) => resolve(answer));
|
|
4036
4206
|
});
|
|
4037
|
-
const values = {
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
if (value === "") {
|
|
4041
|
-
console.error(`${YELLOW}Skipped${RESET} ${key} (empty value)`);
|
|
4042
|
-
continue;
|
|
4043
|
-
}
|
|
4044
|
-
values[key] = value;
|
|
4045
|
-
}
|
|
4207
|
+
const values = await collectEditedValues(editKeys, secretEntries, prompt, (key, reason) => {
|
|
4208
|
+
console.error(`${YELLOW}Skipped${RESET} ${key} (${reason === "empty" ? "empty value" : "kept existing sealed value"})`);
|
|
4209
|
+
});
|
|
4046
4210
|
rl.close();
|
|
4047
4211
|
if (Object.keys(values).length === 0) {
|
|
4048
4212
|
console.error(`${RED}Error:${RESET} No values provided`);
|
|
@@ -4129,6 +4293,27 @@ const runSeal = async (options) => {
|
|
|
4129
4293
|
};
|
|
4130
4294
|
//#endregion
|
|
4131
4295
|
//#region src/cli/commands/secret.ts
|
|
4296
|
+
/**
|
|
4297
|
+
* Fields removable via `--unset` — exactly the metadata fields settable via a flag.
|
|
4298
|
+
* Mental model: you can unset any field you can set. Structural/managed fields
|
|
4299
|
+
* (`created`, `last_rotated_at`, `encrypted_value`, `from_key`, `namespace`) are
|
|
4300
|
+
* intentionally excluded — they're owned by `seal`/`rotate`/`alias`, not `edit`.
|
|
4301
|
+
* Field names are the canonical TOML keys (e.g. `rate_limit`, not `rate-limit`).
|
|
4302
|
+
*/
|
|
4303
|
+
const UNSETTABLE_FIELDS = [
|
|
4304
|
+
"service",
|
|
4305
|
+
"purpose",
|
|
4306
|
+
"comment",
|
|
4307
|
+
"expires",
|
|
4308
|
+
"rotates",
|
|
4309
|
+
"rate_limit",
|
|
4310
|
+
"model_hint",
|
|
4311
|
+
"source",
|
|
4312
|
+
"rotation_url",
|
|
4313
|
+
"required",
|
|
4314
|
+
"capabilities",
|
|
4315
|
+
"tags"
|
|
4316
|
+
];
|
|
4132
4317
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4133
4318
|
const buildSecretBlock = (name, options) => {
|
|
4134
4319
|
const lines = [`[secret.${name}]`];
|
|
@@ -4174,6 +4359,9 @@ const buildFieldUpdates = (options) => {
|
|
|
4174
4359
|
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
4175
4360
|
return `${k} = "${v}"`;
|
|
4176
4361
|
}).join(", ")} }`;
|
|
4362
|
+
options.unset?.forEach((field) => {
|
|
4363
|
+
updates[field] = null;
|
|
4364
|
+
});
|
|
4177
4365
|
return updates;
|
|
4178
4366
|
};
|
|
4179
4367
|
const withConfig = (configFlag, fn) => {
|
|
@@ -4206,12 +4394,12 @@ const runSecretAdd = (name, options) => {
|
|
|
4206
4394
|
process.exit(1);
|
|
4207
4395
|
}
|
|
4208
4396
|
const block = buildSecretBlock(name, options);
|
|
4397
|
+
const updated = appendSection(readFileSync(configPath, "utf-8"), block);
|
|
4209
4398
|
if (options.dryRun) {
|
|
4210
|
-
|
|
4211
|
-
console.log(block);
|
|
4399
|
+
previewIfValid(updated, block);
|
|
4212
4400
|
return;
|
|
4213
4401
|
}
|
|
4214
|
-
writeIfValid(configPath,
|
|
4402
|
+
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
|
|
4215
4403
|
});
|
|
4216
4404
|
});
|
|
4217
4405
|
};
|
|
@@ -4220,6 +4408,12 @@ const runSecretEdit = (name, options) => {
|
|
|
4220
4408
|
console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
|
|
4221
4409
|
process.exit(1);
|
|
4222
4410
|
}
|
|
4411
|
+
const unknownUnset = (options.unset ?? []).filter((f) => !UNSETTABLE_FIELDS.includes(f));
|
|
4412
|
+
if (unknownUnset.length > 0) {
|
|
4413
|
+
console.error(`${RED}Error:${RESET} Cannot --unset unknown field(s): ${unknownUnset.join(", ")}`);
|
|
4414
|
+
console.error(`${DIM}Unsettable fields: ${UNSETTABLE_FIELDS.join(", ")}${RESET}`);
|
|
4415
|
+
process.exit(1);
|
|
4416
|
+
}
|
|
4223
4417
|
withConfig(Option(options.config), (configPath, raw) => {
|
|
4224
4418
|
loadConfig(configPath).fold((err) => {
|
|
4225
4419
|
console.error(formatError(err));
|
|
@@ -4239,8 +4433,7 @@ const runSecretEdit = (name, options) => {
|
|
|
4239
4433
|
process.exit(2);
|
|
4240
4434
|
}, (updated) => {
|
|
4241
4435
|
if (options.dryRun) {
|
|
4242
|
-
|
|
4243
|
-
console.log(updated);
|
|
4436
|
+
previewIfValid(updated);
|
|
4244
4437
|
return;
|
|
4245
4438
|
}
|
|
4246
4439
|
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Updated ${BOLD}${name}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
@@ -4255,8 +4448,7 @@ const runSecretRm = (name, options) => {
|
|
|
4255
4448
|
process.exit(1);
|
|
4256
4449
|
}, (updated) => {
|
|
4257
4450
|
if (options.dryRun) {
|
|
4258
|
-
|
|
4259
|
-
console.log(updated);
|
|
4451
|
+
previewIfValid(updated);
|
|
4260
4452
|
return;
|
|
4261
4453
|
}
|
|
4262
4454
|
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Removed ${BOLD}${name}${RESET} from ${CYAN}${configPath}${RESET}`);
|
|
@@ -4270,8 +4462,7 @@ const runSecretRename = (oldName, newName, options) => {
|
|
|
4270
4462
|
process.exit(1);
|
|
4271
4463
|
}, (updated) => {
|
|
4272
4464
|
if (options.dryRun) {
|
|
4273
|
-
|
|
4274
|
-
console.log(updated);
|
|
4465
|
+
previewIfValid(updated);
|
|
4275
4466
|
return;
|
|
4276
4467
|
}
|
|
4277
4468
|
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Renamed ${BOLD}${oldName}${RESET} → ${BOLD}${newName}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
@@ -4404,8 +4595,7 @@ const runSecretRotate = async (name, options) => {
|
|
|
4404
4595
|
process.exit(2);
|
|
4405
4596
|
}, (result) => {
|
|
4406
4597
|
if (options.dryRun) {
|
|
4407
|
-
|
|
4408
|
-
console.log(result);
|
|
4598
|
+
previewIfValid(result);
|
|
4409
4599
|
return;
|
|
4410
4600
|
}
|
|
4411
4601
|
writeIfValid(configPath, result, `${GREEN}✓${RESET} Stamped ${BOLD}last_rotated_at${RESET} on ${BOLD}${name}${RESET} (unsealed — no ciphertext to update)`);
|
|
@@ -4433,8 +4623,7 @@ const runSecretRotate = async (name, options) => {
|
|
|
4433
4623
|
process.exit(2);
|
|
4434
4624
|
}, (result) => {
|
|
4435
4625
|
if (options.dryRun) {
|
|
4436
|
-
|
|
4437
|
-
console.log(result);
|
|
4626
|
+
previewIfValid(result);
|
|
4438
4627
|
return;
|
|
4439
4628
|
}
|
|
4440
4629
|
writeIfValid(configPath, result, `${GREEN}✓${RESET} Rotated ${BOLD}${name}${RESET} (resealed + stamped ${CYAN}${today}${RESET})`);
|
|
@@ -4446,7 +4635,8 @@ const registerSecretCommands = (program) => {
|
|
|
4446
4635
|
addSecretFlags(secret.command("add").description("Add a new secret entry to envpkt.toml").argument("<name>", "Secret name (becomes the env var key)").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--dry-run", "Preview the TOML block without writing")).action((name, options) => {
|
|
4447
4636
|
runSecretAdd(name, options);
|
|
4448
4637
|
});
|
|
4449
|
-
|
|
4638
|
+
const collect = (value, previous) => [...previous, value];
|
|
4639
|
+
addSecretFlags(secret.command("edit").description("Update metadata fields on an existing secret").argument("<name>", "Secret name to edit").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--no-required", "Mark this secret as not required").option("--unset <field>", "Remove an optional field (repeatable). Field names match TOML keys, e.g. expires, rate_limit", collect, []).option("--dry-run", "Preview the changes without writing")).action((name, options) => {
|
|
4450
4640
|
runSecretEdit(name, options);
|
|
4451
4641
|
});
|
|
4452
4642
|
secret.command("rm").description("Remove a secret entry from envpkt.toml").argument("<name>", "Secret name to remove").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((name, options) => {
|
|
@@ -4464,39 +4654,96 @@ const registerSecretCommands = (program) => {
|
|
|
4464
4654
|
};
|
|
4465
4655
|
//#endregion
|
|
4466
4656
|
//#region src/cli/commands/shell-hook.ts
|
|
4467
|
-
const
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4657
|
+
const zshHook = (audit) => [
|
|
4658
|
+
"# envpkt shell hook — add to ~/.zshrc: eval \"$(envpkt shell-hook zsh)\"",
|
|
4659
|
+
"# Loads the current directory package on cd, restores the prior env on leave, warns on health.",
|
|
4660
|
+
"_envpkt_restore() {",
|
|
4661
|
+
" [[ -n \"$_ENVPKT_INJECTED\" ]] || return",
|
|
4662
|
+
" local k had prev",
|
|
4663
|
+
" for k in ${(s: :)_ENVPKT_INJECTED}; do",
|
|
4664
|
+
" had=\"_ENVPKT_HAD_$k\"",
|
|
4665
|
+
" prev=\"_ENVPKT_PREV_$k\"",
|
|
4666
|
+
" if [[ -n \"${(P)had}\" ]]; then",
|
|
4667
|
+
" export \"$k=${(P)prev}\"",
|
|
4668
|
+
" else",
|
|
4669
|
+
" unset \"$k\"",
|
|
4670
|
+
" fi",
|
|
4671
|
+
" unset \"$had\" \"$prev\"",
|
|
4672
|
+
" done",
|
|
4673
|
+
" unset _ENVPKT_INJECTED",
|
|
4674
|
+
"}",
|
|
4675
|
+
"",
|
|
4676
|
+
"_envpkt_chpwd() {",
|
|
4677
|
+
" local cfg",
|
|
4678
|
+
" cfg=\"$(envpkt config-path 2>/dev/null)\"",
|
|
4679
|
+
" [[ \"$cfg\" == \"$_ENVPKT_DIR\" ]] && return",
|
|
4680
|
+
" _envpkt_restore",
|
|
4681
|
+
" _ENVPKT_DIR=\"$cfg\"",
|
|
4682
|
+
" [[ -z \"$cfg\" ]] && return",
|
|
4683
|
+
" eval \"$(envpkt env export --track)\"",
|
|
4684
|
+
...audit ? [" envpkt audit --format minimal 2>/dev/null"] : [],
|
|
4685
|
+
"}",
|
|
4686
|
+
"",
|
|
4687
|
+
"autoload -Uz add-zsh-hook",
|
|
4688
|
+
"add-zsh-hook chpwd _envpkt_chpwd",
|
|
4689
|
+
"_envpkt_chpwd",
|
|
4690
|
+
""
|
|
4691
|
+
].join("\n");
|
|
4692
|
+
const bashHook = (audit) => [
|
|
4693
|
+
"# envpkt shell hook — add to ~/.bashrc: eval \"$(envpkt shell-hook bash)\"",
|
|
4694
|
+
"# Loads the current directory package on cd, restores the prior env on leave, warns on health.",
|
|
4695
|
+
"_envpkt_restore() {",
|
|
4696
|
+
" [ -n \"$_ENVPKT_INJECTED\" ] || return",
|
|
4697
|
+
" local k had prev",
|
|
4698
|
+
" for k in $_ENVPKT_INJECTED; do",
|
|
4699
|
+
" had=\"_ENVPKT_HAD_$k\"",
|
|
4700
|
+
" prev=\"_ENVPKT_PREV_$k\"",
|
|
4701
|
+
" if [ -n \"${!had}\" ]; then",
|
|
4702
|
+
" export \"$k=${!prev}\"",
|
|
4703
|
+
" else",
|
|
4704
|
+
" unset \"$k\"",
|
|
4705
|
+
" fi",
|
|
4706
|
+
" unset \"$had\" \"$prev\"",
|
|
4707
|
+
" done",
|
|
4708
|
+
" unset _ENVPKT_INJECTED",
|
|
4709
|
+
"}",
|
|
4710
|
+
"",
|
|
4711
|
+
"_envpkt_prompt() {",
|
|
4712
|
+
" [ \"$PWD\" = \"$_ENVPKT_PWD\" ] && return",
|
|
4713
|
+
" _ENVPKT_PWD=\"$PWD\"",
|
|
4714
|
+
" local cfg",
|
|
4715
|
+
" cfg=\"$(envpkt config-path 2>/dev/null)\"",
|
|
4716
|
+
" [ \"$cfg\" = \"$_ENVPKT_DIR\" ] && return",
|
|
4717
|
+
" _envpkt_restore",
|
|
4718
|
+
" _ENVPKT_DIR=\"$cfg\"",
|
|
4719
|
+
" [ -z \"$cfg\" ] && return",
|
|
4720
|
+
" eval \"$(envpkt env export --track)\"",
|
|
4721
|
+
...audit ? [" envpkt audit --format minimal 2>/dev/null"] : [],
|
|
4722
|
+
"}",
|
|
4723
|
+
"",
|
|
4724
|
+
"# Register on PROMPT_COMMAND, handling both the string and (bash 5.1+) array forms.",
|
|
4725
|
+
"if [[ \"$(declare -p PROMPT_COMMAND 2>/dev/null)\" == \"declare -a\"* ]]; then",
|
|
4726
|
+
" case \" ${PROMPT_COMMAND[*]} \" in",
|
|
4727
|
+
" *\" _envpkt_prompt \"*) ;;",
|
|
4728
|
+
" *) PROMPT_COMMAND+=(_envpkt_prompt) ;;",
|
|
4729
|
+
" esac",
|
|
4730
|
+
"else",
|
|
4731
|
+
" case \"$PROMPT_COMMAND\" in",
|
|
4732
|
+
" *_envpkt_prompt*) ;;",
|
|
4733
|
+
" *) PROMPT_COMMAND=\"_envpkt_prompt${PROMPT_COMMAND:+;$PROMPT_COMMAND}\" ;;",
|
|
4734
|
+
" esac",
|
|
4735
|
+
"fi",
|
|
4736
|
+
"_envpkt_prompt",
|
|
4737
|
+
""
|
|
4738
|
+
].join("\n");
|
|
4739
|
+
const runShellHook = (shell, options) => {
|
|
4740
|
+
const audit = options?.audit !== false;
|
|
4494
4741
|
switch (shell) {
|
|
4495
4742
|
case "zsh":
|
|
4496
|
-
console.log(
|
|
4743
|
+
console.log(zshHook(audit));
|
|
4497
4744
|
break;
|
|
4498
4745
|
case "bash":
|
|
4499
|
-
console.log(
|
|
4746
|
+
console.log(bashHook(audit));
|
|
4500
4747
|
break;
|
|
4501
4748
|
default:
|
|
4502
4749
|
console.error(`${RED}Error:${RESET} Unsupported shell: ${shell}. Use "zsh" or "bash".`);
|
|
@@ -4793,7 +5040,7 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
|
|
|
4793
5040
|
const pkgPath = findPkgJson(dirname(fileURLToPath(import.meta.url)));
|
|
4794
5041
|
return pkgPath ? JSON.parse(readFileSync(pkgPath, "utf-8")).version : "0.0.0";
|
|
4795
5042
|
})());
|
|
4796
|
-
program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--identity", "Include [identity] section").option("--name <name>", "Identity name (requires --identity)").option("--capabilities <caps>", "Comma-separated capabilities (requires --identity)").option("--expires <date>", "Credential expiration YYYY-MM-DD (requires --identity)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
|
|
5043
|
+
program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--identity", "Include [identity] section").option("--name <name>", "Identity name (requires --identity)").option("--capabilities <caps>", "Comma-separated capabilities (requires --identity)").option("--expires <date>", "Credential expiration YYYY-MM-DD (requires --identity)").option("--scope <scope>", "Top-level scope: \"shell\" (secrets load ambiently) or \"exec\" (only via envpkt exec)").option("--global", "Scaffold a global/ambient package (implies scope = \"shell\")").option("--force", "Overwrite existing envpkt.toml").action((options) => {
|
|
4797
5044
|
runInit(process.cwd(), options);
|
|
4798
5045
|
});
|
|
4799
5046
|
program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/<project>-key.txt)").option("--global", "Write key to the shared default path (~/.envpkt/age-key.txt) instead of a project-specific one").action((options) => {
|
|
@@ -4831,8 +5078,11 @@ program.command("sort").description("Group [env.*] and [secret.*] sections and a
|
|
|
4831
5078
|
program.command("upgrade").description("Upgrade envpkt to the latest version (npm install -g envpkt@latest)").action(() => {
|
|
4832
5079
|
runUpgrade();
|
|
4833
5080
|
});
|
|
4834
|
-
program.command("
|
|
4835
|
-
|
|
5081
|
+
program.command("config-path").description("Print the envpkt.toml path resolved for the current directory (empty if none). Resolve-only — no decryption.").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
|
|
5082
|
+
runConfigPath(options);
|
|
5083
|
+
});
|
|
5084
|
+
program.command("shell-hook").description("Output a shell hook that loads a project's credentials on cd and restores them on leave").argument("<shell>", "Shell type: zsh | bash").option("--no-audit", "Omit the credential-health audit line from the emitted hook").action((shell, options) => {
|
|
5085
|
+
runShellHook(shell, options);
|
|
4836
5086
|
});
|
|
4837
5087
|
program.parse();
|
|
4838
5088
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ type EnvMeta = Static<typeof EnvMetaSchema>;
|
|
|
78
78
|
declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
|
|
79
79
|
version: import("@sinclair/typebox").TNumber;
|
|
80
80
|
catalog: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
81
|
+
scope: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"shell">, import("@sinclair/typebox").TLiteral<"exec">]>>;
|
|
81
82
|
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
82
83
|
prefix: import("@sinclair/typebox").TString;
|
|
83
84
|
separator: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -327,6 +328,10 @@ type BootError = ConfigError | FnoxError | CatalogError | AliasError | {
|
|
|
327
328
|
readonly _tag: "AuditFailed";
|
|
328
329
|
readonly audit: AuditResult;
|
|
329
330
|
readonly message: string;
|
|
331
|
+
} | {
|
|
332
|
+
readonly _tag: "SealKeyUnavailable";
|
|
333
|
+
readonly sealedKeys: ReadonlyArray<string>;
|
|
334
|
+
readonly searched: ReadonlyArray<string>;
|
|
330
335
|
} | IdentityError;
|
|
331
336
|
type IdentityError = {
|
|
332
337
|
readonly _tag: "AgeNotFound";
|
|
@@ -389,7 +394,7 @@ type DiscoveredConfig = {
|
|
|
389
394
|
readonly path: string;
|
|
390
395
|
readonly source: "cwd" | "search";
|
|
391
396
|
};
|
|
392
|
-
/** Discover config by
|
|
397
|
+
/** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
393
398
|
declare const discoverConfig: (cwd?: string) => Option<DiscoveredConfig>;
|
|
394
399
|
/** Read a config file, returning Either<ConfigError, string> */
|
|
395
400
|
declare const readConfigFile: (path: string) => Either<ConfigError, string>;
|
|
@@ -549,6 +554,35 @@ declare const updateConfigIdentity: (configPath: string, options: UpdateIdentity
|
|
|
549
554
|
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
550
555
|
declare const resolveValues: (keys: ReadonlyArray<string>, profile?: string, agentKey?: string) => Promise<Record<string, string>>;
|
|
551
556
|
//#endregion
|
|
557
|
+
//#region src/core/dotenv.d.ts
|
|
558
|
+
/**
|
|
559
|
+
* Serialize resolved credential entries to `.env` (dotenv) format.
|
|
560
|
+
*
|
|
561
|
+
* Unlike `env export` (shell `export VAR=...` for `eval`) this emits the bare
|
|
562
|
+
* `KEY=value` syntax that the broad `.env`-consuming ecosystem auto-discovers:
|
|
563
|
+
* Wrangler, Docker `--env-file`, Vite/Next/Astro, many GitHub Actions, direnv.
|
|
564
|
+
*
|
|
565
|
+
* Pure and deterministic — no I/O, no timestamps — so regenerating a file
|
|
566
|
+
* produces identical output (clean diffs, reproducible CI).
|
|
567
|
+
*/
|
|
568
|
+
type DotenvEntry = {
|
|
569
|
+
readonly name: string;
|
|
570
|
+
readonly value: string;
|
|
571
|
+
readonly secret: boolean;
|
|
572
|
+
};
|
|
573
|
+
type FormatDotenvOptions = {
|
|
574
|
+
/** Write secret values into the output. Default true (matches `env export`/`env github`). */readonly includeSecrets?: boolean; /** A pre-formatted comment block (each line `#`-prefixed) placed at the top. */
|
|
575
|
+
readonly header?: string;
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* Quote a single value for dotenv output. Returns the value bare when safe,
|
|
579
|
+
* otherwise double-quoted with POSIX-shell-quote escaping (`\`, `"`, `$`) and
|
|
580
|
+
* whitespace collapsed to single-line escapes (`\n`, `\r`, `\t`) for portability.
|
|
581
|
+
*/
|
|
582
|
+
declare const quoteDotenvValue: (value: string) => string;
|
|
583
|
+
/** Serialize entries to dotenv text (no trailing newline). */
|
|
584
|
+
declare const formatDotenv: (entries: ReadonlyArray<DotenvEntry>, options?: FormatDotenvOptions) => string;
|
|
585
|
+
//#endregion
|
|
552
586
|
//#region src/core/toml-edit.d.ts
|
|
553
587
|
/**
|
|
554
588
|
* Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
|
|
@@ -634,4 +668,4 @@ type ToolDef = {
|
|
|
634
668
|
declare const toolDefinitions: readonly ToolDef[];
|
|
635
669
|
declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
|
|
636
670
|
//#endregion
|
|
637
|
-
export { type AgentIdentity, AgentIdentitySchema, type AliasError, type AliasTable, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, ConsumerType, type CredentialPattern, type DirectLogger, type DirectTestLoggerHandle, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type LogEntry, type LogLevel, type LogMetadata, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
|
|
671
|
+
export { type AgentIdentity, AgentIdentitySchema, type AliasError, type AliasTable, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, ConsumerType, type CredentialPattern, type DirectLogger, type DirectTestLoggerHandle, type DotenvEntry, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatDotenvOptions, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type LogEntry, type LogLevel, type LogMetadata, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatDotenv, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, quoteDotenvValue, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,7 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
111
111
|
default: 1
|
|
112
112
|
}),
|
|
113
113
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
114
|
+
scope: Type.Optional(Type.Union([Type.Literal("shell"), Type.Literal("exec")], { description: "Whether `env export` emits this package's secrets for ambient/shell loading (`shell`) or withholds them so they are only available via `envpkt exec` (`exec`). Default `exec`. Never affects `envpkt exec`, which always injects everything." })),
|
|
114
115
|
namespace: Type.Optional(NamespaceSchema),
|
|
115
116
|
identity: Type.Optional(IdentitySchema),
|
|
116
117
|
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
@@ -182,11 +183,24 @@ const buildSearchPaths = () => {
|
|
|
182
183
|
const cloudPaths = Platform.cloudStorageDirs().toArray().map((cloud) => join(cloud.path, ".envpkt", CONFIG_FILENAME$1));
|
|
183
184
|
return [...homePaths, ...cloudPaths];
|
|
184
185
|
};
|
|
185
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Walk up from `dir` to the filesystem root, returning the nearest `envpkt.toml`.
|
|
188
|
+
* Makes a project's config apply throughout its subtree (like git/.env/direnv finding
|
|
189
|
+
* their root file), not only in the directory that literally contains the file.
|
|
190
|
+
* The global package lives in `~/.envpkt/` (a subdir), so it is not matched by this
|
|
191
|
+
* walk — it is resolved by the search-path/Platform fallback below.
|
|
192
|
+
*/
|
|
193
|
+
const walkUpForConfig = (dir) => {
|
|
194
|
+
const candidate = join(dir, CONFIG_FILENAME$1);
|
|
195
|
+
if (Fs.existsSync(candidate)) return Option(candidate);
|
|
196
|
+
const parent = dirname(dir);
|
|
197
|
+
return parent === dir ? Option(void 0) : walkUpForConfig(parent);
|
|
198
|
+
};
|
|
199
|
+
/** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
186
200
|
const discoverConfig = (cwd) => {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
path:
|
|
201
|
+
const walked = walkUpForConfig(cwd ?? process.cwd()).orUndefined();
|
|
202
|
+
if (walked) return Option({
|
|
203
|
+
path: walked,
|
|
190
204
|
source: "cwd"
|
|
191
205
|
});
|
|
192
206
|
const customMatch = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean)).map((template) => ({
|
|
@@ -1949,10 +1963,13 @@ const materializeInlineKey = (key) => {
|
|
|
1949
1963
|
* the caller must dispose().
|
|
1950
1964
|
*/
|
|
1951
1965
|
const resolveSealIdentity = (config, configDir) => {
|
|
1952
|
-
if (config.identity?.key_file)
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1966
|
+
if (config.identity?.key_file) {
|
|
1967
|
+
const keyFilePath = resolve(configDir, expandPath(config.identity.key_file));
|
|
1968
|
+
if (existsSync(keyFilePath)) return Option({
|
|
1969
|
+
path: keyFilePath,
|
|
1970
|
+
dispose: noop
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1956
1973
|
const envFile = process.env["ENVPKT_AGE_KEY_FILE"];
|
|
1957
1974
|
if (envFile && existsSync(envFile)) return Option({
|
|
1958
1975
|
path: envFile,
|
|
@@ -1967,6 +1984,21 @@ const resolveSealIdentity = (config, configDir) => {
|
|
|
1967
1984
|
});
|
|
1968
1985
|
return Option(void 0);
|
|
1969
1986
|
};
|
|
1987
|
+
/** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
|
|
1988
|
+
const describeSealKeySearch = (config, configDir) => {
|
|
1989
|
+
const keyFile = config.identity?.key_file;
|
|
1990
|
+
const keyFileLine = keyFile ? `identity.key_file → ${resolve(configDir, expandPath(keyFile))} (${existsSync(resolve(configDir, expandPath(keyFile))) ? "found" : "missing"})` : `identity.key_file (not set)`;
|
|
1991
|
+
const envFile = process.env["ENVPKT_AGE_KEY_FILE"];
|
|
1992
|
+
const envFileLine = envFile ? `ENVPKT_AGE_KEY_FILE → ${envFile} (${existsSync(envFile) ? "found" : "missing"})` : `ENVPKT_AGE_KEY_FILE (unset)`;
|
|
1993
|
+
const inlineLine = resolveInlineKey().isEmpty ? `ENVPKT_AGE_KEY (unset)` : `ENVPKT_AGE_KEY (set, inline)`;
|
|
1994
|
+
const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
|
|
1995
|
+
return [
|
|
1996
|
+
keyFileLine,
|
|
1997
|
+
envFileLine,
|
|
1998
|
+
inlineLine,
|
|
1999
|
+
`${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
|
|
2000
|
+
];
|
|
2001
|
+
};
|
|
1970
2002
|
const resolveIdentityKey = (config, configDir) => {
|
|
1971
2003
|
return resolveIdentityFilePath(config, configDir, false).fold(() => Right(Option(void 0)), (path) => unwrapAgentKey(path).fold((err) => Left(err), (key) => Right(Option(key))));
|
|
1972
2004
|
};
|
|
@@ -2036,6 +2068,12 @@ const bootSafe = (options) => {
|
|
|
2036
2068
|
message: "unexpected"
|
|
2037
2069
|
}));
|
|
2038
2070
|
const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
|
|
2071
|
+
const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
|
|
2072
|
+
if (hasSealedValues && sealIdentity.isEmpty) return Left({
|
|
2073
|
+
_tag: "SealKeyUnavailable",
|
|
2074
|
+
sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
|
|
2075
|
+
searched: describeSealKeySearch(config, configDir)
|
|
2076
|
+
});
|
|
2039
2077
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
2040
2078
|
const secrets = {};
|
|
2041
2079
|
const injected = [];
|
|
@@ -2049,9 +2087,8 @@ const bootSafe = (options) => {
|
|
|
2049
2087
|
process.env[envEnv(key)] = value;
|
|
2050
2088
|
});
|
|
2051
2089
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
2052
|
-
if (hasSealedValues)
|
|
2090
|
+
if (hasSealedValues) sealIdentity.fold(() => {
|
|
2053
2091
|
log.warn("phase.sealed.no_identity_file", { sealed_keys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value).length });
|
|
2054
|
-
warnings.push("Sealed values found but no identity file available for decryption");
|
|
2055
2092
|
}, ({ path: idPath, dispose }) => {
|
|
2056
2093
|
try {
|
|
2057
2094
|
unsealSecrets(nonAliasSecretEntries, idPath).fold((err) => {
|
|
@@ -2185,6 +2222,19 @@ const formatBootError = (error) => {
|
|
|
2185
2222
|
case "AgeNotFound": return `age not found: ${error.message}`;
|
|
2186
2223
|
case "DecryptFailed": return `Decrypt failed: ${error.message}`;
|
|
2187
2224
|
case "IdentityNotFound": return `Identity file not found: ${error.path}`;
|
|
2225
|
+
case "SealKeyUnavailable": {
|
|
2226
|
+
const keys = error.sealedKeys.length;
|
|
2227
|
+
const searched = error.searched.map((line) => ` • ${line}`).join("\n");
|
|
2228
|
+
return [
|
|
2229
|
+
`${keys} sealed secret(s) can't be decrypted — no age key found.`,
|
|
2230
|
+
`Searched (in order):`,
|
|
2231
|
+
searched,
|
|
2232
|
+
`Fix one:`,
|
|
2233
|
+
` • Restore your key to ~/.envpkt/age-key.txt (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)`,
|
|
2234
|
+
` • Re-provision from source: envpkt seal --edit <KEY>`,
|
|
2235
|
+
`Refusing to inject empty values for sealed secrets.`
|
|
2236
|
+
].join("\n");
|
|
2237
|
+
}
|
|
2188
2238
|
case "AliasInvalidSyntax":
|
|
2189
2239
|
case "AliasTargetMissing":
|
|
2190
2240
|
case "AliasSelfReference":
|
|
@@ -2232,6 +2282,30 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
2232
2282
|
return result;
|
|
2233
2283
|
};
|
|
2234
2284
|
//#endregion
|
|
2285
|
+
//#region src/core/dotenv.ts
|
|
2286
|
+
const BARE_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
2287
|
+
/**
|
|
2288
|
+
* Quote a single value for dotenv output. Returns the value bare when safe,
|
|
2289
|
+
* otherwise double-quoted with POSIX-shell-quote escaping (`\`, `"`, `$`) and
|
|
2290
|
+
* whitespace collapsed to single-line escapes (`\n`, `\r`, `\t`) for portability.
|
|
2291
|
+
*/
|
|
2292
|
+
const quoteDotenvValue = (value) => {
|
|
2293
|
+
if (value === "") return "";
|
|
2294
|
+
if (BARE_SAFE.test(value)) return value;
|
|
2295
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
2296
|
+
};
|
|
2297
|
+
const formatEntry = (entry, includeSecrets) => {
|
|
2298
|
+
if (entry.secret && !includeSecrets) return `# (secret value omitted — re-run without --no-secrets to include)\n${entry.name}=`;
|
|
2299
|
+
return `${entry.name}=${quoteDotenvValue(entry.value)}`;
|
|
2300
|
+
};
|
|
2301
|
+
/** Serialize entries to dotenv text (no trailing newline). */
|
|
2302
|
+
const formatDotenv = (entries, options) => {
|
|
2303
|
+
const includeSecrets = options?.includeSecrets ?? true;
|
|
2304
|
+
const body = entries.map((e) => formatEntry(e, includeSecrets)).join("\n");
|
|
2305
|
+
const header = options?.header;
|
|
2306
|
+
return header ? `${header}\n\n${body}` : body;
|
|
2307
|
+
};
|
|
2308
|
+
//#endregion
|
|
2235
2309
|
//#region src/core/toml-edit.ts
|
|
2236
2310
|
const SECTION_RE = /^\[.+\]\s*$/;
|
|
2237
2311
|
const MULTILINE_OPEN = "\"\"\"";
|
|
@@ -2730,4 +2804,4 @@ const startServer = async () => {
|
|
|
2730
2804
|
await server.connect(transport);
|
|
2731
2805
|
};
|
|
2732
2806
|
//#endregion
|
|
2733
|
-
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
|
|
2807
|
+
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatDotenv, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, quoteDotenvValue, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
|
package/package.json
CHANGED
|
@@ -17,6 +17,19 @@
|
|
|
17
17
|
"description": "Path to shared secret catalog (relative to this config file)",
|
|
18
18
|
"type": "string"
|
|
19
19
|
},
|
|
20
|
+
"scope": {
|
|
21
|
+
"description": "Whether `env export` emits this package's secrets for ambient/shell loading (`shell`) or withholds them so they are only available via `envpkt exec` (`exec`). Default `exec`. Never affects `envpkt exec`, which always injects everything.",
|
|
22
|
+
"anyOf": [
|
|
23
|
+
{
|
|
24
|
+
"const": "shell",
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"const": "exec",
|
|
29
|
+
"type": "string"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
20
33
|
"namespace": {
|
|
21
34
|
"description": "Optional namespace/package prefix for injected environment variable names",
|
|
22
35
|
"type": "object",
|