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 +19 -0
- package/dist/cli.js +87 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.js +55 -1
- package/package.json +1 -1
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 };
|