envpkt 0.4.2 → 0.5.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/dist/cli.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
6
  import { Cond, Left, List, Option, Right, Try } from "functype";
7
- import { homedir } from "node:os";
8
7
  import { TypeCompiler } from "@sinclair/typebox/compiler";
8
+ import { Env, Fs, Path } from "functype-os";
9
9
  import { TomlDate, parse, stringify } from "smol-toml";
10
10
  import { FormatRegistry, Type } from "@sinclair/typebox";
11
11
  import { execFileSync } from "node:child_process";
12
+ import { homedir } from "node:os";
12
13
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
15
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -219,11 +220,11 @@ const normalizeDates = (obj) => {
219
220
  if (obj !== null && typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]));
220
221
  return obj;
221
222
  };
222
- /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string */
223
+ /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string (silent — unresolved vars become "") */
223
224
  const expandPath = (p) => {
224
- return (p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
225
+ return Path.expandTilde(p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
225
226
  const name = braced ?? bare ?? "";
226
- return process.env[name] ?? "";
227
+ return Env.getOrDefault(name, "");
227
228
  });
228
229
  };
229
230
  /**
@@ -232,16 +233,16 @@ const expandPath = (p) => {
232
233
  * Non-glob paths return a single-element array if they exist.
233
234
  */
234
235
  const expandGlobPath = (expanded) => {
235
- if (!expanded.includes("*")) return existsSync(expanded) ? [expanded] : [];
236
+ if (!expanded.includes("*")) return Fs.existsSync(expanded) ? [expanded] : [];
236
237
  const segments = expanded.split("/");
237
238
  const globIdx = segments.findIndex((s) => s.includes("*"));
238
239
  if (globIdx < 0) return [];
239
240
  const parentDir = segments.slice(0, globIdx).join("/");
240
241
  const globSegment = segments[globIdx];
241
242
  const suffix = segments.slice(globIdx + 1).join("/");
242
- if (!existsSync(parentDir)) return [];
243
+ if (!Fs.existsSync(parentDir)) return [];
243
244
  const prefix = globSegment.replace(/\*.*$/, "");
244
- return readdirSync(parentDir).filter((entry) => entry.startsWith(prefix)).map((entry) => join(parentDir, entry, suffix)).filter((p) => existsSync(p));
245
+ return Fs.readdirSync(parentDir).fold(() => [], (entries) => entries.filter((entry) => entry.startsWith(prefix)).map((entry) => join(parentDir, entry, suffix)).filter((p) => Fs.existsSync(p)).toArray());
245
246
  };
246
247
  /** Ordered candidate paths for config discovery beyond CWD */
