envpkt 0.13.2 → 0.13.3

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
@@ -405,6 +405,25 @@ envpkt inspect --secrets --plaintext # Show secret values in plaintext
405
405
 
406
406
  The `--secrets` flag reads values from environment variables matching each secret key. By default values are masked (`pos•••••yapp`). Add `--plaintext` to display full values.
407
407
 
408
+ ### `envpkt diff`
409
+
410
+ Compare two configs — useful for spotting drift between environments (e.g. `dev.envpkt.toml` vs `prod.envpkt.toml`). Reports keys only in each side and field-level metadata changes for shared keys. Sealed ciphertext is ignored (the same secret re-encrypts differently); a sealed↔unsealed change is reported.
411
+
412
+ ```bash
413
+ envpkt diff dev.envpkt.toml prod.envpkt.toml
414
+ # - dev.envpkt.toml
415
+ # + prod.envpkt.toml
416
+ #
417
+ # [secret]
418
+ # - OLD_KEY
419
+ # + NEW_KEY
420
+ # ~ API_KEY
421
+ # expires: 2026-01-01 → 2027-01-01
422
+
423
+ envpkt diff a.toml b.toml --format json # structured diff
424
+ envpkt diff a.toml b.toml --exit-code # exit non-zero on any difference (CI drift gate)
425
+ ```
426
+
408
427
  ### `envpkt exec`
409
428
 
410
429
  Run a pre-flight audit, inject secrets from fnox into the environment, then execute a command.
package/dist/cli.js CHANGED
@@ -930,6 +930,90 @@ const runConfigPath = (options) => {
930
930
  });
931
931
  };
932
932
  //#endregion
933
+ //#region src/core/diff.ts
934
+ /** Normalize a metadata value to a comparable/displayable string (`undefined` = absent). */
935
+ const serialize = (value) => value === void 0 ? void 0 : typeof value === "string" ? value : JSON.stringify(value);
936
+ /**
937
+ * Field-level diff of two entries. `encrypted_value` is excluded from value comparison — the same
938
+ * secret re-encrypts to different ciphertext, so diffing it is noise — but a change in *sealed
939
+ * status* (present ↔ absent) is reported as a synthetic `sealed` field.
940
+ */
941
+ const metaDiff = (a, b) => {
942
+ const ar = a;
943
+ const br = b;
944
+ const sealedChange = !!ar["encrypted_value"] === !!br["encrypted_value"] ? [] : [{
945
+ field: "sealed",
946
+ a: ar["encrypted_value"] ? "yes" : "no",
947
+ b: br["encrypted_value"] ? "yes" : "no"
948
+ }];
949
+ const fieldChanges = [...Object.keys(ar), ...Object.keys(br)].filter((k, i, arr) => k !== "encrypted_value" && arr.indexOf(k) === i).flatMap((field) => {
950
+ const av = serialize(ar[field]);
951
+ const bv = serialize(br[field]);
952
+ return av === bv ? [] : [{
953
+ field,
954
+ a: av,
955
+ b: bv
956
+ }];
957
+ });
958
+ return [...sealedChange, ...fieldChanges.sort((x, y) => x.field.localeCompare(y.field))];
959
+ };
960
+ const sectionDiff = (a, b) => {
961
+ const aKeys = Object.keys(a);
962
+ const bKeys = Object.keys(b);
963
+ return {
964
+ onlyA: aKeys.filter((k) => !(k in b)).sort(),
965
+ onlyB: bKeys.filter((k) => !(k in a)).sort(),
966
+ changed: aKeys.filter((k) => k in b).sort().flatMap((key) => {
967
+ const changes = metaDiff(a[key], b[key]);
968
+ return changes.length === 0 ? [] : [{
969
+ key,
970
+ changes
971
+ }];
972
+ })
973
+ };
974
+ };
975
+ const isEmpty = (s) => s.onlyA.length === 0 && s.onlyB.length === 0 && s.changed.length === 0;
976
+ /** Compare two configs by their `[secret.*]` and `[env.*]` entries (metadata, not ciphertext). */
977
+ const diffConfigs = (a, b) => {
978
+ const secret = sectionDiff(a.secret ?? {}, b.secret ?? {});
979
+ const env = sectionDiff(a.env ?? {}, b.env ?? {});
980
+ return {
981
+ secret,
982
+ env,
983
+ identical: isEmpty(secret) && isEmpty(env)
984
+ };
985
+ };
986
+ //#endregion
987
+ //#region src/cli/commands/diff.ts
988
+ const formatSection = (name, s) => {
989
+ if (s.onlyA.length === 0 && s.onlyB.length === 0 && s.changed.length === 0) return [];
990
+ return [
991
+ `${BOLD}[${name}]${RESET}`,
992
+ ...s.onlyA.map((k) => ` ${RED}- ${k}${RESET}`),
993
+ ...s.onlyB.map((k) => ` ${GREEN}+ ${k}${RESET}`),
994
+ ...s.changed.flatMap((c) => [` ${YELLOW}~ ${c.key}${RESET}`, ...c.changes.map((ch) => ` ${ch.field}: ${DIM}${ch.a ?? "∅"}${RESET} → ${DIM}${ch.b ?? "∅"}${RESET}`)])
995
+ ];
996
+ };
997
+ const loadOrExit = (path, side) => loadConfig(path).fold((err) => {
998
+ console.error(`${RED}Error${RESET} (${side} = ${path}): ${formatError(err)}`);
999
+ process.exit(2);
1000
+ }, (config) => config);
1001
+ /**
1002
+ * Compare two envpkt.toml files by their `[secret.*]` and `[env.*]` entries. Reports keys only in
1003
+ * each side and field-level metadata changes for shared keys (ciphertext is ignored; sealed-status
1004
+ * changes are reported). With `--exit-code`, exits non-zero when the configs differ.
1005
+ */
1006
+ const runDiff = (pathA, pathB, options) => {
1007
+ const diff = diffConfigs(loadOrExit(pathA, "a"), loadOrExit(pathB, "b"));
1008
+ if (options.format === "json") console.log(JSON.stringify(diff, null, 2));
1009
+ else if (diff.identical) console.log(`${GREEN}✓${RESET} no differences`);
1010
+ else {
1011
+ const body = [...formatSection("secret", diff.secret), ...formatSection("env", diff.env)];
1012
+ console.log(`${DIM}- ${pathA}\n+ ${pathB}${RESET}\n\n${body.join("\n")}`);
1013
+ }
1014
+ if (options.exitCode && !diff.identical) process.exit(1);
1015
+ };
1016
+ //#endregion
933
1017
  //#region src/fnox/cli.ts
