@luckystack/secret-manager 0.2.4 → 0.2.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/index.d.ts CHANGED
@@ -1,4 +1,12 @@
1
- /** Bearer token: a literal string, or a file whose entire contents are the token. */
1
+ /**
2
+ * Bearer token: a literal string, or a file whose entire contents are the token.
3
+ *
4
+ * When using `{ fromFile }`, the path MUST NOT be derived from untrusted input.
5
+ * No path-traversal check is applied — the caller is responsible for ensuring
6
+ * the path is a fixed, gitignored file (e.g. `.secret-manager-token`) next to
7
+ * the project root. Paths like `/etc/passwd` or `../../.ssh/id_rsa` would be
8
+ * read without error.
9
+ */
2
10
  type SecretManagerToken = string | {
3
11
  fromFile: string;
4
12
  };
@@ -139,6 +147,11 @@ declare const reloadSecretManagerFromFiles: () => Promise<void>;
139
147
  * serialize the result into an HTTP response, a `/health` payload, or a log line.
140
148
  * For a safe diagnostic use {@link getCachedResolutionMeta}, which never exposes
141
149
  * the values.
150
+ *
151
+ * If you need to act on changed secrets (e.g. to rebuild a DB pool after rotation),
152
+ * use the `onApplied` callback in `SecretManagerConfig` instead — it receives only
153
+ * the changed env NAMES, never the secret values, and is called automatically after
154
+ * each successful resolve.
142
155
  */
143
156
  declare const getCachedResolution: () => CachedResolution | null;
144
157
  /** Values-free view of {@link getCachedResolution}: the timestamp + resolved pointer NAMES only. */
@@ -162,6 +175,13 @@ declare const getCachedResolutionMeta: () => CachedResolutionMeta | null;
162
175
  * debounce timer WITHOUT wiping the resolved cache / active config. Use for a
163
176
  * graceful shutdown of an embedded resolver (worker / CLI) so no timers keep
164
177
  * firing; the last resolved `process.env` values stay in place.
178
+ *
179
+ * **Note:** `activeConfig` is intentionally left set after `stopSecretManager`,
180
+ * so that `reloadSecretManagerFromFiles` can still use it for a final manual
181
+ * reload. As a consequence, calling `refreshSecretManager()` after `stop` will
182
+ * issue a live network request (the `!activeConfig` no-op guard is not met).
183
+ * If you want to prevent ANY further resolution after `stop`, call
184
+ * `resetSecretManagerForTests()` instead (which also clears `activeConfig`).
165
185
  */
166
186
  declare const stopSecretManager: () => void;
167
187
  /** Test-only — clear module state and tear down any dev watchers / timers. */
package/dist/index.js CHANGED
@@ -4,8 +4,15 @@ import path from "path";
4
4
  import { sleep, tryCatchSync } from "@luckystack/core";
5
5
  var DEFAULT_POINTER_PATTERN = /^(.+)_V(\d+)$/;
6
6
  var DEFAULT_ENV_FILES = [".env", ".env.local"];
