envpkt 0.13.6 → 0.14.0

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 CHANGED
@@ -141,6 +141,10 @@ See [`examples/`](./examples/) for more configurations.
141
141
 
142
142
  Sealed packets embed age-encrypted secret values directly in `envpkt.toml`. This makes your config fully self-contained — no external secrets backend needed at runtime.
143
143
 
144
+ > **Requires the [`age`](https://github.com/FiloSottile/age) CLI.** envpkt shells out to `age` to seal and unseal `encrypted_value` — it's a runtime dependency, not bundled, and there is **no built-in / JS fallback**. Every path that decrypts a sealed value (`boot()`, `exec`, `env export`, `env github`, the GitHub Action) needs `age` on `PATH` plus the private key.
145
+ >
146
+ > **Gotcha — partial resolution hides a missing `age`.** Plaintext `[env.*]` defaults and fnox-backed values resolve _without_ `age`, so a config can look like it's working while every sealed secret silently fails to decrypt. If sealed values aren't showing up, confirm `age` is installed — `envpkt doctor` reports its presence.
147
+
144
148
  ### Setup
145
149
 
146
150
  ```bash
@@ -214,7 +218,7 @@ A composite action resolves the credentials in `envpkt.toml` into the CI job —
214
218
 
215
219
  **Inputs:** `config`, `version` (npm version to run, default `latest`), `strict`, `profile`.
216
220
 
217
- > 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. Reference `@v0` for the moving major tag (re-pointed to each `0.x` release), or pin an exact release like `@v0.13.4` for immutability. `@v1` ships when envpkt reaches 1.0. Node is assumed present; add `actions/setup-node` first to pin a version.
221
+ > **The [`age`](https://github.com/FiloSottile/age) CLI is required on the runner to unseal `encrypted_value` packets** (install it first, as above) — envpkt shells out to `age`, with no bundled/JS fallback. Without it, sealed secrets won't decrypt or be injected, even though plaintext `[env.*]` defaults and fnox-backed values still resolve which can mask the missing dependency until a sealed value turns up empty downstream. Reference `@v0` for the moving major tag (re-pointed to each `0.x` release), or pin an exact release like `@v0.13.6` for immutability. `@v1` ships when envpkt reaches 1.0. Node is assumed present; add `actions/setup-node` first to pin a version.
218
222
 
219
223
  ### Anti-rot CI gate
220
224
 
package/dist/cli.js CHANGED
@@ -70,7 +70,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
70
70
  const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
71
71
  const isStale = daysSinceRotation.fold(() => false, (d) => d > staleWarningDays);
72
72
  const hasSealed = !!meta.encrypted_value;
73
- const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
73
+ const isExternal = meta.external === true;
74
+ const isMissing = !isExternal && fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
74
75
  const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
75
76
  if (isExpired) issues.push("Secret has expired");
76
77
  if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
@@ -87,7 +88,7 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
87
88
  return {
88
89
  key,
89
90
  service,
90
- status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
91
+ status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").elseWhen(isExternal, "external").else("healthy"),
91
92
  days_remaining: daysRemaining,
92
93
  rotation_url: rotationUrl,
93
94
  purpose,
@@ -126,7 +127,6 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
126
127
  const secretEntries = config.secret ?? {};
127
128
  const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
128
129
  const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
129
- const nonAliasMetaKeys = Set$1(nonAliasEntries.map(([k]) => k));
130
130
  const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
131
131
  const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
132
132
  const parseTargetKey = (from_key) => {
@@ -152,13 +152,14 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
152
152
  }), (health) => classifyAlias(key, meta, health, targetRef));
153
153
  });
154
154
  const secrets = List([...nonAliasHealth, ...aliasHealth]);
155
- const orphaned = keys.size > 0 ? nonAliasMetaKeys.toArray().filter((k) => !keys.has(k)).length : 0;
155
+ const orphaned = keys.size > 0 ? nonAliasEntries.filter(([k, meta]) => !keys.has(k) && meta.external !== true).length : 0;
156
156
  const total = secrets.size;
157
157
  const expired = secrets.count((s) => s.status === "expired");
158
158
  const missing = secrets.count((s) => s.status === "missing");
159
159
  const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
160
160
  const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
161
161
  const stale = secrets.count((s) => s.status === "stale");
162
+ const external = secrets.count((s) => s.status === "external");
162
163
  const healthy = secrets.count((s) => s.status === "healthy");
163
164
  const aliases = aliasHealth.length;
164
165
  return {
@@ -171,6 +172,7 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
171
172
  stale,
172
173
  missing,
173
174
  missing_metadata,
175
+ external,
174
176
  orphaned,
175
177
  aliases,
176
178
  identity: config.identity
@@ -182,7 +184,8 @@ const computeEnvAudit = (config, env = process.env) => {
182
184
  const envWire = (key) => namer(key, envEntries[key]?.namespace);
183
185
  const resolveEffectiveDefault = (entry) => {
184
186
  return Do(function* () {
185
- return yield* $(Option((yield* $(Option(envEntries[yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]))]))).value));
187
+ const targetKey = yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]));
188
+ return yield* $(Option((yield* $(Option(envEntries[targetKey]))).value));
186
189
  }).orElse(entry.value ?? "");
187
190
  };
188
191
  const entries = Object.entries(envEntries).map(([key, entry]) => {
@@ -266,6 +269,7 @@ const SecretMetaSchema = Type.Object({
266
269
  encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
267
270
  from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
268
271
  required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
272
+ external: Type.Optional(Type.Boolean({ description: "Value is managed outside envpkt (e.g. a Cloudflare/Vault secret). The entry registers the name for inventory and dev/prod parity only: seal never touches it, and audit reports it as 'external' rather than 'missing'." })),
269
273
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
270
274
  namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
271
275
  }, { description: "Metadata about a single secret" });
@@ -385,7 +389,8 @@ const walkUpForConfig = (dir) => {
385
389
  };
386
390
  /** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
387
391
  const discoverConfig = (cwd) => {
388
- const walked = walkUpForConfig(cwd ?? process.cwd()).orUndefined();
392
+ const dir = cwd ?? process.cwd();
393
+ const walked = walkUpForConfig(dir).orUndefined();
389
394
  if (walked) return Option({
390
395
  path: walked,
391
396
  source: "cwd"
@@ -646,6 +651,7 @@ const secretStatusIcon = (status) => {
646
651
  case "stale": return `${YELLOW}○${RESET}`;
647
652
  case "missing": return `${RED}?${RESET}`;
648
653
  case "missing_metadata": return `${YELLOW}!${RESET}`;
654
+ case "external": return `${CYAN}↗${RESET}`;
649
655
  default: return " ";
650
656
  }
651
657
  };
@@ -667,9 +673,10 @@ const formatAudit = (audit) => {
667
673
  audit.stale > 0 ? ` ${YELLOW}${audit.stale}${RESET} stale` : null,
668
674
  audit.missing > 0 ? ` ${RED}${audit.missing}${RESET} missing` : null,
669
675
  audit.missing_metadata > 0 ? ` ${YELLOW}${audit.missing_metadata}${RESET} missing metadata` : null,
676
+ audit.external > 0 ? ` ${CYAN}${audit.external}${RESET} external` : null,
670
677
  audit.orphaned > 0 ? ` ${YELLOW}${audit.orphaned}${RESET} orphaned` : null
671
678
  ].filter(Boolean).join("\n"),
672
- audit.secrets.filter((s) => s.status !== "healthy").map(formatSecretRow).toArray().join("\n")
679
+ audit.secrets.filter((s) => s.status !== "healthy" && s.status !== "external").map(formatSecretRow).toArray().join("\n")
673
680
  ].filter((s) => s.length > 0).join("\n\n");
674
681
  };
675
682
  const formatAuditJson = (audit) => JSON.stringify({
@@ -757,8 +764,9 @@ const confidenceIcon = (confidence) => {
757
764
  }
758
765
  };
759
766
  const formatScanTable = (scan) => {
767
+ const total = scan.discovered.size;
760
768
  return [
761
- `${BOLD}Scan Results${RESET} — ${scan.discovered.size} credential(s) found in ${scan.total_scanned} env vars`,
769
+ `${BOLD}Scan Results${RESET} — ${total} credential(s) found in ${scan.total_scanned} env vars`,
762
770
  [
763
771
  scan.high_confidence > 0 ? ` ${GREEN}${scan.high_confidence}${RESET} high confidence` : null,
764
772
  scan.medium_confidence > 0 ? ` ${YELLOW}${scan.medium_confidence}${RESET} medium confidence` : null,
@@ -955,7 +963,8 @@ const fnoxExport = (profile, agentKey) => {
955
963
  const eq = line.indexOf("=");
956
964
  if (eq > 0) {
957
965
  const key = line.slice(0, eq).trim();
958
- entries[key] = line.slice(eq + 1).trim();
966
+ const value = line.slice(eq + 1).trim();
967
+ entries[key] = value;
959
968
  }
960
969
  });
961
970
  return Right(entries);
@@ -1585,7 +1594,8 @@ const bootSafe = (options) => {
1585
1594
  }
1586
1595
  const targetEntry = envEntries[entry.targetKey];
1587
1596
  if (targetEntry?.value === void 0) return;
1588
- envDefaults[aliasKey] = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
1597
+ const resolvedTarget = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
1598
+ envDefaults[aliasKey] = resolvedTarget;
1589
1599
  });
1590
1600
  if (inject) {
1591
1601
  Object.entries(envDefaults).forEach(([key, value]) => {
@@ -4419,7 +4429,8 @@ const collectEditedValues = async (editKeys, entries, prompt, onSkip) => {
4419
4429
  const values = {};
4420
4430
  for (const key of editKeys) {
4421
4431
  if (entries[key]?.encrypted_value) {
4422
- if (!isAffirmative(await prompt(`Replace the sealed value for ${key}? The previous value can't be recovered without the original key. [y/N] `))) {
4432
+ const confirm = await prompt(`Replace the sealed value for ${key}? The previous value can't be recovered without the original key. [y/N] `);
4433
+ if (!isAffirmative(confirm)) {
4423
4434
  onSkip?.(key, "declined");
4424
4435
  continue;
4425
4436
  }
@@ -4435,7 +4446,8 @@ const collectEditedValues = async (editKeys, entries, prompt, onSkip) => {
4435
4446
  };
4436
4447
  /** Write sealed values back into the TOML file, preserving structure. */
4437
4448
  const writeSealedToml = (configPath, sealedMeta) => {
4438
- const finalContent = applySealedToml(readFileSync(configPath, "utf-8"), sealedMeta);
4449
+ const raw = readFileSync(configPath, "utf-8");
4450
+ const finalContent = applySealedToml(raw, sealedMeta);
4439
4451
  validateOrExit(finalContent);
4440
4452
  writeFileSync(configPath, finalContent);
4441
4453
  };
@@ -4495,6 +4507,12 @@ const runSeal = async (options) => {
4495
4507
  console.error(`${DIM}Aliases reference another secret's value via from_key — seal the target instead.${RESET}`);
4496
4508
  process.exit(2);
4497
4509
  }
4510
+ const externalKeys = editKeys.filter((k) => allSecretEntries[k].external === true);
4511
+ if (externalKeys.length > 0) {
4512
+ console.error(`${RED}Error:${RESET} Cannot seal external entries: ${externalKeys.join(", ")}`);
4513
+ console.error(`${DIM}external = true means the value is managed outside envpkt — set it in that store instead.${RESET}`);
4514
+ process.exit(2);
4515
+ }
4498
4516
  if (!process.stdin.isTTY) {
4499
4517
  console.error(`${RED}Error:${RESET} --edit requires an interactive terminal`);
4500
4518
  process.exit(2);
@@ -4527,14 +4545,16 @@ const runSeal = async (options) => {
4527
4545
  return;
4528
4546
  }
4529
4547
  const allSecretEntries = config.secret ?? {};
4530
- const allKeys = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0);
4548
+ const allKeys = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0 && allSecretEntries[k].external !== true);
4531
4549
  const skippedAliases = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key !== void 0);
