envpkt 0.11.1 → 0.11.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/dist/cli.js +616 -294
- package/dist/index.d.ts +100 -112
- package/dist/index.js +158 -209
- package/package.json +8 -8
- package/schemas/envpkt.schema.json +5 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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";
|
|
@@ -56,6 +56,10 @@ const SecretMetaSchema = Type.Object({
|
|
|
56
56
|
format: "date",
|
|
57
57
|
description: "Date the secret was provisioned (YYYY-MM-DD)"
|
|
58
58
|
})),
|
|
59
|
+
last_rotated_at: Type.Optional(Type.String({
|
|
60
|
+
format: "date",
|
|
61
|
+
description: "Date the secret value was most recently rotated (YYYY-MM-DD). Used by audit for staleness."
|
|
62
|
+
})),
|
|
59
63
|
rotates: Type.Optional(Type.String({ description: "Rotation schedule (e.g. '90d', 'quarterly')" })),
|
|
60
64
|
rate_limit: Type.Optional(Type.String({ description: "Rate limit or quota info (e.g. '1000/min')" })),
|
|
61
65
|
model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
|
|
@@ -294,18 +298,19 @@ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
|
|
|
294
298
|
/** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
|
|
295
299
|
const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
|
|
296
300
|
return agentSecrets.reduce((acc, key) => acc.flatMap((resolved) => {
|
|
297
|
-
|
|
301
|
+
const catalogEntry = catalogMeta[key];
|
|
302
|
+
if (catalogEntry === void 0) return Left({
|
|
298
303
|
_tag: "SecretNotInCatalog",
|
|
299
304
|
key,
|
|
300
305
|
catalogPath
|
|
301
306
|
});
|
|
302
|
-
const
|
|
307
|
+
const merged = Option(agentMeta[key]).fold(() => catalogEntry, (override) => ({
|
|
308
|
+
...catalogEntry,
|
|
309
|
+
...override
|
|
310
|
+
}));
|
|
303
311
|
return Right({
|
|
304
312
|
...resolved,
|
|
305
|
-
[key]:
|
|
306
|
-
...catalogEntry,
|
|
307
|
-
...agentMeta[key]
|
|
308
|
-
} : catalogEntry
|
|
313
|
+
[key]: merged
|
|
309
314
|
});
|
|
310
315
|
}), Right({}));
|
|
311
316
|
};
|
|
@@ -372,12 +377,12 @@ const validateOneSecret = (key, meta, secretEntries) => {
|
|
|
372
377
|
kind: "secret",
|
|
373
378
|
field: "encrypted_value"
|
|
374
379
|
});
|
|
375
|
-
return parseRef(ref).
|
|
380
|
+
return parseRef(ref).toEither({
|
|
376
381
|
_tag: "AliasInvalidSyntax",
|
|
377
382
|
key,
|
|
378
383
|
kind: "secret",
|
|
379
384
|
value: ref
|
|
380
|
-
})
|
|
385
|
+
}).flatMap((parsed) => {
|
|
381
386
|
if (parsed.kind !== "secret") return Left({
|
|
382
387
|
_tag: "AliasCrossType",
|
|
383
388
|
key,
|
|
@@ -388,23 +393,23 @@ const validateOneSecret = (key, meta, secretEntries) => {
|
|
|
388
393
|
_tag: "AliasSelfReference",
|
|
389
394
|
key: `secret.${key}`
|
|
390
395
|
});
|
|
391
|
-
return Option(secretEntries[parsed.key]).
|
|
396
|
+
return Option(secretEntries[parsed.key]).toEither({
|
|
392
397
|
_tag: "AliasTargetMissing",
|
|
393
398
|
key: `secret.${key}`,
|
|
394
399
|
target: ref
|
|
395
|
-
})
|
|
400
|
+
}).flatMap((target) => {
|
|
396
401
|
if (target.from_key !== void 0) return Left({
|
|
397
402
|
_tag: "AliasChained",
|
|
398
403
|
key: `secret.${key}`,
|
|
399
404
|
target: ref
|
|
400
405
|
});
|
|
401
|
-
return Right(
|
|
406
|
+
return Right({
|
|
402
407
|
kind: "secret",
|
|
403
408
|
targetKind: "secret",
|
|
404
409
|
targetKey: parsed.key
|
|
405
|
-
})
|
|
410
|
+
});
|
|
406
411
|
});
|
|
407
|
-
});
|
|
412
|
+
}).map((entry) => Option(entry));
|
|
408
413
|
};
|
|
409
414
|
const validateOneEnv = (key, meta, envEntries) => {
|
|
410
415
|
if (meta.from_key === void 0) return Right(Option(void 0));
|
|
@@ -415,12 +420,12 @@ const validateOneEnv = (key, meta, envEntries) => {
|
|
|
415
420
|
kind: "env",
|
|
416
421
|
field: "value"
|
|
417
422
|
});
|
|
418
|
-
return parseRef(ref).
|
|
423
|
+
return parseRef(ref).toEither({
|
|
419
424
|
_tag: "AliasInvalidSyntax",
|
|
420
425
|
key,
|
|
421
426
|
kind: "env",
|
|
422
427
|
value: ref
|
|
423
|
-
})
|
|
428
|
+
}).flatMap((parsed) => {
|
|
424
429
|
if (parsed.kind !== "env") return Left({
|
|
425
430
|
_tag: "AliasCrossType",
|
|
426
431
|
key,
|
|
@@ -431,57 +436,32 @@ const validateOneEnv = (key, meta, envEntries) => {
|
|
|
431
436
|
_tag: "AliasSelfReference",
|
|
432
437
|
key: `env.${key}`
|
|
433
438
|
});
|
|
434
|
-
return Option(envEntries[parsed.key]).
|
|
439
|
+
return Option(envEntries[parsed.key]).toEither({
|
|
435
440
|
_tag: "AliasTargetMissing",
|
|
436
441
|
key: `env.${key}`,
|
|
437
442
|
target: ref
|
|
438
|
-
})
|
|
443
|
+
}).flatMap((target) => {
|
|
439
444
|
if (target.from_key !== void 0) return Left({
|
|
440
445
|
_tag: "AliasChained",
|
|
441
446
|
key: `env.${key}`,
|
|
442
447
|
target: ref
|
|
443
448
|
});
|
|
444
|
-
return Right(
|
|
449
|
+
return Right({
|
|
445
450
|
kind: "env",
|
|
446
451
|
targetKind: "env",
|
|
447
452
|
targetKey: parsed.key
|
|
448
|
-
})
|
|
453
|
+
});
|
|
449
454
|
});
|
|
450
|
-
});
|
|
455
|
+
}).map((entry) => Option(entry));
|
|
451
456
|
};
|
|
452
|
-
/**
|
|
453
|
-
|
|
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
|
-
*/
|
|
457
|
+
/** Fail-fast reduction: accumulates validated entries, short-circuits on first AliasError */
|
|
458
|
+
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
459
|
const validateAliases = (config) => {
|
|
467
460
|
const secretEntries = config.secret ?? {};
|
|
468
461
|
const envEntries = config.env ?? {};
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
|
|
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 });
|
|
462
|
+
const secretPairs = collectValidated(Object.entries(secretEntries), (k, m) => validateOneSecret(k, m, secretEntries), "secret");
|
|
463
|
+
const envPairs = collectValidated(Object.entries(envEntries), (k, m) => validateOneEnv(k, m, envEntries), "env");
|
|
464
|
+
return secretPairs.flatMap((secrets) => envPairs.map((envs) => [...secrets, ...envs])).map((allEntries) => ({ entries: new Map(allEntries) }));
|
|
485
465
|
};
|
|
486
466
|
/** Does this secret entry point at another entry? */
|
|
487
467
|
const isSecretAlias = (meta) => meta?.from_key !== void 0;
|
|
@@ -555,7 +535,7 @@ const formatPacket = (result, options) => {
|
|
|
555
535
|
if (envEntriesArr.length > 0) {
|
|
556
536
|
const envHeader = `env: ${envEntriesArr.length}`;
|
|
557
537
|
const envLines = envEntriesArr.map(([key, meta]) => {
|
|
558
|
-
return ` ${key}${meta.from_key ? ` [alias → ${meta.from_key}]` : ""}${meta.value
|
|
538
|
+
return ` ${key}${meta.from_key ? ` [alias → ${meta.from_key}]` : ""}${Option(meta.value).fold(() => "", (v) => ` = ${v}`)}${meta.purpose ? `\n purpose: ${meta.purpose}` : ""}`;
|
|
559
539
|
});
|
|
560
540
|
sections.push([envHeader, ...envLines].join("\n"));
|
|
561
541
|
}
|
|
@@ -589,20 +569,26 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
589
569
|
const issues = [];
|
|
590
570
|
const created = Option(meta.created).flatMap(parseDate);
|
|
591
571
|
const expires = Option(meta.expires).flatMap(parseDate);
|
|
572
|
+
const lastRotated = Option(meta.last_rotated_at).flatMap(parseDate);
|
|
592
573
|
const rotationUrl = Option(meta.rotation_url);
|
|
593
574
|
const purpose = Option(meta.purpose);
|
|
594
575
|
const service = Option(meta.service);
|
|
595
576
|
const daysRemaining = expires.map((exp) => daysBetween(today, exp));
|
|
596
|
-
const
|
|
577
|
+
const staleFromRotation = lastRotated.isSome();
|
|
578
|
+
const daysSinceRotation = (staleFromRotation ? lastRotated : created).map((d) => daysBetween(d, today));
|
|
597
579
|
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
598
580
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
599
|
-
const isStale =
|
|
581
|
+
const isStale = daysSinceRotation.fold(() => false, (d) => d > staleWarningDays);
|
|
600
582
|
const hasSealed = !!meta.encrypted_value;
|
|
601
583
|
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
602
584
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
603
585
|
if (isExpired) issues.push("Secret has expired");
|
|
604
586
|
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
605
|
-
if (isStale)
|
|
587
|
+
if (isStale) {
|
|
588
|
+
const since = daysSinceRotation.fold(() => "?", (d) => String(d));
|
|
589
|
+
const label = staleFromRotation ? "last rotated" : "created";
|
|
590
|
+
issues.push(`Secret is stale (${label} ${since} days ago)`);
|
|
591
|
+
}
|
|
606
592
|
if (isMissing) issues.push("Key not found in fnox");
|
|
607
593
|
if (isMissingMetadata) {
|
|
608
594
|
if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
|
|
@@ -617,6 +603,7 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
617
603
|
purpose,
|
|
618
604
|
created: Option(meta.created),
|
|
619
605
|
expires: Option(meta.expires),
|
|
606
|
+
last_rotated_at: Option(meta.last_rotated_at),
|
|
620
607
|
issues: List(issues),
|
|
621
608
|
alias_of: Option(void 0)
|
|
622
609
|
};
|
|
@@ -632,9 +619,10 @@ const classifyAlias = (key, meta, targetHealth, targetRef) => ({
|
|
|
632
619
|
status: targetHealth.status,
|
|
633
620
|
days_remaining: targetHealth.days_remaining,
|
|
634
621
|
rotation_url: targetHealth.rotation_url,
|
|
635
|
-
purpose:
|
|
622
|
+
purpose: Option(meta.purpose).fold(() => targetHealth.purpose, (v) => Option(v)),
|
|
636
623
|
created: targetHealth.created,
|
|
637
624
|
expires: targetHealth.expires,
|
|
625
|
+
last_rotated_at: targetHealth.last_rotated_at,
|
|
638
626
|
issues: List([]),
|
|
639
627
|
alias_of: Option(targetRef)
|
|
640
628
|
});
|
|
@@ -648,17 +636,18 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
648
636
|
const secretEntries = config.secret ?? {};
|
|
649
637
|
const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
650
638
|
const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
|
|
651
|
-
const nonAliasMetaKeys =
|
|
639
|
+
const nonAliasMetaKeys = Set$1(nonAliasEntries.map(([k]) => k));
|
|
652
640
|
const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
|
|
653
|
-
const healthByKey =
|
|
641
|
+
const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
|
|
654
642
|
const parseTargetKey = (from_key) => {
|
|
655
|
-
return /^secret\.(.+)
|
|
643
|
+
return Option(from_key.match(/^secret\.(.+)$/)?.[1]);
|
|
656
644
|
};
|
|
657
645
|
const aliasHealth = aliasEntries.map(([key, meta]) => {
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
|
|
646
|
+
const tableEntry = aliasTable?.entries.get(`secret.${key}`);
|
|
647
|
+
const targetKey = Option(tableEntry?.targetKey).fold(() => Option(meta.from_key).flatMap(parseTargetKey), (k) => Option(k));
|
|
648
|
+
const targetHealth = targetKey.flatMap((k) => healthByKey.get(k));
|
|
649
|
+
const targetRef = Option(meta.from_key).fold(() => targetKey.map((k) => `secret.${k}`), (v) => Option(v)).orElse("");
|
|
650
|
+
return targetHealth.fold(() => ({
|
|
662
651
|
key,
|
|
663
652
|
service: Option(meta.service),
|
|
664
653
|
status: "missing",
|
|
@@ -667,13 +656,13 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
667
656
|
purpose: Option(meta.purpose),
|
|
668
657
|
created: Option(meta.created),
|
|
669
658
|
expires: Option(meta.expires),
|
|
659
|
+
last_rotated_at: Option(meta.last_rotated_at),
|
|
670
660
|
issues: List(["Alias target not resolvable"]),
|
|
671
661
|
alias_of: Option(targetRef)
|
|
672
|
-
};
|
|
673
|
-
return classifyAlias(key, meta, targetHealth, targetRef);
|
|
662
|
+
}), (health) => classifyAlias(key, meta, health, targetRef));
|
|
674
663
|
});
|
|
675
664
|
const secrets = List([...nonAliasHealth, ...aliasHealth]);
|
|
676
|
-
const orphaned = keys.size > 0 ?
|
|
665
|
+
const orphaned = keys.size > 0 ? nonAliasMetaKeys.toArray().filter((k) => !keys.has(k)).length : 0;
|
|
677
666
|
const total = secrets.size;
|
|
678
667
|
const expired = secrets.count((s) => s.status === "expired");
|
|
679
668
|
const missing = secrets.count((s) => s.status === "missing");
|
|
@@ -699,12 +688,14 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
699
688
|
};
|
|
700
689
|
const computeEnvAudit = (config, env = process.env) => {
|
|
701
690
|
const envEntries = config.env ?? {};
|
|
691
|
+
const resolveEffectiveDefault = (entry) => {
|
|
692
|
+
return Do(function* () {
|
|
693
|
+
return yield* $(Option((yield* $(Option(envEntries[yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]))]))).value));
|
|
694
|
+
}).orElse(entry.value ?? "");
|
|
695
|
+
};
|
|
702
696
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
703
697
|
const currentValue = env[key];
|
|
704
|
-
const effectiveDefault = entry
|
|
705
|
-
const targetKey = /^env\.(.+)$/.exec(entry.from_key)?.[1];
|
|
706
|
-
return (targetKey !== void 0 ? envEntries[targetKey] : void 0)?.value ?? "";
|
|
707
|
-
})() : entry.value ?? "";
|
|
698
|
+
const effectiveDefault = resolveEffectiveDefault(entry);
|
|
708
699
|
return {
|
|
709
700
|
key,
|
|
710
701
|
defaultValue: effectiveDefault,
|
|
@@ -724,7 +715,7 @@ const computeEnvAudit = (config, env = process.env) => {
|
|
|
724
715
|
};
|
|
725
716
|
//#endregion
|
|
726
717
|
//#region src/core/patterns.ts
|
|
727
|
-
const EXCLUDED_VARS =
|
|
718
|
+
const EXCLUDED_VARS = Set$1([
|
|
728
719
|
"PATH",
|
|
729
720
|
"HOME",
|
|
730
721
|
"USER",
|
|
@@ -1437,25 +1428,22 @@ const envScan = (env, options) => {
|
|
|
1437
1428
|
};
|
|
1438
1429
|
};
|
|
1439
1430
|
const parseAliasRef = (raw, expectedKind) => {
|
|
1440
|
-
const match = /^(secret|env)\.(.+)
|
|
1441
|
-
if (
|
|
1442
|
-
|
|
1443
|
-
return match[2];
|
|
1431
|
+
const match = raw.match(/^(secret|env)\.(.+)$/);
|
|
1432
|
+
if (match?.[1] !== expectedKind) return Option(void 0);
|
|
1433
|
+
return Option(match[2]);
|
|
1444
1434
|
};
|
|
1445
1435
|
/** Bidirectional drift detection between config and live environment */
|
|
1446
1436
|
const envCheck = (config, env) => {
|
|
1447
1437
|
const secretEntries = config.secret ?? {};
|
|
1448
1438
|
const metaKeys = Object.keys(secretEntries);
|
|
1449
|
-
const
|
|
1439
|
+
const metaKeysSet = Set$1(metaKeys);
|
|
1450
1440
|
const isSecretPresent = (key) => {
|
|
1451
1441
|
if (env[key] !== void 0 && env[key] !== "") return true;
|
|
1452
1442
|
const meta = secretEntries[key];
|
|
1453
1443
|
if (meta?.from_key === void 0) return false;
|
|
1454
|
-
|
|
1455
|
-
return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
|
|
1444
|
+
return parseAliasRef(meta.from_key, "secret").fold(() => false, (targetKey) => env[targetKey] !== void 0 && env[targetKey] !== "");
|
|
1456
1445
|
};
|
|
1457
|
-
const secretDriftEntries =
|
|
1458
|
-
const meta = secretEntries[key];
|
|
1446
|
+
const secretDriftEntries = Object.entries(secretEntries).map(([key, meta]) => {
|
|
1459
1447
|
const present = isSecretPresent(key);
|
|
1460
1448
|
return {
|
|
1461
1449
|
envVar: key,
|
|
@@ -1469,14 +1457,9 @@ const envCheck = (config, env) => {
|
|
|
1469
1457
|
if (env[key] !== void 0 && env[key] !== "") return true;
|
|
1470
1458
|
const meta = envDefaults[key];
|
|
1471
1459
|
if (meta?.from_key === void 0) return false;
|
|
1472
|
-
|
|
1473
|
-
return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
|
|
1460
|
+
return parseAliasRef(meta.from_key, "env").fold(() => false, (targetKey) => env[targetKey] !== void 0 && env[targetKey] !== "");
|
|
1474
1461
|
};
|
|
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) => {
|
|
1462
|
+
const envDefaultEntries = Object.keys(envDefaults).filter((key) => !metaKeysSet.has(key)).map((key) => {
|
|
1480
1463
|
const present = isEnvPresent(key);
|
|
1481
1464
|
return {
|
|
1482
1465
|
envVar: key,
|
|
@@ -1485,7 +1468,8 @@ const envCheck = (config, env) => {
|
|
|
1485
1468
|
confidence: Option(void 0)
|
|
1486
1469
|
};
|
|
1487
1470
|
});
|
|
1488
|
-
const
|
|
1471
|
+
const trackedKeys = Set$1([...metaKeys, ...envDefaultEntries.map((e) => e.envVar)]);
|
|
1472
|
+
const untrackedEntries = scanEnv(env).filter((match) => !trackedKeys.has(match.envVar)).map((match) => ({
|
|
1489
1473
|
envVar: match.envVar,
|
|
1490
1474
|
service: match.service,
|
|
1491
1475
|
status: "untracked",
|
|
@@ -1843,11 +1827,12 @@ const sealSecrets = (meta, values, recipient) => {
|
|
|
1843
1827
|
message: "age CLI not found on PATH"
|
|
1844
1828
|
});
|
|
1845
1829
|
return Object.entries(meta).reduce((acc, [key, secretMeta]) => acc.flatMap((result) => {
|
|
1846
|
-
|
|
1830
|
+
const value = values[key];
|
|
1831
|
+
if (value === void 0) return Right({
|
|
1847
1832
|
...result,
|
|
1848
1833
|
[key]: secretMeta
|
|
1849
1834
|
});
|
|
1850
|
-
return ageEncrypt(
|
|
1835
|
+
return ageEncrypt(value, recipient).mapLeft((err) => ({
|
|
1851
1836
|
_tag: "EncryptFailed",
|
|
1852
1837
|
key,
|
|
1853
1838
|
message: err.message
|
|
@@ -1943,11 +1928,11 @@ const bootSafe = (options) => {
|
|
|
1943
1928
|
const secretEntries = config.secret ?? {};
|
|
1944
1929
|
const envEntries = config.env ?? {};
|
|
1945
1930
|
const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
|
|
1946
|
-
const aliasSecretKeys = Object.
|
|
1931
|
+
const aliasSecretKeys = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
|
|
1947
1932
|
const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
1948
|
-
const aliasEnvKeys = Object.
|
|
1933
|
+
const aliasEnvKeys = Object.entries(envEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
|
|
1949
1934
|
const nonAliasMetaKeys = Object.keys(nonAliasSecretEntries);
|
|
1950
|
-
const hasSealedValues =
|
|
1935
|
+
const hasSealedValues = Object.values(nonAliasSecretEntries).some((meta) => !!meta.encrypted_value);
|
|
1951
1936
|
const identityKeyResult = resolveIdentityKey(config, configDir);
|
|
1952
1937
|
const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
|
|
1953
1938
|
if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
|
|
@@ -1960,7 +1945,7 @@ const bootSafe = (options) => {
|
|
|
1960
1945
|
const injected = [];
|
|
1961
1946
|
const skipped = [];
|
|
1962
1947
|
warnings.push(...checkEnvMisclassification(config));
|
|
1963
|
-
const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => entry.value
|
|
1948
|
+
const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
|
|
1964
1949
|
const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
|
|
1965
1950
|
if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
|
|
1966
1951
|
process.env[key] = value;
|
|
@@ -2020,21 +2005,20 @@ const bootSafe = (options) => {
|
|
|
2020
2005
|
aliasSecretKeys.forEach((aliasKey) => {
|
|
2021
2006
|
const entry = aliasTable.entries.get(`secret.${aliasKey}`);
|
|
2022
2007
|
if (!entry) return;
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
injected.push(aliasKey);
|
|
2027
|
-
log.debug("phase.alias.copied", {
|
|
2008
|
+
Option(secrets[entry.targetKey]).fold(() => {
|
|
2009
|
+
skipped.push(aliasKey);
|
|
2010
|
+
log.debug("phase.alias.target_unresolved", {
|
|
2028
2011
|
alias: aliasKey,
|
|
2029
2012
|
target: entry.targetKey
|
|
2030
2013
|
});
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2014
|
+
}, (targetValue) => {
|
|
2015
|
+
secrets[aliasKey] = targetValue;
|
|
2016
|
+
injected.push(aliasKey);
|
|
2017
|
+
log.debug("phase.alias.copied", {
|
|
2034
2018
|
alias: aliasKey,
|
|
2035
2019
|
target: entry.targetKey
|
|
2036
2020
|
});
|
|
2037
|
-
}
|
|
2021
|
+
});
|
|
2038
2022
|
});
|
|
2039
2023
|
aliasEnvKeys.forEach((aliasKey) => {
|
|
2040
2024
|
const entry = aliasTable.entries.get(`env.${aliasKey}`);
|
|
@@ -2149,6 +2133,22 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
2149
2133
|
//#region src/core/toml-edit.ts
|
|
2150
2134
|
const SECTION_RE = /^\[.+\]\s*$/;
|
|
2151
2135
|
const MULTILINE_OPEN = "\"\"\"";
|
|
2136
|
+
const scanSectionBoundary = (state, line, i) => {
|
|
2137
|
+
if (state.done) return state;
|
|
2138
|
+
if (state.inMultiline) return line.includes(MULTILINE_OPEN) ? {
|
|
2139
|
+
...state,
|
|
2140
|
+
inMultiline: false
|
|
2141
|
+
} : state;
|
|
2142
|
+
if (line.includes(MULTILINE_OPEN)) return (line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1 ? {
|
|
2143
|
+
...state,
|
|
2144
|
+
inMultiline: true
|
|
2145
|
+
} : state;
|
|
2146
|
+
return SECTION_RE.test(line) ? {
|
|
2147
|
+
...state,
|
|
2148
|
+
end: i,
|
|
2149
|
+
done: true
|
|
2150
|
+
} : state;
|
|
2151
|
+
};
|
|
2152
2152
|
/**
|
|
2153
2153
|
* Find the line range [start, end) of a TOML section by its header string.
|
|
2154
2154
|
* The range includes the header line through to (but not including) the next section header or EOF.
|
|
@@ -2157,26 +2157,14 @@ const MULTILINE_OPEN = "\"\"\"";
|
|
|
2157
2157
|
const findSectionRange = (lines, sectionHeader) => {
|
|
2158
2158
|
const start = lines.findIndex((l) => l.trim() === sectionHeader);
|
|
2159
2159
|
if (start === -1) return void 0;
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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
|
-
}
|
|
2160
|
+
const initial = {
|
|
2161
|
+
end: lines.length,
|
|
2162
|
+
inMultiline: false,
|
|
2163
|
+
done: false
|
|
2164
|
+
};
|
|
2177
2165
|
return {
|
|
2178
2166
|
start,
|
|
2179
|
-
end
|
|
2167
|
+
end: List(lines.slice(start + 1)).zipWithIndex().foldLeft(initial)((state, entry) => scanSectionBoundary(state, entry[0], start + 1 + entry[1])).end
|
|
2180
2168
|
};
|
|
2181
2169
|
};
|
|
2182
2170
|
/** Check whether a section header exists in the raw TOML */
|
|
@@ -2192,12 +2180,10 @@ const removeSection = (raw, sectionHeader) => {
|
|
|
2192
2180
|
_tag: "SectionNotFound",
|
|
2193
2181
|
section: sectionHeader
|
|
2194
2182
|
});
|
|
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
2183
|
const after = lines.slice(range.end);
|
|
2199
|
-
|
|
2200
|
-
const
|
|
2184
|
+
const beforeAll = lines.slice(0, range.start);
|
|
2185
|
+
const lastNonBlank = beforeAll.findLastIndex((l) => l.trim() !== "");
|
|
2186
|
+
const result = [...lastNonBlank === -1 ? [] : beforeAll.slice(0, lastNonBlank + 1), ...after].join("\n");
|
|
2201
2187
|
return Either.right(result);
|
|
2202
2188
|
};
|
|
2203
2189
|
/**
|
|
@@ -2233,55 +2219,47 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
|
2233
2219
|
const before = lines.slice(0, range.start + 1);
|
|
2234
2220
|
const after = lines.slice(range.end);
|
|
2235
2221
|
const sectionBody = lines.slice(range.start + 1, range.end);
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
} else {
|
|
2248
|
-
if (updates[multilineKey] === null) continue;
|
|
2249
|
-
if (multilineKey in updates) continue;
|
|
2250
|
-
}
|
|
2251
|
-
remaining.push(line);
|
|
2252
|
-
continue;
|
|
2253
|
-
}
|
|
2222
|
+
const findClosingMultiline = (fromIdx) => {
|
|
2223
|
+
const idx = sectionBody.findIndex((l, j) => j > fromIdx && l.includes(MULTILINE_OPEN));
|
|
2224
|
+
return idx === -1 ? sectionBody.length : idx;
|
|
2225
|
+
};
|
|
2226
|
+
const initial = {
|
|
2227
|
+
remaining: [],
|
|
2228
|
+
updatedKeys: Set$1.empty(),
|
|
2229
|
+
skipUntil: -1
|
|
2230
|
+
};
|
|
2231
|
+
const step = (state, line, i) => {
|
|
2232
|
+
if (i <= state.skipUntil) return state;
|
|
2254
2233
|
const eqIdx = line.indexOf("=");
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
break;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
continue;
|
|
2276
|
-
}
|
|
2234
|
+
const isFieldLine = eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[");
|
|
2235
|
+
const key = isFieldLine ? line.slice(0, eqIdx).trim() : "";
|
|
2236
|
+
if (isFieldLine && key in updates) {
|
|
2237
|
+
const afterEquals = line.slice(eqIdx + 1).trim();
|
|
2238
|
+
const skipUntil = afterEquals.includes(MULTILINE_OPEN) && (afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1 ? findClosingMultiline(i) : state.skipUntil;
|
|
2239
|
+
const updatedKeys = state.updatedKeys.add(key);
|
|
2240
|
+
const value = updates[key];
|
|
2241
|
+
if (value === null) return {
|
|
2242
|
+
...state,
|
|
2243
|
+
updatedKeys,
|
|
2244
|
+
skipUntil
|
|
2245
|
+
};
|
|
2246
|
+
return {
|
|
2247
|
+
remaining: [...state.remaining, `${key} = ${value}`],
|
|
2248
|
+
updatedKeys,
|
|
2249
|
+
skipUntil
|
|
2250
|
+
};
|
|
2277
2251
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2252
|
+
return {
|
|
2253
|
+
...state,
|
|
2254
|
+
remaining: [...state.remaining, line]
|
|
2255
|
+
};
|
|
2256
|
+
};
|
|
2257
|
+
const final = List(sectionBody).zipWithIndex().foldLeft(initial)((state, entry) => step(state, entry[0], entry[1]));
|
|
2258
|
+
const newFields = Object.entries(updates).filter(([key, value]) => value !== null && !final.updatedKeys.has(key)).map(([key, value]) => `${key} = ${value}`);
|
|
2282
2259
|
const result = [
|
|
2283
2260
|
...before,
|
|
2284
|
-
...remaining,
|
|
2261
|
+
...final.remaining,
|
|
2262
|
+
...newFields,
|
|
2285
2263
|
...after
|
|
2286
2264
|
].join("\n");
|
|
2287
2265
|
return Either.right(result);
|
|
@@ -2294,31 +2272,7 @@ const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
|
|
|
2294
2272
|
//#endregion
|
|
2295
2273
|
//#region src/core/fleet.ts
|
|
2296
2274
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
2297
|
-
const SKIP_DIRS =
|
|
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
|
-
]);
|
|
2275
|
+
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
2276
|
function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
|
|
2323
2277
|
if (currentDepth > maxDepth) return;
|
|
2324
2278
|
const configPath = join(dir, CONFIG_FILENAME);
|
|
@@ -2585,24 +2539,19 @@ const handleGetSecretMeta = (args) => {
|
|
|
2585
2539
|
const secretEntries = config.secret ?? {};
|
|
2586
2540
|
return Option(secretEntries[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
|
|
2587
2541
|
const { encrypted_value: _, from_key: fromKey, ...rest } = meta;
|
|
2588
|
-
if (fromKey !== void 0) {
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
key,
|
|
2595
|
-
...targetRest,
|
|
2596
|
-
...rest,
|
|
2597
|
-
alias_of: fromKey
|
|
2598
|
-
}, null, 2));
|
|
2599
|
-
}
|
|
2542
|
+
if (fromKey !== void 0) return Option(fromKey.match(/^secret\.(.+)$/)?.[1]).flatMap((k) => Option(secretEntries[k])).fold(() => textResult(JSON.stringify({
|
|
2543
|
+
key,
|
|
2544
|
+
...rest,
|
|
2545
|
+
alias_of: fromKey
|
|
2546
|
+
}, null, 2)), (t) => {
|
|
2547
|
+
const { encrypted_value: __, from_key: ___, ...targetRest } = t;
|
|
2600
2548
|
return textResult(JSON.stringify({
|
|
2601
2549
|
key,
|
|
2550
|
+
...targetRest,
|
|
2602
2551
|
...rest,
|
|
2603
2552
|
alias_of: fromKey
|
|
2604
2553
|
}, null, 2));
|
|
2605
|
-
}
|
|
2554
|
+
});
|
|
2606
2555
|
return textResult(JSON.stringify({
|
|
2607
2556
|
key,
|
|
2608
2557
|
...rest
|