envpkt 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +96 -22
- package/dist/index.d.ts +15 -0
- package/dist/index.js +73 -15
- package/package.json +10 -10
- package/schemas/envpkt.schema.json +26 -0
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,37 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
15
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
16
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
17
17
|
import { createInterface } from "node:readline";
|
|
18
|
+
//#region src/core/namespace.ts
|
|
19
|
+
const DEFAULT_SEPARATOR = "__";
|
|
20
|
+
const SHELL_SAFE_SEPARATOR_RE = /^[A-Za-z0-9_]+$/;
|
|
21
|
+
/**
|
|
22
|
+
* A separator is shell-safe when the resulting wire name is still a valid POSIX
|
|
23
|
+
* shell identifier. Only `[A-Za-z0-9_]` qualify — `.` and `:` (and empties)
|
|
24
|
+
* break `export NAME=` / `$NAME` and must be flagged.
|
|
25
|
+
*/
|
|
26
|
+
const isShellSafeSeparator = (separator) => SHELL_SAFE_SEPARATOR_RE.test(separator);
|
|
27
|
+
/**
|
|
28
|
+
* Build the logical-key → wire-name transform for a config.
|
|
29
|
+
*
|
|
30
|
+
* The wire name is what gets written to / read from `process.env`. It is the
|
|
31
|
+
* only place a namespace prefix is applied — internal records (audit, aliases,
|
|
32
|
+
* fnox lookup) stay keyed by the canonical logical name.
|
|
33
|
+
*
|
|
34
|
+
* A per-entry namespace overrides the file-level prefix; an explicit empty
|
|
35
|
+
* string opts out (`"" ?? filePrefix` → `""`, a falsy prefix = no prefix).
|
|
36
|
+
*
|
|
37
|
+
* The default separator is `__` because it is the only namespace separator
|
|
38
|
+
* valid in a POSIX shell identifier (`.`/`:` break shell `export`/`$VAR`).
|
|
39
|
+
*/
|
|
40
|
+
const makeEnvNamer = (config) => {
|
|
41
|
+
const filePrefix = config.namespace?.prefix;
|
|
42
|
+
const separator = config.namespace?.separator ?? DEFAULT_SEPARATOR;
|
|
43
|
+
return (logicalKey, entryNamespace) => {
|
|
44
|
+
const prefix = entryNamespace ?? filePrefix;
|
|
45
|
+
return prefix ? `${prefix}${separator}${logicalKey}` : logicalKey;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
18
49
|
//#region src/core/audit.ts
|
|
19
50
|
const MS_PER_DAY = 864e5;
|
|
20
51
|
const WARN_BEFORE_DAYS = 30;
|
|
@@ -146,13 +177,15 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
146
177
|
};
|
|
147
178
|
const computeEnvAudit = (config, env = process.env) => {
|
|
148
179
|
const envEntries = config.env ?? {};
|
|
180
|
+
const namer = makeEnvNamer(config);
|
|
181
|
+
const envWire = (key) => namer(key, envEntries[key]?.namespace);
|
|
149
182
|
const resolveEffectiveDefault = (entry) => {
|
|
150
183
|
return Do(function* () {
|
|
151
184
|
return yield* $(Option((yield* $(Option(envEntries[yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]))]))).value));
|
|
152
185
|
}).orElse(entry.value ?? "");
|
|
153
186
|
};
|
|
154
187
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
155
|
-
const currentValue = env[key];
|
|
188
|
+
const currentValue = env[envWire(key)];
|
|
156
189
|
const effectiveDefault = resolveEffectiveDefault(entry);
|
|
157
190
|
return {
|
|
158
191
|
key,
|
|
@@ -197,6 +230,13 @@ const IdentitySchema = Type.Object({
|
|
|
197
230
|
recipient: Type.Optional(Type.String({ description: "Age public key for encryption" })),
|
|
198
231
|
secrets: Type.Optional(Type.Array(Type.String(), { description: "Secret keys needed from the catalog" }))
|
|
199
232
|
}, { description: "Identity and capabilities of the principal using this envpkt" });
|
|
233
|
+
const NamespaceSchema = Type.Object({
|
|
234
|
+
prefix: Type.String({ description: "Namespace prefix applied to all injected env/secret names (e.g. 'CIV' -> CIV__API_KEY)" }),
|
|
235
|
+
separator: Type.Optional(Type.String({
|
|
236
|
+
default: "__",
|
|
237
|
+
description: "Separator between prefix and key. Default '__' (shell-safe). Note: '.' and ':' are not valid in shell identifiers."
|
|
238
|
+
}))
|
|
239
|
+
}, { description: "Optional namespace/package prefix for injected environment variable names" });
|
|
200
240
|
const SecretMetaSchema = Type.Object({
|
|
201
241
|
service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
|
|
202
242
|
expires: Type.Optional(Type.String({
|
|
@@ -225,7 +265,8 @@ const SecretMetaSchema = Type.Object({
|
|
|
225
265
|
encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
|
|
226
266
|
from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
|
|
227
267
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
228
|
-
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
268
|
+
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
269
|
+
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
|
|
229
270
|
}, { description: "Metadata about a single secret" });
|
|
230
271
|
const LifecycleConfigSchema = Type.Object({
|
|
231
272
|
stale_warning_days: Type.Optional(Type.Number({
|
|
@@ -252,7 +293,8 @@ const EnvMetaSchema = Type.Object({
|
|
|
252
293
|
from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'env.<KEY>') whose resolved value this alias reuses. Mutually exclusive with value." })),
|
|
253
294
|
purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
|
|
254
295
|
comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
|
|
255
|
-
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
296
|
+
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
297
|
+
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
|
|
256
298
|
}, { description: "Metadata for a plaintext environment default (non-secret)" });
|
|
257
299
|
const EnvpktConfigSchema = Type.Object({
|
|
258
300
|
version: Type.Number({
|
|
@@ -260,6 +302,7 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
260
302
|
default: 1
|
|
261
303
|
}),
|
|
262
304
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
305
|
+
namespace: Type.Optional(NamespaceSchema),
|
|
263
306
|
identity: Type.Optional(IdentitySchema),
|
|
264
307
|
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
265
308
|
env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
|
|
@@ -1303,6 +1346,13 @@ const bootSafe = (options) => {
|
|
|
1303
1346
|
log.debug("alias.validate.success", { aliases: aliasTable.entries.size });
|
|
1304
1347
|
const secretEntries = config.secret ?? {};
|
|
1305
1348
|
const envEntries = config.env ?? {};
|
|
1349
|
+
const namer = makeEnvNamer(config);
|
|
1350
|
+
const secretEnv = (key) => namer(key, secretEntries[key]?.namespace);
|
|
1351
|
+
const envEnv = (key) => namer(key, envEntries[key]?.namespace);
|
|
1352
|
+
const envNames = {
|
|
1353
|
+
...Object.fromEntries(Object.keys(envEntries).map((k) => [k, envEnv(k)])),
|
|
1354
|
+
...Object.fromEntries(Object.keys(secretEntries).map((k) => [k, secretEnv(k)]))
|
|
1355
|
+
};
|
|
1306
1356
|
const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
|
|
1307
1357
|
const aliasSecretKeys = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
|
|
1308
1358
|
const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
@@ -1321,10 +1371,12 @@ const bootSafe = (options) => {
|
|
|
1321
1371
|
const injected = [];
|
|
1322
1372
|
const skipped = [];
|
|
1323
1373
|
warnings.push(...checkEnvMisclassification(config));
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1374
|
+
const nsSeparator = config.namespace?.separator;
|
|
1375
|
+
if (nsSeparator !== void 0 && !isShellSafeSeparator(nsSeparator)) warnings.push(`[namespace] separator "${nsSeparator}" is not shell-safe — injected names won't be usable via shell $VAR/export. Use '_' or '__'.`);
|
|
1376
|
+
const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[envEnv(key)]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
|
|
1377
|
+
const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[envEnv(key)]).fold(() => [], () => [key]));
|
|
1326
1378
|
if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
|
|
1327
|
-
process.env[key] = value;
|
|
1379
|
+
process.env[envEnv(key)] = value;
|
|
1328
1380
|
});
|
|
1329
1381
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
1330
1382
|
const identityFilePath = resolveIdentityFilePath(config, configDir, true);
|
|
@@ -1399,20 +1451,20 @@ const bootSafe = (options) => {
|
|
|
1399
1451
|
aliasEnvKeys.forEach((aliasKey) => {
|
|
1400
1452
|
const entry = aliasTable.entries.get(`env.${aliasKey}`);
|
|
1401
1453
|
if (!entry) return;
|
|
1402
|
-
if (process.env[aliasKey] !== void 0) {
|
|
1454
|
+
if (process.env[envEnv(aliasKey)] !== void 0) {
|
|
1403
1455
|
overridden.push(aliasKey);
|
|
1404
1456
|
return;
|
|
1405
1457
|
}
|
|
1406
1458
|
const targetEntry = envEntries[entry.targetKey];
|
|
1407
1459
|
if (targetEntry?.value === void 0) return;
|
|
1408
|
-
envDefaults[aliasKey] = process.env[entry.targetKey] ?? targetEntry.value;
|
|
1460
|
+
envDefaults[aliasKey] = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
|
|
1409
1461
|
});
|
|
1410
1462
|
if (inject) {
|
|
1411
1463
|
Object.entries(envDefaults).forEach(([key, value]) => {
|
|
1412
|
-
process.env[key] ??= value;
|
|
1464
|
+
process.env[envEnv(key)] ??= value;
|
|
1413
1465
|
});
|
|
1414
1466
|
Object.entries(secrets).forEach(([key, value]) => {
|
|
1415
|
-
process.env[key] = value;
|
|
1467
|
+
process.env[secretEnv(key)] = value;
|
|
1416
1468
|
});
|
|
1417
1469
|
}
|
|
1418
1470
|
return {
|
|
@@ -1423,6 +1475,7 @@ const bootSafe = (options) => {
|
|
|
1423
1475
|
warnings,
|
|
1424
1476
|
envDefaults,
|
|
1425
1477
|
overridden,
|
|
1478
|
+
envNames,
|
|
1426
1479
|
configPath,
|
|
1427
1480
|
configSource
|
|
1428
1481
|
};
|
|
@@ -2153,11 +2206,16 @@ const envCheck = (config, env) => {
|
|
|
2153
2206
|
const secretEntries = config.secret ?? {};
|
|
2154
2207
|
const metaKeys = Object.keys(secretEntries);
|
|
2155
2208
|
const metaKeysSet = Set$1(metaKeys);
|
|
2209
|
+
const namer = makeEnvNamer(config);
|
|
2210
|
+
const envDefaultsForWire = config.env ?? {};
|
|
2211
|
+
const secretWire = (key) => namer(key, secretEntries[key]?.namespace);
|
|
2212
|
+
const envWire = (key) => namer(key, envDefaultsForWire[key]?.namespace);
|
|
2213
|
+
const isPresentAt = (wire) => env[wire] !== void 0 && env[wire] !== "";
|
|
2156
2214
|
const isSecretPresent = (key) => {
|
|
2157
|
-
if (
|
|
2215
|
+
if (isPresentAt(secretWire(key))) return true;
|
|
2158
2216
|
const meta = secretEntries[key];
|
|
2159
2217
|
if (meta?.from_key === void 0) return false;
|
|
2160
|
-
return parseAliasRef(meta.from_key, "secret").fold(() => false, (targetKey) =>
|
|
2218
|
+
return parseAliasRef(meta.from_key, "secret").fold(() => false, (targetKey) => isPresentAt(secretWire(targetKey)));
|
|
2161
2219
|
};
|
|
2162
2220
|
const secretDriftEntries = Object.entries(secretEntries).map(([key, meta]) => {
|
|
2163
2221
|
const present = isSecretPresent(key);
|
|
@@ -2170,10 +2228,10 @@ const envCheck = (config, env) => {
|
|
|
2170
2228
|
});
|
|
2171
2229
|
const envDefaults = config.env ?? {};
|
|
2172
2230
|
const isEnvPresent = (key) => {
|
|
2173
|
-
if (
|
|
2231
|
+
if (isPresentAt(envWire(key))) return true;
|
|
2174
2232
|
const meta = envDefaults[key];
|
|
2175
2233
|
if (meta?.from_key === void 0) return false;
|
|
2176
|
-
return parseAliasRef(meta.from_key, "env").fold(() => false, (targetKey) =>
|
|
2234
|
+
return parseAliasRef(meta.from_key, "env").fold(() => false, (targetKey) => isPresentAt(envWire(targetKey)));
|
|
2177
2235
|
};
|
|
2178
2236
|
const envDefaultEntries = Object.keys(envDefaults).filter((key) => !metaKeysSet.has(key)).map((key) => {
|
|
2179
2237
|
const present = isEnvPresent(key);
|
|
@@ -2184,7 +2242,7 @@ const envCheck = (config, env) => {
|
|
|
2184
2242
|
confidence: Option(void 0)
|
|
2185
2243
|
};
|
|
2186
2244
|
});
|
|
2187
|
-
const trackedKeys = Set$1([...metaKeys, ...envDefaultEntries.map((e) => e.envVar)]);
|
|
2245
|
+
const trackedKeys = Set$1([...metaKeys.map(secretWire), ...envDefaultEntries.map((e) => envWire(e.envVar))]);
|
|
2188
2246
|
const untrackedEntries = scanEnv(env).filter((match) => !trackedKeys.has(match.envVar)).map((match) => ({
|
|
2189
2247
|
envVar: match.envVar,
|
|
2190
2248
|
service: match.service,
|
|
@@ -2650,19 +2708,22 @@ const runEnvExport = (options) => {
|
|
|
2650
2708
|
boot.warnings.forEach((warning) => {
|
|
2651
2709
|
console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
2652
2710
|
});
|
|
2711
|
+
const wireName = (key) => boot.envNames[key] ?? key;
|
|
2653
2712
|
Object.entries(boot.envDefaults).forEach(([key, value]) => {
|
|
2654
|
-
console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2713
|
+
console.log(`export ${wireName(key)}='${shellEscape(value)}'`);
|
|
2655
2714
|
});
|
|
2656
2715
|
if (boot.overridden.length > 0) loadConfig(boot.configPath).fold(() => {}, (config) => {
|
|
2657
2716
|
const envEntries = config.env ?? {};
|
|
2658
2717
|
boot.overridden.forEach((key) => {
|
|
2659
2718
|
if (!(key in envEntries)) return;
|
|
2660
|
-
const
|
|
2661
|
-
|
|
2719
|
+
const entry = envEntries[key];
|
|
2720
|
+
const name = wireName(key);
|
|
2721
|
+
const value = entry.value ?? process.env[name] ?? "";
|
|
2722
|
+
console.log(`export ${name}='${shellEscape(value)}'`);
|
|
2662
2723
|
});
|
|
2663
2724
|
});
|
|
2664
2725
|
Object.entries(boot.secrets).forEach(([key, value]) => {
|
|
2665
|
-
console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2726
|
+
console.log(`export ${wireName(key)}='${shellEscape(value)}'`);
|
|
2666
2727
|
});
|
|
2667
2728
|
});
|
|
2668
2729
|
};
|
|
@@ -2919,11 +2980,13 @@ const runExec = (args, options) => {
|
|
|
2919
2980
|
console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
2920
2981
|
});
|
|
2921
2982
|
const env = { ...process.env };
|
|
2983
|
+
const wireName = (key) => boot.envNames[key] ?? key;
|
|
2922
2984
|
Object.entries(boot.envDefaults).forEach(([key, value]) => {
|
|
2923
|
-
|
|
2985
|
+
const name = wireName(key);
|
|
2986
|
+
if (!(name in env)) env[name] = value;
|
|
2924
2987
|
});
|
|
2925
2988
|
Object.entries(boot.secrets).forEach(([key, value]) => {
|
|
2926
|
-
env[key] = value;
|
|
2989
|
+
env[wireName(key)] = value;
|
|
2927
2990
|
});
|
|
2928
2991
|
const [cmd, ...cmdArgs] = args;
|
|
2929
2992
|
Try(() => execFileSync(cmd, cmdArgs, {
|
|
@@ -3850,6 +3913,12 @@ const runSeal = async (options) => {
|
|
|
3850
3913
|
console.error(`${DIM}Available keys: ${Object.keys(allSecretEntries).join(", ")}${RESET}`);
|
|
3851
3914
|
process.exit(2);
|
|
3852
3915
|
}
|
|
3916
|
+
const aliasKeys = editKeys.filter((k) => allSecretEntries[k].from_key !== void 0);
|
|
3917
|
+
if (aliasKeys.length > 0) {
|
|
3918
|
+
console.error(`${RED}Error:${RESET} Cannot seal alias entries: ${aliasKeys.join(", ")}`);
|
|
3919
|
+
console.error(`${DIM}Aliases reference another secret's value via from_key — seal the target instead.${RESET}`);
|
|
3920
|
+
process.exit(2);
|
|
3921
|
+
}
|
|
3853
3922
|
if (!process.stdin.isTTY) {
|
|
3854
3923
|
console.error(`${RED}Error:${RESET} --edit requires an interactive terminal`);
|
|
3855
3924
|
process.exit(2);
|
|
@@ -3888,9 +3957,14 @@ const runSeal = async (options) => {
|
|
|
3888
3957
|
return;
|
|
3889
3958
|
}
|
|
3890
3959
|
const allSecretEntries = config.secret ?? {};
|
|
3891
|
-
const allKeys = Object.keys(allSecretEntries);
|
|
3960
|
+
const allKeys = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0);
|
|
3961
|
+
const skippedAliases = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key !== void 0);
|
|
3892
3962
|
const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
|
|
3893
3963
|
const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
|
|
3964
|
+
if (skippedAliases.length > 0) {
|
|
3965
|
+
const noun = skippedAliases.length === 1 ? "alias" : "aliases";
|
|
3966
|
+
console.error(`${DIM}Skipping ${skippedAliases.length} ${noun}: ${skippedAliases.join(", ")}${RESET}`);
|
|
3967
|
+
}
|
|
3894
3968
|
if (!options.reseal && alreadySealed.length > 0) {
|
|
3895
3969
|
if (unsealed.length === 0) {
|
|
3896
3970
|
console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ declare const SecretMetaSchema: import("@sinclair/typebox").TObject<{
|
|
|
49
49
|
from_key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
50
50
|
required: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
51
51
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
52
|
+
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
52
53
|
}>;
|
|
53
54
|
type SecretMeta = Static<typeof SecretMetaSchema>;
|
|
54
55
|
declare const LifecycleConfigSchema: import("@sinclair/typebox").TObject<{
|
|
@@ -71,11 +72,16 @@ declare const EnvMetaSchema: import("@sinclair/typebox").TObject<{
|
|
|
71
72
|
purpose: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
72
73
|
comment: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
73
74
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
75
|
+
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
74
76
|
}>;
|
|
75
77
|
type EnvMeta = Static<typeof EnvMetaSchema>;
|
|
76
78
|
declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
|
|
77
79
|
version: import("@sinclair/typebox").TNumber;
|
|
78
80
|
catalog: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
81
|
+
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
82
|
+
prefix: import("@sinclair/typebox").TString;
|
|
83
|
+
separator: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
84
|
+
}>>;
|
|
79
85
|
identity: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
80
86
|
name: import("@sinclair/typebox").TString;
|
|
81
87
|
consumer: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"agent">, import("@sinclair/typebox").TLiteral<"service">, import("@sinclair/typebox").TLiteral<"developer">, import("@sinclair/typebox").TLiteral<"ci">]>>;
|
|
@@ -104,6 +110,7 @@ declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
104
110
|
from_key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
105
111
|
required: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
106
112
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
113
|
+
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
107
114
|
}>>>;
|
|
108
115
|
env: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
109
116
|
value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -111,6 +118,7 @@ declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
111
118
|
purpose: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
112
119
|
comment: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
113
120
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
121
|
+
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
114
122
|
}>>>;
|
|
115
123
|
lifecycle: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
116
124
|
stale_warning_days: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
@@ -305,6 +313,13 @@ type BootResult = {
|
|
|
305
313
|
readonly warnings: ReadonlyArray<string>;
|
|
306
314
|
readonly envDefaults: Readonly<Record<string, string>>;
|
|
307
315
|
readonly overridden: ReadonlyArray<string>;
|
|
316
|
+
/**
|
|
317
|
+
* Map of logical key → injected wire name (e.g. API_KEY → CIV__API_KEY).
|
|
318
|
+
* `secrets`/`envDefaults` stay keyed by the logical name; this is how
|
|
319
|
+
* shell-facing consumers (exec, env export) recover the actual process.env
|
|
320
|
+
* name. Identity map when no namespace is configured.
|
|
321
|
+
*/
|
|
322
|
+
readonly envNames: Readonly<Record<string, string>>;
|
|
308
323
|
readonly configPath: string;
|
|
309
324
|
readonly configSource: ConfigSource;
|
|
310
325
|
};
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,13 @@ const IdentitySchema = Type.Object({
|
|
|
39
39
|
}, { description: "Identity and capabilities of the principal using this envpkt" });
|
|
40
40
|
/** @deprecated Use `IdentitySchema` instead */
|
|
41
41
|
const AgentIdentitySchema = IdentitySchema;
|
|
42
|
+
const NamespaceSchema = Type.Object({
|
|
43
|
+
prefix: Type.String({ description: "Namespace prefix applied to all injected env/secret names (e.g. 'CIV' -> CIV__API_KEY)" }),
|
|
44
|
+
separator: Type.Optional(Type.String({
|
|
45
|
+
default: "__",
|
|
46
|
+
description: "Separator between prefix and key. Default '__' (shell-safe). Note: '.' and ':' are not valid in shell identifiers."
|
|
47
|
+
}))
|
|
48
|
+
}, { description: "Optional namespace/package prefix for injected environment variable names" });
|
|
42
49
|
const SecretMetaSchema = Type.Object({
|
|
43
50
|
service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
|
|
44
51
|
expires: Type.Optional(Type.String({
|
|
@@ -67,7 +74,8 @@ const SecretMetaSchema = Type.Object({
|
|
|
67
74
|
encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
|
|
68
75
|
from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
|
|
69
76
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
70
|
-
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
77
|
+
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
78
|
+
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
|
|
71
79
|
}, { description: "Metadata about a single secret" });
|
|
72
80
|
const LifecycleConfigSchema = Type.Object({
|
|
73
81
|
stale_warning_days: Type.Optional(Type.Number({
|
|
@@ -94,7 +102,8 @@ const EnvMetaSchema = Type.Object({
|
|
|
94
102
|
from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'env.<KEY>') whose resolved value this alias reuses. Mutually exclusive with value." })),
|
|
95
103
|
purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
|
|
96
104
|
comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
|
|
97
|
-
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
105
|
+
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
106
|
+
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
|
|
98
107
|
}, { description: "Metadata for a plaintext environment default (non-secret)" });
|
|
99
108
|
const EnvpktConfigSchema = Type.Object({
|
|
100
109
|
version: Type.Number({
|
|
@@ -102,6 +111,7 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
102
111
|
default: 1
|
|
103
112
|
}),
|
|
104
113
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
114
|
+
namespace: Type.Optional(NamespaceSchema),
|
|
105
115
|
identity: Type.Optional(IdentitySchema),
|
|
106
116
|
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
107
117
|
env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
|
|
@@ -557,6 +567,37 @@ const formatPacket = (result, options) => {
|
|
|
557
567
|
return sections.join("\n\n");
|
|
558
568
|
};
|
|
559
569
|
//#endregion
|
|
570
|
+
//#region src/core/namespace.ts
|
|
571
|
+
const DEFAULT_SEPARATOR = "__";
|
|
572
|
+
const SHELL_SAFE_SEPARATOR_RE = /^[A-Za-z0-9_]+$/;
|
|
573
|
+
/**
|
|
574
|
+
* A separator is shell-safe when the resulting wire name is still a valid POSIX
|
|
575
|
+
* shell identifier. Only `[A-Za-z0-9_]` qualify — `.` and `:` (and empties)
|
|
576
|
+
* break `export NAME=` / `$NAME` and must be flagged.
|
|
577
|
+
*/
|
|
578
|
+
const isShellSafeSeparator = (separator) => SHELL_SAFE_SEPARATOR_RE.test(separator);
|
|
579
|
+
/**
|
|
580
|
+
* Build the logical-key → wire-name transform for a config.
|
|
581
|
+
*
|
|
582
|
+
* The wire name is what gets written to / read from `process.env`. It is the
|
|
583
|
+
* only place a namespace prefix is applied — internal records (audit, aliases,
|
|
584
|
+
* fnox lookup) stay keyed by the canonical logical name.
|
|
585
|
+
*
|
|
586
|
+
* A per-entry namespace overrides the file-level prefix; an explicit empty
|
|
587
|
+
* string opts out (`"" ?? filePrefix` → `""`, a falsy prefix = no prefix).
|
|
588
|
+
*
|
|
589
|
+
* The default separator is `__` because it is the only namespace separator
|
|
590
|
+
* valid in a POSIX shell identifier (`.`/`:` break shell `export`/`$VAR`).
|
|
591
|
+
*/
|
|
592
|
+
const makeEnvNamer = (config) => {
|
|
593
|
+
const filePrefix = config.namespace?.prefix;
|
|
594
|
+
const separator = config.namespace?.separator ?? DEFAULT_SEPARATOR;
|
|
595
|
+
return (logicalKey, entryNamespace) => {
|
|
596
|
+
const prefix = entryNamespace ?? filePrefix;
|
|
597
|
+
return prefix ? `${prefix}${separator}${logicalKey}` : logicalKey;
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
//#endregion
|
|
560
601
|
//#region src/core/audit.ts
|
|
561
602
|
const MS_PER_DAY = 864e5;
|
|
562
603
|
const WARN_BEFORE_DAYS = 30;
|
|
@@ -688,13 +729,15 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
688
729
|
};
|
|
689
730
|
const computeEnvAudit = (config, env = process.env) => {
|
|
690
731
|
const envEntries = config.env ?? {};
|
|
732
|
+
const namer = makeEnvNamer(config);
|
|
733
|
+
const envWire = (key) => namer(key, envEntries[key]?.namespace);
|
|
691
734
|
const resolveEffectiveDefault = (entry) => {
|
|
692
735
|
return Do(function* () {
|
|
693
736
|
return yield* $(Option((yield* $(Option(envEntries[yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]))]))).value));
|
|
694
737
|
}).orElse(entry.value ?? "");
|
|
695
738
|
};
|
|
696
739
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
697
|
-
const currentValue = env[key];
|
|
740
|
+
const currentValue = env[envWire(key)];
|
|
698
741
|
const effectiveDefault = resolveEffectiveDefault(entry);
|
|
699
742
|
return {
|
|
700
743
|
key,
|
|
@@ -1437,11 +1480,16 @@ const envCheck = (config, env) => {
|
|
|
1437
1480
|
const secretEntries = config.secret ?? {};
|
|
1438
1481
|
const metaKeys = Object.keys(secretEntries);
|
|
1439
1482
|
const metaKeysSet = Set$1(metaKeys);
|
|
1483
|
+
const namer = makeEnvNamer(config);
|
|
1484
|
+
const envDefaultsForWire = config.env ?? {};
|
|
1485
|
+
const secretWire = (key) => namer(key, secretEntries[key]?.namespace);
|
|
1486
|
+
const envWire = (key) => namer(key, envDefaultsForWire[key]?.namespace);
|
|
1487
|
+
const isPresentAt = (wire) => env[wire] !== void 0 && env[wire] !== "";
|
|
1440
1488
|
const isSecretPresent = (key) => {
|
|
1441
|
-
if (
|
|
1489
|
+
if (isPresentAt(secretWire(key))) return true;
|
|
1442
1490
|
const meta = secretEntries[key];
|
|
1443
1491
|
if (meta?.from_key === void 0) return false;
|
|
1444
|
-
return parseAliasRef(meta.from_key, "secret").fold(() => false, (targetKey) =>
|
|
1492
|
+
return parseAliasRef(meta.from_key, "secret").fold(() => false, (targetKey) => isPresentAt(secretWire(targetKey)));
|
|
1445
1493
|
};
|
|
1446
1494
|
const secretDriftEntries = Object.entries(secretEntries).map(([key, meta]) => {
|
|
1447
1495
|
const present = isSecretPresent(key);
|
|
@@ -1454,10 +1502,10 @@ const envCheck = (config, env) => {
|
|
|
1454
1502
|
});
|
|
1455
1503
|
const envDefaults = config.env ?? {};
|
|
1456
1504
|
const isEnvPresent = (key) => {
|
|
1457
|
-
if (
|
|
1505
|
+
if (isPresentAt(envWire(key))) return true;
|
|
1458
1506
|
const meta = envDefaults[key];
|
|
1459
1507
|
if (meta?.from_key === void 0) return false;
|
|
1460
|
-
return parseAliasRef(meta.from_key, "env").fold(() => false, (targetKey) =>
|
|
1508
|
+
return parseAliasRef(meta.from_key, "env").fold(() => false, (targetKey) => isPresentAt(envWire(targetKey)));
|
|
1461
1509
|
};
|
|
1462
1510
|
const envDefaultEntries = Object.keys(envDefaults).filter((key) => !metaKeysSet.has(key)).map((key) => {
|
|
1463
1511
|
const present = isEnvPresent(key);
|
|
@@ -1468,7 +1516,7 @@ const envCheck = (config, env) => {
|
|
|
1468
1516
|
confidence: Option(void 0)
|
|
1469
1517
|
};
|
|
1470
1518
|
});
|
|
1471
|
-
const trackedKeys = Set$1([...metaKeys, ...envDefaultEntries.map((e) => e.envVar)]);
|
|
1519
|
+
const trackedKeys = Set$1([...metaKeys.map(secretWire), ...envDefaultEntries.map((e) => envWire(e.envVar))]);
|
|
1472
1520
|
const untrackedEntries = scanEnv(env).filter((match) => !trackedKeys.has(match.envVar)).map((match) => ({
|
|
1473
1521
|
envVar: match.envVar,
|
|
1474
1522
|
service: match.service,
|
|
@@ -1927,6 +1975,13 @@ const bootSafe = (options) => {
|
|
|
1927
1975
|
log.debug("alias.validate.success", { aliases: aliasTable.entries.size });
|
|
1928
1976
|
const secretEntries = config.secret ?? {};
|
|
1929
1977
|
const envEntries = config.env ?? {};
|
|
1978
|
+
const namer = makeEnvNamer(config);
|
|
1979
|
+
const secretEnv = (key) => namer(key, secretEntries[key]?.namespace);
|
|
1980
|
+
const envEnv = (key) => namer(key, envEntries[key]?.namespace);
|
|
1981
|
+
const envNames = {
|
|
1982
|
+
...Object.fromEntries(Object.keys(envEntries).map((k) => [k, envEnv(k)])),
|
|
1983
|
+
...Object.fromEntries(Object.keys(secretEntries).map((k) => [k, secretEnv(k)]))
|
|
1984
|
+
};
|
|
1930
1985
|
const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
|
|
1931
1986
|
const aliasSecretKeys = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
|
|
1932
1987
|
const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
@@ -1945,10 +2000,12 @@ const bootSafe = (options) => {
|
|
|
1945
2000
|
const injected = [];
|
|
1946
2001
|
const skipped = [];
|
|
1947
2002
|
warnings.push(...checkEnvMisclassification(config));
|
|
1948
|
-
const
|
|
1949
|
-
|
|
2003
|
+
const nsSeparator = config.namespace?.separator;
|
|
2004
|
+
if (nsSeparator !== void 0 && !isShellSafeSeparator(nsSeparator)) warnings.push(`[namespace] separator "${nsSeparator}" is not shell-safe — injected names won't be usable via shell $VAR/export. Use '_' or '__'.`);
|
|
2005
|
+
const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[envEnv(key)]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
|
|
2006
|
+
const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[envEnv(key)]).fold(() => [], () => [key]));
|
|
1950
2007
|
if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
|
|
1951
|
-
process.env[key] = value;
|
|
2008
|
+
process.env[envEnv(key)] = value;
|
|
1952
2009
|
});
|
|
1953
2010
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
1954
2011
|
const identityFilePath = resolveIdentityFilePath(config, configDir, true);
|
|
@@ -2023,20 +2080,20 @@ const bootSafe = (options) => {
|
|
|
2023
2080
|
aliasEnvKeys.forEach((aliasKey) => {
|
|
2024
2081
|
const entry = aliasTable.entries.get(`env.${aliasKey}`);
|
|
2025
2082
|
if (!entry) return;
|
|
2026
|
-
if (process.env[aliasKey] !== void 0) {
|
|
2083
|
+
if (process.env[envEnv(aliasKey)] !== void 0) {
|
|
2027
2084
|
overridden.push(aliasKey);
|
|
2028
2085
|
return;
|
|
2029
2086
|
}
|
|
2030
2087
|
const targetEntry = envEntries[entry.targetKey];
|
|
2031
2088
|
if (targetEntry?.value === void 0) return;
|
|
2032
|
-
envDefaults[aliasKey] = process.env[entry.targetKey] ?? targetEntry.value;
|
|
2089
|
+
envDefaults[aliasKey] = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
|
|
2033
2090
|
});
|
|
2034
2091
|
if (inject) {
|
|
2035
2092
|
Object.entries(envDefaults).forEach(([key, value]) => {
|
|
2036
|
-
process.env[key] ??= value;
|
|
2093
|
+
process.env[envEnv(key)] ??= value;
|
|
2037
2094
|
});
|
|
2038
2095
|
Object.entries(secrets).forEach(([key, value]) => {
|
|
2039
|
-
process.env[key] = value;
|
|
2096
|
+
process.env[secretEnv(key)] = value;
|
|
2040
2097
|
});
|
|
2041
2098
|
}
|
|
2042
2099
|
return {
|
|
@@ -2047,6 +2104,7 @@ const bootSafe = (options) => {
|
|
|
2047
2104
|
warnings,
|
|
2048
2105
|
envDefaults,
|
|
2049
2106
|
overridden,
|
|
2107
|
+
envNames,
|
|
2050
2108
|
configPath,
|
|
2051
2109
|
configSource
|
|
2052
2110
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envpkt",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Credential lifecycle and fleet management for AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"credentials",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
43
|
"@sinclair/typebox": "^0.34.49",
|
|
44
|
-
"commander": "^
|
|
45
|
-
"functype": "^
|
|
46
|
-
"functype-log": "^
|
|
47
|
-
"functype-os": "^
|
|
44
|
+
"commander": "^15.0.0",
|
|
45
|
+
"functype": "^1.3.0",
|
|
46
|
+
"functype-log": "^1.3.0",
|
|
47
|
+
"functype-os": "^1.3.0",
|
|
48
48
|
"smol-toml": "^1.6.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^24.
|
|
52
|
-
"ts-builds": "^
|
|
53
|
-
"tsdown": "^0.22.
|
|
54
|
-
"tsx": "^4.22.
|
|
51
|
+
"@types/node": "^24.13.1",
|
|
52
|
+
"ts-builds": "^3.0.0",
|
|
53
|
+
"tsdown": "^0.22.2",
|
|
54
|
+
"tsx": "^4.22.4"
|
|
55
55
|
},
|
|
56
56
|
"type": "module",
|
|
57
57
|
"main": "./dist/index.js",
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"schemas"
|
|
72
72
|
],
|
|
73
73
|
"prettier": "ts-builds/prettier",
|
|
74
|
-
"packageManager": "pnpm@
|
|
74
|
+
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
|
|
75
75
|
}
|
|
@@ -17,6 +17,24 @@
|
|
|
17
17
|
"description": "Path to shared secret catalog (relative to this config file)",
|
|
18
18
|
"type": "string"
|
|
19
19
|
},
|
|
20
|
+
"namespace": {
|
|
21
|
+
"description": "Optional namespace/package prefix for injected environment variable names",
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": [
|
|
24
|
+
"prefix"
|
|
25
|
+
],
|
|
26
|
+
"properties": {
|
|
27
|
+
"prefix": {
|
|
28
|
+
"description": "Namespace prefix applied to all injected env/secret names (e.g. 'CIV' -> CIV__API_KEY)",
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"separator": {
|
|
32
|
+
"default": "__",
|
|
33
|
+
"description": "Separator between prefix and key. Default '__' (shell-safe). Note: '.' and ':' are not valid in shell identifiers.",
|
|
34
|
+
"type": "string"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
20
38
|
"identity": {
|
|
21
39
|
"description": "Identity and capabilities of the principal using this envpkt",
|
|
22
40
|
"type": "object",
|
|
@@ -172,6 +190,10 @@
|
|
|
172
190
|
"type": "string"
|
|
173
191
|
}
|
|
174
192
|
}
|
|
193
|
+
},
|
|
194
|
+
"namespace": {
|
|
195
|
+
"description": "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix.",
|
|
196
|
+
"type": "string"
|
|
175
197
|
}
|
|
176
198
|
}
|
|
177
199
|
}
|
|
@@ -209,6 +231,10 @@
|
|
|
209
231
|
"type": "string"
|
|
210
232
|
}
|
|
211
233
|
}
|
|
234
|
+
},
|
|
235
|
+
"namespace": {
|
|
236
|
+
"description": "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix.",
|
|
237
|
+
"type": "string"
|
|
212
238
|
}
|
|
213
239
|
}
|
|
214
240
|
}
|