{"_id":"@chad3814/secret-provider","_rev":"4-cb306e2b7982f93265852181f3f418cc","name":"@chad3814/secret-provider","dist-tags":{"latest":"1.2.0"},"versions":{"0.1.0":{"name":"@chad3814/secret-provider","version":"0.1.0","keywords":["credentials","secrets","provider","chain","config"],"author":{"name":"Chad Walker"},"license":"MIT","_id":"@chad3814/secret-provider@0.1.0","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"homepage":"https://github.com/chad3814/secret-provider#readme","bugs":{"url":"https://github.com/chad3814/secret-provider/issues"},"dist":{"shasum":"91b9cd0e32de323e295b419768e1d2866889236c","tarball":"https://registry.npmjs.org/@chad3814/secret-provider/-/secret-provider-0.1.0.tgz","fileCount":48,"integrity":"sha512-rjxSEWEJcI2P1J73TpIyUPWtzYI4CGQx9DM1AjU6RjphnGwlaN1waUbqdWeNZR0RdlsGQD2xyrTa22SnpmtxkQ==","signatures":[{"sig":"MEYCIQD7yP5421pLnpDNJiimDwd8wd+TH1/J+fKT/yPme8ZzcwIhAPesXTbnknMdN+CmtcTmXQRwLd/WTgghGhk7Y/LgLSeK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":34609},"type":"module","engines":{"node":">=22.18.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"lint":"eslint .","test":"node --test \"src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","verify":"npm run lint && npm run typecheck && npm run test && npm run build","typecheck":"tsc -p tsconfig.json"},"_npmUser":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"},"repository":{"url":"git+https://github.com/chad3814/secret-provider.git","type":"git"},"_npmVersion":"10.9.7","description":"Composable async credential providers — a first-to-resolve chain with memoization, in the shape of the AWS SDK's provider pattern, for any secret source.","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.2","@eslint/js":"^9.39.2","typescript":"^5.9.3","@types/node":"^22.20.1","typescript-eslint":"^8.46.4"},"_npmOperationalInternal":{"tmp":"tmp/secret-provider_0.1.0_1785957432186_0.3605888456962494","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@chad3814/secret-provider","version":"1.1.0","keywords":["credentials","secrets","provider","chain","config"],"author":{"name":"Chad Walker"},"license":"MIT","_id":"@chad3814/secret-provider@1.1.0","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"homepage":"https://github.com/chad3814/secret-provider#readme","bugs":{"url":"https://github.com/chad3814/secret-provider/issues"},"dist":{"shasum":"fbb720b05f5b4bcbf707696d8516f84b57ab3e0e","tarball":"https://registry.npmjs.org/@chad3814/secret-provider/-/secret-provider-1.1.0.tgz","fileCount":48,"integrity":"sha512-9fN5mPgsGdyVVqV034GYKer8qqfp+ZvYLq8qiFj8OhgU2iSNsRaVbJhAu4U5LbupV1sN3Rl4egSFRy4yB++tVg==","signatures":[{"sig":"MEUCIQDMIGqiRkNxodYqT0Exx3+ucqtvzaZuSkd1ums/JfxOgAIgbA4koOZaTca+eINyUyydJKlE1Ubsgyyga82j2rihHJw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@chad3814%2fsecret-provider@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":34609},"type":"module","engines":{"node":">=22.18.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"752c7cbc1b4168468a3a08f26e097bb52cffefa5","scripts":{"lint":"eslint .","test":"node --test \"src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","verify":"npm run lint && npm run typecheck && npm run test && npm run build","typecheck":"tsc -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","approver":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"},"trustedPublisher":{"id":"github","oidcConfigId":"oidc:fafbbba7-2999-4b44-9d03-4697c6c239bc"}},"repository":{"url":"git+https://github.com/chad3814/secret-provider.git","type":"git"},"_npmVersion":"12.0.2","description":"Composable async credential providers — a first-to-resolve chain with memoization, in the shape of the AWS SDK's provider pattern, for any secret source.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.2","@eslint/js":"^9.39.2","typescript":"^5.9.3","@types/node":"^22.20.1","typescript-eslint":"^8.46.4"},"_npmOperationalInternal":{"tmp":"tmp/secret-provider_1.1.0_1785959060165_0.19018195829188045","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@chad3814/secret-provider","version":"1.1.1","keywords":["credentials","secrets","provider","chain","config"],"author":{"name":"Chad Walker"},"license":"MIT","_id":"@chad3814/secret-provider@1.1.1","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"homepage":"https://github.com/chad3814/secret-provider#readme","bugs":{"url":"https://github.com/chad3814/secret-provider/issues"},"dist":{"shasum":"a59c337ac54ee16ffa7a7390e6aefb73e769f6c0","tarball":"https://registry.npmjs.org/@chad3814/secret-provider/-/secret-provider-1.1.1.tgz","fileCount":48,"integrity":"sha512-f5tKJY5bhFeiRYAoQWa3ULJjeYqXCXIZC5REVp/dP5PuqRbmCiT42HrOj51fDl27kOOxMRBazS2Qsp3wCqJivg==","signatures":[{"sig":"MEYCIQDjbCWSBsB6x1Sf64VFg9e6BAADEtgqr/MbISIEtPfZbAIhAJVUoJjXTcmHbglV2wL6nUWtG8tVcpJVGJCzVaH9ktSx","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@chad3814%2fsecret-provider@1.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":37481},"type":"module","engines":{"node":">=22.18.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"98d07a1db0fba03d0c186d916bc602f1bcce1bc2","scripts":{"tag":"git tag -s \"v$npm_package_version\" -m \"v$npm_package_version\"","lint":"eslint .","test":"node --test \"src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","verify":"npm run lint && npm run typecheck && npm run test && npm run build","typecheck":"tsc -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","approver":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"},"trustedPublisher":{"id":"github","oidcConfigId":"oidc:fafbbba7-2999-4b44-9d03-4697c6c239bc"}},"repository":{"url":"git+https://github.com/chad3814/secret-provider.git","type":"git"},"_npmVersion":"12.0.2","description":"Composable async credential providers — a first-to-resolve chain with memoization, in the shape of the AWS SDK's provider pattern, for any secret source.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.2","@eslint/js":"^9.39.2","typescript":"^5.9.3","@types/node":"^22.20.1","typescript-eslint":"^8.46.4"},"_npmOperationalInternal":{"tmp":"tmp/secret-provider_1.1.1_1785964332863_0.2522395781046256","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"_id":"@chad3814/secret-provider@1.2.0","bugs":{"url":"https://github.com/chad3814/secret-provider/issues"},"dist":{"shasum":"d56031c9741160ce101479b776a25e980710194e","tarball":"https://registry.npmjs.org/@chad3814/secret-provider/-/secret-provider-1.2.0.tgz","integrity":"sha512-Yt6N0I46mLNKjuL2BPgng3NJoVHVbdGaOPlRY7BPaVrgaHRIN9NC+Ktq+whPjMNv5FXnOFdGw/Ci+A5ZM5d/wg==","fileCount":58,"unpackedSize":76483,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@chad3814%2fsecret-provider@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCgz0bh3TOxZNV3qkO41ZbI/PZDJpNn9ZWhvKgcbM+6hgIgGjWnjO8pUzI+4nunpyzrRyqcVcFfIw+PNUdBicXJn1Y="}]},"name":"@chad3814/secret-provider","type":"module","author":{"name":"Chad Walker"},"engines":{"node":">=22.18.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"6930015363838c70e1ff58822ed1ca5f480f745a","license":"MIT","scripts":{"tag":"git tag -s \"v$npm_package_version\" -m \"v$npm_package_version\"","lint":"eslint .","test":"node --test \"src/**/*.test.ts\"","build":"tsc -p tsconfig.build.json","verify":"npm run lint && npm run typecheck && npm run test && npm run build","typecheck":"tsc -p tsconfig.json"},"version":"1.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:fafbbba7-2999-4b44-9d03-4697c6c239bc"},"approver":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}},"homepage":"https://github.com/chad3814/secret-provider#readme","keywords":["credentials","secrets","provider","chain","config"],"repository":{"url":"git+https://github.com/chad3814/secret-provider.git","type":"git"},"_npmVersion":"12.0.2","description":"Composable async credential providers — a first-to-resolve chain with memoization, in the shape of the AWS SDK's provider pattern, for any secret source.","directories":{},"maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"devDependencies":{"eslint":"^9.39.2","@eslint/js":"^9.39.2","typescript":"^5.9.3","@types/node":"^22.20.1","typescript-eslint":"^8.46.4"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/secret-provider_1.2.0_1785969938416_0.1311849265914138"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T19:17:11.991Z","modified":"2026-08-05T22:45:38.806Z","0.1.0":"2026-08-05T19:17:12.334Z","1.1.0":"2026-08-05T19:44:20.241Z","1.1.1":"2026-08-05T21:12:12.961Z","1.2.0":"2026-08-05T22:45:38.506Z"},"bugs":{"url":"https://github.com/chad3814/secret-provider/issues"},"author":{"name":"Chad Walker"},"license":"MIT","homepage":"https://github.com/chad3814/secret-provider#readme","keywords":["credentials","secrets","provider","chain","config"],"repository":{"url":"git+https://github.com/chad3814/secret-provider.git","type":"git"},"description":"Composable async credential providers — a first-to-resolve chain with memoization, in the shape of the AWS SDK's provider pattern, for any secret source.","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"readme":"# @chad3814/secret-provider\n\nComposable async credential providers: a chain of sources where the first to\nresolve wins, with memoization and an explicit distinction between \"this source\nisn't configured\" and \"this source is broken\".\n\nA provider is just an async thunk:\n\n```ts\ntype Provider<T> = () => Promise<T>;\n```\n\nWhich means anything can be a link — an env var, a file, a subprocess, an HTTP\ncall to a vault. The shape is deliberately the same as the AWS SDK's\n`fromEnv()` / `fromIni()` credential providers, generalised to any secret.\n\nESM-only, zero runtime dependencies, Node >= 22.18.\n\n## Install\n\n```sh\nnpm install @chad3814/secret-provider\n```\n\n## Usage\n\n```ts\nimport {\n  chain,\n  fromEnv,\n  fromFile,\n  fromStatic,\n  memoize,\n} from '@chad3814/secret-provider';\n\nconst apiKey = memoize(\n  chain(\n    fromEnv('POSTFUL_API_KEY'),\n    fromFile('/run/secrets/postful_api_key'),\n    fromStatic('development-key'),\n  ),\n);\n\n// Resolved once, on first use, then cached.\nconst key = await apiKey();\n```\n\nBecause a provider is only a function, a source this package doesn't ship is\nstill a one-liner — and the secret goes straight from its source into the\nclosure without ever landing in a file on disk:\n\n```ts\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { chain, fromEnv, memoize, type Provider } from '@chad3814/secret-provider';\n\nconst run = promisify(execFile);\n\nconst fromOnePassword = (reference: string): Provider<string> => async () => {\n  const { stdout } = await run('op', ['read', reference]);\n  return stdout.trimEnd();\n};\n\nconst apiKey = memoize(\n  chain(fromEnv('POSTFUL_API_KEY'), fromOnePassword('op://Private/Postful/credential')),\n);\n```\n\n## Absent vs. broken\n\nThe distinction the pattern lives or dies on. A source that simply isn't\nconfigured should be skipped; a source that answered and refused should stop\neverything, rather than silently handing back a weaker credential.\n\n`ProviderError` carries `tryNextLink` to say which happened:\n\n```ts\nthrow new ProviderError('POSTFUL_API_KEY not set');        // skip to next link\nthrow new ProviderError('vault rejected the token', false); // halt the chain\n```\n\nAny error that isn't a `ProviderError` is treated as absent, so an ordinary\nthrow in a custom provider falls through. When every link fails, `chain` throws\nwith all the failure messages aggregated, so you can see each source that was\ntried and why it didn't answer:\n\n```text\nno provider resolved a value (POSTFUL_API_KEY not set; /run/secrets/api_key does not exist)\n```\n\n## API\n\n### `chain<T>(...providers: Provider<T>[]): Provider<T>`\n\nFirst provider to resolve wins. Skips links that report themselves absent,\nhalts immediately on a `ProviderError` with `tryNextLink: false`, and throws an\naggregated error if the links run out.\n\n### `memoize<T>(provider, isExpired?): Provider<T>`\n\nCaches the resolved value. Concurrent callers share a single in-flight\nresolution, so ten requests at cold start make one round-trip rather than ten.\nRejections are not cached, leaving a failed lookup retryable. Pass `isExpired`\nto re-resolve a value that has gone stale:\n\n```ts\nconst token = memoize(fetchToken, (t) => t.expiresAt < Date.now());\n```\n\n### `fromEnv(name: string): Provider<string>`\n\n### `fromEnv<T>(read: (env: Environment) => T | undefined, label: string): Provider<T>`\n\nReads the process environment on every resolution, so a variable set later is\npicked up. The reader form can parse into any type:\n\n```ts\nconst port = fromEnv((env) => {\n  const raw = env.PORT;\n  return raw === undefined ? undefined : Number.parseInt(raw, 10);\n}, 'PORT');\n```\n\n### `fromFile(path: string): Provider<string>`\n\nReads a credential from disk — a Docker or Kubernetes secret mount, a\n`/run/secrets` entry. Trailing whitespace is trimmed, so a file written with a\nfinal newline yields the credential rather than one with a stray `\\n`. Interior\nnewlines are preserved, so a multi-line PEM key survives intact.\n\n### `fromIni(path: string, key: string, options?: IniOptions): Provider<string>`\n\nReads one key out of one section of an ini-style file — the shape of\n`~/.aws/credentials` and its many imitators, where each `[section]` is a named\nprofile.\n\n```ts\nconst apiKey = fromIni('~/.postful/credentials', 'api_key', {\n  profile: 'staging',\n});\n```\n\n```ini\n[default]\napi_key = dev-key\n\n[staging]\napi_key = \"staging-key\"\n```\n\n`profile` defaults to `'default'`. A leading `~` is expanded, which a shell would\nhave done for you but a Node process will not.\n\nParsing is deliberately small and dependency-free, with a few rules worth\nknowing:\n\n- Section and key lookups are **case-sensitive**.\n- Where a key is assigned more than once, the **last wins**.\n- The value is split on the **first `=` only**, so a base64 value keeps its\n  padding.\n- Surrounding quotes are stripped, since people quote out of habit and a\n  credential carrying literal quote marks fails in a way that is hard to spot.\n- `#` and `;` start a comment **only at the beginning of a line**. Truncating at\n  a mid-value `#` would silently mangle a credential that contains one.\n- Lines that are neither a section header nor an assignment are ignored, rather\n  than failing a lookup that would otherwise have succeeded.\n\n### `fromPrompt(prompt: string, options?: PromptOptions): Provider<string>`\n\nAsks the person at the keyboard. Reads from the terminal with echo suppressed,\nso nothing typed is displayed — not even its length. The prompt is written to\n**stderr**, the convention for password prompts, so a CLI's stdout stays clean\nfor piping.\n\n```ts\nconst apiKey = memoize(\n  chain(\n    fromEnv('POSTFUL_API_KEY'),\n    fromFile('/run/secrets/postful_api_key'),\n    fromPrompt('Postful API key: '),\n  ),\n);\n```\n\n`memoize` matters more here than anywhere else: without it, every resolution\nasks again.\n\nBecause there is nothing to prompt *on* in CI, behind a pipe, or in a daemon, a\nmissing TTY simply falls through to the next link — so the same chain works in\nboth a developer's terminal and a deployed process. Options:\n\n| Option | Default | Meaning |\n| --- | --- | --- |\n| `mask` | `false` | `false` echoes nothing at all. A character such as `'*'` echoes one per keystroke, at the cost of revealing the length. |\n| `input` | `process.stdin` | Where keystrokes come from. |\n| `output` | `process.stderr` | Where the prompt is written. |\n\nEditing keys behave the way muscle memory from readline expects:\n\n| Key | Effect |\n| --- | --- |\n| Backspace / Ctrl-H | delete the last character |\n| Ctrl-U | discard the whole line and start again |\n| Enter, Ctrl-D | submit |\n| Ctrl-C | **halt the chain** |\n\nCtrl-C halts rather than falling through: an explicit refusal should not quietly\nfall back to some other credential source.\n\nEvery other control character is **dropped**, and ANSI escape sequences are\nswallowed whole. Cursor keys, Home, End and function keys are meaningless when\nnothing is rendered, and the alternative is worse than useless — an arrow key\nsends `ESC [ A`, so appending what arrives would silently bury `[A` inside the\ncredential where nobody can see it.\n\n### `fromStatic<T>(value: T): Provider<T>`\n\nWraps an already-known value. Useful as an explicit last link, and for\nsupplying a credential in tests.\n\n## When a source counts as absent\n\nThe built-in providers treat a present-but-empty value as absent, on the grounds\nthat an empty credential is a misconfiguration and would otherwise surface as a\nconfusing downstream auth failure.\n\n| Condition | Result |\n| --- | --- |\n| `fromEnv` — variable unset | falls through |\n| `fromEnv` — variable set to `''` | falls through |\n| `fromFile` — path does not exist | falls through |\n| `fromFile` — file empty or whitespace-only | falls through |\n| `fromFile` — no permission, is a directory, bad path prefix | **halts the chain** |\n| `fromIni` — path does not exist | falls through |\n| `fromIni` — profile or key absent | falls through |\n| `fromIni` — value empty, or empty quotes | falls through |\n| `fromIni` — no permission, is a directory, bad path prefix | **halts the chain** |\n| `fromPrompt` — no TTY to prompt on | falls through |\n| `fromPrompt` — submitted empty | falls through |\n| `fromPrompt` — cancelled with Ctrl-C | **halts the chain** |\n\n## Accepting a provider in your own library\n\nIf you are writing a client or library that needs a credential, take a provider\nrather than a resolved string. The caller then decides where the secret comes\nfrom — env, file, vault, `op read` — and you stop forcing them to have it in\nhand before they can construct your object.\n\n**You do not need to depend on this package to accept one.** `Provider<T>` is\nstructurally just a function, so declaring it inline is enough, and your users\ncan pass anything of that shape whether or not they use this library:\n\n```ts\ntype Provider<T> = () => Promise<T>;\n\nexport interface PostfulClientOptions {\n  /** An API key, or anything that resolves one. A string is used as-is. */\n  apiKey: string | Provider<string>;\n}\n```\n\nNormalise once at the boundary, then resolve at each point of use:\n\n```ts\nimport { fromStatic, memoize, type Provider } from '@chad3814/secret-provider';\n\nexport class PostfulClient {\n  readonly #apiKey: Provider<string>;\n\n  constructor(options: PostfulClientOptions) {\n    this.#apiKey = memoize(\n      typeof options.apiKey === 'string'\n        ? fromStatic(options.apiKey)\n        : options.apiKey,\n    );\n  }\n\n  async send(body: string): Promise<Response> {\n    // Resolved per request, so a rotated credential is picked up without\n    // rebuilding the client.\n    const apiKey = await this.#apiKey();\n\n    return fetch('https://api.postful.ai/send', {\n      method: 'POST',\n      headers: { authorization: `Bearer ${apiKey}` },\n      body,\n    });\n  }\n}\n```\n\nA few things worth getting right:\n\n- **Resolve at use, not in the constructor.** Resolving once up front makes\n  construction async and freezes the credential for the object's lifetime, so\n  expiry and rotation never take effect.\n- **Take `() => Promise<T>`, not `Promise<T>`.** A promise resolves once,\n  eagerly, and cannot be re-resolved after expiry or retried after a failure.\n  The thunk is what makes those possible.\n- **Call `memoize` yourself, once.** Then resolving per request costs nothing,\n  and a caller who forgot to memoize does not get a vault round-trip per call.\n  Pass `isExpired` through if your credential has a lifetime.\n- **Let `ProviderError` propagate.** Catching it and rethrowing something\n  generic destroys both the `tryNextLink` distinction and the aggregated list of\n  sources that were tried — which is the part that makes a misconfiguration\n  diagnosable.\n- **Keep the resolved value local.** Don't log it, don't put it in an error\n  message, don't attach it to anything that gets serialised. It should live in\n  the closure and the outbound request, nowhere else.\n\n## Types\n\n`Environment` is a structural `Readonly<Record<string, string | undefined>>`\nrather than `NodeJS.ProcessEnv`, so you don't need `@types/node` installed to\ntypecheck against this package.\n\n`Provider<T>` is exported as a type for convenience, but as above it is only\n`() => Promise<T>` — nothing stops a consumer from satisfying it structurally.\n\n`PromptInput` and `PromptOutput` are likewise structural, describing only the\nhandful of members `fromPrompt` touches. `process.stdin` and `process.stderr`\nsatisfy them without a cast, and so does a test double.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}