4550
+ const skippedExternal = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0 && allSecretEntries[k].external === true);
4532
4551
  const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
4533
4552
  const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
4534
4553
  if (skippedAliases.length > 0) {
4535
4554
  const noun = skippedAliases.length === 1 ? "alias" : "aliases";
4536
4555
  console.error(`${DIM}Skipping ${skippedAliases.length} ${noun}: ${skippedAliases.join(", ")}${RESET}`);
4537
4556
  }
4557
+ if (skippedExternal.length > 0) console.error(`${DIM}Skipping ${skippedExternal.length} external (value managed outside envpkt): ${skippedExternal.join(", ")}${RESET}`);
4538
4558
  if (!options.reseal && alreadySealed.length > 0) {
4539
4559
  if (unsealed.length === 0) {
4540
4560
  console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
@@ -4937,11 +4957,13 @@ const runSecretRotate = async (name, options) => {
4937
4957
  const addSecretFlags = (cmd) => cmd.option("--service <service>", "Service this secret authenticates to").option("--purpose <purpose>", "Why this secret exists").option("--comment <comment>", "Free-form annotation").option("--expires <date>", "Expiration date (YYYY-MM-DD)").option("--capabilities <caps>", "Comma-separated capabilities (e.g. read,write)").option("--rotates <schedule>", "Rotation schedule (e.g. 90d, quarterly)").option("--rate-limit <limit>", "Rate limit info (e.g. 1000/min)").option("--model-hint <hint>", "Suggested model or tier").option("--source <source>", "Where the value originates (e.g. vault, ci)").option("--rotation-url <url>", "URL for secret rotation procedure").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)");
4938
4958
  const registerSecretCommands = (program) => {
4939
4959
  const secret = program.command("secret").description("Manage secret entries in envpkt.toml");
4940
- 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) => {
4960
+ const addCmd = 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");
4961
+ addSecretFlags(addCmd).action((name, options) => {
4941
4962
  runSecretAdd(name, options);
4942
4963
  });
4943
4964
  const collect = (value, previous) => [...previous, value];
4944
- 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) => {
4965
+ const editCmd = 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");
4966
+ addSecretFlags(editCmd).action((name, options) => {
4945
4967
  runSecretEdit(name, options);
4946
4968
  });
4947
4969
  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) => {
@@ -5223,35 +5245,39 @@ const buildReport = (path, raw) => {
5223
5245
  skipped(CHECK_NAMES.sealed)
5224
5246
  ]
5225
5247
  }), (config) => {
5248
+ const schemaOk = {
5249
+ name: CHECK_NAMES.schema,
5250
+ status: "ok"
5251
+ };
5252
+ const catalogCheck = config.catalog ? resolveConfig(config, dirname(path)).fold((err) => ({
5253
+ name: CHECK_NAMES.catalog,
5254
+ status: "failed",
5255
+ error: formatError(err)
5256
+ }), () => ({
5257
+ name: CHECK_NAMES.catalog,
5258
+ status: "ok",
5259
+ detail: config.catalog
5260
+ })) : {
5261
+ name: CHECK_NAMES.catalog,
5262
+ status: "na",
5263
+ detail: "no catalog declared"
5264
+ };
5265
+ const aliasCheck = validateAliases(config).fold((err) => ({
5266
+ name: CHECK_NAMES.aliases,
5267
+ status: "failed",
5268
+ error: formatValidationError(err)
5269
+ }), (table) => ({
5270
+ name: CHECK_NAMES.aliases,
5271
+ status: "ok",
5272
+ detail: `${table.entries.size} alias(es)`
5273
+ }));
5274
+ const sealedCheck = checkSealedBlocks(config);
5226
5275
  const checks = [
5227
5276
  tomlOk,
5228
- {
5229
- name: CHECK_NAMES.schema,
5230
- status: "ok"
5231
- },
5232
- config.catalog ? resolveConfig(config, dirname(path)).fold((err) => ({
5233
- name: CHECK_NAMES.catalog,
5234
- status: "failed",
5235
- error: formatError(err)
5236
- }), () => ({
5237
- name: CHECK_NAMES.catalog,
5238
- status: "ok",
5239
- detail: config.catalog
5240
- })) : {
5241
- name: CHECK_NAMES.catalog,
5242
- status: "na",
5243
- detail: "no catalog declared"
5244
- },
5245
- validateAliases(config).fold((err) => ({
5246
- name: CHECK_NAMES.aliases,
5247
- status: "failed",
5248
- error: formatValidationError(err)
5249
- }), (table) => ({
5250
- name: CHECK_NAMES.aliases,
5251
- status: "ok",
5252
- detail: `${table.entries.size} alias(es)`
5253
- })),
5254
- checkSealedBlocks(config)
5277
+ schemaOk,
5278
+ catalogCheck,
5279
+ aliasCheck,
5280
+ sealedCheck
5255
5281
  ];
5256
5282
  return {
5257
5283
  ok: checks.every((c) => c.status === "ok" || c.status === "na"),
@@ -5316,7 +5342,7 @@ const runValidate = (options) => {
5316
5342
  }, ({ path, source }) => {
5317
5343
  const sourceMsg = formatConfigSource(path, source);
5318
5344
  Try(() => readFileSync(path, "utf-8")).fold((e) => {
5319
- emit({
5345
+ const report = {
5320
5346
  ok: false,
5321
5347
  configPath: path,
5322
5348
  checks: [{
@@ -5324,7 +5350,8 @@ const runValidate = (options) => {
5324
5350
  status: "failed",
5325
5351
  error: e.message
5326
5352
  }]
5327
- }, sourceMsg, options.json === true);
5353
+ };
5354
+ emit(report, sourceMsg, options.json === true);
5328
5355
  process.exit(2);
5329
5356
  }, (raw) => {
5330
5357
  const report = buildReport(path, raw);
package/dist/index.d.ts CHANGED
@@ -4,7 +4,6 @@ import { DirectLogger, DirectTestLoggerHandle, createDirectConsoleLogger, create
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { CallToolResult, ReadResourceResult, Resource } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { DirectLogger as DirectLogger$1, LogEntry, LogLevel, LogMetadata } from "functype-log";
7
-
8
7
  //#region src/core/schema.d.ts
9
8
  declare const ConsumerType: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"agent">, import("@sinclair/typebox").TLiteral<"service">, import("@sinclair/typebox").TLiteral<"developer">, import("@sinclair/typebox").TLiteral<"ci">]>;
10
9
  type ConsumerType = Static<typeof ConsumerType>;
@@ -48,6 +47,7 @@ declare const SecretMetaSchema: import("@sinclair/typebox").TObject<{
48
47
  encrypted_value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
49
48
  from_key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
50
49
  required: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
50
+ external: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
51
51
  tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
52
52
  namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
53
53
  }>;
@@ -110,6 +110,7 @@ declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
110
110
  encrypted_value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
111
111
  from_key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
112
112
  required: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
113
+ external: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
113
114
  tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
114
115
  namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
115
116
  }>>>;
@@ -139,7 +140,7 @@ type EnvpktConfig = Static<typeof EnvpktConfigSchema>;
139
140
  /** @deprecated Use `Identity` instead */
140
141
  type AgentIdentity = Identity;
141
142
  type HealthStatus = "healthy" | "degraded" | "critical";
142
- type SecretStatus = "healthy" | "expiring_soon" | "expired" | "stale" | "missing" | "missing_metadata";
143
+ type SecretStatus = "healthy" | "expiring_soon" | "expired" | "stale" | "missing" | "missing_metadata" | "external";
143
144
  type SecretHealth = {
144
145
  readonly key: string;
145
146
  readonly service: Option<string>;
@@ -150,7 +151,8 @@ type SecretHealth = {
150
151
  readonly created: Option<string>;
151
152
  readonly expires: Option<string>;
152
153
  readonly last_rotated_at: Option<string>;
153
- readonly issues: List<string>; /** If this entry is an alias (from_key), the reference it points at (e.g. "secret.X") */
154
+ readonly issues: List<string>;
155
+ /** If this entry is an alias (from_key), the reference it points at (e.g. "secret.X") */
154
156
  readonly alias_of: Option<string>;
155
157
  };
156
158
  type AuditResult = {
@@ -163,7 +165,10 @@ type AuditResult = {
163
165
  readonly stale: number;
164
166
  readonly missing: number;
165
167
  readonly missing_metadata: number;
166
- readonly orphaned: number; /** Count of entries that are aliases (from_key). Included in `secrets` but reported separately for visibility. */
168
+ /** Count of entries whose value is managed outside envpkt (external = true). Reported, not a health problem. */
169
+ readonly external: number;
170
+ readonly orphaned: number;
171
+ /** Count of entries that are aliases (from_key). Included in `secrets` but reported separately for visibility. */
167
172
  readonly aliases: number;
168
173
  readonly identity?: Identity;
169
174
  };
@@ -173,7 +178,8 @@ type EnvDriftEntry = {
173
178
  readonly defaultValue: string;
174
179
  readonly currentValue: string | undefined;
175
180
  readonly status: EnvDriftStatus;
176
- readonly purpose: string | undefined; /** If this entry is an alias (from_key), the reference it points at (e.g. "env.X") */
181
+ readonly purpose: string | undefined;
182
+ /** If this entry is an alias (from_key), the reference it points at (e.g. "env.X") */
177
183
  readonly alias_of: Option<string>;
178
184
  };
179
185
  type EnvAuditResult = {
@@ -259,7 +265,8 @@ type CatalogError = {
259
265
  readonly message: string;
260
266
  };
261
267
  type AliasTable = {
262
- /** key → { type: "secret"|"env", targetType, targetKey } for every alias entry */readonly entries: ReadonlyMap<string, {
268
+ /** key → { type: "secret"|"env", targetType, targetKey } for every alias entry */
269
+ readonly entries: ReadonlyMap<string, {
263
270
  readonly kind: "secret" | "env";
264
271
  readonly targetKind: "secret" | "env";
265
272
  readonly targetKey: string;
@@ -571,7 +578,9 @@ type DotenvEntry = {
571
578
  readonly secret: boolean;
572
579
  };
573
580
  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. */
581
+ /** Write secret values into the output. Default true (matches `env export`/`env github`). */
582
+ readonly includeSecrets?: boolean;
583
+ /** A pre-formatted comment block (each line `#`-prefixed) placed at the top. */
575
584
  readonly header?: string;
576
585
  };
577
586
  /**
package/dist/index.js CHANGED
@@ -74,6 +74,7 @@ const SecretMetaSchema = Type.Object({
74
74
  encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
75
75
  from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
76
76
  required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
77
+ external: Type.Optional(Type.Boolean({ description: "Value is managed outside envpkt (e.g. a Cloudflare/Vault secret). The entry registers the name for inventory and dev/prod parity only: seal never touches it, and audit reports it as 'external' rather than 'missing'." })),
77
78
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
78
79
  namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
79
80
  }, { description: "Metadata about a single secret" });
@@ -198,7 +199,8 @@ const walkUpForConfig = (dir) => {
198
199
  };
199
200
  /** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
200
201
  const discoverConfig = (cwd) => {
201
- const walked = walkUpForConfig(cwd ?? process.cwd()).orUndefined();
202
+ const dir = cwd ?? process.cwd();
203
+ const walked = walkUpForConfig(dir).orUndefined();
202
204
  if (walked) return Option({
203
205
  path: walked,
204
206
  source: "cwd"
@@ -635,7 +637,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
635
637
  const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
636
638
  const isStale = daysSinceRotation.fold(() => false, (d) => d > staleWarningDays);
637
639
  const hasSealed = !!meta.encrypted_value;
638
- const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
640
+ const isExternal = meta.external === true;
641
+ const isMissing = !isExternal && fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
639
642
  const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
640
643
  if (isExpired) issues.push("Secret has expired");
641
644
  if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
@@ -652,7 +655,7 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
652
655
  return {
653
656
  key,
654
657
  service,
655
- status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
658
+ status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").elseWhen(isExternal, "external").else("healthy"),
656
659
  days_remaining: daysRemaining,
657
660
  rotation_url: rotationUrl,
658
661
  purpose,
@@ -691,7 +694,6 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
691
694
  const secretEntries = config.secret ?? {};
692
695
  const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
693
696
  const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
694
- const nonAliasMetaKeys = Set$1(nonAliasEntries.map(([k]) => k));
695
697
  const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
696
698
  const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
697
699
  const parseTargetKey = (from_key) => {
@@ -717,13 +719,14 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
717
719
  }), (health) => classifyAlias(key, meta, health, targetRef));
718
720
  });
719
721
  const secrets = List([...nonAliasHealth, ...aliasHealth]);
720
- const orphaned = keys.size > 0 ? nonAliasMetaKeys.toArray().filter((k) => !keys.has(k)).length : 0;
722
+ const orphaned = keys.size > 0 ? nonAliasEntries.filter(([k, meta]) => !keys.has(k) && meta.external !== true).length : 0;
721
723
  const total = secrets.size;
722
724
  const expired = secrets.count((s) => s.status === "expired");
723
725
  const missing = secrets.count((s) => s.status === "missing");
724
726
  const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
725
727
  const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
726
728
  const stale = secrets.count((s) => s.status === "stale");
729
+ const external = secrets.count((s) => s.status === "external");
727
730
  const healthy = secrets.count((s) => s.status === "healthy");
728
731
  const aliases = aliasHealth.length;
729
732
  return {
@@ -736,6 +739,7 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
736
739
  stale,
737
740
  missing,
738
741
  missing_metadata,
742
+ external,
739
743
  orphaned,
740
744
  aliases,
741
745
  identity: config.identity
@@ -747,7 +751,8 @@ const computeEnvAudit = (config, env = process.env) => {
747
751
  const envWire = (key) => namer(key, envEntries[key]?.namespace);
748
752
  const resolveEffectiveDefault = (entry) => {
749
753
  return Do(function* () {
750
- return yield* $(Option((yield* $(Option(envEntries[yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]))]))).value));
754
+ const targetKey = yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]));
755
+ return yield* $(Option((yield* $(Option(envEntries[targetKey]))).value));
751
756
  }).orElse(entry.value ?? "");
752
757
  };
753
758
  const entries = Object.entries(envEntries).map(([key, entry]) => {
@@ -1596,7 +1601,8 @@ const fnoxExport = (profile, agentKey) => {
1596
1601
  const eq = line.indexOf("=");
1597
1602
  if (eq > 0) {
1598
1603
  const key = line.slice(0, eq).trim();
1599
- entries[key] = line.slice(eq + 1).trim();
1604
+ const value = line.slice(eq + 1).trim();
1605
+ entries[key] = value;
1600
1606
  }
1601
1607
  });
1602
1608
  return Right(entries);
@@ -2167,7 +2173,8 @@ const bootSafe = (options) => {
2167
2173
  }
2168
2174
  const targetEntry = envEntries[entry.targetKey];
2169
2175
  if (targetEntry?.value === void 0) return;
2170
- envDefaults[aliasKey] = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
2176
+ const resolvedTarget = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
2177
+ envDefaults[aliasKey] = resolvedTarget;
2171
2178
  });
2172
2179
  if (inject) {
2173
2180
  Object.entries(envDefaults).forEach(([key, value]) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.13.6",
3
+ "version": "0.14.0",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",
@@ -40,18 +40,18 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
- "@sinclair/typebox": "^0.34.49",
43
+ "@sinclair/typebox": "^0.34.52",
44
44
  "commander": "^15.0.0",
45
- "functype": "^1.5.0",
46
- "functype-log": "^1.5.0",
47
- "functype-os": "^1.5.0",
45
+ "functype": "^1.6.3",
46
+ "functype-log": "^1.6.3",
47
+ "functype-os": "^1.6.3",
48
48
  "smol-toml": "^1.7.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^24.13.2",
52
- "ts-builds": "^3.2.1",
53
- "tsdown": "^0.22.3",
54
- "tsx": "^4.22.4"
51
+ "@types/node": "^24.13.3",
52
+ "ts-builds": "^3.2.2",
53
+ "tsdown": "^0.22.9",
54
+ "tsx": "^4.23.1"
55
55
  },
56
56
  "type": "module",
57
57
  "main": "./dist/index.js",
@@ -71,5 +71,5 @@
71
71
  "schemas"
72
72
  ],
73
73
  "prettier": "ts-builds/prettier",
74
- "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
74
+ "packageManager": "pnpm@11.15.0+sha512.266f8957a30d2be6e9468e5e66bcdedd35a794175f71b067ba8504d686cce1d0c0f429b33c323c3c569ad4891e667574a49ff71d1b89a22cc66f13c65818c578"
75
75
  }
@@ -195,6 +195,10 @@
195
195
  "description": "Whether this secret is required for operation",
196
196
  "type": "boolean"
197
197
  },
198
+ "external": {
199
+ "description": "Value is managed outside envpkt (e.g. a Cloudflare/Vault secret). The entry registers the name for inventory and dev/prod parity only: seal never touches it, and audit reports it as 'external' rather than 'missing'.",
200
+ "type": "boolean"
201
+ },
198
202
  "tags": {
199
203
  "description": "Key-value tags for grouping and filtering",
200
204
  "type": "object",