envpkt 0.11.0 → 0.11.2

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/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { FormatRegistry, Type } from "@sinclair/typebox";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { TypeCompiler } from "@sinclair/typebox/compiler";
4
- import { Cond, Either, Left, List, None, Option, Right, Some, Try } from "functype";
4
+ import { $, Cond, Do, Either, Left, List, Map as Map$1, None, Option, Right, Set as Set$1, Some, Try } from "functype";
5
5
  import { Env, Fs, Path, Platform } from "functype-os";
6
6
  import { TomlDate, parse } from "smol-toml";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
8
- import { createDirectConsoleLogger, createDirectTestLogger, directSilentLogger, directSilentLogger as directSilentLogger$1 } from "functype-log";
8
+ import { createDirectConsoleLogger, createDirectTestLogger, directSilentLogger, directSilentLogger as directSilentLogger$1 } from "functype-log/direct";
9
9
  import { execFileSync } from "node:child_process";
10
10
  import { homedir } from "node:os";
11
11
  import { createInterface } from "node:readline";
@@ -294,18 +294,19 @@ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
294
294
  /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
295
295
  const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
296
296
  return agentSecrets.reduce((acc, key) => acc.flatMap((resolved) => {
297
- if (!(key in catalogMeta)) return Left({
297
+ const catalogEntry = catalogMeta[key];
298
+ if (catalogEntry === void 0) return Left({
298
299
  _tag: "SecretNotInCatalog",
299
300
  key,
300
301
  catalogPath
301
302
  });
302
- const catalogEntry = catalogMeta[key];
303
+ const merged = Option(agentMeta[key]).fold(() => catalogEntry, (override) => ({
304
+ ...catalogEntry,
305
+ ...override
306
+ }));
303
307
  return Right({
304
308
  ...resolved,
305
- [key]: key in agentMeta ? {
306
- ...catalogEntry,
307
- ...agentMeta[key]
308
- } : catalogEntry
309
+ [key]: merged
309
310
  });
310
311
  }), Right({}));
311
312
  };
@@ -372,12 +373,12 @@ const validateOneSecret = (key, meta, secretEntries) => {
372
373
  kind: "secret",
373
374
  field: "encrypted_value"
374
375
  });