247
248
  const CONFIG_SEARCH_PATHS = [
@@ -267,11 +268,11 @@ const CONFIG_SEARCH_PATHS = [
267
268
  /** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then built-in candidate paths */
268
269
  const discoverConfig = (cwd) => {
269
270
  const cwdCandidate = join(cwd ?? process.cwd(), CONFIG_FILENAME$2);
270
- if (existsSync(cwdCandidate)) return Option({
271
+ if (Fs.existsSync(cwdCandidate)) return Option({
271
272
  path: cwdCandidate,
272
273
  source: "cwd"
273
274
  });
274
- const customPaths = process.env.ENVPKT_SEARCH_PATH?.split(":").filter(Boolean) ?? [];
275
+ const customPaths = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean));
275
276
  for (const template of [...customPaths, ...CONFIG_SEARCH_PATHS]) {
276
277
  const expanded = expandPath(template);
277
278
  if (!expanded || expanded.startsWith("/.envpkt")) continue;
@@ -285,14 +286,14 @@ const discoverConfig = (cwd) => {
285
286
  };
286
287
  /** Read a config file, returning Either<ConfigError, string> */
287
288
  const readConfigFile = (path) => {
288
- if (!existsSync(path)) return Left({
289
+ if (!Fs.existsSync(path)) return Left({
289
290
  _tag: "FileNotFound",
290
291
  path
291
292
  });
292
- return Try(() => readFileSync(path, "utf-8")).fold((err) => Left({
293
+ return Fs.readFileSync(path, "utf-8").mapLeft((err) => ({
293
294
  _tag: "ReadError",
294
- message: String(err)
295
- }), (content) => Right(content));
295
+ message: err.message
296
+ }));
296
297
  };
297
298
  /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit secret) */
298
299
  const applyDefaults = (data) => {
@@ -332,19 +333,19 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
332
333
  path: resolved,
333
334
  source: "flag"
334
335
  };
335
- return existsSync(resolved) ? Right(result) : Left({
336
+ return Fs.existsSync(resolved) ? Right(result) : Left({
336
337
  _tag: "FileNotFound",
337
338
  path: resolved
338
339
  });
339
340
  }
340
- const envPath = envVar ?? process.env[ENV_VAR_CONFIG];
341
+ const envPath = envVar ?? Env.get(ENV_VAR_CONFIG).fold(() => void 0, (v) => v);
341
342
  if (envPath) {
342
343
  const resolved = resolve(envPath);
343
344
  const result = {
344
345
  path: resolved,
345
346
  source: "env"
346
347
  };
347
- return existsSync(resolved) ? Right(result) : Left({
348
+ return Fs.existsSync(resolved) ? Right(result) : Left({
348
349
  _tag: "FileNotFound",
349
350
  path: resolved
350
351
  });
@@ -542,6 +543,10 @@ const formatError = (error) => {
542
543
  case "CatalogLoadError": return `${RED}Error:${RESET} Catalog load error: ${error.message}`;
543
544
  case "SecretNotInCatalog": return `${RED}Error:${RESET} Secret "${error.key}" not found in catalog: ${error.path}`;
544
545
  case "MissingSecretsList": return `${RED}Error:${RESET} ${error.message}`;
546
+ case "KeygenFailed": return `${RED}Error:${RESET} Keygen failed: ${error.message}`;
547
+ case "KeyExists": return `${YELLOW}Warning:${RESET} Identity file already exists: ${error.path}\n${DIM}Use --force to overwrite.${RESET}`;
548
+ case "WriteError": return `${RED}Error:${RESET} Write failed: ${error.message}`;
549
+ case "ConfigUpdateError": return `${RED}Error:${RESET} Config update failed: ${error.message}`;
545
550
  default: return `${RED}Error:${RESET} ${error.message ?? tag}`;
546
551
  }
547
552
  };
@@ -1812,6 +1817,12 @@ created = "${todayIso$1()}"
1812
1817
 
1813
1818
  //#endregion
1814
1819
  //#region src/cli/commands/env.ts
1820
+ const printPostWriteGuidance = () => {
1821
+ console.log(`\n${DIM}Note: Secret values are NOT stored — only metadata.${RESET}`);
1822
+ console.log(`${BOLD}Next steps:${RESET}`);
1823
+ console.log(` ${DIM}1.${RESET} envpkt keygen ${DIM}# generate age key (if no recipient configured)${RESET}`);
1824
+ console.log(` ${DIM}2.${RESET} envpkt seal ${DIM}# encrypt secret values into envpkt.toml${RESET}`);
1825
+ };
1815
1826
  const runEnvScan = (options) => {
1816
1827
  const scan = envScan(process.env, { includeUnknown: options.includeUnknown });
1817
1828
  if (scan.discovered.size === 0) {
@@ -1841,6 +1852,7 @@ const runEnvScan = (options) => {
1841
1852
  process.exit(1);
1842
1853
  }, () => {
1843
1854
  console.log(`\n${GREEN}✓${RESET} Appended ${BOLD}${newEntries.length}${RESET} new entry/entries to ${CYAN}${configPath}${RESET}`);
1855
+ printPostWriteGuidance();
1844
1856
  });
1845
1857
  } else {
1846
1858
  const header = `#:schema https://raw.githubusercontent.com/jordanburke/envpkt/main/schemas/envpkt.schema.json\n\nversion = 1\n\n[lifecycle]\nstale_warning_days = 90\n\n`;
@@ -1849,6 +1861,7 @@ const runEnvScan = (options) => {
1849
1861
  process.exit(1);
1850
1862
  }, () => {
1851
1863
  console.log(`\n${GREEN}✓${RESET} Created ${CYAN}${configPath}${RESET} with ${BOLD}${scan.discovered.size}${RESET} credential(s)`);
1864
+ printPostWriteGuidance();
1852
1865
  });
1853
1866
  }
1854
1867
  }
@@ -2271,6 +2284,143 @@ const runInspect = (options) => {
2271
2284
  });
2272
2285
  };
2273
2286
 
2287
+ //#endregion
2288
+ //#region src/core/keygen.ts
2289
+ /** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
2290
+ const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
2291
+ /** Generate an age keypair and write to disk */
2292
+ const generateKeypair = (options) => {
2293
+ if (!ageAvailable()) return Left({
2294
+ _tag: "AgeNotFound",
2295
+ message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
2296
+ });
2297
+ const outputPath = options?.outputPath ?? resolveKeyPath();
2298
+ if (existsSync(outputPath) && !options?.force) return Left({
2299
+ _tag: "KeyExists",
2300
+ path: outputPath
2301
+ });
2302
+ return Try(() => execFileSync("age-keygen", [], {
2303
+ stdio: [
2304
+ "pipe",
2305
+ "pipe",
2306
+ "pipe"
2307
+ ],
2308
+ encoding: "utf-8"
2309
+ })).fold((err) => Left({
2310
+ _tag: "KeygenFailed",
2311
+ message: `age-keygen failed: ${err}`
2312
+ }), (output) => {
2313
+ const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
2314
+ if (!recipientLine) return Left({
2315
+ _tag: "KeygenFailed",
2316
+ message: "Could not parse public key from age-keygen output"
2317
+ });
2318
+ const recipient = recipientLine.replace("# public key: ", "").trim();
2319
+ const dir = dirname(outputPath);
2320
+ const mkdirFailed = Try(() => {
2321
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
2322
+ }).fold((err) => ({
2323
+ _tag: "WriteError",
2324
+ message: `Failed to create directory ${dir}: ${err}`
2325
+ }), () => void 0);
2326
+ if (mkdirFailed) return Left(mkdirFailed);
2327
+ return Try(() => {
2328
+ writeFileSync(outputPath, output, { mode: 384 });
2329
+ chmodSync(outputPath, 384);
2330
+ }).fold((err) => Left({
2331
+ _tag: "WriteError",
2332
+ message: `Failed to write identity file: ${err}`
2333
+ }), () => Right({
2334
+ recipient,
2335
+ identityPath: outputPath,
2336
+ configUpdated: false
2337
+ }));
2338
+ });
2339
+ };
2340
+ /** Update agent.recipient in an envpkt.toml file, preserving structure */
2341
+ const updateConfigRecipient = (configPath, recipient) => {
2342
+ return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
2343
+ _tag: "ConfigUpdateError",
2344
+ message: `Failed to read config: ${err}`
2345
+ }), (raw) => {
2346
+ const lines = raw.split("\n");
2347
+ const output = [];
2348
+ let inAgentSection = false;
2349
+ let recipientUpdated = false;
2350
+ let hasAgentSection = false;
2351
+ for (const line of lines) {
2352
+ if (/^\[agent\]\s*$/.test(line)) {
2353
+ inAgentSection = true;
2354
+ hasAgentSection = true;
2355
+ output.push(line);
2356
+ continue;
2357
+ }
2358
+ if (/^\[/.test(line) && !/^\[agent\]\s*$/.test(line)) {
2359
+ if (inAgentSection && !recipientUpdated) {
2360
+ output.push(`recipient = "${recipient}"`);
2361
+ recipientUpdated = true;
2362
+ }
2363
+ inAgentSection = false;
2364
+ output.push(line);
2365
+ continue;
2366
+ }
2367
+ if (inAgentSection && /^recipient\s*=/.test(line)) {
2368
+ output.push(`recipient = "${recipient}"`);
2369
+ recipientUpdated = true;
2370
+ continue;
2371
+ }
2372
+ output.push(line);
2373
+ }
2374
+ if (inAgentSection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
2375
+ if (!hasAgentSection) {
2376
+ output.push("");
2377
+ output.push("[agent]");
2378
+ output.push(`recipient = "${recipient}"`);
2379
+ }
2380
+ return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
2381
+ _tag: "ConfigUpdateError",
2382
+ message: `Failed to write config: ${err}`
2383
+ }), () => Right(true));
2384
+ });
2385
+ };
2386
+
2387
+ //#endregion
2388
+ //#region src/cli/commands/keygen.ts
2389
+ const runKeygen = (options) => {
2390
+ const outputPath = options.output ?? resolveKeyPath();
2391
+ generateKeypair({
2392
+ force: options.force,
2393
+ outputPath
2394
+ }).fold((err) => {
2395
+ if (err._tag === "KeyExists") {
2396
+ console.error(`${YELLOW}Warning:${RESET} Identity file already exists: ${CYAN}${err.path}${RESET}`);
2397
+ console.error(`${DIM}Use --force to overwrite.${RESET}`);
2398
+ process.exit(1);
2399
+ }
2400
+ console.error(formatError(err));
2401
+ process.exit(2);
2402
+ }, ({ recipient, identityPath }) => {
2403
+ console.log(`${GREEN}Generated${RESET} age identity: ${CYAN}${identityPath}${RESET}`);
2404
+ console.log(`${BOLD}Recipient:${RESET} ${recipient}`);
2405
+ console.log("");
2406
+ const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
2407
+ if (existsSync(configPath)) updateConfigRecipient(configPath, recipient).fold((err) => {
2408
+ console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
2409
+ console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
2410
+ console.log(` [agent]`);
2411
+ console.log(` recipient = "${recipient}"`);
2412
+ }, () => {
2413
+ console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with agent.recipient`);
2414
+ });
2415
+ else {
2416
+ console.log(`${BOLD}Next steps:${RESET}`);
2417
+ console.log(` ${DIM}1.${RESET} envpkt init ${DIM}# create envpkt.toml${RESET}`);
2418
+ console.log(` ${DIM}2.${RESET} envpkt env scan --write ${DIM}# discover credentials${RESET}`);
2419
+ console.log(` ${DIM}3.${RESET} envpkt seal ${DIM}# encrypt secret values${RESET}`);
2420
+ }
2421
+ });
2422
+ };
2423
+
2274
2424
  //#endregion
2275
2425
  //#region src/mcp/resources.ts
2276
2426
  const loadConfigSafe = () => {
@@ -2747,7 +2897,11 @@ const runSeal = async (options) => {
2747
2897
  }, (c) => c);
2748
2898
  if (!config.agent?.recipient) {
2749
2899
  console.error(`${RED}Error:${RESET} agent.recipient is required for sealing (age public key)`);
2750
- console.error(`${DIM}Add [agent] section with recipient = "age1..." to your envpkt.toml${RESET}`);
2900
+ console.error("");
2901
+ console.error(`${BOLD}Quick fix:${RESET} run ${CYAN}envpkt keygen${RESET} to generate a key and auto-configure recipient`);
2902
+ console.error(`${DIM}Or manually add to your envpkt.toml:${RESET}`);
2903
+ console.error(`${DIM} [agent]${RESET}`);
2904
+ console.error(`${DIM} recipient = "age1..."${RESET}`);
2751
2905
  process.exit(2);
2752
2906
  }
2753
2907
  const { recipient } = config.agent;
@@ -2844,7 +2998,7 @@ const runShellHook = (shell) => {
2844
2998
  //#endregion
2845
2999
  //#region src/cli/index.ts
2846
3000
  const program = new Command();
2847
- program.name("envpkt").description("Credential lifecycle and fleet management for AI agents\n\n Developer workflow: env scan → catalogcloud-synced folder → eval $(envpkt env export)\n Agent / CI workflow: catalog → audit --strict → seal → exec --strict → fleet").version((() => {
3001
+ program.name("envpkt").description("Credential lifecycle and fleet management for AI agents\n\n Developer workflow: env scan → keygenseal → eval $(envpkt env export)\n Agent / CI workflow: catalog → audit --strict → seal → exec --strict → fleet").version((() => {
2848
3002
  const findPkgJson = (dir) => {
2849
3003
  if (existsSync(join(dir, "package.json"))) return join(dir, "package.json");
2850
3004
  const parent = dirname(dir);
@@ -2856,6 +3010,9 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
2856
3010
  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("--agent", "Include [agent] section").option("--name <name>", "Agent name (requires --agent)").option("--capabilities <caps>", "Comma-separated capabilities (requires --agent)").option("--expires <date>", "Agent credential expiration YYYY-MM-DD (requires --agent)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
2857
3011
  runInit(process.cwd(), options);
2858
3012
  });
3013
+ 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 agent.recipient if found)").option("--force", "Overwrite existing identity file").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/age-key.txt)").action((options) => {
3014
+ runKeygen(options);
3015
+ });
2859
3016
  program.command("audit").description("Audit credential health from envpkt.toml (use --strict in CI pipelines to gate deploys)").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").option("--all", "Show both secrets and env defaults").option("--env-only", "Show only env defaults (drift detection)").option("--sealed", "Show only secrets with encrypted_value").option("--external", "Show only secrets without encrypted_value").action((options) => {
2860
3017
  runAudit(options);
2861
3018
  });
package/dist/index.d.ts CHANGED
@@ -272,6 +272,27 @@ type SealError = {
272
272
  readonly _tag: "NoRecipient";
273
273
  readonly message: string;
274
274
  };
275
+ type KeygenError = {
276
+ readonly _tag: "AgeNotFound";
277
+ readonly message: string;
278
+ } | {
279
+ readonly _tag: "KeygenFailed";
280
+ readonly message: string;
281
+ } | {
282
+ readonly _tag: "KeyExists";
283
+ readonly path: string;
284
+ } | {
285
+ readonly _tag: "WriteError";
286
+ readonly message: string;
287
+ } | {
288
+ readonly _tag: "ConfigUpdateError";
289
+ readonly message: string;
290
+ };
291
+ type KeygenResult = {
292
+ readonly recipient: string;
293
+ readonly identityPath: string;
294
+ readonly configUpdated: boolean;
295
+ };
275
296
  //#endregion
276
297
  //#region src/core/config.d.ts
277
298
  /** Find envpkt.toml in the given directory */
@@ -406,6 +427,19 @@ declare const sealSecrets: (meta: Readonly<Record<string, SecretMeta>>, values:
406
427
  /** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
407
428
  declare const unsealSecrets: (meta: Readonly<Record<string, SecretMeta>>, identityPath: string) => Either<SealError, Record<string, string>>;
408
429
  //#endregion
430
+ //#region src/core/keygen.d.ts
431
+ /** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
432
+ declare const resolveKeyPath: () => string;
433
+ /** Resolve an inline age key from ENVPKT_AGE_KEY env var (for CI) */
434
+ declare const resolveInlineKey: () => Option<string>;
435
+ /** Generate an age keypair and write to disk */
436
+ declare const generateKeypair: (options?: {
437
+ readonly force?: boolean;
438
+ readonly outputPath?: string;
439
+ }) => Either<KeygenError, KeygenResult>;
440
+ /** Update agent.recipient in an envpkt.toml file, preserving structure */
441
+ declare const updateConfigRecipient: (configPath: string, recipient: string) => Either<KeygenError, true>;
442
+ //#endregion
409
443
  //#region src/core/resolve-values.d.ts
410
444
  /** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
411
445
  declare const resolveValues: (keys: ReadonlyArray<string>, profile?: string, agentKey?: string) => Promise<Record<string, string>>;
@@ -467,4 +501,4 @@ type ToolDef = {
467
501
  declare const toolDefinitions: readonly ToolDef[];
468
502
  declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
469
503
  //#endregion
470
- export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, 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 IdentityError, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, validateConfig };
504
+ export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, 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 IdentityError, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { FormatRegistry, Type } from "@sinclair/typebox";
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { homedir } from "node:os";
4
2
  import { dirname, join, resolve } from "node:path";
5
3
  import { TypeCompiler } from "@sinclair/typebox/compiler";
6
- import { Cond, Left, List, Option, Right, Try } from "functype";
4
+ import { Cond, Left, List, None, Option, Right, Some, Try } from "functype";
5
+ import { Env, Fs, Path } from "functype-os";
7
6
  import { TomlDate, parse } from "smol-toml";
8
7
  import { execFileSync } from "node:child_process";
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
9
10
  import { createInterface } from "node:readline";
10
11
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -117,17 +118,17 @@ const normalizeDates = (obj) => {
117
118
  if (obj !== null && typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]));
118
119
  return obj;
119
120
  };
120
- /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string */
121
+ /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string (silent — unresolved vars become "") */
121
122
  const expandPath = (p) => {
122
- return (p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
123
+ return Path.expandTilde(p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
123
124
  const name = braced ?? bare ?? "";
124
- return process.env[name] ?? "";
125
+ return Env.getOrDefault(name, "");
125
126
  });
126
127
  };
127
128
  /** Find envpkt.toml in the given directory */
128
129
  const findConfigPath = (dir) => {
129
130
  const candidate = join(dir, CONFIG_FILENAME$1);
130
- return existsSync(candidate) ? Option(candidate) : Option(void 0);
131
+ return Fs.existsSync(candidate) ? Option(candidate) : Option(void 0);
131
132
  };
132
133
  /**
133
134
  * Expand a path template that may contain a single `*` glob segment.
@@ -135,16 +136,16 @@ const findConfigPath = (dir) => {
135
136
  * Non-glob paths return a single-element array if they exist.
136
137
  */
137
138
  const expandGlobPath = (expanded) => {
138
- if (!expanded.includes("*")) return existsSync(expanded) ? [expanded] : [];
139
+ if (!expanded.includes("*")) return Fs.existsSync(expanded) ? [expanded] : [];
139
140
  const segments = expanded.split("/");
140
141
  const globIdx = segments.findIndex((s) => s.includes("*"));
141
142
  if (globIdx < 0) return [];
142
143
  const parentDir = segments.slice(0, globIdx).join("/");
143
144
  const globSegment = segments[globIdx];
144
145
  const suffix = segments.slice(globIdx + 1).join("/");
145
- if (!existsSync(parentDir)) return [];
146
+ if (!Fs.existsSync(parentDir)) return [];
146
147
  const prefix = globSegment.replace(/\*.*$/, "");
147
- return readdirSync(parentDir).filter((entry) => entry.startsWith(prefix)).map((entry) => join(parentDir, entry, suffix)).filter((p) => existsSync(p));
148
+ return Fs.readdirSync(parentDir).fold(() => [], (entries) => entries.filter((entry) => entry.startsWith(prefix)).map((entry) => join(parentDir, entry, suffix)).filter((p) => Fs.existsSync(p)).toArray());
148
149
  };
149
150
  /** Ordered candidate paths for config discovery beyond CWD */
150
151
  const CONFIG_SEARCH_PATHS = [
@@ -170,11 +171,11 @@ const CONFIG_SEARCH_PATHS = [
170
171
  /** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then built-in candidate paths */
171
172
  const discoverConfig = (cwd) => {
172
173
  const cwdCandidate = join(cwd ?? process.cwd(), CONFIG_FILENAME$1);
173
- if (existsSync(cwdCandidate)) return Option({
174
+ if (Fs.existsSync(cwdCandidate)) return Option({
174
175
  path: cwdCandidate,
175
176
  source: "cwd"
176
177
  });
177
- const customPaths = process.env.ENVPKT_SEARCH_PATH?.split(":").filter(Boolean) ?? [];
178
+ const customPaths = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean));
178
179
  for (const template of [...customPaths, ...CONFIG_SEARCH_PATHS]) {
179
180
  const expanded = expandPath(template);
180
181
  if (!expanded || expanded.startsWith("/.envpkt")) continue;
@@ -188,14 +189,14 @@ const discoverConfig = (cwd) => {
188
189
  };
189
190
  /** Read a config file, returning Either<ConfigError, string> */
190
191
  const readConfigFile = (path) => {
191
- if (!existsSync(path)) return Left({
192
+ if (!Fs.existsSync(path)) return Left({
192
193
  _tag: "FileNotFound",
193
194
  path
194
195
  });
195
- return Try(() => readFileSync(path, "utf-8")).fold((err) => Left({
196
+ return Fs.readFileSync(path, "utf-8").mapLeft((err) => ({
196
197
  _tag: "ReadError",
197
- message: String(err)
198
- }), (content) => Right(content));
198
+ message: err.message
199
+ }));
199
200
  };
200
201
  /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit secret) */
201
202
  const applyDefaults = (data) => {
@@ -244,19 +245,19 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
244
245
  path: resolved,
245
246
  source: "flag"
246
247
  };
247
- return existsSync(resolved) ? Right(result) : Left({
248
+ return Fs.existsSync(resolved) ? Right(result) : Left({
248
249
  _tag: "FileNotFound",
249
250
  path: resolved
250
251
  });
251
252
  }
252
- const envPath = envVar ?? process.env[ENV_VAR_CONFIG];
253
+ const envPath = envVar ?? Env.get(ENV_VAR_CONFIG).fold(() => void 0, (v) => v);
253
254
  if (envPath) {
254
255
  const resolved = resolve(envPath);
255
256
  const result = {
256
257
  path: resolved,
257
258
  source: "env"
258
259
  };
259
- return existsSync(resolved) ? Right(result) : Left({
260
+ return Fs.existsSync(resolved) ? Right(result) : Left({
260
261
  _tag: "FileNotFound",
261
262
  path: resolved
262
263
  });
@@ -1666,6 +1667,111 @@ const formatBootError = (error) => {
1666
1667
  }
1667
1668
  };
1668
1669
 
1670
+ //#endregion
1671
+ //#region src/core/keygen.ts
1672
+ /** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
1673
+ const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
1674
+ /** Resolve an inline age key from ENVPKT_AGE_KEY env var (for CI) */
1675
+ const resolveInlineKey = () => {
1676
+ const key = process.env["ENVPKT_AGE_KEY"];
1677
+ return key ? Some(key) : None();
1678
+ };
1679
+ /** Generate an age keypair and write to disk */
1680
+ const generateKeypair = (options) => {
1681
+ if (!ageAvailable()) return Left({
1682
+ _tag: "AgeNotFound",
1683
+ message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
1684
+ });
1685
+ const outputPath = options?.outputPath ?? resolveKeyPath();
1686
+ if (existsSync(outputPath) && !options?.force) return Left({
1687
+ _tag: "KeyExists",
1688
+ path: outputPath
1689
+ });
1690
+ return Try(() => execFileSync("age-keygen", [], {
1691
+ stdio: [
1692
+ "pipe",
1693
+ "pipe",
1694
+ "pipe"
1695
+ ],
1696
+ encoding: "utf-8"
1697
+ })).fold((err) => Left({
1698
+ _tag: "KeygenFailed",
1699
+ message: `age-keygen failed: ${err}`
1700
+ }), (output) => {
1701
+ const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
1702
+ if (!recipientLine) return Left({
1703
+ _tag: "KeygenFailed",
1704
+ message: "Could not parse public key from age-keygen output"
1705
+ });
1706
+ const recipient = recipientLine.replace("# public key: ", "").trim();
1707
+ const dir = dirname(outputPath);
1708
+ const mkdirFailed = Try(() => {
1709
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1710
+ }).fold((err) => ({
1711
+ _tag: "WriteError",
1712
+ message: `Failed to create directory ${dir}: ${err}`
1713
+ }), () => void 0);
1714
+ if (mkdirFailed) return Left(mkdirFailed);
1715
+ return Try(() => {
1716
+ writeFileSync(outputPath, output, { mode: 384 });
1717
+ chmodSync(outputPath, 384);
1718
+ }).fold((err) => Left({
1719
+ _tag: "WriteError",
1720
+ message: `Failed to write identity file: ${err}`
1721
+ }), () => Right({
1722
+ recipient,
1723
+ identityPath: outputPath,
1724
+ configUpdated: false
1725
+ }));
1726
+ });
1727
+ };
1728
+ /** Update agent.recipient in an envpkt.toml file, preserving structure */
1729
+ const updateConfigRecipient = (configPath, recipient) => {
1730
+ return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
1731
+ _tag: "ConfigUpdateError",
1732
+ message: `Failed to read config: ${err}`
1733
+ }), (raw) => {
1734
+ const lines = raw.split("\n");
1735
+ const output = [];
1736
+ let inAgentSection = false;
1737
+ let recipientUpdated = false;
1738
+ let hasAgentSection = false;
1739
+ for (const line of lines) {
1740
+ if (/^\[agent\]\s*$/.test(line)) {
1741
+ inAgentSection = true;
1742
+ hasAgentSection = true;
1743
+ output.push(line);
1744
+ continue;
1745
+ }
1746
+ if (/^\[/.test(line) && !/^\[agent\]\s*$/.test(line)) {
1747
+ if (inAgentSection && !recipientUpdated) {
1748
+ output.push(`recipient = "${recipient}"`);
1749
+ recipientUpdated = true;
1750
+ }
1751
+ inAgentSection = false;
1752
+ output.push(line);
1753
+ continue;
1754
+ }
1755
+ if (inAgentSection && /^recipient\s*=/.test(line)) {
1756
+ output.push(`recipient = "${recipient}"`);
1757
+ recipientUpdated = true;
1758
+ continue;
1759
+ }
1760
+ output.push(line);
1761
+ }
1762
+ if (inAgentSection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
1763
+ if (!hasAgentSection) {
1764
+ output.push("");
1765
+ output.push("[agent]");
1766
+ output.push(`recipient = "${recipient}"`);
1767
+ }
1768
+ return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
1769
+ _tag: "ConfigUpdateError",
1770
+ message: `Failed to write config: ${err}`
1771
+ }), () => Right(true));
1772
+ });
1773
+ };
1774
+
1669
1775
  //#endregion
1670
1776
  //#region src/core/resolve-values.ts
1671
1777
  /** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
@@ -2088,4 +2194,4 @@ const startServer = async () => {
2088
2194
  };
2089
2195
 
2090
2196
  //#endregion
2091
- export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, validateConfig };
2197
+ export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",
@@ -42,7 +42,8 @@
42
42
  "@modelcontextprotocol/sdk": "^1.27.1",
43
43
  "@sinclair/typebox": "^0.34.48",
44
44
  "commander": "^14.0.3",
45
- "functype": "^0.48.0",
45
+ "functype": "^0.49.0",
46
+ "functype-os": "^0.2.0",
46
47
  "smol-toml": "^1.6.0"
47
48
  },
48
49
  "devDependencies": {