envpkt 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,6 +141,29 @@ value = "info"
141
141
  purpose = "Application log verbosity"
142
142
  ```
143
143
 
144
+ ### Aliases
145
+
146
+ When a consumer hardcodes a different env var name than the one you govern
147
+ canonically, use `from_key` to expose the same value under a second name —
148
+ without duplicating the secret:
149
+
150
+ ```toml
151
+ [secret.API_KEY]
152
+ service = "stripe"
153
+ expires = "2027-01-15"
154
+ rotation_url = "https://dashboard.stripe.com/apikeys"
155
+
156
+ # Same governed value, under a legacy name some consumer expects
157
+ [secret.STRIPE_SECRET_KEY]
158
+ from_key = "secret.API_KEY"
159
+ ```
160
+
161
+ Both names are injected at boot, both appear in audit output, and expiration
162
+ tracking lives on the target — an alias is healthy iff its target is. Same
163
+ pattern works for `[env.*]`. Cross-type aliasing (secret → env) is rejected
164
+ at load time. See [TOML Schema → Aliases](https://envpkt.dev/reference/toml-schema/#aliases)
165
+ for the full rules.
166
+
144
167
  See [`examples/`](./examples/) for more configurations.
145
168
 
146
169
  ## Sealed Packets
@@ -150,21 +173,22 @@ Sealed packets embed age-encrypted secret values directly in `envpkt.toml`. This
150
173
  ### Setup
151
174
 
152
175
  ```bash
153
- # Generate an age keypair
154
- age-keygen -o identity.txt
155
- # public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
176
+ # Generate an age keypair — writes to ~/.envpkt/<project>-key.txt and updates envpkt.toml
177
+ envpkt keygen
156
178
  ```
157
179
 
158
- Add the public key to your config and the identity file to `.gitignore`:
180
+ This writes `[identity]` with `name`, `recipient`, and `key_file` to your `envpkt.toml`. Add the key file to `.gitignore`:
159
181
 
160
182
  ```toml
161
183
  [identity]
162
184
  name = "my-agent"
163
185
  recipient = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
