envpkt 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { FormatRegistry, Type } from "@sinclair/typebox";
2
2
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { TypeCompiler } from "@sinclair/typebox/compiler";
5
6
  import { Cond, Left, List, Option, Right, Try } from "functype";
@@ -46,6 +47,7 @@ const SecretMetaSchema = Type.Object({
46
47
  description: "URL or reference for secret rotation procedure"
47
48
  })),
48
49
  purpose: Type.Optional(Type.String({ description: "Why this secret exists and what it enables" })),
50
+ comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
49
51
  capabilities: Type.Optional(Type.Array(Type.String(), { description: "What operations this secret grants (e.g. read, write, admin)" })),
50
52
  created: Type.Optional(Type.String({
51
53
  format: "date",
@@ -79,6 +81,12 @@ const CallbackConfigSchema = Type.Object({
79
81
  on_audit_fail: Type.Optional(Type.String({ description: "Command or webhook on audit failure" }))
80
82
  }, { description: "Automation callbacks for lifecycle events" });
81
83
  const ToolsConfigSchema = Type.Record(Type.String(), Type.Unknown(), { description: "Tool integration configuration — open namespace for third-party extensions" });
84
+ const EnvMetaSchema = Type.Object({
85
+ value: Type.String({ description: "Default value for this environment variable" }),
86
+ purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
87
+ comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
88
+ tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
89
+ }, { description: "Metadata for a plaintext environment default (non-secret)" });
82
90
  const EnvpktConfigSchema = Type.Object({
83
91
  version: Type.Number({
84
92
  description: "Schema version number",
@@ -86,7 +94,8 @@ const EnvpktConfigSchema = Type.Object({
86
94
  }),
87
95
  catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
88
96
  agent: Type.Optional(AgentIdentitySchema),
89
- meta: Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" }),
97
+ secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
98
+ env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
90
99
  lifecycle: Type.Optional(LifecycleConfigSchema),
91
100
  callbacks: Type.Optional(CallbackConfigSchema),
92
101
  tools: Type.Optional(ToolsConfigSchema)
@@ -108,11 +117,75 @@ const normalizeDates = (obj) => {
108
117
  if (obj !== null && typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]));
109
118
  return obj;
110
119
  };
120
+ /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string */
121
+ const expandPath = (p) => {
122
+ return (p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
123
+ const name = braced ?? bare ?? "";
124
+ return process.env[name] ?? "";
125
+ });
126
+ };
111
127
  /** Find envpkt.toml in the given directory */
112
128
  const findConfigPath = (dir) => {
113
129
  const candidate = join(dir, CONFIG_FILENAME$1);
114
130
  return existsSync(candidate) ? Option(candidate) : Option(void 0);
115
131
  };
132
+ /**
133
+ * Expand a path template that may contain a single `*` glob segment.
134
+ * Returns all matching paths (or empty array if parent doesn't exist).
135
+ * Non-glob paths return a single-element array if they exist.
136
+ */
137
+ const expandGlobPath = (expanded) => {
138
+ if (!expanded.includes("*")) return existsSync(expanded) ? [expanded] : [];
139
+ const segments = expanded.split("/");
140
+ const globIdx = segments.findIndex((s) => s.includes("*"));
141
+ if (globIdx < 0) return [];
142
+ const parentDir = segments.slice(0, globIdx).join("/");
143
+ const globSegment = segments[globIdx];
144
+ const suffix = segments.slice(globIdx + 1).join("/");
145
+ if (!existsSync(parentDir)) return [];
146
+ const prefix = globSegment.replace(/\*.*$/, "");
147
+ return readdirSync(parentDir).filter((entry) => entry.startsWith(prefix)).map((entry) => join(parentDir, entry, suffix)).filter((p) => existsSync(p));
148
+ };
149
+ /** Ordered candidate paths for config discovery beyond CWD */
150
+ const CONFIG_SEARCH_PATHS = [
151
+ "~/.envpkt/envpkt.toml",
152
+ "~/OneDrive/.envpkt/envpkt.toml",
153
+ "~/Library/CloudStorage/OneDrive-Personal/.envpkt/envpkt.toml",
154
+ "~/Library/CloudStorage/OneDrive-SharedLibraries-*/.envpkt/envpkt.toml",
155
+ "$WINHOME/OneDrive/.envpkt/envpkt.toml",
156
+ "$USERPROFILE/OneDrive/.envpkt/envpkt.toml",
157
+ "$OneDrive/.envpkt/envpkt.toml",
158
+ "$OneDriveConsumer/.envpkt/envpkt.toml",
159
+ "$OneDriveCommercial/.envpkt/envpkt.toml",
160
+ "/mnt/c/Users/$USER/OneDrive/.envpkt/envpkt.toml",
161
+ "~/Library/Mobile Documents/com~apple~CloudDocs/.envpkt/envpkt.toml",
162
+ "~/Dropbox/.envpkt/envpkt.toml",
163
+ "$DROPBOX_PATH/.envpkt/envpkt.toml",
164
+ "~/Google Drive/My Drive/.envpkt/envpkt.toml",
165
+ "~/Library/CloudStorage/GoogleDrive-*/.envpkt/envpkt.toml",
166
+ "$GOOGLE_DRIVE/.envpkt/envpkt.toml",
167
+ "$WINHOME/.envpkt/envpkt.toml",
168
+ "$USERPROFILE/.envpkt/envpkt.toml"
169
+ ];
170
+ /** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then built-in candidate paths */
171
+ const discoverConfig = (cwd) => {
172
+ const cwdCandidate = join(cwd ?? process.cwd(), CONFIG_FILENAME$1);
173
+ if (existsSync(cwdCandidate)) return Option({
174
+ path: cwdCandidate,
175
+ source: "cwd"
176
+ });
177
+ const customPaths = process.env.ENVPKT_SEARCH_PATH?.split(":").filter(Boolean) ?? [];
178
+ for (const template of [...customPaths, ...CONFIG_SEARCH_PATHS]) {
179
+ const expanded = expandPath(template);
180
+ if (!expanded || expanded.startsWith("/.envpkt")) continue;
181
+ const matches = expandGlobPath(expanded);
182
+ if (matches.length > 0) return Option({
183
+ path: matches[0],
184
+ source: "search"
185
+ });
186
+ }
187
+ return Option(void 0);
188
+ };
116
189
  /** Read a config file, returning Either<ConfigError, string> */
117
190
  const readConfigFile = (path) => {
118
191
  if (!existsSync(path)) return Left({
@@ -124,14 +197,13 @@ const readConfigFile = (path) => {
124
197
  message: String(err)
125
198
  }), (content) => Right(content));
126
199
  };
127
- /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit meta) */
200
+ /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit secret) */
128
201
  const applyDefaults = (data) => {
129
202
  if (data !== null && typeof data === "object" && !Array.isArray(data)) {
130
- const obj = data;
131
- if (!("meta" in obj)) return {
132
- ...obj,
133
- meta: {}
134
- };
203
+ const result = { ...data };
204
+ if (!("secret" in result)) result.secret = {};
205
+ if (!("env" in result)) result.env = {};
206
+ return result;
135
207
  }
136
208
  return data;
137
209
  };
@@ -150,27 +222,29 @@ const validateConfig = (data) => {
150
222
  };
151
223
  /** Load and validate an envpkt.toml from a file path */
152
224
  const loadConfig = (path) => readConfigFile(path).flatMap(parseToml).flatMap(validateConfig);
153
- /** Load config from CWD, returning both path and parsed config */
154
- const loadConfigFromCwd = (cwd) => {
155
- const dir = cwd ?? process.cwd();
156
- return findConfigPath(dir).fold(() => Left({
157
- _tag: "FileNotFound",
158
- path: join(dir, CONFIG_FILENAME$1)
159
- }), (path) => loadConfig(path).map((config) => ({
160
- path,
161
- config
162
- })));
163
- };
225
+ /** Load config from CWD or discovery chain, returning path, source, and parsed config */
226
+ const loadConfigFromCwd = (cwd) => discoverConfig(cwd).fold(() => Left({
227
+ _tag: "FileNotFound",
228
+ path: join(cwd ?? process.cwd(), CONFIG_FILENAME$1)
229
+ }), ({ path, source }) => loadConfig(path).map((config) => ({
230
+ path,
231
+ source,
232
+ config
233
+ })));
164
234
  /**
165
235
  * Resolve config path via priority chain:
166
236
  * 1. Explicit flag path
167
237
  * 2. ENVPKT_CONFIG env var
168
- * 3. CWD discovery
238
+ * 3. CWD + discovery chain (home dir, cloud storage, custom search paths)
169
239
  */
170
240
  const resolveConfigPath = (flagPath, envVar, cwd) => {
171
241
  if (flagPath) {
172
242
  const resolved = resolve(flagPath);
173
- return existsSync(resolved) ? Right(resolved) : Left({
243
+ const result = {
244
+ path: resolved,
245
+ source: "flag"
246
+ };
247
+ return existsSync(resolved) ? Right(result) : Left({
174
248
  _tag: "FileNotFound",
175
249
  path: resolved
176
250
  });
@@ -178,16 +252,22 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
178
252
  const envPath = envVar ?? process.env[ENV_VAR_CONFIG];
179
253
  if (envPath) {
180
254
  const resolved = resolve(envPath);
181
- return existsSync(resolved) ? Right(resolved) : Left({
255
+ const result = {
256
+ path: resolved,
257
+ source: "env"
258
+ };
259
+ return existsSync(resolved) ? Right(result) : Left({
182
260
  _tag: "FileNotFound",
183
261
  path: resolved
184
262
  });
185
263
  }
186
- const dir = cwd ?? process.cwd();
187
- return findConfigPath(dir).fold(() => Left({
264
+ return discoverConfig(cwd).fold(() => Left({
188
265
  _tag: "FileNotFound",
189
- path: join(dir, CONFIG_FILENAME$1)
190
- }), (path) => Right(path));
266
+ path: join(cwd ?? process.cwd(), CONFIG_FILENAME$1)
267
+ }), ({ path, source }) => Right({
268
+ path,
269
+ source
270
+ }));
191
271
  };
192
272
 
193
273
  //#endregion
@@ -236,13 +316,14 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
236
316
  });
237
317
  const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
238
318
  const agentSecrets = agentConfig.agent.secrets;
239
- return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentConfig.meta, catalogConfig.meta, agentSecrets, catalogPath).map((resolvedMeta) => {
319
+ const agentSecretEntries = agentConfig.secret ?? {};
320
+ return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
240
321
  const merged = [];
241
322
  const overridden = [];
242
323
  const warnings = [];
243
324
  for (const key of agentSecrets) {
244
325
  merged.push(key);
245
- if (agentConfig.meta[key]) overridden.push(key);
326
+ if (agentSecretEntries[key]) overridden.push(key);
246
327
  }
247
328
  const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
248
329
  const agentIdentity = agentConfig.agent ? (() => {
@@ -256,7 +337,7 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
256
337
  ...agentIdentity,
257
338
  name: agentIdentity.name
258
339
  } : void 0,
259
- meta: resolvedMeta
340
+ secret: resolvedMeta
260
341
  },
261
342
  catalogPath,
262
343
  merged,
@@ -309,7 +390,8 @@ const formatPacket = (result, options) => {
309
390
  if (config.agent.expires) agentLines.push(` expires: ${config.agent.expires}`);
310
391
  if (agentLines.length > 0) sections.push(agentLines.join("\n"));
311
392
  }
312
- const metaEntries = Object.entries(config.meta);
393
+ const secretConfig = config.secret ?? {};
394
+ const metaEntries = Object.entries(secretConfig);
313
395
  const secretHeader = `secrets: ${metaEntries.length}`;
314
396
  const secretLines = metaEntries.map(([key, meta]) => {
315
397
  const service = meta.service ?? key;
@@ -344,7 +426,7 @@ const MS_PER_DAY = 864e5;
344
426
  const WARN_BEFORE_DAYS = 30;
345
427
  const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
346
428
  const parseDate = (dateStr) => {
347
- const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00Z");
429
+ const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
348
430
  return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
349
431
  };
350
432
  const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
@@ -389,8 +471,9 @@ const computeAudit = (config, fnoxKeys, today) => {
389
471
  const requireExpiration = lifecycle.require_expiration ?? false;
390
472
  const requireService = lifecycle.require_service ?? false;
391
473
  const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
392
- const metaKeys = new Set(Object.keys(config.meta));
393
- const secrets = List(Object.entries(config.meta).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
474
+ const secretEntries = config.secret ?? {};
475
+ const metaKeys = new Set(Object.keys(secretEntries));
476
+ const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
394
477
  const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
395
478
  const total = secrets.size;
396
479
  const expired = secrets.count((s) => s.status === "expired");
@@ -413,6 +496,28 @@ const computeAudit = (config, fnoxKeys, today) => {
413
496
  agent: config.agent
414
497
  };
415
498
  };
499
+ const computeEnvAudit = (config, env = process.env) => {
500
+ const envEntries = config.env ?? {};
501
+ const entries = [];
502
+ for (const [key, entry] of Object.entries(envEntries)) {
503
+ const currentValue = env[key];
504
+ const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
505
+ entries.push({
506
+ key,
507
+ defaultValue: entry.value,
508
+ currentValue,
509
+ status,
510
+ purpose: entry.purpose
511
+ });
512
+ }
513
+ return {
514
+ entries,
515
+ total: entries.length,
516
+ defaults_applied: entries.filter((e) => e.status === "default").length,
517
+ overridden: entries.filter((e) => e.status === "overridden").length,
518
+ missing: entries.filter((e) => e.status === "missing").length
519
+ };
520
+ };
416
521
 
417
522
  //#endregion
418
523
  //#region src/core/patterns.ts
@@ -1045,7 +1150,7 @@ const matchValueShape = (value) => {
1045
1150
  };
1046
1151
  /** Strip common suffixes and derive a service name from an env var name */
1047
1152
  const deriveServiceFromName = (name) => {
1048
- const suffixes = [
1153
+ const matchedSuffix = [
1049
1154
  "_API_KEY",
1050
1155
  "_SECRET_KEY",
1051
1156
  "_ACCESS_KEY",
@@ -1063,13 +1168,8 @@ const deriveServiceFromName = (name) => {
1063
1168
  "_DSN",
1064
1169
  "_URL",
1065
1170
  "_URI"
1066
- ];
1067
- let stripped = name;
1068
- for (const suffix of suffixes) if (stripped.endsWith(suffix)) {
1069
- stripped = stripped.slice(0, -suffix.length);
1070
- break;
1071
- }
1072
- return stripped.toLowerCase().replace(/_/g, "-");
1171
+ ].find((s) => name.endsWith(s));
1172
+ return (matchedSuffix ? name.slice(0, -matchedSuffix.length) : name).toLowerCase().replace(/_/g, "-");
1073
1173
  };
1074
1174
  /** Match a single env var against all patterns */
1075
1175
  const matchEnvVar = (name, value) => {
@@ -1139,10 +1239,11 @@ const envScan = (env, options) => {
1139
1239
  /** Bidirectional drift detection between config and live environment */
1140
1240
  const envCheck = (config, env) => {
1141
1241
  const entries = [];
1142
- const metaKeys = Object.keys(config.meta);
1242
+ const secretEntries = config.secret ?? {};
1243
+ const metaKeys = Object.keys(secretEntries);
1143
1244
  const trackedSet = new Set(metaKeys);
1144
1245
  for (const key of metaKeys) {
1145
- const meta = config.meta[key];
1246
+ const meta = secretEntries[key];
1146
1247
  const present = env[key] !== void 0 && env[key] !== "";
1147
1248
  entries.push({
1148
1249
  envVar: key,
@@ -1151,6 +1252,17 @@ const envCheck = (config, env) => {
1151
1252
  confidence: Option(void 0)
1152
1253
  });
1153
1254
  }
1255
+ const envDefaults = config.env ?? {};
1256
+ for (const key of Object.keys(envDefaults)) if (!trackedSet.has(key)) {
1257
+ trackedSet.add(key);
1258
+ const present = env[key] !== void 0 && env[key] !== "";
1259
+ entries.push({
1260
+ envVar: key,
1261
+ service: Option(void 0),
1262
+ status: present ? "tracked" : "missing_from_env",
1263
+ confidence: Option(void 0)
1264
+ });
1265
+ }
1154
1266
  const envMatches = scanEnv(env);
1155
1267
  for (const match of envMatches) if (!trackedSet.has(match.envVar)) entries.push({
1156
1268
  envVar: match.envVar,
@@ -1170,12 +1282,12 @@ const envCheck = (config, env) => {
1170
1282
  };
1171
1283
  };
1172
1284
  const todayIso = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1173
- /** Generate TOML [meta.*] blocks from scan results, mirroring init.ts pattern */
1285
+ /** Generate TOML [secret.*] blocks from scan results, mirroring init.ts pattern */
1174
1286
  const generateTomlFromScan = (matches) => {
1175
1287
  const blocks = [];
1176
1288
  for (const match of matches) {
1177
1289
  const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
1178
- blocks.push(`[meta.${match.envVar}]
1290
+ blocks.push(`[secret.${match.envVar}]
1179
1291
  service = "${svc}"
1180
1292
  # purpose = "" # Why: what this secret enables
1181
1293
  # capabilities = [] # What operations this grants
@@ -1410,17 +1522,18 @@ const unsealSecrets = (meta, identityPath) => {
1410
1522
 
1411
1523
  //#endregion
1412
1524
  //#region src/core/boot.ts
1413
- const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), (configPath) => loadConfig(configPath).fold((err) => Left(err), (config) => {
1525
+ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), ({ path: configPath, source: configSource }) => loadConfig(configPath).fold((err) => Left(err), (config) => {
1414
1526
  const configDir = dirname(configPath);
1415
1527
  return resolveConfig(config, configDir).fold((err) => Left(err), (result) => Right({
1416
1528
  config: result.config,
1417
1529
  configPath,
1418
- configDir
1530
+ configDir,
1531
+ configSource
1419
1532
  }));
1420
1533
  }));
1421
1534
  const resolveAgentKey = (config, configDir) => {
1422
1535
  if (!config.agent?.identity) return Right(void 0);
1423
- return unwrapAgentKey(resolve(configDir, config.agent.identity)).fold((err) => Left(err), (key) => Right(key));
1536
+ return unwrapAgentKey(resolve(configDir, expandPath(config.agent.identity))).fold((err) => Left(err), (key) => Right(key));
1424
1537
  };
1425
1538
  const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
1426
1539
  const checkExpiration = (audit, failOnExpired, warnOnly) => {
@@ -1433,15 +1546,36 @@ const checkExpiration = (audit, failOnExpired, warnOnly) => {
1433
1546
  if (audit.expired > 0 && warnOnly) warnings.push(`${audit.expired} secret(s) have expired (warn-only mode)`);
1434
1547
  return Right(warnings);
1435
1548
  };
1549
+ const SECRET_PATTERNS = [
1550
+ /^sk-/,
1551
+ /^ghp_/,
1552
+ /^ghu_/,
1553
+ /^AKIA[0-9A-Z]{16}/,
1554
+ /^xox[bpras]-/,
1555
+ /:\/\/[^:]+:[^@]+@/,
1556
+ /^ey[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/
1557
+ ];
1558
+ const looksLikeSecret = (value) => {
1559
+ if (SECRET_PATTERNS.some((p) => p.test(value))) return true;
1560
+ if (value.length > 40 && /^[A-Za-z0-9+/=]+$/.test(value)) return true;
1561
+ return false;
1562
+ };
1563
+ const checkEnvMisclassification = (config) => {
1564
+ const warnings = [];
1565
+ const envEntries = config.env ?? {};
1566
+ for (const [key, entry] of Object.entries(envEntries)) if (looksLikeSecret(entry.value)) warnings.push(`[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1567
+ return warnings;
1568
+ };
1436
1569
  /** Programmatic boot — returns Either<BootError, BootResult> */
1437
1570
  const bootSafe = (options) => {
1438
1571
  const opts = options ?? {};
1439
1572
  const inject = opts.inject !== false;
1440
1573
  const failOnExpired = opts.failOnExpired !== false;
1441
1574
  const warnOnly = opts.warnOnly ?? false;
1442
- return resolveAndLoad(opts).flatMap(({ config, configDir }) => {
1443
- const metaKeys = Object.keys(config.meta);
1444
- const hasSealedValues = metaKeys.some((k) => !!config.meta[k]?.encrypted_value);
1575
+ return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
1576
+ const secretEntries = config.secret ?? {};
1577
+ const metaKeys = Object.keys(secretEntries);
1578
+ const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]?.encrypted_value);
1445
1579
  const agentKeyResult = resolveAgentKey(config, configDir);
1446
1580
  const agentKey = agentKeyResult.fold(() => void 0, (k) => k);
1447
1581
  const agentKeyError = agentKeyResult.fold((err) => err, () => void 0);
@@ -1451,19 +1585,24 @@ const bootSafe = (options) => {
1451
1585
  const secrets = {};
1452
1586
  const injected = [];
1453
1587
  const skipped = [];
1588
+ warnings.push(...checkEnvMisclassification(config));
1589
+ const envEntries = config.env ?? {};
1590
+ const envDefaults = {};
1591
+ const overridden = [];
1592
+ for (const [key, entry] of Object.entries(envEntries)) if (process.env[key] === void 0) {
1593
+ envDefaults[key] = entry.value;
1594
+ if (inject) process.env[key] = entry.value;
1595
+ } else overridden.push(key);
1454
1596
  const sealedKeys = /* @__PURE__ */ new Set();
1455
- if (hasSealedValues && config.agent?.identity) {
1456
- const identityPath = resolve(configDir, config.agent.identity);
1457
- unsealSecrets(config.meta, identityPath).fold((err) => {
1458
- warnings.push(`Sealed value decryption failed: ${err.message}`);
1459
- }, (unsealed) => {
1460
- for (const [key, value] of Object.entries(unsealed)) {
1461
- secrets[key] = value;
1462
- injected.push(key);
1463
- sealedKeys.add(key);
1464
- }
1465
- });
1466
- }
1597
+ if (hasSealedValues && config.agent?.identity) unsealSecrets(secretEntries, resolve(configDir, expandPath(config.agent.identity))).fold((err) => {
1598
+ warnings.push(`Sealed value decryption failed: ${err.message}`);
1599
+ }, (unsealed) => {
1600
+ for (const [key, value] of Object.entries(unsealed)) {
1601
+ secrets[key] = value;
1602
+ injected.push(key);
1603
+ sealedKeys.add(key);
1604
+ }
1605
+ });
1467
1606
  const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
1468
1607
  if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, agentKey).fold((err) => {
1469
1608
  warnings.push(`fnox export failed: ${err.message}`);
@@ -1484,7 +1623,11 @@ const bootSafe = (options) => {
1484
1623
  injected,
1485
1624
  skipped,
1486
1625
  secrets,
1487
- warnings
1626
+ warnings,
1627
+ envDefaults,
1628
+ overridden,
1629
+ configPath,
1630
+ configSource
1488
1631
  };
1489
1632
  });
1490
1633
  });
@@ -1592,10 +1735,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
1592
1735
  const configPath = join(dir, CONFIG_FILENAME);
1593
1736
  if (Try(() => statSync(configPath).isFile()).fold(() => false, (v) => v)) yield configPath;
1594
1737
  if (currentDepth >= maxDepth) return;
1595
- let entries = [];
1596
- Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => {}, (e) => {
1597
- entries = e;
1598
- });
1738
+ const entries = Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => [], (e) => e);
1599
1739
  for (const entry of entries) if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) yield* findEnvpktFiles(join(dir, entry.name), maxDepth, currentDepth + 1);
1600
1740
  }
1601
1741
  const scanFleet = (rootDir, options) => {
@@ -1640,7 +1780,7 @@ const compareFnoxAndEnvpkt = (fnoxKeys, envpktKeys) => {
1640
1780
  //#endregion
1641
1781
  //#region src/mcp/resources.ts
1642
1782
  const loadConfigSafe = () => {
1643
- return resolveConfigPath().fold(() => void 0, (path) => loadConfig(path).fold(() => void 0, (config) => ({
1783
+ return resolveConfigPath().fold(() => void 0, ({ path }) => loadConfig(path).fold(() => void 0, (config) => ({
1644
1784
  config,
1645
1785
  path
1646
1786
  })));
@@ -1690,7 +1830,8 @@ const readCapabilities = () => {
1690
1830
  const { config } = loaded;
1691
1831
  const agentCapabilities = config.agent?.capabilities ?? [];
1692
1832
  const secretCapabilities = {};
1693
- for (const [key, meta] of Object.entries(config.meta)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1833
+ const secretEntries = config.secret ?? {};
1834
+ for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1694
1835
  return { contents: [{
1695
1836
  uri: "envpkt://capabilities",
1696
1837
  mimeType: "application/json",
@@ -1731,7 +1872,7 @@ const loadConfigForTool = (configPath) => {
1731
1872
  return resolveConfigPath(configPath).fold((err) => ({
1732
1873
  ok: false,
1733
1874
  result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
1734
- }), (path) => loadConfig(path).fold((err) => ({
1875
+ }), ({ path }) => loadConfig(path).fold((err) => ({
1735
1876
  ok: false,
1736
1877
  result: errorResult(`Config error: ${err._tag} — ${err._tag === "ValidationError" ? err.errors.toArray().join(", ") : ""}`)
1737
1878
  }), (config) => ({
@@ -1798,6 +1939,17 @@ const toolDefinitions = [
1798
1939
  },
1799
1940
  required: ["key"]
1800
1941
  }
1942
+ },
1943
+ {
1944
+ name: "getEnvMeta",
1945
+ description: "Get metadata for environment defaults — returns configured default values, purposes, and current drift status",
1946
+ inputSchema: {
1947
+ type: "object",
1948
+ properties: { configPath: {
1949
+ type: "string",
1950
+ description: "Optional path to envpkt.toml"
1951
+ } }
1952
+ }
1801
1953
  }
1802
1954
  ];
1803
1955
  const handleGetPacketHealth = (args) => {
@@ -1831,7 +1983,8 @@ const handleListCapabilities = (args) => {
1831
1983
  const { config } = loaded;
1832
1984
  const agentCapabilities = config.agent?.capabilities ?? [];
1833
1985
  const secretCapabilities = {};
1834
- for (const [key, meta] of Object.entries(config.meta)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1986
+ const secretEntries = config.secret ?? {};
1987
+ for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1835
1988
  return textResult(JSON.stringify({
1836
1989
  agent: config.agent ? {
1837
1990
  name: config.agent.name,
@@ -1839,7 +1992,8 @@ const handleListCapabilities = (args) => {
1839
1992
  description: config.agent.description,
1840
1993
  capabilities: agentCapabilities
1841
1994
  } : null,
1842
- secrets: secretCapabilities
1995
+ secrets: secretCapabilities,
1996
+ env_defaults: Object.keys(config.env ?? {}).length
1843
1997
  }, null, 2));
1844
1998
  };
1845
1999
  const handleGetSecretMeta = (args) => {
@@ -1848,11 +2002,12 @@ const handleGetSecretMeta = (args) => {
1848
2002
  const loaded = loadConfigForTool(args.configPath);
1849
2003
  if (!loaded.ok) return loaded.result;
1850
2004
  const { config } = loaded;
1851
- const meta = config.meta[key];
2005
+ const meta = (config.secret ?? {})[key];
1852
2006
  if (!meta) return errorResult(`Secret not found: ${key}`);
2007
+ const { encrypted_value: _, ...safeMeta } = meta;
1853
2008
  return textResult(JSON.stringify({
1854
2009
  key,
1855
- ...meta
2010
+ ...safeMeta
1856
2011
  }, null, 2));
1857
2012
  };
1858
2013
  const handleCheckExpiration = (args) => {
@@ -1871,11 +2026,19 @@ const handleCheckExpiration = (args) => {
1871
2026
  issues: s.issues.toArray()
1872
2027
  }, null, 2)));
1873
2028
  };
2029
+ const handleGetEnvMeta = (args) => {
2030
+ const loaded = loadConfigForTool(args.configPath);
2031
+ if (!loaded.ok) return loaded.result;
2032
+ const { config } = loaded;
2033
+ const envAudit = computeEnvAudit(config);
2034
+ return textResult(JSON.stringify(envAudit, null, 2));
2035
+ };
1874
2036
  const handlers = {
1875
2037
  getPacketHealth: handleGetPacketHealth,
1876
2038
  listCapabilities: handleListCapabilities,
1877
2039
  getSecretMeta: handleGetSecretMeta,
1878
- checkExpiration: handleCheckExpiration
2040
+ checkExpiration: handleCheckExpiration,
2041
+ getEnvMeta: handleGetEnvMeta
1879
2042
  };
1880
2043
  const callTool = (name, args) => {
1881
2044
  const handler = handlers[name];
@@ -1896,17 +2059,17 @@ const createServer = () => {
1896
2059
  },
1897
2060
  instructions: "envpkt provides credential lifecycle awareness for AI agents. Use tools to check health, capabilities, and secret metadata. No secret values are ever exposed."
1898
2061
  });
1899
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions.map((t) => ({
2062
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: toolDefinitions.map((t) => ({
1900
2063
  name: t.name,
1901
2064
  description: t.description,
1902
2065
  inputSchema: t.inputSchema
1903
2066
  })) }));
1904
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2067
+ server.setRequestHandler(CallToolRequestSchema, (request) => {
1905
2068
  const { name, arguments: args } = request.params;
1906
2069
  return callTool(name, args ?? {});
1907
2070
  });
1908
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [...resourceDefinitions] }));
1909
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2071
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
2072
+ server.setRequestHandler(ReadResourceRequestSchema, (request) => {
1910
2073
  const { uri } = request.params;
1911
2074
  const result = readResource(uri);
1912
2075
  if (!result) return { contents: [{
@@ -1925,4 +2088,4 @@ const startServer = async () => {
1925
2088
  };
1926
2089
 
1927
2090
  //#endregion
1928
- export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, createServer, deriveServiceFromName, detectFnox, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, validateConfig };
2091
+ export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",