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/README.md +245 -144
- package/dist/cli.js +685 -297
- package/dist/index.d.ts +54 -7
- package/dist/index.js +243 -80
- package/package.json +1 -1
- package/schemas/envpkt.schema.json +42 -3
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
|
-
|
|
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
|
|
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
|
|
131
|
-
if (!("
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
154
|
-
const loadConfigFromCwd = (cwd) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
return findConfigPath(dir).fold(() => Left({
|
|
264
|
+
return discoverConfig(cwd).fold(() => Left({
|
|
188
265
|
_tag: "FileNotFound",
|
|
189
|
-
path: join(
|
|
190
|
-
}), (path) => Right(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
393
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 [
|
|
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(`[
|
|
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
|
|
1444
|
-
const
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
...
|
|
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,
|
|
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,
|
|
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,
|
|
1909
|
-
server.setRequestHandler(ReadResourceRequestSchema,
|
|
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 };
|