375
- return parseRef(ref).fold(() => Left({
376
+ return parseRef(ref).toEither({
376
377
  _tag: "AliasInvalidSyntax",
377
378
  key,
378
379
  kind: "secret",
379
380
  value: ref
380
- }), (parsed) => {
381
+ }).flatMap((parsed) => {
381
382
  if (parsed.kind !== "secret") return Left({
382
383
  _tag: "AliasCrossType",
383
384
  key,
@@ -388,23 +389,23 @@ const validateOneSecret = (key, meta, secretEntries) => {
388
389
  _tag: "AliasSelfReference",
389
390
  key: `secret.${key}`
390
391
  });
391
- return Option(secretEntries[parsed.key]).fold(() => Left({
392
+ return Option(secretEntries[parsed.key]).toEither({
392
393
  _tag: "AliasTargetMissing",
393
394
  key: `secret.${key}`,
394
395
  target: ref
395
- }), (target) => {
396
+ }).flatMap((target) => {
396
397
  if (target.from_key !== void 0) return Left({
397
398
  _tag: "AliasChained",
398
399
  key: `secret.${key}`,
399
400
  target: ref
400
401
  });
401
- return Right(Option({
402
+ return Right({
402
403
  kind: "secret",
403
404
  targetKind: "secret",
404
405
  targetKey: parsed.key
405
- }));
406
+ });
406
407
  });
407
- });
408
+ }).map((entry) => Option(entry));
408
409
  };
409
410
  const validateOneEnv = (key, meta, envEntries) => {
410
411
  if (meta.from_key === void 0) return Right(Option(void 0));
@@ -415,12 +416,12 @@ const validateOneEnv = (key, meta, envEntries) => {
415
416
  kind: "env",
416
417
  field: "value"
417
418
  });
418
- return parseRef(ref).fold(() => Left({
419
+ return parseRef(ref).toEither({
419
420
  _tag: "AliasInvalidSyntax",
420
421
  key,
421
422
  kind: "env",
422
423
  value: ref
423
- }), (parsed) => {
424
+ }).flatMap((parsed) => {
424
425
  if (parsed.kind !== "env") return Left({
425
426
  _tag: "AliasCrossType",
426
427
  key,
@@ -431,57 +432,32 @@ const validateOneEnv = (key, meta, envEntries) => {
431
432
  _tag: "AliasSelfReference",
432
433
  key: `env.${key}`
433
434
  });
434
- return Option(envEntries[parsed.key]).fold(() => Left({
435
+ return Option(envEntries[parsed.key]).toEither({
435
436
  _tag: "AliasTargetMissing",
436
437
  key: `env.${key}`,
437
438
  target: ref
438
- }), (target) => {
439
+ }).flatMap((target) => {
439
440
  if (target.from_key !== void 0) return Left({
440
441
  _tag: "AliasChained",
441
442
  key: `env.${key}`,
442
443
  target: ref
443
444
  });
444
- return Right(Option({
445
+ return Right({
445
446
  kind: "env",
446
447
  targetKind: "env",
447
448
  targetKey: parsed.key
448
- }));
449
+ });
449
450
  });
450
- });
451
+ }).map((entry) => Option(entry));
451
452
  };
452
- /**
453
- * Validate all `from_key` references in a resolved config. Produces an
454
- * AliasTable mapping each alias to its target, or an AliasError describing
455
- * the first failure.
456
- *
457
- * Rules:
458
- * - Ref must be "secret.<KEY>" or "env.<KEY>"
459
- * - Target must exist in the same resolved config
460
- * - Target must be the same type (secret→secret, env→env only)
461
- * - Target must not itself be a from_key entry (single hop only)
462
- * - Self-reference is rejected
463
- * - An alias entry cannot also carry a value field (encrypted_value for
464
- * secrets, value for env)
465
- */
453
+ /** Fail-fast reduction: accumulates validated entries, short-circuits on first AliasError */
454
+ const collectValidated = (items, validate, prefix) => items.reduce((acc, [key, meta]) => acc.flatMap((entries) => validate(key, meta).map((opt) => opt.fold(() => entries, (entry) => [...entries, [`${prefix}.${key}`, entry]]))), Right([]));
466
455
  const validateAliases = (config) => {
467
456
  const secretEntries = config.secret ?? {};
468
457
  const envEntries = config.env ?? {};
469
- const entries = /* @__PURE__ */ new Map();
470
- const secretResults = Object.entries(secretEntries).map(([key, meta]) => [key, validateOneSecret(key, meta, secretEntries)]);
471
- for (const [key, result] of secretResults) {
472
- const outcome = result.fold((err) => err, (opt) => opt.orUndefined());
473
- if (outcome === void 0) continue;
474
- if ("_tag" in outcome) return Left(outcome);
475
- entries.set(`secret.${key}`, outcome);
476
- }
477
- const envResults = Object.entries(envEntries).map(([key, meta]) => [key, validateOneEnv(key, meta, envEntries)]);
478
- for (const [key, result] of envResults) {
479
- const outcome = result.fold((err) => err, (opt) => opt.orUndefined());
480
- if (outcome === void 0) continue;
481
- if ("_tag" in outcome) return Left(outcome);
482
- entries.set(`env.${key}`, outcome);
483
- }
484
- return Right({ entries });
458
+ const secretPairs = collectValidated(Object.entries(secretEntries), (k, m) => validateOneSecret(k, m, secretEntries), "secret");
459
+ const envPairs = collectValidated(Object.entries(envEntries), (k, m) => validateOneEnv(k, m, envEntries), "env");
460
+ return secretPairs.flatMap((secrets) => envPairs.map((envs) => [...secrets, ...envs])).map((allEntries) => ({ entries: new Map(allEntries) }));
485
461
  };
486
462
  /** Does this secret entry point at another entry? */
487
463
  const isSecretAlias = (meta) => meta?.from_key !== void 0;
@@ -555,7 +531,7 @@ const formatPacket = (result, options) => {
555
531
  if (envEntriesArr.length > 0) {
556
532
  const envHeader = `env: ${envEntriesArr.length}`;
557
533
  const envLines = envEntriesArr.map(([key, meta]) => {
558
- return ` ${key}${meta.from_key ? ` [alias → ${meta.from_key}]` : ""}${meta.value !== void 0 ? ` = ${meta.value}` : ""}${meta.purpose ? `\n purpose: ${meta.purpose}` : ""}`;
534
+ return ` ${key}${meta.from_key ? ` [alias → ${meta.from_key}]` : ""}${Option(meta.value).fold(() => "", (v) => ` = ${v}`)}${meta.purpose ? `\n purpose: ${meta.purpose}` : ""}`;
559
535
  });
560
536
  sections.push([envHeader, ...envLines].join("\n"));
561
537
  }
@@ -632,7 +608,7 @@ const classifyAlias = (key, meta, targetHealth, targetRef) => ({
632
608
  status: targetHealth.status,
633
609
  days_remaining: targetHealth.days_remaining,
634
610
  rotation_url: targetHealth.rotation_url,
635
- purpose: meta.purpose !== void 0 ? Option(meta.purpose) : targetHealth.purpose,
611
+ purpose: Option(meta.purpose).fold(() => targetHealth.purpose, (v) => Option(v)),
636
612
  created: targetHealth.created,
637
613
  expires: targetHealth.expires,
638
614
  issues: List([]),
@@ -648,17 +624,18 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
648
624
  const secretEntries = config.secret ?? {};
649
625
  const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
650
626
  const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
651
- const nonAliasMetaKeys = new Set(nonAliasEntries.map(([k]) => k));
627
+ const nonAliasMetaKeys = Set$1(nonAliasEntries.map(([k]) => k));
652
628
  const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
653
- const healthByKey = new Map(nonAliasHealth.map((h) => [h.key, h]));
629
+ const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
654
630
  const parseTargetKey = (from_key) => {
655
- return /^secret\.(.+)$/.exec(from_key)?.[1];
631
+ return Option(from_key.match(/^secret\.(.+)$/)?.[1]);
656
632
  };
657
633
  const aliasHealth = aliasEntries.map(([key, meta]) => {
658
- const targetKey = (aliasTable?.entries.get(`secret.${key}`))?.targetKey ?? (meta.from_key !== void 0 ? parseTargetKey(meta.from_key) : void 0);
659
- const targetHealth = targetKey !== void 0 ? healthByKey.get(targetKey) : void 0;
660
- const targetRef = meta.from_key ?? (targetKey !== void 0 ? `secret.${targetKey}` : "");
661
- if (!targetHealth) return {
634
+ const tableEntry = aliasTable?.entries.get(`secret.${key}`);
635
+ const targetKey = Option(tableEntry?.targetKey).fold(() => Option(meta.from_key).flatMap(parseTargetKey), (k) => Option(k));
636
+ const targetHealth = targetKey.flatMap((k) => healthByKey.get(k));
637
+ const targetRef = Option(meta.from_key).fold(() => targetKey.map((k) => `secret.${k}`), (v) => Option(v)).orElse("");
638
+ return targetHealth.fold(() => ({
662
639
  key,
663
640
  service: Option(meta.service),
664
641
  status: "missing",
@@ -669,11 +646,10 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
669
646
  expires: Option(meta.expires),
670
647
  issues: List(["Alias target not resolvable"]),
671
648
  alias_of: Option(targetRef)
672
- };
673
- return classifyAlias(key, meta, targetHealth, targetRef);
649
+ }), (health) => classifyAlias(key, meta, health, targetRef));
674
650
  });
675
651
  const secrets = List([...nonAliasHealth, ...aliasHealth]);
676
- const orphaned = keys.size > 0 ? [...nonAliasMetaKeys].filter((k) => !keys.has(k)).length : 0;
652
+ const orphaned = keys.size > 0 ? nonAliasMetaKeys.toArray().filter((k) => !keys.has(k)).length : 0;
677
653
  const total = secrets.size;
678
654
  const expired = secrets.count((s) => s.status === "expired");
679
655
  const missing = secrets.count((s) => s.status === "missing");
@@ -699,12 +675,14 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
699
675
  };
700
676
  const computeEnvAudit = (config, env = process.env) => {
701
677
  const envEntries = config.env ?? {};
678
+ const resolveEffectiveDefault = (entry) => {
679
+ return Do(function* () {
680
+ return yield* $(Option((yield* $(Option(envEntries[yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]))]))).value));
681
+ }).orElse(entry.value ?? "");
682
+ };
702
683
  const entries = Object.entries(envEntries).map(([key, entry]) => {
703
684
  const currentValue = env[key];
704
- const effectiveDefault = entry.from_key !== void 0 ? (() => {
705
- const targetKey = /^env\.(.+)$/.exec(entry.from_key)?.[1];
706
- return (targetKey !== void 0 ? envEntries[targetKey] : void 0)?.value ?? "";
707
- })() : entry.value ?? "";
685
+ const effectiveDefault = resolveEffectiveDefault(entry);
708
686
  return {
709
687
  key,
710
688
  defaultValue: effectiveDefault,
@@ -724,7 +702,7 @@ const computeEnvAudit = (config, env = process.env) => {
724
702
  };
725
703
  //#endregion
726
704
  //#region src/core/patterns.ts
727
- const EXCLUDED_VARS = new Set([
705
+ const EXCLUDED_VARS = Set$1([
728
706
  "PATH",
729
707
  "HOME",
730
708
  "USER",
@@ -1437,25 +1415,22 @@ const envScan = (env, options) => {
1437
1415
  };
1438
1416
  };
1439
1417
  const parseAliasRef = (raw, expectedKind) => {
1440
- const match = /^(secret|env)\.(.+)$/.exec(raw);
1441
- if (!match) return void 0;
1442
- if (match[1] !== expectedKind) return void 0;
1443
- return match[2];
1418
+ const match = raw.match(/^(secret|env)\.(.+)$/);
1419
+ if (match?.[1] !== expectedKind) return Option(void 0);
1420
+ return Option(match[2]);
1444
1421
  };
1445
1422
  /** Bidirectional drift detection between config and live environment */
1446
1423
  const envCheck = (config, env) => {
1447
1424
  const secretEntries = config.secret ?? {};
1448
1425
  const metaKeys = Object.keys(secretEntries);
1449
- const trackedSet = new Set(metaKeys);
1426
+ const metaKeysSet = Set$1(metaKeys);
1450
1427
  const isSecretPresent = (key) => {
1451
1428
  if (env[key] !== void 0 && env[key] !== "") return true;
1452
1429
  const meta = secretEntries[key];
1453
1430
  if (meta?.from_key === void 0) return false;
1454
- const targetKey = parseAliasRef(meta.from_key, "secret");
1455
- return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
1431
+ return parseAliasRef(meta.from_key, "secret").fold(() => false, (targetKey) => env[targetKey] !== void 0 && env[targetKey] !== "");
1456
1432
  };
1457
- const secretDriftEntries = metaKeys.map((key) => {
1458
- const meta = secretEntries[key];
1433
+ const secretDriftEntries = Object.entries(secretEntries).map(([key, meta]) => {
1459
1434
  const present = isSecretPresent(key);
1460
1435
  return {
1461
1436
  envVar: key,
@@ -1469,14 +1444,9 @@ const envCheck = (config, env) => {
1469
1444
  if (env[key] !== void 0 && env[key] !== "") return true;
1470
1445
  const meta = envDefaults[key];
1471
1446
  if (meta?.from_key === void 0) return false;
1472
- const targetKey = parseAliasRef(meta.from_key, "env");
1473
- return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
1447
+ return parseAliasRef(meta.from_key, "env").fold(() => false, (targetKey) => env[targetKey] !== void 0 && env[targetKey] !== "");
1474
1448
  };
1475
- const envDefaultEntries = Object.keys(envDefaults).filter((key) => {
1476
- if (trackedSet.has(key)) return false;
1477
- trackedSet.add(key);
1478
- return true;
1479
- }).map((key) => {
1449
+ const envDefaultEntries = Object.keys(envDefaults).filter((key) => !metaKeysSet.has(key)).map((key) => {
1480
1450
  const present = isEnvPresent(key);
1481
1451
  return {
1482
1452
  envVar: key,
@@ -1485,7 +1455,8 @@ const envCheck = (config, env) => {
1485
1455
  confidence: Option(void 0)
1486
1456
  };
1487
1457
  });
1488
- const untrackedEntries = scanEnv(env).filter((match) => !trackedSet.has(match.envVar)).map((match) => ({
1458
+ const trackedKeys = Set$1([...metaKeys, ...envDefaultEntries.map((e) => e.envVar)]);
1459
+ const untrackedEntries = scanEnv(env).filter((match) => !trackedKeys.has(match.envVar)).map((match) => ({
1489
1460
  envVar: match.envVar,
1490
1461
  service: match.service,
1491
1462
  status: "untracked",
@@ -1843,11 +1814,12 @@ const sealSecrets = (meta, values, recipient) => {
1843
1814
  message: "age CLI not found on PATH"
1844
1815
  });
1845
1816
  return Object.entries(meta).reduce((acc, [key, secretMeta]) => acc.flatMap((result) => {
1846
- if (!(key in values)) return Right({
1817
+ const value = values[key];
1818
+ if (value === void 0) return Right({
1847
1819
  ...result,
1848
1820
  [key]: secretMeta
1849
1821
  });
1850
- return ageEncrypt(values[key], recipient).mapLeft((err) => ({
1822
+ return ageEncrypt(value, recipient).mapLeft((err) => ({
1851
1823
  _tag: "EncryptFailed",
1852
1824
  key,
1853
1825
  message: err.message
@@ -1943,11 +1915,11 @@ const bootSafe = (options) => {
1943
1915
  const secretEntries = config.secret ?? {};
1944
1916
  const envEntries = config.env ?? {};
1945
1917
  const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
1946
- const aliasSecretKeys = Object.keys(secretEntries).filter((k) => secretEntries[k].from_key !== void 0);
1918
+ const aliasSecretKeys = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
1947
1919
  const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
1948
- const aliasEnvKeys = Object.keys(envEntries).filter((k) => envEntries[k].from_key !== void 0);
1920
+ const aliasEnvKeys = Object.entries(envEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
1949
1921
  const nonAliasMetaKeys = Object.keys(nonAliasSecretEntries);
1950
- const hasSealedValues = nonAliasMetaKeys.some((k) => !!nonAliasSecretEntries[k].encrypted_value);
1922
+ const hasSealedValues = Object.values(nonAliasSecretEntries).some((meta) => !!meta.encrypted_value);
1951
1923
  const identityKeyResult = resolveIdentityKey(config, configDir);
1952
1924
  const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
1953
1925
  if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
@@ -1960,7 +1932,7 @@ const bootSafe = (options) => {
1960
1932
  const injected = [];
1961
1933
  const skipped = [];
1962
1934
  warnings.push(...checkEnvMisclassification(config));
1963
- const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => entry.value !== void 0 ? [[key, entry.value]] : [], () => [])));
1935
+ const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
1964
1936
  const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
1965
1937
  if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
1966
1938
  process.env[key] = value;
@@ -2020,21 +1992,20 @@ const bootSafe = (options) => {
2020
1992
  aliasSecretKeys.forEach((aliasKey) => {
2021
1993
  const entry = aliasTable.entries.get(`secret.${aliasKey}`);
2022
1994
  if (!entry) return;
2023
- const targetValue = secrets[entry.targetKey];
2024
- if (targetValue !== void 0) {
2025
- secrets[aliasKey] = targetValue;
2026
- injected.push(aliasKey);
2027
- log.debug("phase.alias.copied", {
1995
+ Option(secrets[entry.targetKey]).fold(() => {
1996
+ skipped.push(aliasKey);
1997
+ log.debug("phase.alias.target_unresolved", {
2028
1998
  alias: aliasKey,
2029
1999
  target: entry.targetKey
2030
2000
  });
2031
- } else {
2032
- skipped.push(aliasKey);
2033
- log.debug("phase.alias.target_unresolved", {
2001
+ }, (targetValue) => {
2002
+ secrets[aliasKey] = targetValue;
2003
+ injected.push(aliasKey);
2004
+ log.debug("phase.alias.copied", {
2034
2005
  alias: aliasKey,
2035
2006
  target: entry.targetKey
2036
2007
  });
2037
- }
2008
+ });
2038
2009
  });
2039
2010
  aliasEnvKeys.forEach((aliasKey) => {
2040
2011
  const entry = aliasTable.entries.get(`env.${aliasKey}`);
@@ -2149,6 +2120,22 @@ const resolveValues = async (keys, profile, agentKey) => {
2149
2120
  //#region src/core/toml-edit.ts
2150
2121
  const SECTION_RE = /^\[.+\]\s*$/;
2151
2122
  const MULTILINE_OPEN = "\"\"\"";
2123
+ const scanSectionBoundary = (state, line, i) => {
2124
+ if (state.done) return state;
2125
+ if (state.inMultiline) return line.includes(MULTILINE_OPEN) ? {
2126
+ ...state,
2127
+ inMultiline: false
2128
+ } : state;
2129
+ if (line.includes(MULTILINE_OPEN)) return (line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1 ? {
2130
+ ...state,
2131
+ inMultiline: true
2132
+ } : state;
2133
+ return SECTION_RE.test(line) ? {
2134
+ ...state,
2135
+ end: i,
2136
+ done: true
2137
+ } : state;
2138
+ };
2152
2139
  /**
2153
2140
  * Find the line range [start, end) of a TOML section by its header string.
2154
2141
  * The range includes the header line through to (but not including) the next section header or EOF.
@@ -2157,26 +2144,14 @@ const MULTILINE_OPEN = "\"\"\"";
2157
2144
  const findSectionRange = (lines, sectionHeader) => {
2158
2145
  const start = lines.findIndex((l) => l.trim() === sectionHeader);
2159
2146
  if (start === -1) return void 0;
2160
- let end = lines.length;
2161
- let inMultiline = false;
2162
- for (let i = start + 1; i < lines.length; i++) {
2163
- const line = lines[i];
2164
- if (inMultiline) {
2165
- if (line.includes(MULTILINE_OPEN)) inMultiline = false;
2166
- continue;
2167
- }
2168
- if (line.includes(MULTILINE_OPEN)) {
2169
- if ((line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) inMultiline = true;
2170
- continue;
2171
- }
2172
- if (SECTION_RE.test(line)) {
2173
- end = i;
2174
- break;
2175
- }
2176
- }
2147
+ const initial = {
2148
+ end: lines.length,
2149
+ inMultiline: false,
2150
+ done: false
2151
+ };
2177
2152
  return {
2178
2153
  start,
2179
- end
2154
+ end: List(lines.slice(start + 1)).zipWithIndex().foldLeft(initial)((state, entry) => scanSectionBoundary(state, entry[0], start + 1 + entry[1])).end
2180
2155
  };
2181
2156
  };
2182
2157
  /** Check whether a section header exists in the raw TOML */
@@ -2192,12 +2167,10 @@ const removeSection = (raw, sectionHeader) => {
2192
2167
  _tag: "SectionNotFound",
2193
2168
  section: sectionHeader
2194
2169
  });
2195
- let removeEnd = range.end;
2196
- while (removeEnd > range.start && removeEnd - 1 >= range.start && lines[removeEnd - 1].trim() === "") removeEnd--;
2197
- const before = lines.slice(0, range.start);
2198
2170
  const after = lines.slice(range.end);
2199
- while (before.length > 0 && before[before.length - 1].trim() === "") before.pop();
2200
- const result = [...before, ...after].join("\n");
2171
+ const beforeAll = lines.slice(0, range.start);
2172
+ const lastNonBlank = beforeAll.findLastIndex((l) => l.trim() !== "");
2173
+ const result = [...lastNonBlank === -1 ? [] : beforeAll.slice(0, lastNonBlank + 1), ...after].join("\n");
2201
2174
  return Either.right(result);
2202
2175
  };
2203
2176
  /**
@@ -2233,55 +2206,47 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
2233
2206
  const before = lines.slice(0, range.start + 1);
2234
2207
  const after = lines.slice(range.end);
2235
2208
  const sectionBody = lines.slice(range.start + 1, range.end);
2236
- const remaining = [];
2237
- const updatedKeys = /* @__PURE__ */ new Set();
2238
- let inMultiline = false;
2239
- let multilineKey = "";
2240
- for (let i = 0; i < sectionBody.length; i++) {
2241
- const line = sectionBody[i];
2242
- if (inMultiline) {
2243
- if (line.includes(MULTILINE_OPEN)) {
2244
- inMultiline = false;
2245
- if (updates[multilineKey] === null) continue;
2246
- if (multilineKey in updates) continue;
2247
- } else {
2248
- if (updates[multilineKey] === null) continue;
2249
- if (multilineKey in updates) continue;
2250
- }
2251
- remaining.push(line);
2252
- continue;
2253
- }
2209
+ const findClosingMultiline = (fromIdx) => {
2210
+ const idx = sectionBody.findIndex((l, j) => j > fromIdx && l.includes(MULTILINE_OPEN));
2211
+ return idx === -1 ? sectionBody.length : idx;
2212
+ };
2213
+ const initial = {
2214
+ remaining: [],
2215
+ updatedKeys: Set$1.empty(),
2216
+ skipUntil: -1
2217
+ };
2218
+ const step = (state, line, i) => {
2219
+ if (i <= state.skipUntil) return state;
2254
2220
  const eqIdx = line.indexOf("=");
2255
- if (eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[")) {
2256
- const key = line.slice(0, eqIdx).trim();
2257
- if (key in updates) {
2258
- updatedKeys.add(key);
2259
- const afterEquals = line.slice(eqIdx + 1).trim();
2260
- if (afterEquals.includes(MULTILINE_OPEN)) {
2261
- if ((afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) {
2262
- inMultiline = true;
2263
- multilineKey = key;
2264
- }
2265
- }
2266
- if (updates[key] === null) continue;
2267
- remaining.push(`${key} = ${updates[key]}`);
2268
- if (inMultiline) {
2269
- for (let j = i + 1; j < sectionBody.length; j++) if (sectionBody[j].includes(MULTILINE_OPEN)) {
2270
- i = j;
2271
- inMultiline = false;
2272
- break;
2273
- }
2274
- }
2275
- continue;
2276
- }
2221
+ const isFieldLine = eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[");
2222
+ const key = isFieldLine ? line.slice(0, eqIdx).trim() : "";
2223
+ if (isFieldLine && key in updates) {
2224
+ const afterEquals = line.slice(eqIdx + 1).trim();
2225
+ const skipUntil = afterEquals.includes(MULTILINE_OPEN) && (afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1 ? findClosingMultiline(i) : state.skipUntil;
2226
+ const updatedKeys = state.updatedKeys.add(key);
2227
+ const value = updates[key];
2228
+ if (value === null) return {
2229
+ ...state,
2230
+ updatedKeys,
2231
+ skipUntil
2232
+ };
2233
+ return {
2234
+ remaining: [...state.remaining, `${key} = ${value}`],
2235
+ updatedKeys,
2236
+ skipUntil
2237
+ };
2277
2238
  }
2278
- remaining.push(line);
2279
- }
2280
- const newFields = Object.entries(updates).filter(([key, value]) => value !== null && !updatedKeys.has(key)).map(([key, value]) => `${key} = ${value}`);
2281
- remaining.push(...newFields);
2239
+ return {
2240
+ ...state,
2241
+ remaining: [...state.remaining, line]
2242
+ };
2243
+ };
2244
+ const final = List(sectionBody).zipWithIndex().foldLeft(initial)((state, entry) => step(state, entry[0], entry[1]));
2245
+ const newFields = Object.entries(updates).filter(([key, value]) => value !== null && !final.updatedKeys.has(key)).map(([key, value]) => `${key} = ${value}`);
2282
2246
  const result = [
2283
2247
  ...before,
2284
- ...remaining,
2248
+ ...final.remaining,
2249
+ ...newFields,
2285
2250
  ...after
2286
2251
  ].join("\n");
2287
2252
  return Either.right(result);
@@ -2294,31 +2259,7 @@ const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
2294
2259
  //#endregion
2295
2260
  //#region src/core/fleet.ts
2296
2261
  const CONFIG_FILENAME = "envpkt.toml";
2297
- const SKIP_DIRS = new Set([
2298
- "node_modules",
2299
- ".git",
2300
- ".hg",
2301
- ".svn",
2302
- "dist",
2303
- "build",
2304
- "lib",
2305
- ".claude",
2306
- "__pycache__",
2307
- "target",
2308
- "out",
2309
- "tmp",
2310
- ".terraform",
2311
- ".gradle",
2312
- ".cargo",
2313
- ".venv",
2314
- ".next",
2315
- ".cache",
2316
- ".tox",
2317
- "vendor",
2318
- "coverage",
2319
- ".nyc_output",
2320
- ".turbo"
2321
- ]);
2262
+ const SKIP_DIRS = Set$1.of("node_modules", ".git", ".hg", ".svn", "dist", "build", "lib", ".claude", "__pycache__", "target", "out", "tmp", ".terraform", ".gradle", ".cargo", ".venv", ".next", ".cache", ".tox", "vendor", "coverage", ".nyc_output", ".turbo");
2322
2263
  function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
2323
2264
  if (currentDepth > maxDepth) return;
2324
2265
  const configPath = join(dir, CONFIG_FILENAME);
@@ -2585,24 +2526,19 @@ const handleGetSecretMeta = (args) => {
2585
2526
  const secretEntries = config.secret ?? {};
2586
2527
  return Option(secretEntries[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
2587
2528
  const { encrypted_value: _, from_key: fromKey, ...rest } = meta;
2588
- if (fromKey !== void 0) {
2589
- const targetKey = /^secret\.(.+)$/.exec(fromKey)?.[1];
2590
- const target = targetKey !== void 0 ? secretEntries[targetKey] : void 0;
2591
- if (target) {
2592
- const { encrypted_value: __, from_key: ___, ...targetRest } = target;
2593
- return textResult(JSON.stringify({
2594
- key,
2595
- ...targetRest,
2596
- ...rest,
2597
- alias_of: fromKey
2598
- }, null, 2));
2599
- }
2529
+ if (fromKey !== void 0) return Option(fromKey.match(/^secret\.(.+)$/)?.[1]).flatMap((k) => Option(secretEntries[k])).fold(() => textResult(JSON.stringify({
2530
+ key,
2531
+ ...rest,
2532
+ alias_of: fromKey
2533
+ }, null, 2)), (t) => {
2534
+ const { encrypted_value: __, from_key: ___, ...targetRest } = t;
2600
2535
  return textResult(JSON.stringify({
2601
2536
  key,
2537
+ ...targetRest,
2602
2538
  ...rest,
2603
2539
  alias_of: fromKey
2604
2540
  }, null, 2));
2605
- }
2541
+ });
2606
2542
  return textResult(JSON.stringify({
2607
2543
  key,
2608
2544
  ...rest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",
@@ -42,16 +42,16 @@
42
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
43
  "@sinclair/typebox": "^0.34.49",
44
44
  "commander": "^14.0.3",
45
- "functype": "^0.60.0",
46
- "functype-log": "^0.60.0",
47
- "functype-os": "^0.60.0",
45
+ "functype": "^0.60.7",
46
+ "functype-log": "^0.60.7",
47
+ "functype-os": "^0.60.7",
48
48
  "smol-toml": "^1.6.1"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^24.12.2",
52
- "ts-builds": "^2.7.0",
53
- "tsdown": "^0.21.9",
54
- "tsx": "^4.21.0"
51
+ "@types/node": "^24.12.4",
52
+ "ts-builds": "^2.8.1",
53
+ "tsdown": "^0.22.0",
54
+ "tsx": "^4.22.3"
55
55
  },
56
56
  "type": "module",
57
57
  "main": "./dist/index.js",