934
1018
  /** Export all secrets from fnox as key=value pairs for a given profile */
935
1019
  const fnoxExport = (profile, agentKey) => {
@@ -5146,6 +5230,9 @@ program.command("sort").description("Group [env.*] and [secret.*] sections and a
5146
5230
  program.command("upgrade").description("Upgrade envpkt to the latest version (npm install -g envpkt@latest)").action(() => {
5147
5231
  runUpgrade();
5148
5232
  });
5233
+ program.command("diff").description("Compare two envpkt.toml configs by their secret/env entries (keys + metadata)").argument("<a>", "First config path").argument("<b>", "Second config path").option("--format <format>", "Output format: text | json", "text").option("--exit-code", "Exit non-zero when the configs differ (for CI drift gates)").action((a, b, options) => {
5234
+ runDiff(a, b, options);
5235
+ });
5149
5236
  program.command("doctor").description("Check that age is installed and that the resolved config's sealed secrets can be decrypted").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
5150
5237
  runDoctor(options);
5151
5238
  });
package/dist/index.d.ts CHANGED
@@ -583,6 +583,32 @@ declare const quoteDotenvValue: (value: string) => string;
583
583
  /** Serialize entries to dotenv text (no trailing newline). */
584
584
  declare const formatDotenv: (entries: ReadonlyArray<DotenvEntry>, options?: FormatDotenvOptions) => string;
585
585
  //#endregion
586
+ //#region src/core/diff.d.ts
587
+ /** A single field that differs between two entries. `undefined` means the field is absent on that side. */
588
+ type FieldChange = {
589
+ readonly field: string;
590
+ readonly a: string | undefined;
591
+ readonly b: string | undefined;
592
+ };
593
+ /** An entry present in both configs whose metadata differs. */
594
+ type ChangedEntry = {
595
+ readonly key: string;
596
+ readonly changes: ReadonlyArray<FieldChange>;
597
+ };
598
+ /** Diff of one keyed section (`[secret.*]` or `[env.*]`). Key lists are sorted. */
599
+ type SectionDiff = {
600
+ readonly onlyA: ReadonlyArray<string>;
601
+ readonly onlyB: ReadonlyArray<string>;
602
+ readonly changed: ReadonlyArray<ChangedEntry>;
603
+ };
604
+ type ConfigDiff = {
605
+ readonly secret: SectionDiff;
606
+ readonly env: SectionDiff;
607
+ readonly identical: boolean;
608
+ };
609
+ /** Compare two configs by their `[secret.*]` and `[env.*]` entries (metadata, not ciphertext). */
610
+ declare const diffConfigs: (a: EnvpktConfig, b: EnvpktConfig) => ConfigDiff;
611
+ //#endregion
586
612
  //#region src/core/toml-edit.d.ts