164
- key_file = "identity.txt"
186
+ key_file = "~/.envpkt/my-agent-key.txt"
165
187
  ```
166
188
 
167
- The `key_file` path supports `~` expansion and environment variables (`$VAR`, `${VAR}`), so you can use paths like `~/keys/identity.txt` or `$KEYS_DIR/identity.txt`. Relative paths are resolved from the config file's directory. When omitted, envpkt falls back to `ENVPKT_AGE_KEY_FILE` env var, then `~/.envpkt/age-key.txt`.
189
+ `envpkt keygen` defaults to a **project-specific path** (`~/.envpkt/<project>-key.txt`), so separate projects never collide. For multi-environment projects (e.g. `prod.envpkt.toml` + `dev.envpkt.toml`), each config gets its own key automatically. Pass `--global` to use the shared `~/.envpkt/age-key.txt` path instead.
190
+
191
+ The `key_file` path supports `~` expansion and environment variables (`$VAR`, `${VAR}`). Relative paths are resolved from the config file's directory. When omitted, envpkt falls back to `ENVPKT_AGE_KEY_FILE` env var, then `~/.envpkt/age-key.txt` — but it's best to set `key_file` explicitly so the config tells you which key it needs.
168
192
 
169
193
  ### Seal
170
194
 
package/dist/cli.js CHANGED
@@ -54,10 +54,28 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
54
54
  purpose,
55
55
  created: Option(meta.created),
56
56
  expires: Option(meta.expires),
57
- issues: List(issues)
57
+ issues: List(issues),
58
+ alias_of: Option(void 0)
58
59
  };
59
60
  };
60
- const computeAudit = (config, fnoxKeys, today) => {
61
+ /**
62
+ * Build a SecretHealth row for an alias entry. Status is inherited from the
63
+ * target; metadata (purpose, tags) comes from the alias entry itself where
64
+ * set, otherwise falls through to the target so operators see context.
65
+ */
66
+ const classifyAlias = (key, meta, targetHealth, targetRef) => ({
67
+ key,
68
+ service: targetHealth.service,
69
+ status: targetHealth.status,
70
+ days_remaining: targetHealth.days_remaining,
71
+ rotation_url: targetHealth.rotation_url,
72
+ purpose: meta.purpose !== void 0 ? Option(meta.purpose) : targetHealth.purpose,
73
+ created: targetHealth.created,
74
+ expires: targetHealth.expires,
75
+ issues: List([]),
76
+ alias_of: Option(targetRef)
77
+ });
78
+ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
61
79
  const now = today ?? /* @__PURE__ */ new Date();
62
80
  const lifecycle = config.lifecycle ?? {};
63
81
  const staleWarningDays = lifecycle.stale_warning_days ?? 90;
@@ -65,9 +83,31 @@ const computeAudit = (config, fnoxKeys, today) => {
65
83
  const requireService = lifecycle.require_service ?? false;
66
84
  const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
67
85
  const secretEntries = config.secret ?? {};
68
- const metaKeys = new Set(Object.keys(secretEntries));
69
- const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
70
- const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
86
+ const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
87
+ const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
88
+ const nonAliasMetaKeys = new Set(nonAliasEntries.map(([k]) => k));
89
+ const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
90
+ const healthByKey = new Map(nonAliasHealth.map((h) => [h.key, h]));
91
+ const aliasHealth = aliasEntries.map(([key, meta]) => {
92
+ const targetKey = (aliasTable?.entries.get(`secret.${key}`))?.targetKey;
93
+ const targetHealth = targetKey !== void 0 ? healthByKey.get(targetKey) : void 0;
94
+ const targetRef = meta.from_key ?? (targetKey !== void 0 ? `secret.${targetKey}` : "");
95
+ if (!targetHealth) return {
96
+ key,
97
+ service: Option(meta.service),
98
+ status: "missing",
99
+ days_remaining: Option(void 0),
100
+ rotation_url: Option(meta.rotation_url),
101
+ purpose: Option(meta.purpose),
102
+ created: Option(meta.created),
103
+ expires: Option(meta.expires),
104
+ issues: List(["Alias target not resolvable"]),
105
+ alias_of: Option(targetRef)
106
+ };
107
+ return classifyAlias(key, meta, targetHealth, targetRef);
108
+ });
109
+ const secrets = List([...nonAliasHealth, ...aliasHealth]);
110
+ const orphaned = keys.size > 0 ? [...nonAliasMetaKeys].filter((k) => !keys.has(k)).length : 0;
71
111
  const total = secrets.size;
72
112
  const expired = secrets.count((s) => s.status === "expired");
73
113
  const missing = secrets.count((s) => s.status === "missing");
@@ -75,6 +115,7 @@ const computeAudit = (config, fnoxKeys, today) => {
75
115
  const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
76
116
  const stale = secrets.count((s) => s.status === "stale");
77
117
  const healthy = secrets.count((s) => s.status === "healthy");
118
+ const aliases = aliasHealth.length;
78
119
  return {
79
120
  status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
80
121
  secrets,
@@ -86,6 +127,7 @@ const computeAudit = (config, fnoxKeys, today) => {
86
127
  missing,
87
128
  missing_metadata,
88
129
  orphaned,
130
+ aliases,
89
131
  identity: config.identity
90
132
  };
91
133
  };
@@ -93,13 +135,17 @@ const computeEnvAudit = (config, env = process.env) => {
93
135
  const envEntries = config.env ?? {};
94
136
  const entries = Object.entries(envEntries).map(([key, entry]) => {
95
137
  const currentValue = env[key];
96
- const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
138
+ const effectiveDefault = entry.from_key !== void 0 ? (() => {
139
+ const targetKey = /^env\.(.+)$/.exec(entry.from_key)?.[1];
140
+ return (targetKey !== void 0 ? envEntries[targetKey] : void 0)?.value ?? "";
141
+ })() : entry.value ?? "";
97
142
  return {
98
143
  key,
99
- defaultValue: entry.value,
144
+ defaultValue: effectiveDefault,
100
145
  currentValue,
101
- status,
102
- purpose: entry.purpose
146
+ status: Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== effectiveDefault, "overridden").else("default"),
147
+ purpose: entry.purpose,
148
+ alias_of: Option(entry.from_key)
103
149
  };
104
150
  });
105
151
  return {
@@ -158,6 +204,7 @@ const SecretMetaSchema = Type.Object({
158
204
  model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
159
205
  source: Type.Optional(Type.String({ description: "Where the secret value originates (e.g. 'vault', 'ci')" })),
160
206
  encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
207
+ from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
161
208
  required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
162
209
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
163
210
  }, { description: "Metadata about a single secret" });
@@ -182,7 +229,8 @@ const CallbackConfigSchema = Type.Object({
182
229
  }, { description: "Automation callbacks for lifecycle events" });
183
230
  const ToolsConfigSchema = Type.Record(Type.String(), Type.Unknown(), { description: "Tool integration configuration — open namespace for third-party extensions" });
184
231
  const EnvMetaSchema = Type.Object({
185
- value: Type.String({ description: "Default value for this environment variable" }),
232
+ value: Type.Optional(Type.String({ description: "Default value for this environment variable. Optional when from_key is set; required otherwise." })),
233
+ from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'env.<KEY>') whose resolved value this alias reuses. Mutually exclusive with value." })),
186
234
  purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
187
235
  comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
188
236
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
@@ -822,6 +870,140 @@ const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((er
822
870
  /** Extract the set of secret key names from a parsed fnox config */
823
871
  const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
824
872
  //#endregion
873
+ //#region src/core/alias.ts
874
+ const ALIAS_REF_RE = /^(secret|env)\.(.+)$/;
875
+ const parseRef = (raw) => {
876
+ const match = ALIAS_REF_RE.exec(raw);
877
+ if (!match) return Option(void 0);
878
+ const kind = match[1];
879
+ const key = match[2];
880
+ if (!key) return Option(void 0);
881
+ return Option({
882
+ kind,
883
+ key
884
+ });
885
+ };
886
+ const validateOneSecret = (key, meta, secretEntries) => {
887
+ if (meta.from_key === void 0) return Right(Option(void 0));
888
+ const ref = meta.from_key;
889
+ if (meta.encrypted_value !== void 0) return Left({
890
+ _tag: "AliasValueConflict",
891
+ key,
892
+ kind: "secret",
893
+ field: "encrypted_value"
894
+ });
895
+ return parseRef(ref).fold(() => Left({
896
+ _tag: "AliasInvalidSyntax",
897
+ key,
898
+ kind: "secret",
899
+ value: ref
900
+ }), (parsed) => {
901
+ if (parsed.kind !== "secret") return Left({
902
+ _tag: "AliasCrossType",
903
+ key,
904
+ kind: "secret",
905
+ targetKind: parsed.kind
906
+ });
907
+ if (parsed.key === key) return Left({
908
+ _tag: "AliasSelfReference",
909
+ key: `secret.${key}`
910
+ });
911
+ return Option(secretEntries[parsed.key]).fold(() => Left({
912
+ _tag: "AliasTargetMissing",
913
+ key: `secret.${key}`,
914
+ target: ref
915
+ }), (target) => {
916
+ if (target.from_key !== void 0) return Left({
917
+ _tag: "AliasChained",
918
+ key: `secret.${key}`,
919
+ target: ref
920
+ });
921
+ return Right(Option({
922
+ kind: "secret",
923
+ targetKind: "secret",
924
+ targetKey: parsed.key
925
+ }));
926
+ });
927
+ });
928
+ };
929
+ const validateOneEnv = (key, meta, envEntries) => {
930
+ if (meta.from_key === void 0) return Right(Option(void 0));
931
+ const ref = meta.from_key;
932
+ if (meta.value !== void 0) return Left({
933
+ _tag: "AliasValueConflict",
934
+ key,
935
+ kind: "env",
936
+ field: "value"
937
+ });
938
+ return parseRef(ref).fold(() => Left({
939
+ _tag: "AliasInvalidSyntax",
940
+ key,
941
+ kind: "env",
942
+ value: ref
943
+ }), (parsed) => {
944
+ if (parsed.kind !== "env") return Left({
945
+ _tag: "AliasCrossType",
946
+ key,
947
+ kind: "env",
948
+ targetKind: parsed.kind
949
+ });
950
+ if (parsed.key === key) return Left({
951
+ _tag: "AliasSelfReference",
952
+ key: `env.${key}`
953
+ });
954
+ return Option(envEntries[parsed.key]).fold(() => Left({
955
+ _tag: "AliasTargetMissing",
956
+ key: `env.${key}`,
957
+ target: ref
958
+ }), (target) => {
959
+ if (target.from_key !== void 0) return Left({
960
+ _tag: "AliasChained",
961
+ key: `env.${key}`,
962
+ target: ref
963
+ });
964
+ return Right(Option({
965
+ kind: "env",
966
+ targetKind: "env",
967
+ targetKey: parsed.key
968
+ }));
969
+ });
970
+ });
971
+ };
972
+ /**
973
+ * Validate all `from_key` references in a resolved config. Produces an
974
+ * AliasTable mapping each alias to its target, or an AliasError describing
975
+ * the first failure.
976
+ *
977
+ * Rules:
978
+ * - Ref must be "secret.<KEY>" or "env.<KEY>"
979
+ * - Target must exist in the same resolved config
980
+ * - Target must be the same type (secret→secret, env→env only)
981
+ * - Target must not itself be a from_key entry (single hop only)
982
+ * - Self-reference is rejected
983
+ * - An alias entry cannot also carry a value field (encrypted_value for
984
+ * secrets, value for env)
985
+ */
986
+ const validateAliases = (config) => {
987
+ const secretEntries = config.secret ?? {};
988
+ const envEntries = config.env ?? {};
989
+ const entries = /* @__PURE__ */ new Map();
990
+ const secretResults = Object.entries(secretEntries).map(([key, meta]) => [key, validateOneSecret(key, meta, secretEntries)]);
991
+ for (const [key, result] of secretResults) {
992
+ const outcome = result.fold((err) => err, (opt) => opt.orUndefined());
993
+ if (outcome === void 0) continue;
994
+ if ("_tag" in outcome) return Left(outcome);
995
+ entries.set(`secret.${key}`, outcome);
996
+ }
997
+ const envResults = Object.entries(envEntries).map(([key, meta]) => [key, validateOneEnv(key, meta, envEntries)]);
998
+ for (const [key, result] of envResults) {
999
+ const outcome = result.fold((err) => err, (opt) => opt.orUndefined());
1000
+ if (outcome === void 0) continue;
1001
+ if ("_tag" in outcome) return Left(outcome);
1002
+ entries.set(`env.${key}`, outcome);
1003
+ }
1004
+ return Right({ entries });
1005
+ };
1006
+ //#endregion
825
1007
  //#region src/core/keygen.ts
826
1008
  /** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
827
1009
  const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
@@ -1089,7 +1271,7 @@ const looksLikeSecret = (value) => {
1089
1271
  };
1090
1272
  const checkEnvMisclassification = (config) => {
1091
1273
  const envEntries = config.env ?? {};
1092
- return Object.entries(envEntries).filter(([, entry]) => looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1274
+ return Object.entries(envEntries).filter(([, entry]) => entry.value !== void 0 && looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1093
1275
  };
1094
1276
  /** Programmatic boot — returns Either<BootError, BootResult> */
1095
1277
  const bootSafe = (options) => {
@@ -1097,26 +1279,29 @@ const bootSafe = (options) => {
1097
1279
  const inject = opts.inject !== false;
1098
1280
  const failOnExpired = opts.failOnExpired !== false;
1099
1281
  const warnOnly = opts.warnOnly ?? false;
1100
- return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
1282
+ return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => validateAliases(config).fold((err) => Left(err), (aliasTable) => {
1101
1283
  const secretEntries = config.secret ?? {};
1102
- const metaKeys = Object.keys(secretEntries);
1103
- const hasSealedValues = metaKeys.some((k) => !!secretEntries[k].encrypted_value);
1284
+ const envEntries = config.env ?? {};
1285
+ const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
1286
+ const aliasSecretKeys = Object.keys(secretEntries).filter((k) => secretEntries[k].from_key !== void 0);
1287
+ const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
1288
+ const aliasEnvKeys = Object.keys(envEntries).filter((k) => envEntries[k].from_key !== void 0);
1289
+ const nonAliasMetaKeys = Object.keys(nonAliasSecretEntries);
1290
+ const hasSealedValues = nonAliasMetaKeys.some((k) => !!nonAliasSecretEntries[k].encrypted_value);
1104
1291
  const identityKeyResult = resolveIdentityKey(config, configDir);
1105
1292
  const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
1106
1293
  if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
1107
1294
  _tag: "ReadError",
1108
1295
  message: "unexpected"
1109
1296
  }));
1110
- const audit = computeAudit(config, detectFnoxKeys(configDir));
1297
+ const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
1111
1298
  return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
1112
1299
  const secrets = {};
1113
1300
  const injected = [];
1114
1301
  const skipped = [];
1115
1302
  warnings.push(...checkEnvMisclassification(config));
1116
- const envEntries = config.env ?? {};
1117
- const envEntriesArr = Object.entries(envEntries);
1118
- const envDefaults = Object.fromEntries(envEntriesArr.flatMap(([key, entry]) => Option(process.env[key]).fold(() => [[key, entry.value]], () => [])));
1119
- const overridden = envEntriesArr.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
1303
+ const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => entry.value !== void 0 ? [[key, entry.value]] : [], () => [])));
1304
+ const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
1120
1305
  if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
1121
1306
  process.env[key] = value;
1122
1307
  });
@@ -1125,7 +1310,7 @@ const bootSafe = (options) => {
1125
1310
  if (hasSealedValues) identityFilePath.fold(() => {
1126
1311
  warnings.push("Sealed values found but no identity file available for decryption");
1127
1312
  }, (idPath) => {
1128
- unsealSecrets(secretEntries, idPath).fold((err) => {
1313
+ unsealSecrets(nonAliasSecretEntries, idPath).fold((err) => {
1129
1314
  warnings.push(`Sealed value decryption failed: ${err.message}`);
1130
1315
  }, (unsealed) => {
1131
1316
  const unsealedEntries = Object.entries(unsealed);
@@ -1134,7 +1319,7 @@ const bootSafe = (options) => {
1134
1319
  unsealedEntries.map(([key]) => key).forEach((key) => sealedKeys.add(key));
1135
1320
  });
1136
1321
  });
1137
- const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
1322
+ const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
1138
1323
  if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
1139
1324
  warnings.push(`fnox export failed: ${err.message}`);
1140
1325
  skipped.push(...remainingKeys);
@@ -1152,9 +1337,34 @@ const bootSafe = (options) => {
1152
1337
  else warnings.push("fnox not available — unsealed secrets could not be resolved");
1153
1338
  skipped.push(...remainingKeys);
1154
1339
  }
1155
- if (inject) Object.entries(secrets).forEach(([key, value]) => {
1156
- process.env[key] = value;
1340
+ aliasSecretKeys.forEach((aliasKey) => {
1341
+ const entry = aliasTable.entries.get(`secret.${aliasKey}`);
1342
+ if (!entry) return;
1343
+ const targetValue = secrets[entry.targetKey];
1344
+ if (targetValue !== void 0) {
1345
+ secrets[aliasKey] = targetValue;
1346
+ injected.push(aliasKey);
1347
+ } else skipped.push(aliasKey);
1348
+ });
1349
+ aliasEnvKeys.forEach((aliasKey) => {
1350
+ const entry = aliasTable.entries.get(`env.${aliasKey}`);
1351
+ if (!entry) return;
1352
+ if (process.env[aliasKey] !== void 0) {
1353
+ overridden.push(aliasKey);
1354
+ return;
1355
+ }
1356
+ const targetEntry = envEntries[entry.targetKey];
1357
+ if (targetEntry?.value === void 0) return;
1358
+ envDefaults[aliasKey] = process.env[entry.targetKey] ?? targetEntry.value;
1157
1359
  });
1360
+ if (inject) {
1361
+ Object.entries(envDefaults).forEach(([key, value]) => {
1362
+ process.env[key] ??= value;
1363
+ });
1364
+ Object.entries(secrets).forEach(([key, value]) => {
1365
+ process.env[key] = value;
1366
+ });
1367
+ }
1158
1368
  return {
1159
1369
  audit,
1160
1370
  injected,
@@ -1167,7 +1377,7 @@ const bootSafe = (options) => {
1167
1377
  configSource
1168
1378
  };
1169
1379
  });
1170
- });
1380
+ }));
1171
1381
  };
1172
1382
  //#endregion
1173
1383
  //#region src/core/patterns.ts
@@ -1883,14 +2093,27 @@ const envScan = (env, options) => {
1883
2093
  low_confidence
1884
2094
  };
1885
2095
  };
2096
+ const parseAliasRef = (raw, expectedKind) => {
2097
+ const match = /^(secret|env)\.(.+)$/.exec(raw);
2098
+ if (!match) return void 0;
2099
+ if (match[1] !== expectedKind) return void 0;
2100
+ return match[2];
2101
+ };
1886
2102
  /** Bidirectional drift detection between config and live environment */
1887
2103
  const envCheck = (config, env) => {
1888
2104
  const secretEntries = config.secret ?? {};
1889
2105
  const metaKeys = Object.keys(secretEntries);
1890
2106
  const trackedSet = new Set(metaKeys);
2107
+ const isSecretPresent = (key) => {
2108
+ if (env[key] !== void 0 && env[key] !== "") return true;
2109
+ const meta = secretEntries[key];
2110
+ if (meta?.from_key === void 0) return false;
2111
+ const targetKey = parseAliasRef(meta.from_key, "secret");
2112
+ return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
2113
+ };
1891
2114
  const secretDriftEntries = metaKeys.map((key) => {
1892
2115
  const meta = secretEntries[key];
1893
- const present = env[key] !== void 0 && env[key] !== "";
2116
+ const present = isSecretPresent(key);
1894
2117
  return {
1895
2118
  envVar: key,
1896
2119
  service: Option(meta.service),
@@ -1899,12 +2122,19 @@ const envCheck = (config, env) => {
1899
2122
  };
1900
2123
  });
1901
2124
  const envDefaults = config.env ?? {};
2125
+ const isEnvPresent = (key) => {
2126
+ if (env[key] !== void 0 && env[key] !== "") return true;
2127
+ const meta = envDefaults[key];
2128
+ if (meta?.from_key === void 0) return false;
2129
+ const targetKey = parseAliasRef(meta.from_key, "env");
2130
+ return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
2131
+ };
1902
2132
  const envDefaultEntries = Object.keys(envDefaults).filter((key) => {
1903
2133
  if (trackedSet.has(key)) return false;
1904
2134
  trackedSet.add(key);
1905
2135
  return true;
1906
2136
  }).map((key) => {
1907
- const present = env[key] !== void 0 && env[key] !== "";
2137
+ const present = isEnvPresent(key);
1908
2138
  return {
1909
2139
  envVar: key,
1910
2140
  service: Option(void 0),
@@ -2193,7 +2423,9 @@ const runEnvExport = (options) => {
2193
2423
  if (boot.overridden.length > 0) loadConfig(boot.configPath).fold(() => {}, (config) => {
2194
2424
  const envEntries = config.env ?? {};
2195
2425
  boot.overridden.forEach((key) => {
2196
- if (key in envEntries) console.log(`export ${key}='${shellEscape(envEntries[key].value)}'`);
2426
+ if (!(key in envEntries)) return;
2427
+ const value = envEntries[key].value ?? process.env[key] ?? "";
2428
+ console.log(`export ${key}='${shellEscape(value)}'`);
2197
2429
  });
2198
2430
  });
2199
2431
  Object.entries(boot.secrets).forEach(([key, value]) => {
@@ -2969,6 +3201,7 @@ const handleGetPacketHealth = (args) => {
2969
3201
  status: s.status,
2970
3202
  days_remaining: s.days_remaining.fold(() => null, (d) => d),
2971
3203
  rotation_url: s.rotation_url.fold(() => null, (u) => u),
3204
+ alias_of: s.alias_of.fold(() => null, (a) => a),
2972
3205
  issues: s.issues.toArray()
2973
3206
  }));
2974
3207
  return textResult(JSON.stringify({
@@ -2980,6 +3213,7 @@ const handleGetPacketHealth = (args) => {
2980
3213
  expired: audit.expired,
2981
3214
  stale: audit.stale,
2982
3215
  missing: audit.missing,
3216
+ aliases: audit.aliases,
2983
3217
  secrets: secretDetails
2984
3218
  }, null, 2));
2985
3219
  };
@@ -3009,11 +3243,30 @@ const handleGetSecretMeta = (args) => {
3009
3243
  const loaded = loadConfigForTool(configPathArg(args));
3010
3244
  if (!loaded.ok) return loaded.result;
3011
3245
  const { config } = loaded;
3012
- return Option((config.secret ?? {})[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
3013
- const { encrypted_value: _, ...safeMeta } = meta;
3246
+ const secretEntries = config.secret ?? {};
3247
+ return Option(secretEntries[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
3248
+ const { encrypted_value: _, from_key: fromKey, ...rest } = meta;
3249
+ if (fromKey !== void 0) {
3250
+ const targetKey = /^secret\.(.+)$/.exec(fromKey)?.[1];
3251
+ const target = targetKey !== void 0 ? secretEntries[targetKey] : void 0;
3252
+ if (target) {
3253
+ const { encrypted_value: __, from_key: ___, ...targetRest } = target;
3254
+ return textResult(JSON.stringify({
3255
+ key,
3256
+ ...targetRest,
3257
+ ...rest,
3258
+ alias_of: fromKey
3259
+ }, null, 2));
3260
+ }
3261
+ return textResult(JSON.stringify({
3262
+ key,
3263
+ ...rest,
3264
+ alias_of: fromKey
3265
+ }, null, 2));
3266
+ }
3014
3267
  return textResult(JSON.stringify({
3015
3268
  key,
3016
- ...safeMeta
3269
+ ...rest
3017
3270
  }, null, 2));
3018
3271
  });
3019
3272
  };
package/dist/index.d.ts CHANGED
@@ -44,6 +44,7 @@ declare const SecretMetaSchema: _$_sinclair_typebox0.TObject<{
44
44
  model_hint: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
45
45
  source: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
46
46
  encrypted_value: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
47
+ from_key: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
47
48
  required: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TBoolean>;
48
49
  tags: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TRecord<_$_sinclair_typebox0.TString, _$_sinclair_typebox0.TString>>;
49
50
  }>;
@@ -63,7 +64,8 @@ type CallbackConfig = Static<typeof CallbackConfigSchema>;
63
64
  declare const ToolsConfigSchema: _$_sinclair_typebox0.TRecord<_$_sinclair_typebox0.TString, _$_sinclair_typebox0.TUnknown>;
64
65
  type ToolsConfig = Static<typeof ToolsConfigSchema>;
65
66
  declare const EnvMetaSchema: _$_sinclair_typebox0.TObject<{
66
- value: _$_sinclair_typebox0.TString;
67
+ value: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
68
+ from_key: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
67
69
  purpose: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
68
70
  comment: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
69
71
  tags: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TRecord<_$_sinclair_typebox0.TString, _$_sinclair_typebox0.TString>>;
@@ -96,11 +98,13 @@ declare const EnvpktConfigSchema: _$_sinclair_typebox0.TObject<{
96
98
  model_hint: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
97
99
  source: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
98
100
  encrypted_value: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
101
+ from_key: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
99
102
  required: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TBoolean>;
100
103
  tags: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TRecord<_$_sinclair_typebox0.TString, _$_sinclair_typebox0.TString>>;
101
104
  }>>>;
102
105
  env: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TRecord<_$_sinclair_typebox0.TString, _$_sinclair_typebox0.TObject<{
103
- value: _$_sinclair_typebox0.TString;
106
+ value: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
107
+ from_key: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
104
108
  purpose: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
105
109
  comment: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TString>;
106
110
  tags: _$_sinclair_typebox0.TOptional<_$_sinclair_typebox0.TRecord<_$_sinclair_typebox0.TString, _$_sinclair_typebox0.TString>>;
@@ -133,7 +137,8 @@ type SecretHealth = {
133
137
  readonly purpose: Option<string>;
134
138
  readonly created: Option<string>;
135
139
  readonly expires: Option<string>;
136
- readonly issues: List<string>;
140
+ readonly issues: List<string>; /** If this entry is an alias (from_key), the reference it points at (e.g. "secret.X") */
141
+ readonly alias_of: Option<string>;
137
142
  };
138
143
  type AuditResult = {
139
144
  readonly status: HealthStatus;
@@ -145,7 +150,8 @@ type AuditResult = {
145
150
  readonly stale: number;
146
151
  readonly missing: number;
147
152
  readonly missing_metadata: number;
148
- readonly orphaned: number;
153
+ readonly orphaned: number; /** Count of entries that are aliases (from_key). Included in `secrets` but reported separately for visibility. */
154
+ readonly aliases: number;
149
155
  readonly identity?: Identity;
150
156
  };
151
157
  type EnvDriftStatus = "default" | "overridden" | "missing";
@@ -154,7 +160,8 @@ type EnvDriftEntry = {
154
160
  readonly defaultValue: string;
155
161
  readonly currentValue: string | undefined;
156
162
  readonly status: EnvDriftStatus;
157
- readonly purpose: string | undefined;
163
+ readonly purpose: string | undefined; /** If this entry is an alias (from_key), the reference it points at (e.g. "env.X") */
164
+ readonly alias_of: Option<string>;
158
165
  };
159
166
  type EnvAuditResult = {
160
167
  readonly entries: ReadonlyArray<EnvDriftEntry>;
@@ -238,6 +245,40 @@ type CatalogError = {
238
245
  readonly _tag: "MissingSecretsList";
239
246
  readonly message: string;
240
247
  };
248
+ type AliasTable = {
249
+ /** key → { type: "secret"|"env", targetType, targetKey } for every alias entry */readonly entries: ReadonlyMap<string, {
250
+ readonly kind: "secret" | "env";
251
+ readonly targetKind: "secret" | "env";
252
+ readonly targetKey: string;
253
+ }>;
254
+ };
255
+ type AliasError = {
256
+ readonly _tag: "AliasInvalidSyntax";
257
+ readonly key: string;
258
+ readonly kind: "secret" | "env";
259
+ readonly value: string;
260
+ } | {
261
+ readonly _tag: "AliasTargetMissing";
262
+ readonly key: string;
263
+ readonly target: string;
264
+ } | {
265
+ readonly _tag: "AliasSelfReference";
266
+ readonly key: string;
267
+ } | {
268
+ readonly _tag: "AliasChained";
269
+ readonly key: string;
270
+ readonly target: string;
271
+ } | {
272
+ readonly _tag: "AliasCrossType";
273
+ readonly key: string;
274
+ readonly kind: "secret" | "env";
275
+ readonly targetKind: "secret" | "env";
276
+ } | {
277
+ readonly _tag: "AliasValueConflict";
278
+ readonly key: string;
279
+ readonly kind: "secret" | "env";
280
+ readonly field: string;
281
+ };
241
282
  type BootOptions = {
242
283
  readonly configPath?: string;
243
284
  readonly profile?: string;
@@ -256,7 +297,7 @@ type BootResult = {
256
297
  readonly configPath: string;
257
298
  readonly configSource: ConfigSource;
258
299
  };
259
- type BootError = ConfigError | FnoxError | CatalogError | {
300
+ type BootError = ConfigError | FnoxError | CatalogError | AliasError | {
260
301
  readonly _tag: "AuditFailed";
261
302
  readonly audit: AuditResult;
262
303
  readonly message: string;
@@ -354,6 +395,33 @@ declare const resolveSecrets: (agentMeta: Record<string, SecretMeta>, catalogMet
354
395
  /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
355
396
  declare const resolveConfig: (agentConfig: EnvpktConfig, agentConfigDir: string) => Either<CatalogError, ResolveResult>;
356
397
  //#endregion
398
+ //#region src/core/alias.d.ts
399
+ /**
400
+ * Validate all `from_key` references in a resolved config. Produces an
401
+ * AliasTable mapping each alias to its target, or an AliasError describing
402
+ * the first failure.
403
+ *
404
+ * Rules:
405
+ * - Ref must be "secret.<KEY>" or "env.<KEY>"
406
+ * - Target must exist in the same resolved config
407
+ * - Target must be the same type (secret→secret, env→env only)
408
+ * - Target must not itself be a from_key entry (single hop only)
409
+ * - Self-reference is rejected
410
+ * - An alias entry cannot also carry a value field (encrypted_value for
411
+ * secrets, value for env)
412
+ */
413
+ declare const validateAliases: (config: EnvpktConfig) => Either<AliasError, AliasTable>;
414
+ /** Does this secret entry point at another entry? */
415
+ declare const isSecretAlias: (meta: {
416
+ from_key?: string;
417
+ } | undefined) => boolean;
418
+ /** Does this env entry point at another entry? */
419
+ declare const isEnvAlias: (meta: {
420
+ from_key?: string;
421
+ } | undefined) => boolean;
422
+ /** Format an alias error into a human-readable message */
423
+ declare const formatAliasError: (error: AliasError) => string;
424
+ //#endregion
357
425
  //#region src/core/format.d.ts
358
426
  type SecretDisplay = "encrypted" | "plaintext";
359
427
  type FormatPacketOptions = {
@@ -364,7 +432,7 @@ declare const maskValue: (value: string) => string;
364
432
  declare const formatPacket: (result: ResolveResult, options?: FormatPacketOptions) => string;
365
433
  //#endregion
366
434
  //#region src/core/audit.d.ts
367
- declare const computeAudit: (config: EnvpktConfig, fnoxKeys?: ReadonlySet<string>, today?: Date) => AuditResult;
435
+ declare const computeAudit: (config: EnvpktConfig, fnoxKeys?: ReadonlySet<string>, today?: Date, aliasTable?: AliasTable) => AuditResult;
368
436
  declare const computeEnvAudit: (config: EnvpktConfig, env?: Readonly<Record<string, string | undefined>>) => EnvAuditResult;
369
437
  //#endregion
370
438
  //#region src/core/patterns.d.ts
@@ -554,4 +622,4 @@ type ToolDef = {
554
622
  declare const toolDefinitions: readonly ToolDef[];
555
623
  declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
556
624
  //#endregion
557
- 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 Identity, type IdentityError, IdentitySchema, 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 TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, 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, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateConfig };
625
+ export { type AgentIdentity, AgentIdentitySchema, type AliasError, type AliasTable, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, 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 Identity, type IdentityError, IdentitySchema, 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 TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
package/dist/index.js CHANGED
@@ -60,6 +60,7 @@ const SecretMetaSchema = Type.Object({
60
60
  model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
61
61
  source: Type.Optional(Type.String({ description: "Where the secret value originates (e.g. 'vault', 'ci')" })),
62
62
  encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
63
+ from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
63
64
  required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
64
65
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
65
66
  }, { description: "Metadata about a single secret" });
@@ -84,7 +85,8 @@ const CallbackConfigSchema = Type.Object({
84
85
  }, { description: "Automation callbacks for lifecycle events" });
85
86
  const ToolsConfigSchema = Type.Record(Type.String(), Type.Unknown(), { description: "Tool integration configuration — open namespace for third-party extensions" });
86
87
  const EnvMetaSchema = Type.Object({
87
- value: Type.String({ description: "Default value for this environment variable" }),
88
+ value: Type.Optional(Type.String({ description: "Default value for this environment variable. Optional when from_key is set; required otherwise." })),
89
+ from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'env.<KEY>') whose resolved value this alias reuses. Mutually exclusive with value." })),
88
90
  purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
89
91
  comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
90
92
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
@@ -347,6 +349,155 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
347
349
  }));
348
350
  };
349
351
  //#endregion
352
+ //#region src/core/alias.ts
353
+ const ALIAS_REF_RE = /^(secret|env)\.(.+)$/;
354
+ const parseRef = (raw) => {
355
+ const match = ALIAS_REF_RE.exec(raw);
356
+ if (!match) return Option(void 0);
357
+ const kind = match[1];
358
+ const key = match[2];
359
+ if (!key) return Option(void 0);
360
+ return Option({
361
+ kind,
362
+ key
363
+ });
364
+ };
365
+ const validateOneSecret = (key, meta, secretEntries) => {
366
+ if (meta.from_key === void 0) return Right(Option(void 0));
367
+ const ref = meta.from_key;
368
+ if (meta.encrypted_value !== void 0) return Left({
369
+ _tag: "AliasValueConflict",
370
+ key,
371
+ kind: "secret",
372
+ field: "encrypted_value"
373
+ });
374
+ return parseRef(ref).fold(() => Left({
375
+ _tag: "AliasInvalidSyntax",
376
+ key,
377
+ kind: "secret",
378
+ value: ref
379
+ }), (parsed) => {
380
+ if (parsed.kind !== "secret") return Left({
381
+ _tag: "AliasCrossType",
382
+ key,
383
+ kind: "secret",
384
+ targetKind: parsed.kind
385
+ });
386
+ if (parsed.key === key) return Left({
387
+ _tag: "AliasSelfReference",
388
+ key: `secret.${key}`
389
+ });
390
+ return Option(secretEntries[parsed.key]).fold(() => Left({
391
+ _tag: "AliasTargetMissing",
392
+ key: `secret.${key}`,
393
+ target: ref
394
+ }), (target) => {
395
+ if (target.from_key !== void 0) return Left({
396
+ _tag: "AliasChained",
397
+ key: `secret.${key}`,
398
+ target: ref
399
+ });
400
+ return Right(Option({
401
+ kind: "secret",
402
+ targetKind: "secret",
403
+ targetKey: parsed.key
404
+ }));
405
+ });
406
+ });
407
+ };
408
+ const validateOneEnv = (key, meta, envEntries) => {
409
+ if (meta.from_key === void 0) return Right(Option(void 0));
410
+ const ref = meta.from_key;
411
+ if (meta.value !== void 0) return Left({
412
+ _tag: "AliasValueConflict",
413
+ key,
414
+ kind: "env",
415
+ field: "value"
416
+ });
417
+ return parseRef(ref).fold(() => Left({
418
+ _tag: "AliasInvalidSyntax",
419
+ key,
420
+ kind: "env",
421
+ value: ref
422
+ }), (parsed) => {
423
+ if (parsed.kind !== "env") return Left({
424
+ _tag: "AliasCrossType",
425
+ key,
426
+ kind: "env",
427
+ targetKind: parsed.kind
428
+ });
429
+ if (parsed.key === key) return Left({
430
+ _tag: "AliasSelfReference",
431
+ key: `env.${key}`
432
+ });
433
+ return Option(envEntries[parsed.key]).fold(() => Left({
434
+ _tag: "AliasTargetMissing",
435
+ key: `env.${key}`,
436
+ target: ref
437
+ }), (target) => {
438
+ if (target.from_key !== void 0) return Left({
439
+ _tag: "AliasChained",
440
+ key: `env.${key}`,
441
+ target: ref
442
+ });
443
+ return Right(Option({
444
+ kind: "env",
445
+ targetKind: "env",
446
+ targetKey: parsed.key
447
+ }));
448
+ });
449
+ });
450
+ };
451
+ /**
452
+ * Validate all `from_key` references in a resolved config. Produces an
453
+ * AliasTable mapping each alias to its target, or an AliasError describing
454
+ * the first failure.
455
+ *
456
+ * Rules:
457
+ * - Ref must be "secret.<KEY>" or "env.<KEY>"
458
+ * - Target must exist in the same resolved config
459
+ * - Target must be the same type (secret→secret, env→env only)
460
+ * - Target must not itself be a from_key entry (single hop only)
461
+ * - Self-reference is rejected
462
+ * - An alias entry cannot also carry a value field (encrypted_value for
463
+ * secrets, value for env)
464
+ */
465
+ const validateAliases = (config) => {
466
+ const secretEntries = config.secret ?? {};
467
+ const envEntries = config.env ?? {};
468
+ const entries = /* @__PURE__ */ new Map();
469
+ const secretResults = Object.entries(secretEntries).map(([key, meta]) => [key, validateOneSecret(key, meta, secretEntries)]);
470
+ for (const [key, result] of secretResults) {
471
+ const outcome = result.fold((err) => err, (opt) => opt.orUndefined());
472
+ if (outcome === void 0) continue;
473
+ if ("_tag" in outcome) return Left(outcome);
474
+ entries.set(`secret.${key}`, outcome);
475
+ }
476
+ const envResults = Object.entries(envEntries).map(([key, meta]) => [key, validateOneEnv(key, meta, envEntries)]);
477
+ for (const [key, result] of envResults) {
478
+ const outcome = result.fold((err) => err, (opt) => opt.orUndefined());
479
+ if (outcome === void 0) continue;
480
+ if ("_tag" in outcome) return Left(outcome);
481
+ entries.set(`env.${key}`, outcome);
482
+ }
483
+ return Right({ entries });
484
+ };
485
+ /** Does this secret entry point at another entry? */
486
+ const isSecretAlias = (meta) => meta?.from_key !== void 0;
487
+ /** Does this env entry point at another entry? */
488
+ const isEnvAlias = (meta) => meta?.from_key !== void 0;
489
+ /** Format an alias error into a human-readable message */
490
+ const formatAliasError = (error) => {
491
+ switch (error._tag) {
492
+ case "AliasInvalidSyntax": return `[${error.kind}.${error.key}] from_key = "${error.value}" — expected "secret.<KEY>" or "env.<KEY>"`;
493
+ case "AliasTargetMissing": return `[${error.key}] from_key target "${error.target}" not found in config`;
494
+ case "AliasSelfReference": return `[${error.key}] from_key cannot reference itself`;
495
+ case "AliasChained": return `[${error.key}] from_key target "${error.target}" is itself an alias; chained aliases are not supported`;
496
+ case "AliasCrossType": return `[${error.kind}.${error.key}] cannot alias a ${error.targetKind} entry; same-type aliasing only (secret→secret, env→env)`;
497
+ case "AliasValueConflict": return `[${error.kind}.${error.key}] cannot declare both from_key and ${error.field}; an alias has no value of its own`;
498
+ }
499
+ };
500
+ //#endregion
350
501
  //#region src/core/format.ts
351
502
  const maskValue = (value) => {
352
503
  if (value.length > 8) return `${value.slice(0, 3)}${"•".repeat(5)}${value.slice(-4)}`;
@@ -393,11 +544,20 @@ const formatPacket = (result, options) => {
393
544
  const metaEntries = Object.entries(secretConfig);
394
545
  const secretHeader = `secrets: ${metaEntries.length}`;
395
546
  const secretLines = metaEntries.map(([key, meta]) => {
396
- const header = ` ${key} → ${meta.service ?? key}${meta.encrypted_value ? " [sealed]" : ""}${Option(options?.secrets?.[key]).fold(() => "", (secretValue) => ` = ${(options?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}`)}`;
547
+ const header = ` ${key} → ${meta.service ?? key}${meta.encrypted_value ? " [sealed]" : ""}${meta.from_key ? ` [alias → ${meta.from_key}]` : ""}${Option(options?.secrets?.[key]).fold(() => "", (secretValue) => ` = ${(options?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}`)}`;
397
548
  const fields = formatSecretFields(meta, " ");
398
549
  return fields ? `${header}\n${fields}` : header;
399
550
  });
400
551
  sections.push([secretHeader, ...secretLines].join("\n"));
552
+ const envConfig = config.env ?? {};
553
+ const envEntriesArr = Object.entries(envConfig);
554
+ if (envEntriesArr.length > 0) {
555
+ const envHeader = `env: ${envEntriesArr.length}`;
556
+ const envLines = envEntriesArr.map(([key, meta]) => {
557
+ return ` ${key}${meta.from_key ? ` [alias → ${meta.from_key}]` : ""}${meta.value !== void 0 ? ` = ${meta.value}` : ""}${meta.purpose ? `\n purpose: ${meta.purpose}` : ""}`;
558
+ });
559
+ sections.push([envHeader, ...envLines].join("\n"));
560
+ }
401
561
  if (config.lifecycle) {
402
562
  const lcLines = ["lifecycle:"];
403
563
  if (config.lifecycle.stale_warning_days !== void 0) lcLines.push(` stale_warning_days: ${config.lifecycle.stale_warning_days}`);
@@ -456,10 +616,28 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
456
616
  purpose,
457
617
  created: Option(meta.created),
458
618
  expires: Option(meta.expires),
459
- issues: List(issues)
619
+ issues: List(issues),
620
+ alias_of: Option(void 0)
460
621
  };
461
622
  };
462
- const computeAudit = (config, fnoxKeys, today) => {
623
+ /**
624
+ * Build a SecretHealth row for an alias entry. Status is inherited from the
625
+ * target; metadata (purpose, tags) comes from the alias entry itself where
626
+ * set, otherwise falls through to the target so operators see context.
627
+ */
628
+ const classifyAlias = (key, meta, targetHealth, targetRef) => ({
629
+ key,
630
+ service: targetHealth.service,
631
+ status: targetHealth.status,
632
+ days_remaining: targetHealth.days_remaining,
633
+ rotation_url: targetHealth.rotation_url,
634
+ purpose: meta.purpose !== void 0 ? Option(meta.purpose) : targetHealth.purpose,
635
+ created: targetHealth.created,
636
+ expires: targetHealth.expires,
637
+ issues: List([]),
638
+ alias_of: Option(targetRef)
639
+ });
640
+ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
463
641
  const now = today ?? /* @__PURE__ */ new Date();
464
642
  const lifecycle = config.lifecycle ?? {};
465
643
  const staleWarningDays = lifecycle.stale_warning_days ?? 90;
@@ -467,9 +645,31 @@ const computeAudit = (config, fnoxKeys, today) => {
467
645
  const requireService = lifecycle.require_service ?? false;
468
646
  const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
469
647
  const secretEntries = config.secret ?? {};
470
- const metaKeys = new Set(Object.keys(secretEntries));
471
- const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
472
- const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
648
+ const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
649
+ const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
650
+ const nonAliasMetaKeys = new Set(nonAliasEntries.map(([k]) => k));
651
+ const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
652
+ const healthByKey = new Map(nonAliasHealth.map((h) => [h.key, h]));
653
+ const aliasHealth = aliasEntries.map(([key, meta]) => {
654
+ const targetKey = (aliasTable?.entries.get(`secret.${key}`))?.targetKey;
655
+ const targetHealth = targetKey !== void 0 ? healthByKey.get(targetKey) : void 0;
656
+ const targetRef = meta.from_key ?? (targetKey !== void 0 ? `secret.${targetKey}` : "");
657
+ if (!targetHealth) return {
658
+ key,
659
+ service: Option(meta.service),
660
+ status: "missing",
661
+ days_remaining: Option(void 0),
662
+ rotation_url: Option(meta.rotation_url),
663
+ purpose: Option(meta.purpose),
664
+ created: Option(meta.created),
665
+ expires: Option(meta.expires),
666
+ issues: List(["Alias target not resolvable"]),
667
+ alias_of: Option(targetRef)
668
+ };
669
+ return classifyAlias(key, meta, targetHealth, targetRef);
670
+ });
671
+ const secrets = List([...nonAliasHealth, ...aliasHealth]);
672
+ const orphaned = keys.size > 0 ? [...nonAliasMetaKeys].filter((k) => !keys.has(k)).length : 0;
473
673
  const total = secrets.size;
474
674
  const expired = secrets.count((s) => s.status === "expired");
475
675
  const missing = secrets.count((s) => s.status === "missing");
@@ -477,6 +677,7 @@ const computeAudit = (config, fnoxKeys, today) => {
477
677
  const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
478
678
  const stale = secrets.count((s) => s.status === "stale");
479
679
  const healthy = secrets.count((s) => s.status === "healthy");
680
+ const aliases = aliasHealth.length;
480
681
  return {
481
682
  status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
482
683
  secrets,
@@ -488,6 +689,7 @@ const computeAudit = (config, fnoxKeys, today) => {
488
689
  missing,
489
690
  missing_metadata,
490
691
  orphaned,
692
+ aliases,
491
693
  identity: config.identity
492
694
  };
493
695
  };
@@ -495,13 +697,17 @@ const computeEnvAudit = (config, env = process.env) => {
495
697
  const envEntries = config.env ?? {};
496
698
  const entries = Object.entries(envEntries).map(([key, entry]) => {
497
699
  const currentValue = env[key];
498
- const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
700
+ const effectiveDefault = entry.from_key !== void 0 ? (() => {
701
+ const targetKey = /^env\.(.+)$/.exec(entry.from_key)?.[1];
702
+ return (targetKey !== void 0 ? envEntries[targetKey] : void 0)?.value ?? "";
703
+ })() : entry.value ?? "";
499
704
  return {
500
705
  key,
501
- defaultValue: entry.value,
706
+ defaultValue: effectiveDefault,
502
707
  currentValue,
503
- status,
504
- purpose: entry.purpose
708
+ status: Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== effectiveDefault, "overridden").else("default"),
709
+ purpose: entry.purpose,
710
+ alias_of: Option(entry.from_key)
505
711
  };
506
712
  });
507
713
  return {
@@ -1226,14 +1432,27 @@ const envScan = (env, options) => {
1226
1432
  low_confidence
1227
1433
  };
1228
1434
  };
1435
+ const parseAliasRef = (raw, expectedKind) => {
1436
+ const match = /^(secret|env)\.(.+)$/.exec(raw);
1437
+ if (!match) return void 0;
1438
+ if (match[1] !== expectedKind) return void 0;
1439
+ return match[2];
1440
+ };
1229
1441
  /** Bidirectional drift detection between config and live environment */
1230
1442
  const envCheck = (config, env) => {
1231
1443
  const secretEntries = config.secret ?? {};
1232
1444
  const metaKeys = Object.keys(secretEntries);
1233
1445
  const trackedSet = new Set(metaKeys);
1446
+ const isSecretPresent = (key) => {
1447
+ if (env[key] !== void 0 && env[key] !== "") return true;
1448
+ const meta = secretEntries[key];
1449
+ if (meta?.from_key === void 0) return false;
1450
+ const targetKey = parseAliasRef(meta.from_key, "secret");
1451
+ return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
1452
+ };
1234
1453
  const secretDriftEntries = metaKeys.map((key) => {
1235
1454
  const meta = secretEntries[key];
1236
- const present = env[key] !== void 0 && env[key] !== "";
1455
+ const present = isSecretPresent(key);
1237
1456
  return {
1238
1457
  envVar: key,
1239
1458
  service: Option(meta.service),
@@ -1242,12 +1461,19 @@ const envCheck = (config, env) => {
1242
1461
  };
1243
1462
  });
1244
1463
  const envDefaults = config.env ?? {};
1464
+ const isEnvPresent = (key) => {
1465
+ if (env[key] !== void 0 && env[key] !== "") return true;
1466
+ const meta = envDefaults[key];
1467
+ if (meta?.from_key === void 0) return false;
1468
+ const targetKey = parseAliasRef(meta.from_key, "env");
1469
+ return targetKey !== void 0 && env[targetKey] !== void 0 && env[targetKey] !== "";
1470
+ };
1245
1471
  const envDefaultEntries = Object.keys(envDefaults).filter((key) => {
1246
1472
  if (trackedSet.has(key)) return false;
1247
1473
  trackedSet.add(key);
1248
1474
  return true;
1249
1475
  }).map((key) => {
1250
- const present = env[key] !== void 0 && env[key] !== "";
1476
+ const present = isEnvPresent(key);
1251
1477
  return {
1252
1478
  envVar: key,
1253
1479
  service: Option(void 0),
@@ -1693,7 +1919,7 @@ const looksLikeSecret = (value) => {
1693
1919
  };
1694
1920
  const checkEnvMisclassification = (config) => {
1695
1921
  const envEntries = config.env ?? {};
1696
- return Object.entries(envEntries).filter(([, entry]) => looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1922
+ return Object.entries(envEntries).filter(([, entry]) => entry.value !== void 0 && looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1697
1923
  };
1698
1924
  /** Programmatic boot — returns Either<BootError, BootResult> */
1699
1925
  const bootSafe = (options) => {
@@ -1701,26 +1927,29 @@ const bootSafe = (options) => {
1701
1927
  const inject = opts.inject !== false;
1702
1928
  const failOnExpired = opts.failOnExpired !== false;
1703
1929
  const warnOnly = opts.warnOnly ?? false;
1704
- return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
1930
+ return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => validateAliases(config).fold((err) => Left(err), (aliasTable) => {
1705
1931
  const secretEntries = config.secret ?? {};
1706
- const metaKeys = Object.keys(secretEntries);
1707
- const hasSealedValues = metaKeys.some((k) => !!secretEntries[k].encrypted_value);
1932
+ const envEntries = config.env ?? {};
1933
+ const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
1934
+ const aliasSecretKeys = Object.keys(secretEntries).filter((k) => secretEntries[k].from_key !== void 0);
1935
+ const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
1936
+ const aliasEnvKeys = Object.keys(envEntries).filter((k) => envEntries[k].from_key !== void 0);
1937
+ const nonAliasMetaKeys = Object.keys(nonAliasSecretEntries);
1938
+ const hasSealedValues = nonAliasMetaKeys.some((k) => !!nonAliasSecretEntries[k].encrypted_value);
1708
1939
  const identityKeyResult = resolveIdentityKey(config, configDir);
1709
1940
  const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
1710
1941
  if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
1711
1942
  _tag: "ReadError",
1712
1943
  message: "unexpected"
1713
1944
  }));
1714
- const audit = computeAudit(config, detectFnoxKeys(configDir));
1945
+ const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
1715
1946
  return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
1716
1947
  const secrets = {};
1717
1948
  const injected = [];
1718
1949
  const skipped = [];
1719
1950
  warnings.push(...checkEnvMisclassification(config));
1720
- const envEntries = config.env ?? {};
1721
- const envEntriesArr = Object.entries(envEntries);
1722
- const envDefaults = Object.fromEntries(envEntriesArr.flatMap(([key, entry]) => Option(process.env[key]).fold(() => [[key, entry.value]], () => [])));
1723
- const overridden = envEntriesArr.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
1951
+ const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => entry.value !== void 0 ? [[key, entry.value]] : [], () => [])));
1952
+ const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
1724
1953
  if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
1725
1954
  process.env[key] = value;
1726
1955
  });
@@ -1729,7 +1958,7 @@ const bootSafe = (options) => {
1729
1958
  if (hasSealedValues) identityFilePath.fold(() => {
1730
1959
  warnings.push("Sealed values found but no identity file available for decryption");
1731
1960
  }, (idPath) => {
1732
- unsealSecrets(secretEntries, idPath).fold((err) => {
1961
+ unsealSecrets(nonAliasSecretEntries, idPath).fold((err) => {
1733
1962
  warnings.push(`Sealed value decryption failed: ${err.message}`);
1734
1963
  }, (unsealed) => {
1735
1964
  const unsealedEntries = Object.entries(unsealed);
@@ -1738,7 +1967,7 @@ const bootSafe = (options) => {
1738
1967
  unsealedEntries.map(([key]) => key).forEach((key) => sealedKeys.add(key));
1739
1968
  });
1740
1969
  });
1741
- const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
1970
+ const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
1742
1971
  if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
1743
1972
  warnings.push(`fnox export failed: ${err.message}`);
1744
1973
  skipped.push(...remainingKeys);
@@ -1756,9 +1985,34 @@ const bootSafe = (options) => {
1756
1985
  else warnings.push("fnox not available — unsealed secrets could not be resolved");
1757
1986
  skipped.push(...remainingKeys);
1758
1987
  }
1759
- if (inject) Object.entries(secrets).forEach(([key, value]) => {
1760
- process.env[key] = value;
1988
+ aliasSecretKeys.forEach((aliasKey) => {
1989
+ const entry = aliasTable.entries.get(`secret.${aliasKey}`);
1990
+ if (!entry) return;
1991
+ const targetValue = secrets[entry.targetKey];
1992
+ if (targetValue !== void 0) {
1993
+ secrets[aliasKey] = targetValue;
1994
+ injected.push(aliasKey);
1995
+ } else skipped.push(aliasKey);
1761
1996
  });
1997
+ aliasEnvKeys.forEach((aliasKey) => {
1998
+ const entry = aliasTable.entries.get(`env.${aliasKey}`);
1999
+ if (!entry) return;
2000
+ if (process.env[aliasKey] !== void 0) {
2001
+ overridden.push(aliasKey);
2002
+ return;
2003
+ }
2004
+ const targetEntry = envEntries[entry.targetKey];
2005
+ if (targetEntry?.value === void 0) return;
2006
+ envDefaults[aliasKey] = process.env[entry.targetKey] ?? targetEntry.value;
2007
+ });
2008
+ if (inject) {
2009
+ Object.entries(envDefaults).forEach(([key, value]) => {
2010
+ process.env[key] ??= value;
2011
+ });
2012
+ Object.entries(secrets).forEach(([key, value]) => {
2013
+ process.env[key] = value;
2014
+ });
2015
+ }
1762
2016
  return {
1763
2017
  audit,
1764
2018
  injected,
@@ -1771,7 +2025,7 @@ const bootSafe = (options) => {
1771
2025
  configSource
1772
2026
  };
1773
2027
  });
1774
- });
2028
+ }));
1775
2029
  };
1776
2030
  /** Programmatic boot — throws EnvpktBootError on failure (intentional throwing wrapper over bootSafe) */
1777
2031
  const boot = (options) => bootSafe(options).fold((err) => {
@@ -1803,6 +2057,12 @@ const formatBootError = (error) => {
1803
2057
  case "AgeNotFound": return `age not found: ${error.message}`;
1804
2058
  case "DecryptFailed": return `Decrypt failed: ${error.message}`;
1805
2059
  case "IdentityNotFound": return `Identity file not found: ${error.path}`;
2060
+ case "AliasInvalidSyntax":
2061
+ case "AliasTargetMissing":
2062
+ case "AliasSelfReference":
2063
+ case "AliasChained":
2064
+ case "AliasCrossType":
2065
+ case "AliasValueConflict": return formatAliasError(error);
1806
2066
  default: return `Boot error: ${JSON.stringify(error)}`;
1807
2067
  }
1808
2068
  };
@@ -2238,6 +2498,7 @@ const handleGetPacketHealth = (args) => {
2238
2498
  status: s.status,
2239
2499
  days_remaining: s.days_remaining.fold(() => null, (d) => d),
2240
2500
  rotation_url: s.rotation_url.fold(() => null, (u) => u),
2501
+ alias_of: s.alias_of.fold(() => null, (a) => a),
2241
2502
  issues: s.issues.toArray()
2242
2503
  }));
2243
2504
  return textResult(JSON.stringify({
@@ -2249,6 +2510,7 @@ const handleGetPacketHealth = (args) => {
2249
2510
  expired: audit.expired,
2250
2511
  stale: audit.stale,
2251
2512
  missing: audit.missing,
2513
+ aliases: audit.aliases,
2252
2514
  secrets: secretDetails
2253
2515
  }, null, 2));
2254
2516
  };
@@ -2278,11 +2540,30 @@ const handleGetSecretMeta = (args) => {
2278
2540
  const loaded = loadConfigForTool(configPathArg(args));
2279
2541
  if (!loaded.ok) return loaded.result;
2280
2542
  const { config } = loaded;
2281
- return Option((config.secret ?? {})[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
2282
- const { encrypted_value: _, ...safeMeta } = meta;
2543
+ const secretEntries = config.secret ?? {};
2544
+ return Option(secretEntries[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
2545
+ const { encrypted_value: _, from_key: fromKey, ...rest } = meta;
2546
+ if (fromKey !== void 0) {
2547
+ const targetKey = /^secret\.(.+)$/.exec(fromKey)?.[1];
2548
+ const target = targetKey !== void 0 ? secretEntries[targetKey] : void 0;
2549
+ if (target) {
2550
+ const { encrypted_value: __, from_key: ___, ...targetRest } = target;
2551
+ return textResult(JSON.stringify({
2552
+ key,
2553
+ ...targetRest,
2554
+ ...rest,
2555
+ alias_of: fromKey
2556
+ }, null, 2));
2557
+ }
2558
+ return textResult(JSON.stringify({
2559
+ key,
2560
+ ...rest,
2561
+ alias_of: fromKey
2562
+ }, null, 2));
2563
+ }
2283
2564
  return textResult(JSON.stringify({
2284
2565
  key,
2285
- ...safeMeta
2566
+ ...rest
2286
2567
  }, null, 2));
2287
2568
  });
2288
2569
  };
@@ -2356,4 +2637,4 @@ const startServer = async () => {
2356
2637
  await server.connect(transport);
2357
2638
  };
2358
2639
  //#endregion
2359
- export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, 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, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateConfig };
2640
+ export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",
@@ -20,18 +20,36 @@
20
20
  "bin": {
21
21
  "envpkt": "dist/cli.js"
22
22
  },
23
+ "scripts": {
24
+ "validate": "ts-builds validate",
25
+ "format": "ts-builds format",
26
+ "format:check": "ts-builds format:check",
27
+ "lint": "ts-builds lint",
28
+ "lint:check": "ts-builds lint:check",
29
+ "typecheck": "ts-builds typecheck",
30
+ "test": "ts-builds test",
31
+ "test:watch": "ts-builds test:watch",
32
+ "test:coverage": "ts-builds test:coverage",
33
+ "build": "ts-builds build",
34
+ "build:schema": "tsx scripts/build-schema.ts",
35
+ "demo": "tsx scripts/generate-demo-html.ts",
36
+ "dev": "ts-builds dev",
37
+ "docs:dev": "pnpm --dir site dev",
38
+ "docs:build": "pnpm --dir site build",
39
+ "prepublishOnly": "pnpm validate"
40
+ },
23
41
  "dependencies": {
24
42
  "@modelcontextprotocol/sdk": "^1.29.0",
25
43
  "@sinclair/typebox": "^0.34.49",
26
44
  "commander": "^14.0.3",
27
- "functype": "^0.56.0",
45
+ "functype": "^0.58.1",
28
46
  "functype-os": "^0.4.2",
29
47
  "smol-toml": "^1.6.1"
30
48
  },
31
49
  "devDependencies": {
32
50
  "@types/node": "^24.12.2",
33
- "ts-builds": "^2.6.3",
34
- "tsdown": "^0.21.7",
51
+ "ts-builds": "^2.7.0",
52
+ "tsdown": "^0.21.9",
35
53
  "tsx": "^4.21.0"
36
54
  },
37
55
  "type": "module",
@@ -52,21 +70,5 @@
52
70
  "schemas"
53
71
  ],
54
72
  "prettier": "ts-builds/prettier",
55
- "scripts": {
56
- "validate": "ts-builds validate",
57
- "format": "ts-builds format",
58
- "format:check": "ts-builds format:check",
59
- "lint": "ts-builds lint",
60
- "lint:check": "ts-builds lint:check",
61
- "typecheck": "ts-builds typecheck",
62
- "test": "ts-builds test",
63
- "test:watch": "ts-builds test:watch",
64
- "test:coverage": "ts-builds test:coverage",
65
- "build": "ts-builds build",
66
- "build:schema": "tsx scripts/build-schema.ts",
67
- "demo": "tsx scripts/generate-demo-html.ts",
68
- "dev": "ts-builds dev",
69
- "docs:dev": "pnpm --dir site dev",
70
- "docs:build": "pnpm --dir site build"
71
- }
72
- }
73
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
74
+ }
@@ -151,6 +151,10 @@
151
151
  "description": "Age-encrypted secret value (armored ciphertext, safe to commit)",
152
152
  "type": "string"
153
153
  },
154
+ "from_key": {
155
+ "description": "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value.",
156
+ "type": "string"
157
+ },
154
158
  "required": {
155
159
  "description": "Whether this secret is required for operation",
156
160
  "type": "boolean"
@@ -175,12 +179,13 @@
175
179
  "^(.*)$": {
176
180
  "description": "Metadata for a plaintext environment default (non-secret)",
177
181
  "type": "object",
178
- "required": [
179
- "value"
180
- ],
181
182
  "properties": {
182
183
  "value": {
183
- "description": "Default value for this environment variable",
184
+ "description": "Default value for this environment variable. Optional when from_key is set; required otherwise.",
185
+ "type": "string"
186
+ },
187
+ "from_key": {
188
+ "description": "Reference another entry (format: 'env.<KEY>') whose resolved value this alias reuses. Mutually exclusive with value.",
184
189
  "type": "string"
185
190
  },
186
191
  "purpose": {