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 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 envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
1325
- const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
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 (env[key] !== void 0 && env[key] !== "") return true;
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) => env[targetKey] !== void 0 && env[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 (env[key] !== void 0 && env[key] !== "") return true;
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) => env[targetKey] !== void 0 && env[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 value = envEntries[key].value ?? process.env[key] ?? "";
2661
- console.log(`export ${key}='${shellEscape(value)}'`);
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
- if (!(key in env)) env[key] = value;
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 (env[key] !== void 0 && env[key] !== "") return true;
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) => env[targetKey] !== void 0 && env[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 (env[key] !== void 0 && env[key] !== "") return true;
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) => env[targetKey] !== void 0 && env[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 envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[key]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
1949
- const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
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.4",
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": "^14.0.3",
45
- "functype": "^0.60.7",
46
- "functype-log": "^0.60.7",
47
- "functype-os": "^0.60.7",
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.12.4",
52
- "ts-builds": "^2.8.1",
53
- "tsdown": "^0.22.0",
54
- "tsx": "^4.22.3"
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@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
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
  }