envpkt 0.1.0 → 0.2.0
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 +95 -7
- package/dist/cli.js +248 -2
- package/dist/index.d.ts +32 -1
- package/dist/index.js +170 -11
- package/package.json +22 -20
- package/schemas/envpkt.schema.json +4 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Secrets managers store values. envpkt stores _metadata about_ those values — w
|
|
|
18
18
|
- Automate rotation workflows
|
|
19
19
|
- Share secret metadata across agents via a central catalog
|
|
20
20
|
|
|
21
|
-
envpkt
|
|
21
|
+
envpkt works alongside your existing secrets manager (Vault, fnox, CI variables, etc.). Optionally, you can embed age-encrypted secret values directly in the TOML via **sealed packets** — making configs fully self-contained and safe to commit to git.
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
@@ -154,6 +154,47 @@ This produces a self-contained config with catalog metadata merged in and agent
|
|
|
154
154
|
- Omitted fields keep the catalog value
|
|
155
155
|
- `agent.secrets` is the source of truth for which keys the agent needs
|
|
156
156
|
|
|
157
|
+
## Sealed Packets
|
|
158
|
+
|
|
159
|
+
Sealed packets embed age-encrypted secret values directly in `envpkt.toml`. This makes your config fully self-contained — no external secrets backend needed at runtime.
|
|
160
|
+
|
|
161
|
+
### Setup
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Generate an age keypair
|
|
165
|
+
age-keygen -o identity.txt
|
|
166
|
+
# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Add the public key to your config and the identity file to `.gitignore`:
|
|
170
|
+
|
|
171
|
+
```toml
|
|
172
|
+
[agent]
|
|
173
|
+
name = "my-agent"
|
|
174
|
+
recipient = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
|
|
175
|
+
identity = "identity.txt"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Seal
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
envpkt seal
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each secret gets an `encrypted_value` field with age-armored ciphertext. The TOML (including ciphertext) is safe to commit.
|
|
185
|
+
|
|
186
|
+
### Boot
|
|
187
|
+
|
|
188
|
+
At runtime, sealed values are automatically decrypted:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { boot } from "envpkt"
|
|
192
|
+
|
|
193
|
+
const result = boot() // decrypts sealed values, injects into process.env
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Mixed mode is supported — sealed values take priority, with fnox as fallback for keys without `encrypted_value`.
|
|
197
|
+
|
|
157
198
|
## CLI Commands
|
|
158
199
|
|
|
159
200
|
### `envpkt init`
|
|
@@ -234,6 +275,26 @@ envpkt exec --strict -- ./deploy.sh # Abort if audit is not healthy
|
|
|
234
275
|
envpkt exec --profile staging -- ... # Use a specific fnox profile
|
|
235
276
|
```
|
|
236
277
|
|
|
278
|
+
### `envpkt seal`
|
|
279
|
+
|
|
280
|
+
Encrypt secret values into `envpkt.toml` using [age](https://age-encryption.org/). Sealed values are safe to commit to git — only the holder of the private key can decrypt them.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
envpkt seal # Seal all secrets in envpkt.toml
|
|
284
|
+
envpkt seal -c path/to/envpkt.toml # Specify config path
|
|
285
|
+
envpkt seal --profile staging # Use a specific fnox profile for value resolution
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Requires `agent.recipient` (age public key) in your config. Values are resolved via cascade:
|
|
289
|
+
|
|
290
|
+
1. **fnox** (if available)
|
|
291
|
+
2. **Environment variables** (e.g. `OPENAI_API_KEY` in your shell)
|
|
292
|
+
3. **Interactive prompt** (asks you to paste each value)
|
|
293
|
+
|
|
294
|
+
After sealing, each secret gets an `encrypted_value` field. At boot time, `envpkt exec` or `boot()` automatically decrypts sealed values using the `agent.identity` file.
|
|
295
|
+
|
|
296
|
+
See [`examples/sealed-agent.toml`](./examples/sealed-agent.toml) for a complete example.
|
|
297
|
+
|
|
237
298
|
### `envpkt env scan`
|
|
238
299
|
|
|
239
300
|
Auto-discover credentials from your shell environment. Matches env vars against ~45 known services (exact name), ~13 generic suffix patterns (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, etc.), and ~29 value shape patterns (`sk-*`, `ghp_*`, `AKIA*`, `postgres://`, etc.).
|
|
@@ -328,12 +389,13 @@ The schema is published at:
|
|
|
328
389
|
|
|
329
390
|
Each `[meta.<KEY>]` section describes a secret:
|
|
330
391
|
|
|
331
|
-
| Tier | Fields | Description
|
|
332
|
-
| --------------- | ----------------------------------------------- |
|
|
333
|
-
| **Scan-first** | `service`, `expires`, `rotation_url` | Key health indicators for audit
|
|
334
|
-
| **Context** | `purpose`, `capabilities`, `created` | Why this secret exists and what it grants
|
|
335
|
-
| **Operational** | `rotates`, `rate_limit`, `model_hint`, `source` | Runtime and provisioning info
|
|
336
|
-
| **
|
|
392
|
+
| Tier | Fields | Description |
|
|
393
|
+
| --------------- | ----------------------------------------------- | ------------------------------------------- |
|
|
394
|
+
| **Scan-first** | `service`, `expires`, `rotation_url` | Key health indicators for audit |
|
|
395
|
+
| **Context** | `purpose`, `capabilities`, `created` | Why this secret exists and what it grants |
|
|
396
|
+
| **Operational** | `rotates`, `rate_limit`, `model_hint`, `source` | Runtime and provisioning info |
|
|
397
|
+
| **Sealed** | `encrypted_value` | Age-encrypted secret value (safe to commit) |
|
|
398
|
+
| **Enforcement** | `required`, `tags` | Filtering, grouping, and policy |
|
|
337
399
|
|
|
338
400
|
### Agent Identity
|
|
339
401
|
|
|
@@ -460,6 +522,32 @@ matchEnvVar("OPENAI_API_KEY", "sk-test123").fold(
|
|
|
460
522
|
)
|
|
461
523
|
```
|
|
462
524
|
|
|
525
|
+
### Seal API
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
import { ageEncrypt, ageDecrypt, sealSecrets, unsealSecrets } from "envpkt"
|
|
529
|
+
|
|
530
|
+
// Encrypt a single value
|
|
531
|
+
const encrypted = ageEncrypt("sk-my-api-key", "age1ql3z7hjy...")
|
|
532
|
+
encrypted.fold(
|
|
533
|
+
(err) => console.error("Encrypt failed:", err.message),
|
|
534
|
+
(ciphertext) => console.log(ciphertext), // -----BEGIN AGE ENCRYPTED FILE-----
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
// Decrypt a single value
|
|
538
|
+
const decrypted = ageDecrypt(ciphertext, "/path/to/identity.txt")
|
|
539
|
+
|
|
540
|
+
// Seal all secrets in a config's meta
|
|
541
|
+
const sealed = sealSecrets(config.meta, { OPENAI_API_KEY: "sk-..." }, recipientPublicKey)
|
|
542
|
+
|
|
543
|
+
// Unseal all encrypted_value entries
|
|
544
|
+
const values = unsealSecrets(config.meta, "/path/to/identity.txt")
|
|
545
|
+
values.fold(
|
|
546
|
+
(err) => console.error("Unseal failed:", err.message),
|
|
547
|
+
(secrets) => console.log(secrets), // { OPENAI_API_KEY: "sk-..." }
|
|
548
|
+
)
|
|
549
|
+
```
|
|
550
|
+
|
|
463
551
|
### Catalog Resolution API
|
|
464
552
|
|
|
465
553
|
```typescript
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { execFileSync } from "node:child_process";
|
|
|
10
10
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
12
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
13
14
|
|
|
14
15
|
//#region src/core/audit.ts
|
|
15
16
|
const MS_PER_DAY = 864e5;
|
|
@@ -31,7 +32,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
31
32
|
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
32
33
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
33
34
|
const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
|
|
34
|
-
const
|
|
35
|
+
const hasSealed = !!meta?.encrypted_value;
|
|
36
|
+
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
35
37
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
36
38
|
if (isExpired) issues.push("Secret has expired");
|
|
37
39
|
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
@@ -131,6 +133,7 @@ const SecretMetaSchema = Type.Object({
|
|
|
131
133
|
rate_limit: Type.Optional(Type.String({ description: "Rate limit or quota info (e.g. '1000/min')" })),
|
|
132
134
|
model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
|
|
133
135
|
source: Type.Optional(Type.String({ description: "Where the secret value originates (e.g. 'vault', 'ci')" })),
|
|
136
|
+
encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
|
|
134
137
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
135
138
|
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
136
139
|
}, { description: "Metadata about a single secret" });
|
|
@@ -1791,7 +1794,8 @@ const printConfig = (config, path, resolveResult, opts) => {
|
|
|
1791
1794
|
for (const [key, meta] of Object.entries(config.meta)) {
|
|
1792
1795
|
const secretValue = opts?.secrets?.[key];
|
|
1793
1796
|
const valueSuffix = secretValue !== void 0 ? ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}` : "";
|
|
1794
|
-
|
|
1797
|
+
const sealedTag = meta.encrypted_value ? ` ${CYAN}[sealed]${RESET}` : "";
|
|
1798
|
+
console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${sealedTag}${valueSuffix}`);
|
|
1795
1799
|
printSecretMeta(meta, " ");
|
|
1796
1800
|
}
|
|
1797
1801
|
if (config.lifecycle) {
|
|
@@ -2171,6 +2175,245 @@ const runResolve = (options) => {
|
|
|
2171
2175
|
});
|
|
2172
2176
|
};
|
|
2173
2177
|
|
|
2178
|
+
//#endregion
|
|
2179
|
+
//#region src/fnox/cli.ts
|
|
2180
|
+
/** Export all secrets from fnox as key=value pairs for a given profile */
|
|
2181
|
+
const fnoxExport = (profile, agentKey) => {
|
|
2182
|
+
const args = profile ? [
|
|
2183
|
+
"export",
|
|
2184
|
+
"--profile",
|
|
2185
|
+
profile
|
|
2186
|
+
] : ["export"];
|
|
2187
|
+
const env = agentKey ? {
|
|
2188
|
+
...process.env,
|
|
2189
|
+
FNOX_AGE_KEY: agentKey
|
|
2190
|
+
} : void 0;
|
|
2191
|
+
return Try(() => execFileSync("fnox", args, {
|
|
2192
|
+
stdio: "pipe",
|
|
2193
|
+
encoding: "utf-8",
|
|
2194
|
+
env
|
|
2195
|
+
})).fold((err) => Left({
|
|
2196
|
+
_tag: "FnoxCliError",
|
|
2197
|
+
message: `fnox export failed: ${err}`
|
|
2198
|
+
}), (output) => {
|
|
2199
|
+
const entries = {};
|
|
2200
|
+
for (const line of output.split("\n")) {
|
|
2201
|
+
const eq = line.indexOf("=");
|
|
2202
|
+
if (eq > 0) {
|
|
2203
|
+
const key = line.slice(0, eq).trim();
|
|
2204
|
+
entries[key] = line.slice(eq + 1).trim();
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return Right(entries);
|
|
2208
|
+
});
|
|
2209
|
+
};
|
|
2210
|
+
|
|
2211
|
+
//#endregion
|
|
2212
|
+
//#region src/core/resolve-values.ts
|
|
2213
|
+
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
2214
|
+
const resolveValues = async (keys, profile, agentKey) => {
|
|
2215
|
+
const result = {};
|
|
2216
|
+
const remaining = new Set(keys);
|
|
2217
|
+
if (fnoxAvailable()) fnoxExport(profile, agentKey).fold(() => {}, (exported) => {
|
|
2218
|
+
for (const key of [...remaining]) if (key in exported) {
|
|
2219
|
+
result[key] = exported[key];
|
|
2220
|
+
remaining.delete(key);
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
for (const key of [...remaining]) {
|
|
2224
|
+
const envValue = process.env[key];
|
|
2225
|
+
if (envValue !== void 0 && envValue !== "") {
|
|
2226
|
+
result[key] = envValue;
|
|
2227
|
+
remaining.delete(key);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
if (remaining.size > 0 && process.stdin.isTTY) {
|
|
2231
|
+
const rl = createInterface({
|
|
2232
|
+
input: process.stdin,
|
|
2233
|
+
output: process.stderr
|
|
2234
|
+
});
|
|
2235
|
+
const prompt = (question) => new Promise((resolve) => {
|
|
2236
|
+
rl.question(question, (answer) => resolve(answer));
|
|
2237
|
+
});
|
|
2238
|
+
for (const key of remaining) {
|
|
2239
|
+
const value = await prompt(`Enter value for ${key}: `);
|
|
2240
|
+
if (value !== "") result[key] = value;
|
|
2241
|
+
}
|
|
2242
|
+
rl.close();
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/core/seal.ts
|
|
2249
|
+
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
2250
|
+
const ageEncrypt = (plaintext, recipient) => {
|
|
2251
|
+
if (!ageAvailable()) return Left({
|
|
2252
|
+
_tag: "AgeNotFound",
|
|
2253
|
+
message: "age CLI not found on PATH"
|
|
2254
|
+
});
|
|
2255
|
+
return Try(() => execFileSync("age", [
|
|
2256
|
+
"--encrypt",
|
|
2257
|
+
"--recipient",
|
|
2258
|
+
recipient,
|
|
2259
|
+
"--armor"
|
|
2260
|
+
], {
|
|
2261
|
+
input: plaintext,
|
|
2262
|
+
stdio: [
|
|
2263
|
+
"pipe",
|
|
2264
|
+
"pipe",
|
|
2265
|
+
"pipe"
|
|
2266
|
+
],
|
|
2267
|
+
encoding: "utf-8"
|
|
2268
|
+
})).fold((err) => Left({
|
|
2269
|
+
_tag: "EncryptFailed",
|
|
2270
|
+
key: "",
|
|
2271
|
+
message: `age encrypt failed: ${err}`
|
|
2272
|
+
}), (output) => Right(output.trim()));
|
|
2273
|
+
};
|
|
2274
|
+
/** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
|
|
2275
|
+
const sealSecrets = (meta, values, recipient) => {
|
|
2276
|
+
if (!ageAvailable()) return Left({
|
|
2277
|
+
_tag: "AgeNotFound",
|
|
2278
|
+
message: "age CLI not found on PATH"
|
|
2279
|
+
});
|
|
2280
|
+
const result = {};
|
|
2281
|
+
for (const [key, secretMeta] of Object.entries(meta)) {
|
|
2282
|
+
const plaintext = values[key];
|
|
2283
|
+
if (plaintext === void 0) {
|
|
2284
|
+
result[key] = secretMeta;
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
|
|
2288
|
+
_tag: "EncryptFailed",
|
|
2289
|
+
key,
|
|
2290
|
+
message: err.message
|
|
2291
|
+
}), (ciphertext) => Right(ciphertext));
|
|
2292
|
+
const failed = outcome.fold((err) => err, () => void 0);
|
|
2293
|
+
if (failed) return Left(failed);
|
|
2294
|
+
const ciphertext = outcome.fold(() => "", (v) => v);
|
|
2295
|
+
result[key] = {
|
|
2296
|
+
...secretMeta,
|
|
2297
|
+
encrypted_value: ciphertext
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
return Right(result);
|
|
2301
|
+
};
|
|
2302
|
+
|
|
2303
|
+
//#endregion
|
|
2304
|
+
//#region src/cli/commands/seal.ts
|
|
2305
|
+
/** Write sealed values back into the TOML file, preserving structure */
|
|
2306
|
+
const writeSealedToml = (configPath, sealedMeta) => {
|
|
2307
|
+
const lines = readFileSync(configPath, "utf-8").split("\n");
|
|
2308
|
+
const output = [];
|
|
2309
|
+
let currentMetaKey;
|
|
2310
|
+
let insideMetaBlock = false;
|
|
2311
|
+
let hasEncryptedValue = false;
|
|
2312
|
+
const pendingSeals = /* @__PURE__ */ new Map();
|
|
2313
|
+
for (const [key, meta] of Object.entries(sealedMeta)) if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
|
|
2314
|
+
const metaSectionRe = /^\[meta\.(.+)\]\s*$/;
|
|
2315
|
+
const encryptedValueRe = /^encrypted_value\s*=/;
|
|
2316
|
+
const newSectionRe = /^\[/;
|
|
2317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2318
|
+
const line = lines[i];
|
|
2319
|
+
const metaMatch = metaSectionRe.exec(line);
|
|
2320
|
+
if (metaMatch) {
|
|
2321
|
+
if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
|
|
2322
|
+
output.push(`encrypted_value = """`);
|
|
2323
|
+
output.push(pendingSeals.get(currentMetaKey));
|
|
2324
|
+
output.push(`"""`);
|
|
2325
|
+
pendingSeals.delete(currentMetaKey);
|
|
2326
|
+
}
|
|
2327
|
+
currentMetaKey = metaMatch[1];
|
|
2328
|
+
insideMetaBlock = true;
|
|
2329
|
+
hasEncryptedValue = false;
|
|
2330
|
+
output.push(line);
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
if (insideMetaBlock && newSectionRe.test(line) && !metaSectionRe.test(line)) {
|
|
2334
|
+
if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
|
|
2335
|
+
output.push(`encrypted_value = """`);
|
|
2336
|
+
output.push(pendingSeals.get(currentMetaKey));
|
|
2337
|
+
output.push(`"""`);
|
|
2338
|
+
pendingSeals.delete(currentMetaKey);
|
|
2339
|
+
}
|
|
2340
|
+
insideMetaBlock = false;
|
|
2341
|
+
currentMetaKey = void 0;
|
|
2342
|
+
output.push(line);
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
if (insideMetaBlock && encryptedValueRe.test(line)) {
|
|
2346
|
+
hasEncryptedValue = true;
|
|
2347
|
+
if (currentMetaKey && pendingSeals.has(currentMetaKey)) {
|
|
2348
|
+
output.push(`encrypted_value = """`);
|
|
2349
|
+
output.push(pendingSeals.get(currentMetaKey));
|
|
2350
|
+
output.push(`"""`);
|
|
2351
|
+
pendingSeals.delete(currentMetaKey);
|
|
2352
|
+
if (line.includes("\"\"\"") && !line.endsWith("\"\"\"")) {
|
|
2353
|
+
const afterEquals = line.slice(line.indexOf("=") + 1).trim();
|
|
2354
|
+
if (afterEquals.startsWith("\"\"\"") && !afterEquals.slice(3).includes("\"\"\"")) {
|
|
2355
|
+
while (i + 1 < lines.length && !lines[i + 1].includes("\"\"\"")) i++;
|
|
2356
|
+
if (i + 1 < lines.length) i++;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
} else output.push(line);
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
output.push(line);
|
|
2363
|
+
}
|
|
2364
|
+
if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
|
|
2365
|
+
output.push(`encrypted_value = """`);
|
|
2366
|
+
output.push(pendingSeals.get(currentMetaKey));
|
|
2367
|
+
output.push(`"""`);
|
|
2368
|
+
pendingSeals.delete(currentMetaKey);
|
|
2369
|
+
}
|
|
2370
|
+
writeFileSync(configPath, output.join("\n"));
|
|
2371
|
+
};
|
|
2372
|
+
const runSeal = async (options) => {
|
|
2373
|
+
const configPath = resolveConfigPath(options.config).fold((err) => {
|
|
2374
|
+
console.error(formatError(err));
|
|
2375
|
+
process.exit(2);
|
|
2376
|
+
return "";
|
|
2377
|
+
}, (p) => p);
|
|
2378
|
+
const config = loadConfig(configPath).fold((err) => {
|
|
2379
|
+
console.error(formatError(err));
|
|
2380
|
+
process.exit(2);
|
|
2381
|
+
}, (c) => c);
|
|
2382
|
+
if (!config.agent?.recipient) {
|
|
2383
|
+
console.error(`${RED}Error:${RESET} agent.recipient is required for sealing (age public key)`);
|
|
2384
|
+
console.error(`${DIM}Add [agent] section with recipient = "age1..." to your envpkt.toml${RESET}`);
|
|
2385
|
+
process.exit(2);
|
|
2386
|
+
}
|
|
2387
|
+
const recipient = config.agent.recipient;
|
|
2388
|
+
const configDir = dirname(configPath);
|
|
2389
|
+
let agentKey;
|
|
2390
|
+
if (config.agent.identity) agentKey = unwrapAgentKey(resolve(configDir, config.agent.identity)).fold((err) => {
|
|
2391
|
+
const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
|
|
2392
|
+
console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
|
|
2393
|
+
}, (k) => k);
|
|
2394
|
+
const metaKeys = Object.keys(config.meta);
|
|
2395
|
+
console.log(`${BOLD}Sealing ${metaKeys.length} secret(s)${RESET} with recipient ${CYAN}${recipient.slice(0, 20)}...${RESET}`);
|
|
2396
|
+
console.log("");
|
|
2397
|
+
const values = await resolveValues(metaKeys, options.profile, agentKey);
|
|
2398
|
+
const resolved = Object.keys(values).length;
|
|
2399
|
+
const skipped = metaKeys.length - resolved;
|
|
2400
|
+
if (resolved === 0) {
|
|
2401
|
+
console.error(`${RED}Error:${RESET} No values resolved for any secret key`);
|
|
2402
|
+
process.exit(2);
|
|
2403
|
+
}
|
|
2404
|
+
if (skipped > 0) {
|
|
2405
|
+
const skippedKeys = metaKeys.filter((k) => !(k in values));
|
|
2406
|
+
console.log(`${YELLOW}Skipped${RESET} ${skipped} key(s) with no value: ${skippedKeys.join(", ")}`);
|
|
2407
|
+
}
|
|
2408
|
+
sealSecrets(config.meta, values, recipient).fold((err) => {
|
|
2409
|
+
console.error(`${RED}Error:${RESET} Seal failed: ${err.message}`);
|
|
2410
|
+
process.exit(2);
|
|
2411
|
+
}, (sealedMeta) => {
|
|
2412
|
+
writeSealedToml(configPath, sealedMeta);
|
|
2413
|
+
console.log(`${GREEN}Sealed${RESET} ${resolved} secret(s) into ${DIM}${configPath}${RESET}`);
|
|
2414
|
+
});
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2174
2417
|
//#endregion
|
|
2175
2418
|
//#region src/cli/commands/shell-hook.ts
|
|
2176
2419
|
const ZSH_HOOK = `# envpkt shell hook — add to your .zshrc
|
|
@@ -2235,6 +2478,9 @@ program.command("exec").description("Run pre-flight audit then execute a command
|
|
|
2235
2478
|
program.command("resolve").description("Resolve catalog references and output a flat, self-contained config").option("-c, --config <path>", "Path to envpkt.toml").option("-o, --output <path>", "Write resolved config to file (default: stdout)").option("--format <format>", "Output format: toml | json", "toml").option("--dry-run", "Show what would be resolved without writing").action((options) => {
|
|
2236
2479
|
runResolve(options);
|
|
2237
2480
|
});
|
|
2481
|
+
program.command("seal").description("Encrypt secret values into envpkt.toml using age (sealed packets)").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use for value resolution").action(async (options) => {
|
|
2482
|
+
await runSeal(options);
|
|
2483
|
+
});
|
|
2238
2484
|
program.command("mcp").description("Start the envpkt MCP server (stdio transport)").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
|
|
2239
2485
|
runMcp(options);
|
|
2240
2486
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ declare const SecretMetaSchema: _sinclair_typebox0.TObject<{
|
|
|
30
30
|
rate_limit: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
31
31
|
model_hint: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
32
32
|
source: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
33
|
+
encrypted_value: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
33
34
|
required: _sinclair_typebox0.TOptional<_sinclair_typebox0.TBoolean>;
|
|
34
35
|
tags: _sinclair_typebox0.TOptional<_sinclair_typebox0.TRecord<_sinclair_typebox0.TString, _sinclair_typebox0.TString>>;
|
|
35
36
|
}>;
|
|
@@ -73,6 +74,7 @@ declare const EnvpktConfigSchema: _sinclair_typebox0.TObject<{
|
|
|
73
74
|
rate_limit: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
74
75
|
model_hint: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
75
76
|
source: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
77
|
+
encrypted_value: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
76
78
|
required: _sinclair_typebox0.TOptional<_sinclair_typebox0.TBoolean>;
|
|
77
79
|
tags: _sinclair_typebox0.TOptional<_sinclair_typebox0.TRecord<_sinclair_typebox0.TString, _sinclair_typebox0.TString>>;
|
|
78
80
|
}>>;
|
|
@@ -216,6 +218,21 @@ type IdentityError = {
|
|
|
216
218
|
readonly _tag: "IdentityNotFound";
|
|
217
219
|
readonly path: string;
|
|
218
220
|
};
|
|
221
|
+
type SealError = {
|
|
222
|
+
readonly _tag: "AgeNotFound";
|
|
223
|
+
readonly message: string;
|
|
224
|
+
} | {
|
|
225
|
+
readonly _tag: "EncryptFailed";
|
|
226
|
+
readonly key: string;
|
|
227
|
+
readonly message: string;
|
|
228
|
+
} | {
|
|
229
|
+
readonly _tag: "DecryptFailed";
|
|
230
|
+
readonly key: string;
|
|
231
|
+
readonly message: string;
|
|
232
|
+
} | {
|
|
233
|
+
readonly _tag: "NoRecipient";
|
|
234
|
+
readonly message: string;
|
|
235
|
+
};
|
|
219
236
|
//#endregion
|
|
220
237
|
//#region src/core/config.d.ts
|
|
221
238
|
/** Find envpkt.toml in the given directory */
|
|
@@ -332,6 +349,20 @@ declare class EnvpktBootError extends Error {
|
|
|
332
349
|
constructor(error: BootError);
|
|
333
350
|
}
|
|
334
351
|
//#endregion
|
|
352
|
+
//#region src/core/seal.d.ts
|
|
353
|
+
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
354
|
+
declare const ageEncrypt: (plaintext: string, recipient: string) => Either<SealError, string>;
|
|
355
|
+
/** Decrypt an age-armored ciphertext using the given identity file */
|
|
356
|
+
declare const ageDecrypt: (ciphertext: string, identityPath: string) => Either<SealError, string>;
|
|
357
|
+
/** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
|
|
358
|
+
declare const sealSecrets: (meta: Readonly<Record<string, SecretMeta>>, values: Readonly<Record<string, string>>, recipient: string) => Either<SealError, Record<string, SecretMeta>>;
|
|
359
|
+
/** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
|
|
360
|
+
declare const unsealSecrets: (meta: Readonly<Record<string, SecretMeta>>, identityPath: string) => Either<SealError, Record<string, string>>;
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/core/resolve-values.d.ts
|
|
363
|
+
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
364
|
+
declare const resolveValues: (keys: ReadonlyArray<string>, profile?: string, agentKey?: string) => Promise<Record<string, string>>;
|
|
365
|
+
//#endregion
|
|
335
366
|
//#region src/core/fleet.d.ts
|
|
336
367
|
declare const scanFleet: (rootDir: string, options?: {
|
|
337
368
|
maxDepth?: number;
|
|
@@ -389,4 +420,4 @@ type ToolDef = {
|
|
|
389
420
|
declare const toolDefinitions: readonly ToolDef[];
|
|
390
421
|
declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
|
|
391
422
|
//#endregion
|
|
392
|
-
export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type IdentityError, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ScanOptions, type ScanResult, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, 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, resourceDefinitions, scanEnv, scanFleet, startServer, toolDefinitions, unwrapAgentKey, validateConfig };
|
|
423
|
+
export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type IdentityError, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, 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 };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
|
5
5
|
import { Cond, Left, List, Option, Right, Try } from "functype";
|
|
6
6
|
import { TomlDate, parse } from "smol-toml";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
8
9
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
11
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -54,6 +55,7 @@ const SecretMetaSchema = Type.Object({
|
|
|
54
55
|
rate_limit: Type.Optional(Type.String({ description: "Rate limit or quota info (e.g. '1000/min')" })),
|
|
55
56
|
model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
|
|
56
57
|
source: Type.Optional(Type.String({ description: "Where the secret value originates (e.g. 'vault', 'ci')" })),
|
|
58
|
+
encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
|
|
57
59
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
58
60
|
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
59
61
|
}, { description: "Metadata about a single secret" });
|
|
@@ -311,8 +313,9 @@ const formatPacket = (result, options) => {
|
|
|
311
313
|
const secretHeader = `secrets: ${metaEntries.length}`;
|
|
312
314
|
const secretLines = metaEntries.map(([key, meta]) => {
|
|
313
315
|
const service = meta.service ?? key;
|
|
316
|
+
const sealedTag = meta.encrypted_value ? " [sealed]" : "";
|
|
314
317
|
const secretValue = options?.secrets?.[key];
|
|
315
|
-
const header = ` ${key} → ${service}${secretValue !== void 0 ? ` = ${(options?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}` : ""}`;
|
|
318
|
+
const header = ` ${key} → ${service}${sealedTag}${secretValue !== void 0 ? ` = ${(options?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}` : ""}`;
|
|
316
319
|
const fields = formatSecretFields(meta, " ");
|
|
317
320
|
return fields ? `${header}\n${fields}` : header;
|
|
318
321
|
});
|
|
@@ -356,7 +359,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
356
359
|
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
357
360
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
358
361
|
const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
|
|
359
|
-
const
|
|
362
|
+
const hasSealed = !!meta?.encrypted_value;
|
|
363
|
+
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
360
364
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
361
365
|
if (isExpired) issues.push("Secret has expired");
|
|
362
366
|
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
@@ -1304,6 +1308,106 @@ const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((er
|
|
|
1304
1308
|
/** Extract the set of secret key names from a parsed fnox config */
|
|
1305
1309
|
const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
|
|
1306
1310
|
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#region src/core/seal.ts
|
|
1313
|
+
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
1314
|
+
const ageEncrypt = (plaintext, recipient) => {
|
|
1315
|
+
if (!ageAvailable()) return Left({
|
|
1316
|
+
_tag: "AgeNotFound",
|
|
1317
|
+
message: "age CLI not found on PATH"
|
|
1318
|
+
});
|
|
1319
|
+
return Try(() => execFileSync("age", [
|
|
1320
|
+
"--encrypt",
|
|
1321
|
+
"--recipient",
|
|
1322
|
+
recipient,
|
|
1323
|
+
"--armor"
|
|
1324
|
+
], {
|
|
1325
|
+
input: plaintext,
|
|
1326
|
+
stdio: [
|
|
1327
|
+
"pipe",
|
|
1328
|
+
"pipe",
|
|
1329
|
+
"pipe"
|
|
1330
|
+
],
|
|
1331
|
+
encoding: "utf-8"
|
|
1332
|
+
})).fold((err) => Left({
|
|
1333
|
+
_tag: "EncryptFailed",
|
|
1334
|
+
key: "",
|
|
1335
|
+
message: `age encrypt failed: ${err}`
|
|
1336
|
+
}), (output) => Right(output.trim()));
|
|
1337
|
+
};
|
|
1338
|
+
/** Decrypt an age-armored ciphertext using the given identity file */
|
|
1339
|
+
const ageDecrypt = (ciphertext, identityPath) => {
|
|
1340
|
+
if (!ageAvailable()) return Left({
|
|
1341
|
+
_tag: "AgeNotFound",
|
|
1342
|
+
message: "age CLI not found on PATH"
|
|
1343
|
+
});
|
|
1344
|
+
return Try(() => execFileSync("age", [
|
|
1345
|
+
"--decrypt",
|
|
1346
|
+
"--identity",
|
|
1347
|
+
identityPath
|
|
1348
|
+
], {
|
|
1349
|
+
input: ciphertext,
|
|
1350
|
+
stdio: [
|
|
1351
|
+
"pipe",
|
|
1352
|
+
"pipe",
|
|
1353
|
+
"pipe"
|
|
1354
|
+
],
|
|
1355
|
+
encoding: "utf-8"
|
|
1356
|
+
})).fold((err) => Left({
|
|
1357
|
+
_tag: "DecryptFailed",
|
|
1358
|
+
key: "",
|
|
1359
|
+
message: `age decrypt failed: ${err}`
|
|
1360
|
+
}), (output) => Right(output.trim()));
|
|
1361
|
+
};
|
|
1362
|
+
/** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
|
|
1363
|
+
const sealSecrets = (meta, values, recipient) => {
|
|
1364
|
+
if (!ageAvailable()) return Left({
|
|
1365
|
+
_tag: "AgeNotFound",
|
|
1366
|
+
message: "age CLI not found on PATH"
|
|
1367
|
+
});
|
|
1368
|
+
const result = {};
|
|
1369
|
+
for (const [key, secretMeta] of Object.entries(meta)) {
|
|
1370
|
+
const plaintext = values[key];
|
|
1371
|
+
if (plaintext === void 0) {
|
|
1372
|
+
result[key] = secretMeta;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
|
|
1376
|
+
_tag: "EncryptFailed",
|
|
1377
|
+
key,
|
|
1378
|
+
message: err.message
|
|
1379
|
+
}), (ciphertext) => Right(ciphertext));
|
|
1380
|
+
const failed = outcome.fold((err) => err, () => void 0);
|
|
1381
|
+
if (failed) return Left(failed);
|
|
1382
|
+
const ciphertext = outcome.fold(() => "", (v) => v);
|
|
1383
|
+
result[key] = {
|
|
1384
|
+
...secretMeta,
|
|
1385
|
+
encrypted_value: ciphertext
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
return Right(result);
|
|
1389
|
+
};
|
|
1390
|
+
/** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
|
|
1391
|
+
const unsealSecrets = (meta, identityPath) => {
|
|
1392
|
+
if (!ageAvailable()) return Left({
|
|
1393
|
+
_tag: "AgeNotFound",
|
|
1394
|
+
message: "age CLI not found on PATH"
|
|
1395
|
+
});
|
|
1396
|
+
const result = {};
|
|
1397
|
+
for (const [key, secretMeta] of Object.entries(meta)) {
|
|
1398
|
+
if (!secretMeta.encrypted_value) continue;
|
|
1399
|
+
const outcome = ageDecrypt(secretMeta.encrypted_value, identityPath).fold((err) => Left({
|
|
1400
|
+
_tag: "DecryptFailed",
|
|
1401
|
+
key,
|
|
1402
|
+
message: err.message
|
|
1403
|
+
}), (plaintext) => Right(plaintext));
|
|
1404
|
+
const failed = outcome.fold((err) => err, () => void 0);
|
|
1405
|
+
if (failed) return Left(failed);
|
|
1406
|
+
result[key] = outcome.fold(() => "", (v) => v);
|
|
1407
|
+
}
|
|
1408
|
+
return Right(result);
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1307
1411
|
//#endregion
|
|
1308
1412
|
//#region src/core/boot.ts
|
|
1309
1413
|
const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), (configPath) => loadConfig(configPath).fold((err) => Left(err), (config) => {
|
|
@@ -1335,25 +1439,44 @@ const bootSafe = (options) => {
|
|
|
1335
1439
|
const inject = opts.inject !== false;
|
|
1336
1440
|
const failOnExpired = opts.failOnExpired !== false;
|
|
1337
1441
|
const warnOnly = opts.warnOnly ?? false;
|
|
1338
|
-
return resolveAndLoad(opts).flatMap(({ config, configDir }) =>
|
|
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);
|
|
1445
|
+
const agentKeyResult = resolveAgentKey(config, configDir);
|
|
1446
|
+
const agentKey = agentKeyResult.fold(() => void 0, (k) => k);
|
|
1447
|
+
const agentKeyError = agentKeyResult.fold((err) => err, () => void 0);
|
|
1448
|
+
if (agentKeyError && !hasSealedValues) return Left(agentKeyError);
|
|
1339
1449
|
const audit = computeAudit(config, detectFnoxKeys(configDir));
|
|
1340
1450
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
1341
1451
|
const secrets = {};
|
|
1342
1452
|
const injected = [];
|
|
1343
1453
|
const skipped = [];
|
|
1344
|
-
const
|
|
1345
|
-
if (
|
|
1454
|
+
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
|
+
}
|
|
1467
|
+
const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
|
|
1468
|
+
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, agentKey).fold((err) => {
|
|
1346
1469
|
warnings.push(`fnox export failed: ${err.message}`);
|
|
1347
|
-
for (const key of
|
|
1470
|
+
for (const key of remainingKeys) skipped.push(key);
|
|
1348
1471
|
}, (exported) => {
|
|
1349
|
-
for (const key of
|
|
1472
|
+
for (const key of remainingKeys) if (key in exported) {
|
|
1350
1473
|
secrets[key] = exported[key];
|
|
1351
1474
|
injected.push(key);
|
|
1352
1475
|
} else skipped.push(key);
|
|
1353
1476
|
});
|
|
1354
1477
|
else {
|
|
1355
|
-
warnings.push("fnox not available — no secrets injected");
|
|
1356
|
-
for (const key of
|
|
1478
|
+
if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
|
|
1479
|
+
for (const key of remainingKeys) skipped.push(key);
|
|
1357
1480
|
}
|
|
1358
1481
|
if (inject) for (const [key, value] of Object.entries(secrets)) process.env[key] = value;
|
|
1359
1482
|
return {
|
|
@@ -1364,7 +1487,7 @@ const bootSafe = (options) => {
|
|
|
1364
1487
|
warnings
|
|
1365
1488
|
};
|
|
1366
1489
|
});
|
|
1367
|
-
})
|
|
1490
|
+
});
|
|
1368
1491
|
};
|
|
1369
1492
|
/** Programmatic boot — throws EnvpktBootError on failure */
|
|
1370
1493
|
const boot = (options) => bootSafe(options).fold((err) => {
|
|
@@ -1400,6 +1523,42 @@ const formatBootError = (error) => {
|
|
|
1400
1523
|
}
|
|
1401
1524
|
};
|
|
1402
1525
|
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/core/resolve-values.ts
|
|
1528
|
+
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
1529
|
+
const resolveValues = async (keys, profile, agentKey) => {
|
|
1530
|
+
const result = {};
|
|
1531
|
+
const remaining = new Set(keys);
|
|
1532
|
+
if (fnoxAvailable()) fnoxExport(profile, agentKey).fold(() => {}, (exported) => {
|
|
1533
|
+
for (const key of [...remaining]) if (key in exported) {
|
|
1534
|
+
result[key] = exported[key];
|
|
1535
|
+
remaining.delete(key);
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
for (const key of [...remaining]) {
|
|
1539
|
+
const envValue = process.env[key];
|
|
1540
|
+
if (envValue !== void 0 && envValue !== "") {
|
|
1541
|
+
result[key] = envValue;
|
|
1542
|
+
remaining.delete(key);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (remaining.size > 0 && process.stdin.isTTY) {
|
|
1546
|
+
const rl = createInterface({
|
|
1547
|
+
input: process.stdin,
|
|
1548
|
+
output: process.stderr
|
|
1549
|
+
});
|
|
1550
|
+
const prompt = (question) => new Promise((resolve) => {
|
|
1551
|
+
rl.question(question, (answer) => resolve(answer));
|
|
1552
|
+
});
|
|
1553
|
+
for (const key of remaining) {
|
|
1554
|
+
const value = await prompt(`Enter value for ${key}: `);
|
|
1555
|
+
if (value !== "") result[key] = value;
|
|
1556
|
+
}
|
|
1557
|
+
rl.close();
|
|
1558
|
+
}
|
|
1559
|
+
return result;
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1403
1562
|
//#endregion
|
|
1404
1563
|
//#region src/core/fleet.ts
|
|
1405
1564
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
@@ -1766,4 +1925,4 @@ const startServer = async () => {
|
|
|
1766
1925
|
};
|
|
1767
1926
|
|
|
1768
1927
|
//#endregion
|
|
1769
|
-
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, 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, resourceDefinitions, scanEnv, scanFleet, startServer, toolDefinitions, unwrapAgentKey, validateConfig };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envpkt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Credential lifecycle and fleet management for AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"credentials",
|
|
@@ -20,6 +20,24 @@
|
|
|
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.27.1",
|
|
25
43
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -28,7 +46,7 @@
|
|
|
28
46
|
"smol-toml": "^1.6.0"
|
|
29
47
|
},
|
|
30
48
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^24.
|
|
49
|
+
"@types/node": "^24.11.0",
|
|
32
50
|
"ts-builds": "^2.5.0",
|
|
33
51
|
"tsdown": "^0.20.3",
|
|
34
52
|
"tsx": "^4.21.0"
|
|
@@ -51,21 +69,5 @@
|
|
|
51
69
|
"schemas"
|
|
52
70
|
],
|
|
53
71
|
"prettier": "ts-builds/prettier",
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"format": "ts-builds format",
|
|
57
|
-
"format:check": "ts-builds format:check",
|
|
58
|
-
"lint": "ts-builds lint",
|
|
59
|
-
"lint:check": "ts-builds lint:check",
|
|
60
|
-
"typecheck": "ts-builds typecheck",
|
|
61
|
-
"test": "ts-builds test",
|
|
62
|
-
"test:watch": "ts-builds test:watch",
|
|
63
|
-
"test:coverage": "ts-builds test:coverage",
|
|
64
|
-
"build": "ts-builds build",
|
|
65
|
-
"build:schema": "tsx scripts/build-schema.ts",
|
|
66
|
-
"demo": "tsx scripts/generate-demo-html.ts",
|
|
67
|
-
"dev": "ts-builds dev",
|
|
68
|
-
"docs:dev": "pnpm --dir site dev",
|
|
69
|
-
"docs:build": "pnpm --dir site build"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
+
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017"
|
|
73
|
+
}
|
|
@@ -144,6 +144,10 @@
|
|
|
144
144
|
"description": "Where the secret value originates (e.g. 'vault', 'ci')",
|
|
145
145
|
"type": "string"
|
|
146
146
|
},
|
|
147
|
+
"encrypted_value": {
|
|
148
|
+
"description": "Age-encrypted secret value (armored ciphertext, safe to commit)",
|
|
149
|
+
"type": "string"
|
|
150
|
+
},
|
|
147
151
|
"required": {
|
|
148
152
|
"description": "Whether this secret is required for operation",
|
|
149
153
|
"type": "boolean"
|