7
- var isSafeEnvFile = (file) => {
8
- if (path.isAbsolute(file)) return true;
7
+ var isSafeEnvFile = (file, warnAbsolute = false) => {
8
+ if (path.isAbsolute(file)) {
9
+ if (warnAbsolute) {
10
+ console.warn(
11
+ `[secret-manager] dev.envFiles contains an absolute path: "${file}". Absolute paths are permitted as an explicit choice (e.g. a shared secrets file) but are not checked for safety. Ensure this path is intentional.`
12
+ );
13
+ }
14
+ return true;
15
+ }
9
16
  const rel = path.relative(process.cwd(), path.resolve(process.cwd(), file));
10
17
  return !rel.startsWith("..");
11
18
  };
@@ -104,15 +111,16 @@ var validateToken = (token) => {
104
111
  };
105
112
  var resolveToken = (token) => {
106
113
  if (typeof token === "string") return validateToken(token);
107
- let raw;
108
- try {
109
- raw = readFileSync(token.fromFile, "utf8");
110
- } catch (error) {
111
- const code = error?.code;
114
+ const [readError, raw] = tryCatchSync(() => readFileSync(token.fromFile, "utf8"));
115
+ if (readError) {
116
+ const code = readError instanceof Error && "code" in readError ? readError.code : void 0;
112
117
  if (code === "ENOENT") {
113
118
  throw new Error(`[secret-manager] Token file not found: "${token.fromFile}".`);
114
119
  }
115
- throw new Error(`[secret-manager] Failed to read token file "${token.fromFile}": ${String(error?.message ?? error)}`);
120
+ throw new Error(`[secret-manager] Failed to read token file "${token.fromFile}": ${errorMessage(readError)}`);
121
+ }
122
+ if (raw === null) {
123
+ throw new Error(`[secret-manager] Token file "${token.fromFile}" could not be read.`);
116
124
  }
117
125
  return validateToken(raw);
118
126
  };
@@ -276,8 +284,17 @@ var parseEnvFile = (content) => {
276
284
  }
277
285
  let value = line.slice(eq + 1).trim();
278
286
  const quote = value[0];
279
- if ((quote === '"' || quote === "'") && value.length >= 2 && value.endsWith(quote)) {
280
- value = value.slice(1, -1);
287
+ if (quote === '"' || quote === "'") {
288
+ const closeIdx = value.lastIndexOf(quote);
289
+ if (closeIdx > 0) {
290
+ value = value.slice(1, closeIdx);
291
+ } else {
292
+ console.warn(
293
+ `[secret-manager] parseEnvFile: "${key}" starts with a quote but has no matching closing quote on the same line. Multi-line values are not supported \u2014 the raw text will be used as-is. If this is a multi-line value (e.g. a PEM key), do not rely on this parser.`
294
+ );
295
+ const commentAt = value.indexOf(" #");
296
+ if (commentAt !== -1) value = value.slice(0, commentAt).trim();
297
+ }
281
298
  } else {
282
299
  const commentAt = value.indexOf(" #");
283
300
  if (commentAt !== -1) value = value.slice(0, commentAt).trim();
@@ -306,7 +323,7 @@ var startDevReload = (config) => {
306
323
  devReloadStarted = true;
307
324
  if (watch) {
308
325
  for (const file of config.dev.envFiles ?? DEFAULT_ENV_FILES) {
309
- if (!isSafeEnvFile(file)) {
326
+ if (!isSafeEnvFile(file, true)) {
310
327
  console.warn(`[secret-manager] ignoring unsafe dev envFile path (must be relative + within the project): ${file}`);
311
328
  continue;
312
329
  }
@@ -360,7 +377,7 @@ var reloadSecretManagerFromFiles = async () => {
360
377
  const pattern = stripStatefulFlags(config.pointerPattern ?? DEFAULT_POINTER_PATTERN);
361
378
  const merged = {};
362
379
  for (const file of files) {
363
- if (!isSafeEnvFile(file)) {
380
+ if (!isSafeEnvFile(file, false)) {
364
381
  console.warn(`[secret-manager] ignoring unsafe dev envFile path: ${file}`);
365
382
  continue;
366
383
  }
@@ -413,6 +430,7 @@ var stopSecretManager = () => {
413
430
  watcher.close();
414
431
  }
415
432
  fileWatchers.length = 0;
433
+ warnedEnvNamesUnset = false;
416
434
  };
417
435
  var resetSecretManagerForTests = () => {
418
436
  stopSecretManager();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["//? @luckystack/secret-manager — rotation-aware secret resolver client.\n//?\n//? Idea: secrets live in a central secret-manager server (append-only,\n//? versioned, one shared bearer token). Apps commit their `.env` to git, but\n//? instead of real secrets it holds POINTERS:\n//?\n//? OPENAI_KEY=OPENAI_AUTHORIZATION_KEY_V5\n//?\n//? A pointer has the shape `<BASE>_V<number>`. At boot this client scans\n//? `process.env`, collects every pointer-shaped value, asks the server to\n//? resolve them in ONE request, and overwrites each `process.env` entry with\n//? the real secret — so downstream code reads `process.env.OPENAI_KEY` and gets\n//? the resolved value, not the pointer. Rotating a secret means publishing a\n//? new version (`..._V6`) on the server, never editing an old one, so old git\n//? branches that still point at `..._V5` keep booting.\n//?\n//? Three modes:\n//? 1. `source: 'remote'` (default) — resolve from the server. A missing\n//? pointer or an unreachable server throws — production hard-stop.\n//? 2. `source: 'local'` — no network. Pointers are left untouched. Use in\n//? tests / offline dev when the server isn't reachable.\n//? 3. `source: 'hybrid'` — try the server, but on failure warn and leave\n//? whatever `process.env` already holds. Use for staging / canary.\n//?\n//? Optional dev-only hot reload (`config.dev`) re-resolves on `.env` file\n//? changes and/or on an interval, so server-side rotations are picked up\n//? without restarting a long-running dev process. It is a no-op in production.\n\nimport { readFileSync, watch as fsWatch, type FSWatcher } from 'node:fs';\nimport path from 'node:path';\n\nimport { sleep, tryCatchSync } from '@luckystack/core';\n\n/** Bearer token: a literal string, or a file whose entire contents are the token. */\nexport type SecretManagerToken = string | { fromFile: string };\n\nexport interface SecretManagerConfig {\n /** Base URL of the secret-manager server (trailing slash optional). */\n url: string;\n /**\n * Shared bearer token. Either the literal string or `{ fromFile }` pointing\n * at a gitignored file whose entire contents are the token (read at resolve\n * time, so rotating the file is picked up by the next poll / refresh).\n */\n token: SecretManagerToken;\n /** Resolution mode. Default `'remote'`. */\n source?: 'remote' | 'local' | 'hybrid';\n /**\n * Override the pointer-shape detector. Any `process.env` value matching this\n * is treated as a pointer; anything else is a literal and left untouched.\n * Default `/^(.+)_V(\\d+)$/`. A `g`/`y` flag is stripped (a stateful regex would\n * misclassify alternating entries).\n */\n pointerPattern?: RegExp;\n /**\n * Scope which `process.env` entries are pointer-eligible by NAME. This allowlist\n * is REQUIRED to resolve anything off-host: an array of names or a predicate.\n *\n * SECURE DEFAULT: when unset, NOTHING is resolved off-host (a clear boot warning\n * is emitted instead). Scanning the entire inherited environment would POST any\n * unrelated, pointer-shaped inherited value (`RELEASE_TAG=build_2024_V2`) to the\n * secret-manager server — so an explicit allowlist is mandatory. To opt back into\n * scanning every name, pass `() => true` as a deliberate, auditable choice.\n */\n envNames?: string[] | ((name: string) => boolean);\n /**\n * Allow a plain-`http:` server `url`. By default only `https:` is accepted for\n * non-loopback hosts (the channel carries the bearer token + plaintext secrets);\n * loopback (`localhost`/`127.0.0.1`/`[::1]`) is always permitted. Set `true` to\n * permit `http:` to any host — a loud warning is still emitted.\n */\n allowInsecureHttp?: boolean;\n /**\n * Abort a resolve request that has not responded within this many ms. Prevents a\n * black-hole server (accepts the TCP connection, never responds) from hanging boot\n * until undici's ~300s default — and, in `'hybrid'`, from never reaching the\n * warn-and-keep-local fallback (a hang never rejects). Default `10_000`. Set `0`\n * to disable the timeout.\n */\n timeoutMs?: number;\n /**\n * Retry a failed resolve before giving up (transport error or non-2xx). Default\n * `{ count: 0 }` (no retry). After exhaustion `'remote'` still throws and\n * `'hybrid'` still warns-and-keeps-local.\n */\n retries?: { count: number; delayMs?: number };\n /** Override the resolve path appended to `url`. Default `'/resolve'`. */\n resolvePath?: string;\n /** Extra request headers merged onto every resolve request (cannot override `Authorization`). */\n headers?: Record<string, string>;\n /**\n * Called after a resolve writes new values into `process.env`, with ONLY the env\n * NAMES whose value actually changed (never the secret values). A client that\n * captured a secret at construction time (Prisma from `DATABASE_URL`, a Redis /\n * Stripe / OpenAI SDK) can re-create its pool/client here so a rotation lands.\n */\n onApplied?: (changes: { name: string; pointer: string }[]) => void | Promise<void>;\n /**\n * Called when a resolve fails, alongside the existing `console.warn` (current\n * behavior is unchanged when unset). Route the failure to Sentry/metrics —\n * useful for `'hybrid'`/dev where a failure otherwise silently keeps stale env.\n */\n onResolveError?: (error: unknown, context: { phase: 'boot' | 'refresh' | 'file-reload' }) => void;\n /** Override the global `fetch` (tests / non-Node-20 hosts). */\n fetchImpl?: typeof fetch;\n /**\n * Re-resolve the current pointers every N ms in ALL environments (the production\n * rotation poll), distinct from the dev-only `dev.pollIntervalMs` file-watch\n * channel. Default `0` (disabled). Unref'd, so it never blocks process exit.\n */\n pollIntervalMs?: number;\n /**\n * Opt-in dev-only hot reload. Ignored when `NODE_ENV === 'production'`.\n * Provide an (even empty) object to enable it.\n */\n dev?: {\n /**\n * Watch the env files and hot-reload on change. Default `true`. On change\n * the files are re-parsed and applied: plain (non-pointer) values are\n * injected straight into `process.env` (live config reload), and\n * pointer-shaped values are re-resolved against the server.\n */\n watch?: boolean;\n /** Re-resolve the current pointers every N ms (server-rotation poll). Default `0` (disabled). */\n pollIntervalMs?: number;\n /** Env files to watch + reparse, in load order (later overrides earlier). Default `['.env', '.env.local']`. */\n envFiles?: string[];\n };\n}\n\nexport interface CachedResolution {\n /** `Date.now()` of the last successful resolve. */\n fetchedAt: number;\n /** Map of pointer string -> resolved value (what the server returned). */\n values: Record<string, string>;\n}\n\nconst DEFAULT_POINTER_PATTERN = /^(.+)_V(\\d+)$/;\nconst DEFAULT_ENV_FILES = ['.env', '.env.local'];\n\n//? Dev hot-reload reads these files and injects their values into `process.env`.\n//? `dev.envFiles` is consumer config (not runtime user input), so an ABSOLUTE\n//? path is treated as an explicit, allowed choice (e.g. a shared secrets file).\n//? A RELATIVE path, however, must stay within the project root — reject `..`\n//? traversal (the plausible \"injected via a relative path\" escape). The caller\n//? skips + warns on a rejected entry (fail-open, consistent with the package's\n//? swallow-on-missing-file behaviour).\nconst isSafeEnvFile = (file: string): boolean => {\n if (path.isAbsolute(file)) return true;\n const rel = path.relative(process.cwd(), path.resolve(process.cwd(), file));\n return !rel.startsWith('..');\n};\n\n//? Module state. The resolver is meant to run once per process at boot; these\n//? bindings keep it idempotent and let dev hot-reload re-resolve later.\nlet cachedResolution: CachedResolution | null = null;\n//? envName -> pointer string, captured once on first resolve. Reused on every\n//? refresh because the first resolve OVERWRITES the env value with the real\n//? secret, after which it no longer looks like a pointer.\nlet pointerMap: Record<string, string> | null = null;\nlet activeConfig: SecretManagerConfig | null = null;\n\n//? In-flight resolve, used to SERIALIZE concurrent resolves. Four channels can\n//? funnel into `doResolve` (boot, the production rotation poll, the dev poll, the\n//? dev file-watch + a manual `refreshSecretManager()`); without a guard a slow\n//? in-flight resolve can land AFTER a newer one, leaving `process.env` and\n//? `cachedResolution` disagreeing (stale secrets). Each call awaits the previous\n//? one's settlement before starting, so resolves apply strictly in order.\nlet resolveChain: Promise<void> = Promise.resolve();\n\n//? Dev hot-reload + rotation-poll handles, torn down by stopSecretManager.\nlet devReloadStarted = false;\nlet pollTimer: ReturnType<typeof setInterval> | null = null;\nlet rotationPollTimer: ReturnType<typeof setInterval> | null = null;\nlet debounceTimer: ReturnType<typeof setTimeout> | null = null;\nconst fileWatchers: FSWatcher[] = [];\n\n//? A consumer-supplied `pointerPattern` with a `g`/`y` flag makes `.test()`\n//? stateful (advances `lastIndex`), silently misclassifying alternating entries.\n//? Strip those flags up front; everything else is preserved.\nconst stripStatefulFlags = (pattern: RegExp): RegExp => {\n const flags = pattern.flags.replaceAll(/[gy]/g, '');\n return flags === pattern.flags ? pattern : new RegExp(pattern.source, flags);\n};\n\n//? No-op used to settle the in-flight resolve chain tail regardless of outcome.\nconst noop = (): void => {\n /* intentionally empty */\n};\n\n//? Reduce any thrown value to a safe message string for logging — never log the\n//? raw error object (its `cause`/own-properties can carry a URL/token/PII string).\nconst errorMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\n//? Invoke a consumer callback in isolation: a throwing hook must never abort an\n//? otherwise-successful resolve or mask the original error. Failures are warned,\n//? not propagated.\nconst runHook = (fn: () => void, label: string): void => {\n const [error] = tryCatchSync(fn);\n if (error) console.warn(`[secret-manager] ${label} callback threw (ignored):`, errorMessage(error));\n};\n\n//? Async sibling of `runHook` for a callback that may return a Promise (onApplied\n//? re-creates pools/SDK clients). Awaited but isolated — a rejection/throw is\n//? warned, never propagated, so the resolve that already applied stays successful.\n//? CC-7 exemption (no-raw-try-catch): `tryCatchSync` can't wrap an `await`, and\n//? the async framework `tryCatch` would auto-capture to the error tracker — a\n//? deliberate non-goal here (a consumer hook failure must stay a local warn in\n//? this dependency-light client, not a tracked event).\nconst runHookAsync = async (fn: () => void | Promise<void>, label: string): Promise<void> => {\n try {\n await fn();\n } catch (error) {\n console.warn(`[secret-manager] ${label} callback threw (ignored):`, errorMessage(error));\n }\n};\n\n//? RFC-style loopback hosts: plain http to these never leaves the machine, so it\n//? is always permitted regardless of `allowInsecureHttp`.\nconst isLoopbackHost = (hostname: string): boolean =>\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '::1' ||\n hostname === '[::1]';\n\nconst validateUrl = (url: string, allowInsecureHttp: boolean): void => {\n //? Reject relative / non-http(s) URLs (e.g. `file://`) up front so the resolve\n //? endpoint can't be pointed at the local filesystem or another protocol.\n const [parseError, parsed] = tryCatchSync(() => new URL(url));\n if (parseError || !parsed) {\n throw new Error(`[secret-manager] Invalid \\`url\\`: \"${url}\" is not an absolute URL.`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`[secret-manager] Invalid \\`url\\` scheme \"${parsed.protocol}\": only http(s) is supported.`);\n }\n //? The channel carries the bearer token + plaintext secrets, so plain http is a\n //? cleartext leak. Permit it only for loopback, or behind an explicit opt-in\n //? (still warned loudly) — otherwise reject before any secret is sent.\n if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {\n if (!allowInsecureHttp) {\n throw new Error(\n `[secret-manager] Refusing plain-http \\`url\\` \"${url}\": the bearer token and resolved secrets would travel in cleartext. Use https, point at a loopback host, or set \\`allowInsecureHttp: true\\` to override.`,\n );\n }\n console.warn(\n `[secret-manager] Using plain-http transport to a non-loopback host (\"${parsed.hostname}\"): the bearer token and resolved secrets travel in CLEARTEXT. Prefer https.`,\n );\n }\n};\n\n//? Build the name-eligibility predicate from `envNames` (allowlist array or\n//? predicate). SECURE DEFAULT: when `envNames` is unset, NOTHING is eligible —\n//? the resolver must never scan the whole inherited environment and POST every\n//? pointer-shaped value off-host. An explicit allowlist (or `() => true`) is\n//? required to opt in. The boot-time warning for the unset case is emitted by\n//? `warnIfEnvNamesUnset` at the resolve path.\nconst toNameFilter = (\n envNames: SecretManagerConfig['envNames'],\n): ((name: string) => boolean) => {\n if (envNames === undefined) return () => false;\n if (typeof envNames === 'function') return envNames;\n const allow = new Set(envNames);\n return (name) => allow.has(name);\n};\n\n//? Warn ONCE per process when `envNames` is unset: the resolver is deny-all in\n//? that state, so an operator who expected secrets to resolve gets a clear,\n//? actionable boot message instead of a silent no-op. Guarded so the production\n//? rotation poll (`refreshSecretManager` on an interval) doesn't re-emit it every\n//? cycle and flood the log sink.\nlet warnedEnvNamesUnset = false;\nconst warnIfEnvNamesUnset = (envNames: SecretManagerConfig['envNames']): void => {\n if (envNames !== undefined || warnedEnvNamesUnset) return;\n warnedEnvNamesUnset = true;\n console.warn(\n '[secret-manager] `envNames` is not set: NO environment values will be resolved off-host. Set `envNames` to an allowlist of the env names to resolve (or `() => true` to deliberately scan every name).',\n );\n};\n\n//? Split a set of `{ name -> value }` entries into pointer-shaped vs plain\n//? values, applying the `envNames` allowlist FIRST so a name excluded by\n//? `envNames` is treated as neither a pointer nor a plain value (it is dropped\n//? entirely). Shared by both ingest paths (boot `capturePointers` + the dev\n//? file-reload split) so the scoping rule can never drift between them.\nconst splitPointers = (\n entries: Iterable<[string, string]>,\n pattern: RegExp,\n envNames: SecretManagerConfig['envNames'],\n): { pointers: Record<string, string>; plain: Record<string, string> } => {\n const nameAllowed = toNameFilter(envNames);\n const pointers: Record<string, string> = {};\n const plain: Record<string, string> = {};\n for (const [name, value] of entries) {\n if (!nameAllowed(name)) continue;\n if (pattern.test(value)) pointers[name] = value;\n else plain[name] = value;\n }\n return { pointers, plain };\n};\n\nconst capturePointers = (\n pattern: RegExp,\n envNames: SecretManagerConfig['envNames'],\n): Record<string, string> => {\n const entries: [string, string][] = [];\n for (const [name, value] of Object.entries(process.env)) {\n if (typeof value === 'string') entries.push([name, value]);\n }\n return splitPointers(entries, pattern, envNames).pointers;\n};\n\nconst validateToken = (token: string): string => {\n //? An empty/whitespace token yields an `Authorization: Bearer ` header that\n //? silently auth-fails (and in hybrid mode falls back to local env) — reject it.\n const trimmed = token.trim();\n if (trimmed.length === 0) {\n throw new Error('[secret-manager] Bearer token is empty or whitespace-only.');\n }\n //? The `Bearer ` scheme is added at the call site. A token that already carries\n //? it would produce a malformed double-prefix `Bearer Bearer <...>` header —\n //? strip the redundant prefix (case-insensitive) and warn so the operator knows\n //? their config is wrong. Stripping is the forgiving path: a warn-but-pass-through\n //? would silently break every request, making this a very hard-to-debug boot issue.\n if (/^bearer\\s/i.test(trimmed)) {\n const stripped = trimmed.replace(/^bearer\\s+/i, '');\n console.warn(\n '[secret-manager] Token starts with a \"Bearer \" prefix; the scheme is added automatically — the prefix was stripped. Drop it from your configured token to silence this warning.',\n );\n return stripped;\n }\n return trimmed;\n};\n\nconst resolveToken = (token: SecretManagerToken): string => {\n if (typeof token === 'string') return validateToken(token);\n let raw: string;\n try {\n raw = readFileSync(token.fromFile, 'utf8');\n } catch (error) {\n //? Distinguish a missing/deleted file from other I/O errors so a dev\n //? hot-reload poll over a transiently-absent token file gives a clear log.\n const code = (error as NodeJS.ErrnoException | undefined)?.code;\n if (code === 'ENOENT') {\n throw new Error(`[secret-manager] Token file not found: \"${token.fromFile}\".`);\n }\n throw new Error(`[secret-manager] Failed to read token file \"${token.fromFile}\": ${String((error as Error | undefined)?.message ?? error)}`);\n }\n return validateToken(raw);\n};\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n//? Hard cap on the resolve response body. The server returns a flat\n//? `{ pointer -> secret }` map; even hundreds of secrets stay well under 1 MB.\n//? A response larger than this is a compromised/buggy server (or a MITM on an\n//? `allowInsecureHttp` channel) and is rejected BEFORE it can OOM the client.\nconst MAX_RESOLVE_BODY_BYTES = 1_048_576;\n\n//? Read the response body as text, aborting once more than `maxBytes` have been\n//? received — so a server that lies about (or omits) Content-Length still can't\n//? stream an unbounded body into memory. Falls back to `response.text()` when the\n//? body isn't a readable stream (e.g. a mocked Response).\nconst readBodyCapped = async (response: Response, maxBytes: number): Promise<string> => {\n const body = response.body;\n if (!body) return response.text();\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let received = 0;\n let out = '';\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n received += value.byteLength;\n if (received > maxBytes) {\n void reader.cancel();\n throw new Error(`[secret-manager] Resolve response exceeded the ${String(maxBytes)}-byte cap.`);\n }\n out += decoder.decode(value, { stream: true });\n }\n out += decoder.decode();\n return out;\n};\n\n//? One resolve round-trip: POST the pointers, validate the response, return the\n//? filtered `{ pointer -> value }` map. No retry/timeout orchestration here — the\n//? caller (`fetchResolve`) owns that so a hang can't slip past the abort signal.\nconst fetchResolveOnce = async (\n config: SecretManagerConfig,\n pointers: string[],\n): Promise<Record<string, string>> => {\n //? Defaults to the Node 20+ global fetch; pass fetchImpl for older hosts.\n const fetchFn = config.fetchImpl ?? globalThis.fetch;\n const resolvePath = config.resolvePath ?? '/resolve';\n const suffix = resolvePath.startsWith('/') ? resolvePath : `/${resolvePath}`;\n const endpoint = `${config.url.replace(/\\/+$/, '')}${suffix}`;\n\n //? Abort a black-hole server (accepts the TCP connection, never responds) so a\n //? hang surfaces as a rejection — boot can't freeze, and 'hybrid' reaches its\n //? warn-and-keep-local fallback. `timeoutMs: 0` disables the abort entirely.\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const signal = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;\n\n //? Consumer `headers` are merged first so they can never override Authorization.\n const response = await fetchFn(endpoint, {\n method: 'POST',\n headers: {\n ...config.headers,\n 'Authorization': `Bearer ${resolveToken(config.token)}`,\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n },\n body: JSON.stringify({ keys: pointers }),\n signal,\n });\n\n if (!response.ok) {\n //? Discard the unconsumed error body so a long-lived poll loop can't leave\n //? response bodies pending GC; failures carry only status text upward.\n void response.body?.cancel();\n throw new Error(`[secret-manager] Resolve request failed: ${String(response.status)} ${response.statusText}`);\n }\n\n //? The resolve response is the network input this client trusts least (a\n //? compromised/buggy server, or a MITM on an `allowInsecureHttp` channel).\n //? Reject an oversized body BEFORE buffering/parsing it so a hostile server\n //? can't OOM the booting client. A Content-Length header is advisory (can be\n //? absent/lying), so it is a fast pre-check only — the real guard is the\n //? streamed byte cap below.\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > MAX_RESOLVE_BODY_BYTES) {\n void response.body?.cancel();\n throw new Error(`[secret-manager] Resolve response too large (${String(declaredLength)} bytes > ${String(MAX_RESOLVE_BODY_BYTES)} cap).`);\n }\n const raw = await readBodyCapped(response, MAX_RESOLVE_BODY_BYTES);\n\n //? Parse as `unknown`, then narrow `values` with a runtime guard rather than\n //? trusting an up-front cast — the response is attacker-influenced (a\n //? compromised/buggy server) so its shape is not assumed.\n const [parseError, body] = tryCatchSync((): unknown => JSON.parse(raw));\n if (parseError) {\n throw new Error('[secret-manager] Resolve response was not valid JSON.');\n }\n const values = (body as { values?: unknown } | null)?.values;\n if (values === null || typeof values !== 'object') {\n throw new Error('[secret-manager] Resolve response missing `values` object.');\n }\n\n //? Filter the response down to the pointers we actually requested, and require\n //? each value to be a string. A compromised/buggy server could otherwise inject\n //? extra keys (cached + surfaced via getCachedResolution) or a non-string that\n //? coerces to '123' / '[object Object]' once written into process.env — only\n //? requested, string-valued pointers are trusted; a non-string is dropped (and\n //? thus treated as an unresolved pointer downstream: fatal in 'remote', warned\n //? in 'hybrid').\n const requested = new Set(pointers);\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(values as Record<string, unknown>)) {\n if (!requested.has(key)) continue;\n if (typeof value !== 'string') {\n console.warn(`[secret-manager] Pointer \"${key}\" resolved to a non-string value (${typeof value}); ignoring.`);\n continue;\n }\n filtered[key] = value;\n }\n return filtered;\n};\n\nconst fetchResolve = async (\n config: SecretManagerConfig,\n pointers: string[],\n): Promise<Record<string, string>> => {\n //? `?? 0` only guards null/undefined — a configured `NaN` would survive and make\n //? `0 <= NaN` false, so the loop body never runs, `lastError` stays undefined,\n //? and we'd `throw undefined`. Coerce to a finite, non-negative count/delay.\n const rawRetryCount = config.retries?.count ?? 0;\n const retryCount = Number.isFinite(rawRetryCount) ? Math.max(0, rawRetryCount) : 0;\n const rawDelayMs = config.retries?.delayMs ?? 0;\n const delayMs = Number.isFinite(rawDelayMs) ? Math.max(0, rawDelayMs) : 0;\n\n let lastError: unknown = new Error('[secret-manager] resolve failed: no attempt was made');\n for (let attempt = 0; attempt <= retryCount; attempt++) {\n try {\n return await fetchResolveOnce(config, pointers);\n } catch (error) {\n lastError = error;\n if (attempt < retryCount && delayMs > 0) await sleep(delayMs);\n }\n }\n throw lastError;\n};\n\nconst applyResolved = (\n map: Record<string, string>,\n values: Record<string, string>,\n source: 'remote' | 'hybrid',\n): { name: string; pointer: string }[] => {\n //? In remote mode a single unresolved pointer is a hard boot failure. Check\n //? everything BEFORE mutating process.env so the failure is atomic.\n if (source === 'remote') {\n const missing = Object.entries(map).filter(([, pointer]) => values[pointer] === undefined);\n if (missing.length > 0) {\n const detail = missing.map(([name, pointer]) => `${pointer} (referenced by ${name})`).join(', ');\n throw new Error(`[secret-manager] Server did not resolve: ${detail}.`);\n }\n }\n\n //? Track only the env NAMES whose value actually changed — surfaced to\n //? `onApplied` so a client that captured a secret at construction time can\n //? re-create its pool/SDK client. Never carries the secret values themselves.\n const changes: { name: string; pointer: string }[] = [];\n for (const [name, pointer] of Object.entries(map)) {\n const value = values[pointer];\n if (value === undefined) {\n //? hybrid only — leave the pointer in place and warn so the operator sees it.\n console.warn(`[secret-manager] Pointer \"${pointer}\" (referenced by ${name}) not resolved; leaving \"${name}\" as-is.`);\n continue;\n }\n if (process.env[name] !== value) changes.push({ name, pointer });\n process.env[name] = value;\n }\n return changes;\n};\n\n//? Public resolve entry: SERIALIZE every resolve behind a single in-flight chain\n//? so a slow resolve can never land after a newer one (TOCTOU on `process.env` /\n//? `cachedResolution`). We chain off `resolveChain.then(...)` regardless of whether\n//? the prior resolve resolved or rejected, then re-publish the tail so the next\n//? caller waits on THIS one. The returned promise mirrors the inner outcome so\n//? `'remote'` still rejects the caller on a hard failure.\nconst doResolve = (\n config: SecretManagerConfig,\n phase: 'boot' | 'refresh' | 'file-reload',\n): Promise<void> => {\n const run = resolveChain.then(\n () => doResolveInner(config, phase),\n () => doResolveInner(config, phase),\n );\n //? Keep the chain alive even if this resolve rejects (swallow on the tail copy\n //? only — the returned `run` keeps the original rejection for the caller).\n resolveChain = run.then(noop, noop);\n return run;\n};\n\nconst doResolveInner = async (\n config: SecretManagerConfig,\n phase: 'boot' | 'refresh' | 'file-reload',\n): Promise<void> => {\n const source = config.source ?? 'remote';\n if (source === 'local') return;\n\n //? Secure default: an unset `envNames` resolves NOTHING off-host — warn loudly\n //? on every resolve so the deny-all state is never silent (an operator who\n //? expected secrets to resolve sees exactly what to set).\n warnIfEnvNamesUnset(config.envNames);\n\n //? Capture once on first resolve and reuse: the first resolve OVERWRITES the\n //? env value with the real secret, after which it no longer looks like a pointer.\n //? Re-capture when the map is empty so a pointer that wasn't present at boot\n //? (e.g. set into `process.env` after init) is still picked up by a later\n //? refresh — a non-empty `{}` would otherwise pin the resolver to zero pointers.\n if (pointerMap === null || Object.keys(pointerMap).length === 0) {\n pointerMap = capturePointers(\n stripStatefulFlags(config.pointerPattern ?? DEFAULT_POINTER_PATTERN),\n config.envNames,\n );\n }\n const activePointerMap = pointerMap;\n const pointers = [...new Set(Object.values(activePointerMap))];\n if (pointers.length === 0) {\n cachedResolution = { fetchedAt: Date.now(), values: {} };\n return;\n }\n\n //? CC-7 exemption (no-raw-try-catch): the async framework `tryCatch` auto-captures\n //? to the error tracker, but this is a deliberate fail-OPEN boot-time guard in an\n //? intentionally dependency-light client — a 'hybrid' resolve failure must stay a\n //? silent warn with NO error-tracker side-effect (and 'remote' re-throws untouched\n //? for a hard boot stop). `tryCatchSync` can't wrap the `await`, so the raw\n //? try/catch is kept on purpose; the fail-OPEN contract is the load-bearing detail.\n let values: Record<string, string>;\n let changes: { name: string; pointer: string }[];\n try {\n values = await fetchResolve(config, pointers);\n changes = applyResolved(activePointerMap, values, source);\n cachedResolution = { fetchedAt: Date.now(), values };\n } catch (error) {\n //? Opt-in observability seam: route the failure to Sentry/metrics. Kept\n //? alongside (not replacing) the warn below so the fail-open default is unchanged.\n //? Hooks run isolated: a throwing/hanging `onResolveError` must not mask the\n //? original resolve error (it is re-thrown below in 'remote').\n runHook(() => config.onResolveError?.(error, { phase }), 'onResolveError');\n if (source === 'hybrid') {\n //? Log the message only — never the raw error object: error objects are the\n //? classic accidental channel for a URL/token/PII string reaching a log sink.\n //? Full-fidelity routing is the `onResolveError` hook's job.\n console.warn('[secret-manager] Resolve failed, leaving local env as-is:', errorMessage(error));\n return;\n }\n throw error;\n }\n\n //? Notify AFTER the cache + process.env are coherent so a consumer that reads\n //? process.env inside the callback sees the applied values. Isolated so a\n //? throwing/hanging `onApplied` can't abort an otherwise-successful resolve\n //? (it has already written process.env + the cache).\n if (changes.length > 0) await runHookAsync(() => config.onApplied?.(changes), 'onApplied');\n};\n\n//? Minimal .env parser kept in-package so the resolver stays dependency-free.\n//? Handles standard `KEY=VALUE` lines, full-line + inline (` #`) comments, and\n//? single/double-quoted values. Not multi-line values or escape sequences.\nconst parseEnvFile = (content: string): Record<string, string> => {\n const out: Record<string, string> = {};\n for (const rawLine of content.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (!line || line.startsWith('#')) continue;\n const eq = line.indexOf('=');\n if (eq <= 0) continue;\n const key = line.slice(0, eq).trim();\n //? Restrict to POSIX-shell env-var names. `.`/`-` keys can't be read via the\n //? normal `process.env.NAME` lookup, so accepting them is a silent footgun.\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.warn(`[secret-manager] Ignoring env key \"${key}\": not a valid environment variable name (^[A-Za-z_][A-Za-z0-9_]*$).`);\n continue;\n }\n let value = line.slice(eq + 1).trim();\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length >= 2 && value.endsWith(quote)) {\n value = value.slice(1, -1);\n } else {\n const commentAt = value.indexOf(' #');\n if (commentAt !== -1) value = value.slice(0, commentAt).trim();\n if (value.startsWith('#')) value = '';\n }\n out[key] = value;\n }\n return out;\n};\n\nconst scheduleReload = (): void => {\n //? Debounce — fs.watch can fire several events for one save.\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n void reloadSecretManagerFromFiles().catch((error: unknown) => {\n console.warn('[secret-manager] dev reload failed:', errorMessage(error));\n });\n }, 200);\n debounceTimer.unref();\n};\n\nconst startDevReload = (config: SecretManagerConfig): void => {\n if (devReloadStarted || config.dev === undefined) return;\n //? Allowlist the dev environments rather than exact-matching 'production': an\n //? unset / 'prod' / 'staging' NODE_ENV must NOT silently start fs watchers + a\n //? poll on a host the operator believes is production. Only an explicit\n //? 'development' or 'test' enables dev hot reload.\n const nodeEnv = process.env.NODE_ENV;\n if (nodeEnv !== 'development' && nodeEnv !== 'test') return;\n\n const watch = config.dev.watch ?? true;\n const pollIntervalMs = config.dev.pollIntervalMs ?? 0;\n if (!watch && pollIntervalMs <= 0) return;\n devReloadStarted = true;\n\n if (watch) {\n for (const file of config.dev.envFiles ?? DEFAULT_ENV_FILES) {\n if (!isSafeEnvFile(file)) {\n console.warn(`[secret-manager] ignoring unsafe dev envFile path (must be relative + within the project): ${file}`);\n continue;\n }\n try {\n const watcher = fsWatch(file, () => {\n scheduleReload();\n });\n watcher.unref();\n fileWatchers.push(watcher);\n } catch {\n //? The file may not exist (e.g. no .env.local) — nothing to watch.\n }\n }\n }\n\n if (pollIntervalMs > 0) {\n pollTimer = setInterval(() => {\n void refreshSecretManager().catch((error: unknown) => {\n console.warn('[secret-manager] poll refresh failed:', errorMessage(error));\n });\n }, pollIntervalMs);\n pollTimer.unref();\n }\n};\n\n/**\n * Initialize the secret-manager resolver. Call once as the very first line of\n * `server.ts`, BEFORE any other framework code reads `process.env`. In\n * `'remote'` / `'hybrid'` mode the resolved secrets are written into\n * `process.env` before this resolves, so downstream code sees them via the\n * standard `process.env.FOO` lookup. In `'local'` mode it is a no-op.\n */\nexport const initSecretManager = async (config: SecretManagerConfig): Promise<void> => {\n //? Validate the URL BEFORE recording activeConfig, so a failed init can't leave\n //? a later refreshSecretManager() resolving against an invalid config.\n if ((config.source ?? 'remote') !== 'local') {\n //? Only validate the URL when we'll actually hit the network ('remote' /\n //? 'hybrid'); 'local' may carry a placeholder url.\n validateUrl(config.url, config.allowInsecureHttp ?? false);\n }\n activeConfig = config;\n if ((config.source ?? 'remote') === 'local') return;\n await doResolve(config, 'boot');\n startRotationPoll(config);\n startDevReload(config);\n};\n\n//? Production-capable rotation poll: re-resolve every `config.pollIntervalMs` ms\n//? in ALL environments (distinct from the dev-only `dev.pollIntervalMs` file-watch\n//? channel, which is gated off in production). Unref'd so it never blocks exit.\nconst startRotationPoll = (config: SecretManagerConfig): void => {\n const intervalMs = config.pollIntervalMs ?? 0;\n if (intervalMs <= 0 || rotationPollTimer) return;\n rotationPollTimer = setInterval(() => {\n void refreshSecretManager().catch((error: unknown) => {\n console.warn('[secret-manager] rotation poll refresh failed:', errorMessage(error));\n });\n }, intervalMs);\n rotationPollTimer.unref();\n};\n\n/**\n * Re-resolve against the server, ignoring nothing — used by the dev hot-reload\n * watch/poll and callable manually when an admin rotates a secret and you want\n * a long-running process to pick it up without a restart.\n */\nexport const refreshSecretManager = async (): Promise<void> => {\n if (!activeConfig || (activeConfig.source ?? 'remote') === 'local') return;\n await doResolve(activeConfig, 'refresh');\n};\n\n/**\n * Dev hot-reload entry: re-parse the configured env files and apply them — plain\n * (non-pointer) values are injected straight into `process.env` (live config\n * reload), pointer-shaped values are re-resolved against the server. Wired to the\n * `.env` / `.env.local` file watch; also callable manually.\n */\nexport const reloadSecretManagerFromFiles = async (): Promise<void> => {\n const config = activeConfig;\n if (!config || (config.source ?? 'remote') === 'local') return;\n\n const files = config.dev?.envFiles ?? DEFAULT_ENV_FILES;\n const pattern = stripStatefulFlags(config.pointerPattern ?? DEFAULT_POINTER_PATTERN);\n\n //? Re-read every file in load order; later files (e.g. .env.local) override.\n const merged: Record<string, string> = {};\n for (const file of files) {\n if (!isSafeEnvFile(file)) {\n console.warn(`[secret-manager] ignoring unsafe dev envFile path: ${file}`);\n continue;\n }\n let content: string;\n try {\n content = readFileSync(file, 'utf8');\n } catch {\n continue; //? file may not exist (e.g. no .env.local)\n }\n Object.assign(merged, parseEnvFile(content));\n }\n\n //? Split plain (live config reload) from pointer-shaped values through the SAME\n //? `envNames` allowlist the boot path uses (`capturePointers`), so a name excluded\n //? by `envNames` is dropped on BOTH channels — never POSTed off-host as a pointer,\n //? never injected into `process.env` as a plain value. This closes the scoping\n //? drift where a file-reload could send/apply names the boot scan rejected.\n const { pointers: freshPointerMap, plain: plainValues } = splitPointers(\n Object.entries(merged),\n pattern,\n config.envNames,\n );\n\n //? MERGE the fresh file-sourced pointers over the existing map (don't replace):\n //? a pointer captured at boot from the inherited shell/CI env that isn't in a\n //? watched file would otherwise be dropped and stop rotating. A file-sourced\n //? pointer with the same name still wins.\n //? Build the merged map locally and commit it ONLY after a successful resolve so\n //? a failed remote reload can't permanently poison the in-memory pointer map and\n //? cause every subsequent refreshSecretManager / poll to resolve the bad set.\n const previousPointerMap = pointerMap;\n pointerMap = { ...pointerMap, ...freshPointerMap };\n\n //? Resolve the pointers FIRST. In 'remote' mode an unresolved pointer throws —\n //? mirror the atomic boot path and inject the plain values only AFTER a\n //? successful resolve, so a throw never leaves half-applied state. On failure\n //? roll back the pointer map to its pre-reload snapshot.\n try {\n await doResolve(config, 'file-reload');\n } catch (error) {\n pointerMap = previousPointerMap;\n throw error;\n }\n for (const [name, value] of Object.entries(plainValues)) {\n process.env[name] = value;\n }\n};\n\n/**\n * Read the last `{ fetchedAt, values }` resolution (pointer -> value), or `null`.\n *\n * ⚠️ SENSITIVE: `values` maps each pointer to its RAW resolved secret. The result\n * is a shallow copy (values are flat strings, so mutating the returned object does\n * not corrupt the cache), but the values are still the real secrets — NEVER\n * serialize the result into an HTTP response, a `/health` payload, or a log line.\n * For a safe diagnostic use {@link getCachedResolutionMeta}, which never exposes\n * the values.\n */\nexport const getCachedResolution = (): CachedResolution | null =>\n cachedResolution === null\n ? null\n : { fetchedAt: cachedResolution.fetchedAt, values: { ...cachedResolution.values } };\n\n/** Values-free view of {@link getCachedResolution}: the timestamp + resolved pointer NAMES only. */\nexport interface CachedResolutionMeta {\n /** `Date.now()` of the last successful resolve. */\n fetchedAt: number;\n /** The resolved pointer strings (never the secret values). */\n pointerNames: string[];\n /** How many pointers were resolved. */\n pointerCount: number;\n}\n\n/**\n * Safe diagnostic accessor: the last resolution's timestamp + resolved pointer\n * NAMES, with the secret values stripped. Use this on any surface that might be\n * logged or served (`/health`, metrics) so the convenient path is also the safe\n * one — {@link getCachedResolution} is the values-carrying escape hatch.\n */\nexport const getCachedResolutionMeta = (): CachedResolutionMeta | null => {\n if (cachedResolution === null) return null;\n const pointerNames = Object.keys(cachedResolution.values);\n return { fetchedAt: cachedResolution.fetchedAt, pointerNames, pointerCount: pointerNames.length };\n};\n\n/**\n * Tear down the dev file watchers, the dev poll, the rotation poll, and the\n * debounce timer WITHOUT wiping the resolved cache / active config. Use for a\n * graceful shutdown of an embedded resolver (worker / CLI) so no timers keep\n * firing; the last resolved `process.env` values stay in place.\n */\nexport const stopSecretManager = (): void => {\n devReloadStarted = false;\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n if (rotationPollTimer) {\n clearInterval(rotationPollTimer);\n rotationPollTimer = null;\n }\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n for (const watcher of fileWatchers) {\n watcher.close();\n }\n fileWatchers.length = 0;\n};\n\n/** Test-only — clear module state and tear down any dev watchers / timers. */\nexport const resetSecretManagerForTests = (): void => {\n stopSecretManager();\n cachedResolution = null;\n pointerMap = null;\n activeConfig = null;\n resolveChain = Promise.resolve();\n warnedEnvNamesUnset = false;\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["//? @luckystack/secret-manager — rotation-aware secret resolver client.\n//?\n//? Idea: secrets live in a central secret-manager server (append-only,\n//? versioned, one shared bearer token). Apps commit their `.env` to git, but\n//? instead of real secrets it holds POINTERS:\n//?\n//? OPENAI_KEY=OPENAI_AUTHORIZATION_KEY_V5\n//?\n//? A pointer has the shape `<BASE>_V<number>`. At boot this client scans\n//? `process.env`, collects every pointer-shaped value, asks the server to\n//? resolve them in ONE request, and overwrites each `process.env` entry with\n//? the real secret — so downstream code reads `process.env.OPENAI_KEY` and gets\n//? the resolved value, not the pointer. Rotating a secret means publishing a\n//? new version (`..._V6`) on the server, never editing an old one, so old git\n//? branches that still point at `..._V5` keep booting.\n//?\n//? Three modes:\n//? 1. `source: 'remote'` (default) — resolve from the server. A missing\n//? pointer or an unreachable server throws — production hard-stop.\n//? 2. `source: 'local'` — no network. Pointers are left untouched. Use in\n//? tests / offline dev when the server isn't reachable.\n//? 3. `source: 'hybrid'` — try the server, but on failure warn and leave\n//? whatever `process.env` already holds. Use for staging / canary.\n//?\n//? Optional dev-only hot reload (`config.dev`) re-resolves on `.env` file\n//? changes and/or on an interval, so server-side rotations are picked up\n//? without restarting a long-running dev process. It is a no-op in production.\n\nimport { readFileSync, watch as fsWatch, type FSWatcher } from 'node:fs';\nimport path from 'node:path';\n\nimport { sleep, tryCatchSync } from '@luckystack/core';\n\n/**\n * Bearer token: a literal string, or a file whose entire contents are the token.\n *\n * When using `{ fromFile }`, the path MUST NOT be derived from untrusted input.\n * No path-traversal check is applied — the caller is responsible for ensuring\n * the path is a fixed, gitignored file (e.g. `.secret-manager-token`) next to\n * the project root. Paths like `/etc/passwd` or `../../.ssh/id_rsa` would be\n * read without error.\n */\nexport type SecretManagerToken = string | { fromFile: string };\n\nexport interface SecretManagerConfig {\n /** Base URL of the secret-manager server (trailing slash optional). */\n url: string;\n /**\n * Shared bearer token. Either the literal string or `{ fromFile }` pointing\n * at a gitignored file whose entire contents are the token (read at resolve\n * time, so rotating the file is picked up by the next poll / refresh).\n */\n token: SecretManagerToken;\n /** Resolution mode. Default `'remote'`. */\n source?: 'remote' | 'local' | 'hybrid';\n /**\n * Override the pointer-shape detector. Any `process.env` value matching this\n * is treated as a pointer; anything else is a literal and left untouched.\n * Default `/^(.+)_V(\\d+)$/`. A `g`/`y` flag is stripped (a stateful regex would\n * misclassify alternating entries).\n */\n pointerPattern?: RegExp;\n /**\n * Scope which `process.env` entries are pointer-eligible by NAME. This allowlist\n * is REQUIRED to resolve anything off-host: an array of names or a predicate.\n *\n * SECURE DEFAULT: when unset, NOTHING is resolved off-host (a clear boot warning\n * is emitted instead). Scanning the entire inherited environment would POST any\n * unrelated, pointer-shaped inherited value (`RELEASE_TAG=build_2024_V2`) to the\n * secret-manager server — so an explicit allowlist is mandatory. To opt back into\n * scanning every name, pass `() => true` as a deliberate, auditable choice.\n */\n envNames?: string[] | ((name: string) => boolean);\n /**\n * Allow a plain-`http:` server `url`. By default only `https:` is accepted for\n * non-loopback hosts (the channel carries the bearer token + plaintext secrets);\n * loopback (`localhost`/`127.0.0.1`/`[::1]`) is always permitted. Set `true` to\n * permit `http:` to any host — a loud warning is still emitted.\n */\n allowInsecureHttp?: boolean;\n /**\n * Abort a resolve request that has not responded within this many ms. Prevents a\n * black-hole server (accepts the TCP connection, never responds) from hanging boot\n * until undici's ~300s default — and, in `'hybrid'`, from never reaching the\n * warn-and-keep-local fallback (a hang never rejects). Default `10_000`. Set `0`\n * to disable the timeout.\n */\n timeoutMs?: number;\n /**\n * Retry a failed resolve before giving up (transport error or non-2xx). Default\n * `{ count: 0 }` (no retry). After exhaustion `'remote'` still throws and\n * `'hybrid'` still warns-and-keeps-local.\n */\n retries?: { count: number; delayMs?: number };\n /** Override the resolve path appended to `url`. Default `'/resolve'`. */\n resolvePath?: string;\n /** Extra request headers merged onto every resolve request (cannot override `Authorization`). */\n headers?: Record<string, string>;\n /**\n * Called after a resolve writes new values into `process.env`, with ONLY the env\n * NAMES whose value actually changed (never the secret values). A client that\n * captured a secret at construction time (Prisma from `DATABASE_URL`, a Redis /\n * Stripe / OpenAI SDK) can re-create its pool/client here so a rotation lands.\n */\n onApplied?: (changes: { name: string; pointer: string }[]) => void | Promise<void>;\n /**\n * Called when a resolve fails, alongside the existing `console.warn` (current\n * behavior is unchanged when unset). Route the failure to Sentry/metrics —\n * useful for `'hybrid'`/dev where a failure otherwise silently keeps stale env.\n */\n onResolveError?: (error: unknown, context: { phase: 'boot' | 'refresh' | 'file-reload' }) => void;\n /** Override the global `fetch` (tests / non-Node-20 hosts). */\n fetchImpl?: typeof fetch;\n /**\n * Re-resolve the current pointers every N ms in ALL environments (the production\n * rotation poll), distinct from the dev-only `dev.pollIntervalMs` file-watch\n * channel. Default `0` (disabled). Unref'd, so it never blocks process exit.\n */\n pollIntervalMs?: number;\n /**\n * Opt-in dev-only hot reload. Ignored when `NODE_ENV === 'production'`.\n * Provide an (even empty) object to enable it.\n */\n dev?: {\n /**\n * Watch the env files and hot-reload on change. Default `true`. On change\n * the files are re-parsed and applied: plain (non-pointer) values are\n * injected straight into `process.env` (live config reload), and\n * pointer-shaped values are re-resolved against the server.\n */\n watch?: boolean;\n /** Re-resolve the current pointers every N ms (server-rotation poll). Default `0` (disabled). */\n pollIntervalMs?: number;\n /** Env files to watch + reparse, in load order (later overrides earlier). Default `['.env', '.env.local']`. */\n envFiles?: string[];\n };\n}\n\nexport interface CachedResolution {\n /** `Date.now()` of the last successful resolve. */\n fetchedAt: number;\n /** Map of pointer string -> resolved value (what the server returned). */\n values: Record<string, string>;\n}\n\nconst DEFAULT_POINTER_PATTERN = /^(.+)_V(\\d+)$/;\nconst DEFAULT_ENV_FILES = ['.env', '.env.local'];\n\n//? Dev hot-reload reads these files and injects their values into `process.env`.\n//? `dev.envFiles` is consumer config (not runtime user input), so an ABSOLUTE\n//? path is treated as an explicit, allowed choice (e.g. a shared secrets file on\n//? a developer machine). A RELATIVE path, however, must stay within the project\n//? root — reject `..` traversal (the plausible \"injected via a relative path\"\n//? escape). The caller skips + warns on a rejected entry (fail-open, consistent\n//? with the package's swallow-on-missing-file behaviour).\n//? Note: absolute paths are accepted without further validation. Consumers who\n//? configure `dev.envFiles` with absolute paths take responsibility for ensuring\n//? those paths are appropriate (e.g. a shared developer-machine secrets file).\n//? A loud warn is emitted so the choice is never silent in logs.\nconst isSafeEnvFile = (file: string, warnAbsolute = false): boolean => {\n if (path.isAbsolute(file)) {\n if (warnAbsolute) {\n console.warn(\n `[secret-manager] dev.envFiles contains an absolute path: \"${file}\". Absolute paths are permitted as an explicit choice (e.g. a shared secrets file) but are not checked for safety. Ensure this path is intentional.`,\n );\n }\n return true;\n }\n const rel = path.relative(process.cwd(), path.resolve(process.cwd(), file));\n return !rel.startsWith('..');\n};\n\n//? Module state. The resolver is meant to run once per process at boot; these\n//? bindings keep it idempotent and let dev hot-reload re-resolve later.\nlet cachedResolution: CachedResolution | null = null;\n//? envName -> pointer string, captured once on first resolve. Reused on every\n//? refresh because the first resolve OVERWRITES the env value with the real\n//? secret, after which it no longer looks like a pointer.\nlet pointerMap: Record<string, string> | null = null;\nlet activeConfig: SecretManagerConfig | null = null;\n\n//? In-flight resolve, used to SERIALIZE concurrent resolves. Four channels can\n//? funnel into `doResolve` (boot, the production rotation poll, the dev poll, the\n//? dev file-watch + a manual `refreshSecretManager()`); without a guard a slow\n//? in-flight resolve can land AFTER a newer one, leaving `process.env` and\n//? `cachedResolution` disagreeing (stale secrets). Each call awaits the previous\n//? one's settlement before starting, so resolves apply strictly in order.\nlet resolveChain: Promise<void> = Promise.resolve();\n\n//? Dev hot-reload + rotation-poll handles, torn down by stopSecretManager.\nlet devReloadStarted = false;\nlet pollTimer: ReturnType<typeof setInterval> | null = null;\nlet rotationPollTimer: ReturnType<typeof setInterval> | null = null;\nlet debounceTimer: ReturnType<typeof setTimeout> | null = null;\nconst fileWatchers: FSWatcher[] = [];\n\n//? A consumer-supplied `pointerPattern` with a `g`/`y` flag makes `.test()`\n//? stateful (advances `lastIndex`), silently misclassifying alternating entries.\n//? Strip those flags up front; everything else is preserved.\nconst stripStatefulFlags = (pattern: RegExp): RegExp => {\n const flags = pattern.flags.replaceAll(/[gy]/g, '');\n return flags === pattern.flags ? pattern : new RegExp(pattern.source, flags);\n};\n\n//? No-op used to settle the in-flight resolve chain tail regardless of outcome.\nconst noop = (): void => {\n /* intentionally empty */\n};\n\n//? Reduce any thrown value to a safe message string for logging — never log the\n//? raw error object (its `cause`/own-properties can carry a URL/token/PII string).\nconst errorMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\n//? Invoke a consumer callback in isolation: a throwing hook must never abort an\n//? otherwise-successful resolve or mask the original error. Failures are warned,\n//? not propagated.\nconst runHook = (fn: () => void, label: string): void => {\n const [error] = tryCatchSync(fn);\n if (error) console.warn(`[secret-manager] ${label} callback threw (ignored):`, errorMessage(error));\n};\n\n//? Async sibling of `runHook` for a callback that may return a Promise (onApplied\n//? re-creates pools/SDK clients). Awaited but isolated — a rejection/throw is\n//? warned, never propagated, so the resolve that already applied stays successful.\n//? CC-7 exemption (no-raw-try-catch): `tryCatchSync` can't wrap an `await`, and\n//? the async framework `tryCatch` would auto-capture to the error tracker — a\n//? deliberate non-goal here (a consumer hook failure must stay a local warn in\n//? this dependency-light client, not a tracked event).\nconst runHookAsync = async (fn: () => void | Promise<void>, label: string): Promise<void> => {\n try {\n await fn();\n } catch (error) {\n console.warn(`[secret-manager] ${label} callback threw (ignored):`, errorMessage(error));\n }\n};\n\n//? RFC-style loopback hosts: plain http to these never leaves the machine, so it\n//? is always permitted regardless of `allowInsecureHttp`.\nconst isLoopbackHost = (hostname: string): boolean =>\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '::1' ||\n hostname === '[::1]';\n\nconst validateUrl = (url: string, allowInsecureHttp: boolean): void => {\n //? Reject relative / non-http(s) URLs (e.g. `file://`) up front so the resolve\n //? endpoint can't be pointed at the local filesystem or another protocol.\n const [parseError, parsed] = tryCatchSync(() => new URL(url));\n if (parseError || !parsed) {\n throw new Error(`[secret-manager] Invalid \\`url\\`: \"${url}\" is not an absolute URL.`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`[secret-manager] Invalid \\`url\\` scheme \"${parsed.protocol}\": only http(s) is supported.`);\n }\n //? The channel carries the bearer token + plaintext secrets, so plain http is a\n //? cleartext leak. Permit it only for loopback, or behind an explicit opt-in\n //? (still warned loudly) — otherwise reject before any secret is sent.\n if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {\n if (!allowInsecureHttp) {\n throw new Error(\n `[secret-manager] Refusing plain-http \\`url\\` \"${url}\": the bearer token and resolved secrets would travel in cleartext. Use https, point at a loopback host, or set \\`allowInsecureHttp: true\\` to override.`,\n );\n }\n console.warn(\n `[secret-manager] Using plain-http transport to a non-loopback host (\"${parsed.hostname}\"): the bearer token and resolved secrets travel in CLEARTEXT. Prefer https.`,\n );\n }\n};\n\n//? Build the name-eligibility predicate from `envNames` (allowlist array or\n//? predicate). SECURE DEFAULT: when `envNames` is unset, NOTHING is eligible —\n//? the resolver must never scan the whole inherited environment and POST every\n//? pointer-shaped value off-host. An explicit allowlist (or `() => true`) is\n//? required to opt in. The boot-time warning for the unset case is emitted by\n//? `warnIfEnvNamesUnset` at the resolve path.\nconst toNameFilter = (\n envNames: SecretManagerConfig['envNames'],\n): ((name: string) => boolean) => {\n if (envNames === undefined) return () => false;\n if (typeof envNames === 'function') return envNames;\n const allow = new Set(envNames);\n return (name) => allow.has(name);\n};\n\n//? Warn ONCE per process when `envNames` is unset: the resolver is deny-all in\n//? that state, so an operator who expected secrets to resolve gets a clear,\n//? actionable boot message instead of a silent no-op. Guarded so the production\n//? rotation poll (`refreshSecretManager` on an interval) doesn't re-emit it every\n//? cycle and flood the log sink.\nlet warnedEnvNamesUnset = false;\nconst warnIfEnvNamesUnset = (envNames: SecretManagerConfig['envNames']): void => {\n if (envNames !== undefined || warnedEnvNamesUnset) return;\n warnedEnvNamesUnset = true;\n console.warn(\n '[secret-manager] `envNames` is not set: NO environment values will be resolved off-host. Set `envNames` to an allowlist of the env names to resolve (or `() => true` to deliberately scan every name).',\n );\n};\n\n//? Split a set of `{ name -> value }` entries into pointer-shaped vs plain\n//? values, applying the `envNames` allowlist FIRST so a name excluded by\n//? `envNames` is treated as neither a pointer nor a plain value (it is dropped\n//? entirely). Shared by both ingest paths (boot `capturePointers` + the dev\n//? file-reload split) so the scoping rule can never drift between them.\nconst splitPointers = (\n entries: Iterable<[string, string]>,\n pattern: RegExp,\n envNames: SecretManagerConfig['envNames'],\n): { pointers: Record<string, string>; plain: Record<string, string> } => {\n const nameAllowed = toNameFilter(envNames);\n const pointers: Record<string, string> = {};\n const plain: Record<string, string> = {};\n for (const [name, value] of entries) {\n if (!nameAllowed(name)) continue;\n if (pattern.test(value)) pointers[name] = value;\n else plain[name] = value;\n }\n return { pointers, plain };\n};\n\nconst capturePointers = (\n pattern: RegExp,\n envNames: SecretManagerConfig['envNames'],\n): Record<string, string> => {\n const entries: [string, string][] = [];\n for (const [name, value] of Object.entries(process.env)) {\n if (typeof value === 'string') entries.push([name, value]);\n }\n return splitPointers(entries, pattern, envNames).pointers;\n};\n\nconst validateToken = (token: string): string => {\n //? An empty/whitespace token yields an `Authorization: Bearer ` header that\n //? silently auth-fails (and in hybrid mode falls back to local env) — reject it.\n const trimmed = token.trim();\n if (trimmed.length === 0) {\n throw new Error('[secret-manager] Bearer token is empty or whitespace-only.');\n }\n //? The `Bearer ` scheme is added at the call site. A token that already carries\n //? it would produce a malformed double-prefix `Bearer Bearer <...>` header —\n //? strip the redundant prefix (case-insensitive) and warn so the operator knows\n //? their config is wrong. Stripping is the forgiving path: a warn-but-pass-through\n //? would silently break every request, making this a very hard-to-debug boot issue.\n if (/^bearer\\s/i.test(trimmed)) {\n const stripped = trimmed.replace(/^bearer\\s+/i, '');\n console.warn(\n '[secret-manager] Token starts with a \"Bearer \" prefix; the scheme is added automatically — the prefix was stripped. Drop it from your configured token to silence this warning.',\n );\n return stripped;\n }\n return trimmed;\n};\n\nconst resolveToken = (token: SecretManagerToken): string => {\n if (typeof token === 'string') return validateToken(token);\n //? Distinguish a missing/deleted file from other I/O errors so a dev\n //? hot-reload poll over a transiently-absent token file gives a clear log.\n //? Note: error messages include `token.fromFile`. In hybrid mode these propagate\n //? to console.warn via `errorMessage(error)` (line ~597). If the path contains\n //? sensitive directory names (e.g. /home/admin/.keys/prod-token), those names\n //? will appear in log aggregators. Treat the token file path as infrastructure\n //? metadata and ensure your log sink is appropriately access-controlled.\n const [readError, raw] = tryCatchSync(() => readFileSync(token.fromFile, 'utf8'));\n if (readError) {\n const code = readError instanceof Error && 'code' in readError ? (readError as { code?: string }).code : undefined;\n if (code === 'ENOENT') {\n throw new Error(`[secret-manager] Token file not found: \"${token.fromFile}\".`);\n }\n throw new Error(`[secret-manager] Failed to read token file \"${token.fromFile}\": ${errorMessage(readError)}`);\n }\n if (raw === null) {\n throw new Error(`[secret-manager] Token file \"${token.fromFile}\" could not be read.`);\n }\n return validateToken(raw);\n};\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n//? Hard cap on the resolve response body. The server returns a flat\n//? `{ pointer -> secret }` map; even hundreds of secrets stay well under 1 MB.\n//? A response larger than this is a compromised/buggy server (or a MITM on an\n//? `allowInsecureHttp` channel) and is rejected BEFORE it can OOM the client.\nconst MAX_RESOLVE_BODY_BYTES = 1_048_576;\n\n//? Read the response body as text, aborting once more than `maxBytes` have been\n//? received — so a server that lies about (or omits) Content-Length still can't\n//? stream an unbounded body into memory. Falls back to `response.text()` when the\n//? body isn't a readable stream (e.g. a mocked Response).\nconst readBodyCapped = async (response: Response, maxBytes: number): Promise<string> => {\n const body = response.body;\n if (!body) return response.text();\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let received = 0;\n let out = '';\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n received += value.byteLength;\n if (received > maxBytes) {\n void reader.cancel();\n throw new Error(`[secret-manager] Resolve response exceeded the ${String(maxBytes)}-byte cap.`);\n }\n out += decoder.decode(value, { stream: true });\n }\n out += decoder.decode();\n return out;\n};\n\n//? One resolve round-trip: POST the pointers, validate the response, return the\n//? filtered `{ pointer -> value }` map. No retry/timeout orchestration here — the\n//? caller (`fetchResolve`) owns that so a hang can't slip past the abort signal.\nconst fetchResolveOnce = async (\n config: SecretManagerConfig,\n pointers: string[],\n): Promise<Record<string, string>> => {\n //? Defaults to the Node 20+ global fetch; pass fetchImpl for older hosts.\n const fetchFn = config.fetchImpl ?? globalThis.fetch;\n const resolvePath = config.resolvePath ?? '/resolve';\n const suffix = resolvePath.startsWith('/') ? resolvePath : `/${resolvePath}`;\n const endpoint = `${config.url.replace(/\\/+$/, '')}${suffix}`;\n\n //? Abort a black-hole server (accepts the TCP connection, never responds) so a\n //? hang surfaces as a rejection — boot can't freeze, and 'hybrid' reaches its\n //? warn-and-keep-local fallback. `timeoutMs: 0` disables the abort entirely.\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const signal = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;\n\n //? Consumer `headers` are merged first so they can never override Authorization.\n const response = await fetchFn(endpoint, {\n method: 'POST',\n headers: {\n ...config.headers,\n 'Authorization': `Bearer ${resolveToken(config.token)}`,\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n },\n body: JSON.stringify({ keys: pointers }),\n signal,\n });\n\n if (!response.ok) {\n //? Discard the unconsumed error body so a long-lived poll loop can't leave\n //? response bodies pending GC; failures carry only status text upward.\n void response.body?.cancel();\n throw new Error(`[secret-manager] Resolve request failed: ${String(response.status)} ${response.statusText}`);\n }\n\n //? The resolve response is the network input this client trusts least (a\n //? compromised/buggy server, or a MITM on an `allowInsecureHttp` channel).\n //? Reject an oversized body BEFORE buffering/parsing it so a hostile server\n //? can't OOM the booting client. A Content-Length header is advisory (can be\n //? absent/lying), so it is a fast pre-check only — the real guard is the\n //? streamed byte cap below.\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > MAX_RESOLVE_BODY_BYTES) {\n void response.body?.cancel();\n throw new Error(`[secret-manager] Resolve response too large (${String(declaredLength)} bytes > ${String(MAX_RESOLVE_BODY_BYTES)} cap).`);\n }\n const raw = await readBodyCapped(response, MAX_RESOLVE_BODY_BYTES);\n\n //? Parse as `unknown`, then narrow `values` with a runtime guard rather than\n //? trusting an up-front cast — the response is attacker-influenced (a\n //? compromised/buggy server) so its shape is not assumed.\n const [parseError, body] = tryCatchSync((): unknown => JSON.parse(raw));\n if (parseError) {\n throw new Error('[secret-manager] Resolve response was not valid JSON.');\n }\n const values = (body as { values?: unknown } | null)?.values;\n if (values === null || typeof values !== 'object') {\n throw new Error('[secret-manager] Resolve response missing `values` object.');\n }\n\n //? Filter the response down to the pointers we actually requested, and require\n //? each value to be a string. A compromised/buggy server could otherwise inject\n //? extra keys (cached + surfaced via getCachedResolution) or a non-string that\n //? coerces to '123' / '[object Object]' once written into process.env — only\n //? requested, string-valued pointers are trusted; a non-string is dropped (and\n //? thus treated as an unresolved pointer downstream: fatal in 'remote', warned\n //? in 'hybrid').\n const requested = new Set(pointers);\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(values as Record<string, unknown>)) {\n if (!requested.has(key)) continue;\n if (typeof value !== 'string') {\n console.warn(`[secret-manager] Pointer \"${key}\" resolved to a non-string value (${typeof value}); ignoring.`);\n continue;\n }\n filtered[key] = value;\n }\n return filtered;\n};\n\nconst fetchResolve = async (\n config: SecretManagerConfig,\n pointers: string[],\n): Promise<Record<string, string>> => {\n //? Defensive normalization: a configured `NaN` would make `attempt <= NaN` always\n //? false, so the loop body would never run. `lastError` is now pre-initialized\n //? (below) so we wouldn't `throw undefined` anymore, but coercing to a finite\n //? non-negative value is still the right guard — NaN retries is a config bug, not\n //? a valid \"zero retries\" signal.\n const rawRetryCount = config.retries?.count ?? 0;\n const retryCount = Number.isFinite(rawRetryCount) ? Math.max(0, rawRetryCount) : 0;\n const rawDelayMs = config.retries?.delayMs ?? 0;\n const delayMs = Number.isFinite(rawDelayMs) ? Math.max(0, rawDelayMs) : 0;\n\n let lastError: unknown = new Error('[secret-manager] resolve failed: no attempt was made');\n for (let attempt = 0; attempt <= retryCount; attempt++) {\n try {\n return await fetchResolveOnce(config, pointers);\n } catch (error) {\n lastError = error;\n if (attempt < retryCount && delayMs > 0) await sleep(delayMs);\n }\n }\n throw lastError;\n};\n\nconst applyResolved = (\n map: Record<string, string>,\n values: Record<string, string>,\n source: 'remote' | 'hybrid',\n): { name: string; pointer: string }[] => {\n //? In remote mode a single unresolved pointer is a hard boot failure. Check\n //? everything BEFORE mutating process.env so the failure is atomic.\n if (source === 'remote') {\n const missing = Object.entries(map).filter(([, pointer]) => values[pointer] === undefined);\n if (missing.length > 0) {\n const detail = missing.map(([name, pointer]) => `${pointer} (referenced by ${name})`).join(', ');\n throw new Error(`[secret-manager] Server did not resolve: ${detail}.`);\n }\n }\n\n //? Track only the env NAMES whose value actually changed — surfaced to\n //? `onApplied` so a client that captured a secret at construction time can\n //? re-create its pool/SDK client. Never carries the secret values themselves.\n const changes: { name: string; pointer: string }[] = [];\n for (const [name, pointer] of Object.entries(map)) {\n const value = values[pointer];\n if (value === undefined) {\n //? hybrid only — leave the pointer in place and warn so the operator sees it.\n console.warn(`[secret-manager] Pointer \"${pointer}\" (referenced by ${name}) not resolved; leaving \"${name}\" as-is.`);\n continue;\n }\n if (process.env[name] !== value) changes.push({ name, pointer });\n process.env[name] = value;\n }\n return changes;\n};\n\n//? Public resolve entry: SERIALIZE every resolve behind a single in-flight chain\n//? so a slow resolve can never land after a newer one (TOCTOU on `process.env` /\n//? `cachedResolution`). We chain off `resolveChain.then(...)` regardless of whether\n//? the prior resolve resolved or rejected, then re-publish the tail so the next\n//? caller waits on THIS one. The returned promise mirrors the inner outcome so\n//? `'remote'` still rejects the caller on a hard failure.\nconst doResolve = (\n config: SecretManagerConfig,\n phase: 'boot' | 'refresh' | 'file-reload',\n): Promise<void> => {\n const run = resolveChain.then(\n () => doResolveInner(config, phase),\n () => doResolveInner(config, phase),\n );\n //? Keep the chain alive even if this resolve rejects (swallow on the tail copy\n //? only — the returned `run` keeps the original rejection for the caller).\n resolveChain = run.then(noop, noop);\n return run;\n};\n\nconst doResolveInner = async (\n config: SecretManagerConfig,\n phase: 'boot' | 'refresh' | 'file-reload',\n): Promise<void> => {\n const source = config.source ?? 'remote';\n if (source === 'local') return;\n\n //? Secure default: an unset `envNames` resolves NOTHING off-host — warn loudly\n //? on every resolve so the deny-all state is never silent (an operator who\n //? expected secrets to resolve sees exactly what to set).\n warnIfEnvNamesUnset(config.envNames);\n\n //? Capture once on first resolve and reuse: the first resolve OVERWRITES the\n //? env value with the real secret, after which it no longer looks like a pointer.\n //? Re-capture when the map is empty so a pointer that wasn't present at boot\n //? (e.g. set into `process.env` after init) is still picked up by a later\n //? refresh — a non-empty `{}` would otherwise pin the resolver to zero pointers.\n if (pointerMap === null || Object.keys(pointerMap).length === 0) {\n pointerMap = capturePointers(\n stripStatefulFlags(config.pointerPattern ?? DEFAULT_POINTER_PATTERN),\n config.envNames,\n );\n }\n const activePointerMap = pointerMap;\n const pointers = [...new Set(Object.values(activePointerMap))];\n if (pointers.length === 0) {\n cachedResolution = { fetchedAt: Date.now(), values: {} };\n return;\n }\n\n //? CC-7 exemption (no-raw-try-catch): the async framework `tryCatch` auto-captures\n //? to the error tracker, but this is a deliberate fail-OPEN boot-time guard in an\n //? intentionally dependency-light client — a 'hybrid' resolve failure must stay a\n //? silent warn with NO error-tracker side-effect (and 'remote' re-throws untouched\n //? for a hard boot stop). `tryCatchSync` can't wrap the `await`, so the raw\n //? try/catch is kept on purpose; the fail-OPEN contract is the load-bearing detail.\n let values: Record<string, string>;\n let changes: { name: string; pointer: string }[];\n try {\n values = await fetchResolve(config, pointers);\n changes = applyResolved(activePointerMap, values, source);\n cachedResolution = { fetchedAt: Date.now(), values };\n } catch (error) {\n //? Opt-in observability seam: route the failure to Sentry/metrics. Kept\n //? alongside (not replacing) the warn below so the fail-open default is unchanged.\n //? Hooks run isolated: a throwing/hanging `onResolveError` must not mask the\n //? original resolve error (it is re-thrown below in 'remote').\n runHook(() => config.onResolveError?.(error, { phase }), 'onResolveError');\n if (source === 'hybrid') {\n //? Log the message only — never the raw error object: error objects are the\n //? classic accidental channel for a URL/token/PII string reaching a log sink.\n //? Full-fidelity routing is the `onResolveError` hook's job.\n console.warn('[secret-manager] Resolve failed, leaving local env as-is:', errorMessage(error));\n return;\n }\n throw error;\n }\n\n //? Notify AFTER the cache + process.env are coherent so a consumer that reads\n //? process.env inside the callback sees the applied values. Isolated so a\n //? throwing/hanging `onApplied` can't abort an otherwise-successful resolve\n //? (it has already written process.env + the cache).\n if (changes.length > 0) await runHookAsync(() => config.onApplied?.(changes), 'onApplied');\n};\n\n//? Minimal .env parser kept in-package so the resolver stays dependency-free.\n//? Handles standard `KEY=VALUE` lines, full-line + inline (` #`) comments, and\n//? single/double-quoted values. Not multi-line values or escape sequences.\nconst parseEnvFile = (content: string): Record<string, string> => {\n const out: Record<string, string> = {};\n for (const rawLine of content.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (!line || line.startsWith('#')) continue;\n const eq = line.indexOf('=');\n if (eq <= 0) continue;\n const key = line.slice(0, eq).trim();\n //? Restrict to POSIX-shell env-var names. `.`/`-` keys can't be read via the\n //? normal `process.env.NAME` lookup, so accepting them is a silent footgun.\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.warn(`[secret-manager] Ignoring env key \"${key}\": not a valid environment variable name (^[A-Za-z_][A-Za-z0-9_]*$).`);\n continue;\n }\n let value = line.slice(eq + 1).trim();\n const quote = value[0];\n if (quote === '\"' || quote === \"'\") {\n //? Quoted value. The closing quote is the LAST occurrence of the same quote\n //? char; anything after it (whitespace + `# ...`) is an inline comment and is\n //? dropped (`KEY=\"v\" # note` → `v`). A `#` BEFORE the closing quote is literal\n //? and preserved (`KEY=\"a#b\"` → `a#b`).\n const closeIdx = value.lastIndexOf(quote);\n if (closeIdx > 0) {\n value = value.slice(1, closeIdx);\n } else {\n //? Opening quote with no matching closing quote on this line means a\n //? multi-line value (dotenv supports it, this parser does not). Warn so the\n //? value isn't silently truncated, then strip any inline comment from the\n //? raw remainder so a trailing `# ...` doesn't leak into the value.\n console.warn(\n `[secret-manager] parseEnvFile: \"${key}\" starts with a quote but has no matching closing quote on the same line. Multi-line values are not supported — the raw text will be used as-is. If this is a multi-line value (e.g. a PEM key), do not rely on this parser.`,\n );\n const commentAt = value.indexOf(' #');\n if (commentAt !== -1) value = value.slice(0, commentAt).trim();\n }\n } else {\n const commentAt = value.indexOf(' #');\n if (commentAt !== -1) value = value.slice(0, commentAt).trim();\n if (value.startsWith('#')) value = '';\n }\n out[key] = value;\n }\n return out;\n};\n\nconst scheduleReload = (): void => {\n //? Debounce — fs.watch can fire several events for one save.\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n void reloadSecretManagerFromFiles().catch((error: unknown) => {\n console.warn('[secret-manager] dev reload failed:', errorMessage(error));\n });\n }, 200);\n debounceTimer.unref();\n};\n\nconst startDevReload = (config: SecretManagerConfig): void => {\n if (devReloadStarted || config.dev === undefined) return;\n //? Allowlist the dev environments rather than exact-matching 'production': an\n //? unset / 'prod' / 'staging' NODE_ENV must NOT silently start fs watchers + a\n //? poll on a host the operator believes is production. Only an explicit\n //? 'development' or 'test' enables dev hot reload.\n const nodeEnv = process.env.NODE_ENV;\n if (nodeEnv !== 'development' && nodeEnv !== 'test') return;\n\n const watch = config.dev.watch ?? true;\n const pollIntervalMs = config.dev.pollIntervalMs ?? 0;\n if (!watch && pollIntervalMs <= 0) return;\n devReloadStarted = true;\n\n if (watch) {\n for (const file of config.dev.envFiles ?? DEFAULT_ENV_FILES) {\n if (!isSafeEnvFile(file, true)) {\n console.warn(`[secret-manager] ignoring unsafe dev envFile path (must be relative + within the project): ${file}`);\n continue;\n }\n try {\n const watcher = fsWatch(file, () => {\n scheduleReload();\n });\n watcher.unref();\n fileWatchers.push(watcher);\n } catch {\n //? The file may not exist (e.g. no .env.local) — nothing to watch.\n }\n }\n }\n\n if (pollIntervalMs > 0) {\n pollTimer = setInterval(() => {\n void refreshSecretManager().catch((error: unknown) => {\n console.warn('[secret-manager] poll refresh failed:', errorMessage(error));\n });\n }, pollIntervalMs);\n pollTimer.unref();\n }\n};\n\n/**\n * Initialize the secret-manager resolver. Call once as the very first line of\n * `server.ts`, BEFORE any other framework code reads `process.env`. In\n * `'remote'` / `'hybrid'` mode the resolved secrets are written into\n * `process.env` before this resolves, so downstream code sees them via the\n * standard `process.env.FOO` lookup. In `'local'` mode it is a no-op.\n */\nexport const initSecretManager = async (config: SecretManagerConfig): Promise<void> => {\n //? Validate the URL BEFORE recording activeConfig, so a failed init can't leave\n //? a later refreshSecretManager() resolving against an invalid config.\n if ((config.source ?? 'remote') !== 'local') {\n //? Only validate the URL when we'll actually hit the network ('remote' /\n //? 'hybrid'); 'local' may carry a placeholder url.\n validateUrl(config.url, config.allowInsecureHttp ?? false);\n }\n activeConfig = config;\n if ((config.source ?? 'remote') === 'local') return;\n await doResolve(config, 'boot');\n startRotationPoll(config);\n startDevReload(config);\n};\n\n//? Production-capable rotation poll: re-resolve every `config.pollIntervalMs` ms\n//? in ALL environments (distinct from the dev-only `dev.pollIntervalMs` file-watch\n//? channel, which is gated off in production). Unref'd so it never blocks exit.\nconst startRotationPoll = (config: SecretManagerConfig): void => {\n const intervalMs = config.pollIntervalMs ?? 0;\n if (intervalMs <= 0 || rotationPollTimer) return;\n rotationPollTimer = setInterval(() => {\n void refreshSecretManager().catch((error: unknown) => {\n console.warn('[secret-manager] rotation poll refresh failed:', errorMessage(error));\n });\n }, intervalMs);\n rotationPollTimer.unref();\n};\n\n/**\n * Re-resolve against the server, ignoring nothing — used by the dev hot-reload\n * watch/poll and callable manually when an admin rotates a secret and you want\n * a long-running process to pick it up without a restart.\n */\nexport const refreshSecretManager = async (): Promise<void> => {\n if (!activeConfig || (activeConfig.source ?? 'remote') === 'local') return;\n await doResolve(activeConfig, 'refresh');\n};\n\n/**\n * Dev hot-reload entry: re-parse the configured env files and apply them — plain\n * (non-pointer) values are injected straight into `process.env` (live config\n * reload), pointer-shaped values are re-resolved against the server. Wired to the\n * `.env` / `.env.local` file watch; also callable manually.\n */\nexport const reloadSecretManagerFromFiles = async (): Promise<void> => {\n const config = activeConfig;\n if (!config || (config.source ?? 'remote') === 'local') return;\n\n const files = config.dev?.envFiles ?? DEFAULT_ENV_FILES;\n const pattern = stripStatefulFlags(config.pointerPattern ?? DEFAULT_POINTER_PATTERN);\n\n //? Re-read every file in load order; later files (e.g. .env.local) override.\n //? `warnAbsolute=false`: the absolute-path notice already fired once at boot\n //? (startDevReload). Repeating it on every debounced hot-reload would flood the\n //? dev log for anyone legitimately using an absolute envFile path.\n const merged: Record<string, string> = {};\n for (const file of files) {\n if (!isSafeEnvFile(file, false)) {\n console.warn(`[secret-manager] ignoring unsafe dev envFile path: ${file}`);\n continue;\n }\n let content: string;\n try {\n content = readFileSync(file, 'utf8');\n } catch {\n continue; //? file may not exist (e.g. no .env.local)\n }\n Object.assign(merged, parseEnvFile(content));\n }\n\n //? Split plain (live config reload) from pointer-shaped values through the SAME\n //? `envNames` allowlist the boot path uses (`capturePointers`), so a name excluded\n //? by `envNames` is dropped on BOTH channels — never POSTed off-host as a pointer,\n //? never injected into `process.env` as a plain value. This closes the scoping\n //? drift where a file-reload could send/apply names the boot scan rejected.\n const { pointers: freshPointerMap, plain: plainValues } = splitPointers(\n Object.entries(merged),\n pattern,\n config.envNames,\n );\n\n //? MERGE the fresh file-sourced pointers over the existing map (don't replace):\n //? a pointer captured at boot from the inherited shell/CI env that isn't in a\n //? watched file would otherwise be dropped and stop rotating. A file-sourced\n //? pointer with the same name still wins.\n //? Build the merged map locally and commit it ONLY after a successful resolve so\n //? a failed remote reload can't permanently poison the in-memory pointer map and\n //? cause every subsequent refreshSecretManager / poll to resolve the bad set.\n const previousPointerMap = pointerMap;\n pointerMap = { ...pointerMap, ...freshPointerMap };\n\n //? Resolve the pointers FIRST. In 'remote' mode an unresolved pointer throws —\n //? mirror the atomic boot path and inject the plain values only AFTER a\n //? successful resolve, so a throw never leaves half-applied state. On failure\n //? roll back the pointer map to its pre-reload snapshot.\n try {\n await doResolve(config, 'file-reload');\n } catch (error) {\n pointerMap = previousPointerMap;\n throw error;\n }\n for (const [name, value] of Object.entries(plainValues)) {\n process.env[name] = value;\n }\n};\n\n/**\n * Read the last `{ fetchedAt, values }` resolution (pointer -> value), or `null`.\n *\n * ⚠️ SENSITIVE: `values` maps each pointer to its RAW resolved secret. The result\n * is a shallow copy (values are flat strings, so mutating the returned object does\n * not corrupt the cache), but the values are still the real secrets — NEVER\n * serialize the result into an HTTP response, a `/health` payload, or a log line.\n * For a safe diagnostic use {@link getCachedResolutionMeta}, which never exposes\n * the values.\n *\n * If you need to act on changed secrets (e.g. to rebuild a DB pool after rotation),\n * use the `onApplied` callback in `SecretManagerConfig` instead — it receives only\n * the changed env NAMES, never the secret values, and is called automatically after\n * each successful resolve.\n */\nexport const getCachedResolution = (): CachedResolution | null =>\n cachedResolution === null\n ? null\n : { fetchedAt: cachedResolution.fetchedAt, values: { ...cachedResolution.values } };\n\n/** Values-free view of {@link getCachedResolution}: the timestamp + resolved pointer NAMES only. */\nexport interface CachedResolutionMeta {\n /** `Date.now()` of the last successful resolve. */\n fetchedAt: number;\n /** The resolved pointer strings (never the secret values). */\n pointerNames: string[];\n /** How many pointers were resolved. */\n pointerCount: number;\n}\n\n/**\n * Safe diagnostic accessor: the last resolution's timestamp + resolved pointer\n * NAMES, with the secret values stripped. Use this on any surface that might be\n * logged or served (`/health`, metrics) so the convenient path is also the safe\n * one — {@link getCachedResolution} is the values-carrying escape hatch.\n */\nexport const getCachedResolutionMeta = (): CachedResolutionMeta | null => {\n if (cachedResolution === null) return null;\n const pointerNames = Object.keys(cachedResolution.values);\n return { fetchedAt: cachedResolution.fetchedAt, pointerNames, pointerCount: pointerNames.length };\n};\n\n/**\n * Tear down the dev file watchers, the dev poll, the rotation poll, and the\n * debounce timer WITHOUT wiping the resolved cache / active config. Use for a\n * graceful shutdown of an embedded resolver (worker / CLI) so no timers keep\n * firing; the last resolved `process.env` values stay in place.\n *\n * **Note:** `activeConfig` is intentionally left set after `stopSecretManager`,\n * so that `reloadSecretManagerFromFiles` can still use it for a final manual\n * reload. As a consequence, calling `refreshSecretManager()` after `stop` will\n * issue a live network request (the `!activeConfig` no-op guard is not met).\n * If you want to prevent ANY further resolution after `stop`, call\n * `resetSecretManagerForTests()` instead (which also clears `activeConfig`).\n */\nexport const stopSecretManager = (): void => {\n devReloadStarted = false;\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n if (rotationPollTimer) {\n clearInterval(rotationPollTimer);\n rotationPollTimer = null;\n }\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n for (const watcher of fileWatchers) {\n watcher.close();\n }\n fileWatchers.length = 0;\n //? Reset the \"warned once\" guard so a subsequent initSecretManager() call that\n //? also omits envNames re-emits the boot warning rather than silently silencing it\n //? (the first, valid config could have set envNames correctly while the second,\n //? broken one doesn't — the guard must not carry across a full stop-and-reinit).\n warnedEnvNamesUnset = false;\n};\n\n/** Test-only — clear module state and tear down any dev watchers / timers. */\nexport const resetSecretManagerForTests = (): void => {\n stopSecretManager();\n cachedResolution = null;\n pointerMap = null;\n activeConfig = null;\n resolveChain = Promise.resolve();\n warnedEnvNamesUnset = false;\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/secret-manager",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -15,7 +15,7 @@
15
15
  "url": "git+https://github.com/ItsLucky23/LuckyStack-v2.git",
16
16
  "directory": "packages/secret-manager"
17
17
  },
18
- "homepage": "https://github.com/ItsLucky23/LuckyStack-v2/tree/master/packages/secret-manager#readme",
18
+ "homepage": "https://github.com/ItsLucky23/LuckyStack-v2/tree/main/packages/secret-manager#readme",
19
19
  "bugs": {
20
20
  "url": "https://github.com/ItsLucky23/LuckyStack-v2/issues"
21
21
  },
@@ -53,6 +53,6 @@
53
53
  "test": "vitest run"
54
54
  },
55
55
  "dependencies": {
56
- "@luckystack/core": "^0.2.4"
56
+ "@luckystack/core": "^0.2.6"
57
57
  }
58
58
  }