587
613
  /**
588
614
  * Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
@@ -668,4 +694,4 @@ type ToolDef = {
668
694
  declare const toolDefinitions: readonly ToolDef[];
669
695
  declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
670
696
  //#endregion
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 };
697
+ export { type AgentIdentity, AgentIdentitySchema, type AliasError, type AliasTable, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type ChangedEntry, type CheckResult, type ConfidenceLevel, type ConfigDiff, 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 FieldChange, 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 SectionDiff, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, diffConfigs, 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
@@ -2306,6 +2306,60 @@ const formatDotenv = (entries, options) => {
2306
2306
  return header ? `${header}\n\n${body}` : body;
2307
2307
  };
2308
2308
  //#endregion
2309
+ //#region src/core/diff.ts
2310
+ /** Normalize a metadata value to a comparable/displayable string (`undefined` = absent). */
2311
+ const serialize = (value) => value === void 0 ? void 0 : typeof value === "string" ? value : JSON.stringify(value);
2312
+ /**
2313
+ * Field-level diff of two entries. `encrypted_value` is excluded from value comparison — the same
2314
+ * secret re-encrypts to different ciphertext, so diffing it is noise — but a change in *sealed
2315
+ * status* (present ↔ absent) is reported as a synthetic `sealed` field.
2316
+ */
2317
+ const metaDiff = (a, b) => {
2318
+ const ar = a;
2319
+ const br = b;
2320
+ const sealedChange = !!ar["encrypted_value"] === !!br["encrypted_value"] ? [] : [{
2321
+ field: "sealed",
2322
+ a: ar["encrypted_value"] ? "yes" : "no",
2323
+ b: br["encrypted_value"] ? "yes" : "no"
2324
+ }];
2325
+ const fieldChanges = [...Object.keys(ar), ...Object.keys(br)].filter((k, i, arr) => k !== "encrypted_value" && arr.indexOf(k) === i).flatMap((field) => {
2326
+ const av = serialize(ar[field]);
2327
+ const bv = serialize(br[field]);
2328
+ return av === bv ? [] : [{
2329
+ field,
2330
+ a: av,
2331
+ b: bv
2332
+ }];
2333
+ });
2334
+ return [...sealedChange, ...fieldChanges.sort((x, y) => x.field.localeCompare(y.field))];
2335
+ };
2336
+ const sectionDiff = (a, b) => {
2337
+ const aKeys = Object.keys(a);
2338
+ const bKeys = Object.keys(b);
2339
+ return {
2340
+ onlyA: aKeys.filter((k) => !(k in b)).sort(),
2341
+ onlyB: bKeys.filter((k) => !(k in a)).sort(),
2342
+ changed: aKeys.filter((k) => k in b).sort().flatMap((key) => {
2343
+ const changes = metaDiff(a[key], b[key]);
2344
+ return changes.length === 0 ? [] : [{
2345
+ key,
2346
+ changes
2347
+ }];
2348
+ })
2349
+ };
2350
+ };
2351
+ const isEmpty = (s) => s.onlyA.length === 0 && s.onlyB.length === 0 && s.changed.length === 0;
2352
+ /** Compare two configs by their `[secret.*]` and `[env.*]` entries (metadata, not ciphertext). */
2353
+ const diffConfigs = (a, b) => {
2354
+ const secret = sectionDiff(a.secret ?? {}, b.secret ?? {});
2355
+ const env = sectionDiff(a.env ?? {}, b.env ?? {});
2356
+ return {
2357
+ secret,
2358
+ env,
2359
+ identical: isEmpty(secret) && isEmpty(env)
2360
+ };
2361
+ };
2362
+ //#endregion
2309
2363
  //#region src/core/toml-edit.ts
2310
2364
  const SECTION_RE = /^\[.+\]\s*$/;
2311
2365
  const MULTILINE_OPEN = "\"\"\"";
@@ -2804,4 +2858,4 @@ const startServer = async () => {
2804
2858
  await server.connect(transport);
2805
2859
  };
2806
2860
  //#endregion
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 };
2861
+ 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, diffConfigs, 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",