@basic-tools/secrets-resolver
v1.3.4
Published
Secrets Resolver Package for any node project.
Readme
@basic-tools/secrets-resolver
Load secrets from different sources — AWS Secrets Manager, .env files, JSON files, or in-memory data — with a single consistent API. Switch sources per environment without changing your application code.
Requires Node.js >= 26.3.0
Installation
npm install @basic-tools/secrets-resolverQuick start
Configure once at startup (main.ts / index.ts), then call get() anywhere.
import { SecretResolver, RESOLVER_TYPES, SecretResolverByEnviromentOptions } from '@basic-tools/secrets-resolver'
const options: SecretResolverByEnviromentOptions = {
resolverInDevelopment: { resolveFrom: RESOLVER_TYPES.environments },
resolverInProduction: { resolveFrom: RESOLVER_TYPES.aws },
resolverInTest: { resolveFrom: RESOLVER_TYPES.environments },
}
await SecretResolver.config(options)// anywhere in your code
SecretResolver.get('DB_HOST')
SecretResolver.get('API_KEY')Secrets are cached in memory after config(), so get() is synchronous and cheap.
Forcing a refresh
Pass { refresh: true } to re-read the secret from its source before returning the value —
useful after a rotation. Because it hits the network, this overload is async (returns a Promise):
const token = await SecretResolver.get('API_KEY', { refresh: true })- For the
awsresolver it re-fetches only the secret that owns the key and rebuilds the in-memory cache, so subsequent plainget()calls also see the refreshed values. Ownership is resolved by namespace:get('db.HOST', { refresh: true })re-reads only thedbsecret, and a bare key refreshes the first (default) secret. If ownership can't be determined (legacy flat merge, or a prefix that matches no alias) it falls back to re-fetching every secret. - For
environmentsit reads the always-liveprocess.env(no cache to bust). - Plain
get(key)stays synchronous and served from cache — nothing changes for existing code.
AWS auto-detection — when running on ECS or Lambda (
AWS_EXECUTION_ENVor the three AWS credential env vars are present), the library ignorescurrentEnvand always uses theawsresolver.
Resolver types
environments — process.env / .env file
Reads directly from process.env. No caching — always reflects the current value of the variable.
resolverInDevelopment: { resolveFrom: RESOLVER_TYPES.environments }Required env vars: none beyond what your app already uses.
aws — AWS Secrets Manager
Fetches secrets from AWS and caches them in memory for the lifetime of the process.
Choosing the secret(s)
Secrets are configured through the AWS_SECRET_ID env var. In code you only pick the
resolver — no secret ids there:
# .env or system environment
AWS_REGION=us-east-1
AWS_SECRET_ID=arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prodresolverInProduction: { resolveFrom: RESOLVER_TYPES.aws } // reads AWS_SECRET_IDAWS_SECRET_ID accepts one or many ids and supports namespaces — everything below is set
through it.
Multiple secrets — flat merge
Pass a comma-separated list (whitespace trimmed, empty entries ignored). Secrets are fetched in parallel and merged left→right — the last entry wins on key conflict.
AWS_SECRET_ID='arn:aws:...:secret:myapp/db, arn:aws:...:secret:myapp/api-keys, myapp/shared-config'Merge order — if
myapp/dbandmyapp/api-keysboth defineHOST, the value frommyapp/api-keysis used because it appears last. To avoid this silent overwrite, use namespaces (below).
Namespaced secrets (avoid key collisions)
When two secrets define the same key, the flat merge silently overwrites it. To keep them
apart, prefix each id with alias=. Keys are then exposed as alias.key, and the first
entry is also exposed with bare keys (the default namespace).
AWS_SECRET_ID='db=arn:aws:secretsmanager:...:secret:myapp/db, api=arn:aws:secretsmanager:...:secret:myapp/api'// secretsmanager myapp/db → { HOST: 'db.internal', PORT: '5432' }
// secretsmanager myapp/api → { HOST: 'api.internal', KEY: 'xyz' }
SecretResolver.get('HOST') // 'db.internal' — bare → first secret (db)
SecretResolver.get('db.HOST') // 'db.internal'
SecretResolver.get('api.HOST') // 'api.internal'
SecretResolver.get('api.KEY') // 'xyz'
SecretResolver.get('KEY') // undefined — only reachable as api.KEYMixing is allowed: an entry without an alias only contributes bare keys, and only when it is the first one.
Reserved
defaultnamespace — bare keys are always mirrored underdefault., in every mode (single secret, flat merge, or namespaced). Soget('KEY')andget('default.KEY')return the same value, giving the default/first secret a stable explicit prefix alongside the old bare style.get('db.HOST', { refresh: true })refreshes thedbsecret;get('default.HOST', …)refreshes the default (first) one.
Local parity —
dotenvsupports dotted keys, so mirror the layout in your.envand the exact sameget()calls work locally:HOST=localhost # default (db) db.HOST=localhost api.HOST=localhost api.KEY=dev-key
Without any
alias=, behavior is unchanged: flat merge left→right, last wins.
Local development with AWS
Add these to your local .env:
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_SESSION_TOKEN=your-session-token
AWS_REGION=us-east-1
AWS_SECRET_ID=arn:aws:secretsmanager:... # single id, or a comma-separated list (optionally with alias=)file — JSON file
Reads a local JSON file once and caches it in memory.
resolverInDevelopment: {
resolveFrom: RESOLVER_TYPES.file,
optionsResolveFrom: { data: './secrets.json' },
}secrets.json must be a flat JSON object:
{
"DB_HOST": "localhost",
"API_KEY": "dev-key-123"
}data — in-memory object
Wraps a plain object. Useful for tests or when secrets are already loaded from another source.
resolverInTest: {
resolveFrom: RESOLVER_TYPES.data,
optionsResolveFrom: {
data: { DB_HOST: 'localhost', API_KEY: 'test-key' },
},
}Environment-aware configuration
SecretResolverByEnviromentOptions lets you declare a resolver per environment. Pass currentEnv to control which one is used:
import { CURRENT_ENVS } from '@basic-tools/secrets-resolver'
const options: SecretResolverByEnviromentOptions = {
resolverInDevelopment: { resolveFrom: RESOLVER_TYPES.environments },
resolverInTest: { resolveFrom: RESOLVER_TYPES.data, optionsResolveFrom: { data: mockSecrets } },
resolverInProduction: { resolveFrom: RESOLVER_TYPES.aws }, // secrets from AWS_SECRET_ID
currentEnv: CURRENT_ENVS.development, // omit to default to development
}For production, set the secret ids in the
AWS_SECRET_IDenv var, e.g.AWS_SECRET_ID='arn:...myapp/db, arn:...myapp/api-keys'.
If omitted, currentEnv defaults to development. AWS auto-detection overrides this regardless of the value.
Debugging
Set RESOLVER_SECRETS_DEBUG=true to enable verbose logging — AWS account ID, secret IDs being fetched, and resolved values.
RESOLVER_SECRETS_DEBUG=true