Features
Async Overlay
Folds an async OpenBao KV-v2 fetch into @simpleworkjs/conf's synchronous, require-time config object.
Deep Merge
Deep-merges secret/data/<app>/conf over the live conf in place — overlay only the secrets, keep the rest.
Live Mutation
Call-time readers (conf.ldap.bindPassword) see the overlay automatically; no reload, no re-export.
Fail-Soft Boot
If OpenBao is unreachable or the path is absent, boot continues with the file-loaded config — never crashes on a missing overlay.
Scoped Tokens
No root-token fallback. VAULT_TOKEN is required for explicit reads/writes; the boot-time init overlay is fail-soft when it is absent, so deployments without an OpenBao sidecar still boot.
Low-Level Broker Hook
request() exposes the raw OpenBao API for server-side token/policy brokers (per-user & per-app scoped tokens).
Installation
npm install --save @simpleworkjs/bao-conf @simpleworkjs/conf
Requires Node.js >= 18 (uses the global fetch).
Quick Start
After @simpleworkjs/conf has loaded, overlay secrets from OpenBao:
const conf = require('@simpleworkjs/conf');
const baoConf = require('@simpleworkjs/bao-conf');
// Fetches secret/data/sso-manager/conf and deep-merges it into `conf`.
await baoConf.init({ path: 'sso-manager', conf });
console.log(conf.ldap.bindPassword); // now the OpenBao value
console.log(conf.oidc.clientSecret); // now the OpenBao value
init() is fail-soft: if OpenBao is unreachable or the path is absent, it logs a warning, leaves conf untouched, and resolves. Keep your file-loaded config as a safe fallback.
The Boot-Order Constraint
Some code captures a secret at require() time rather than reading it at call time. The canonical example is an OIDC client built during require('../models'):
// models/index.js — runs at require time
const oidcClient = createOidcClient({ clientSecret: conf.oidc.clientSecret });
init() mutates conf after it returns, so a value already captured into a closure will not see the overlay. Ensure init() resolves before the capturing require() runs.
Pattern A — defer require('../app') (transitive models)
const conf = require('@simpleworkjs/conf');
const baoConf = require('@simpleworkjs/bao-conf');
baoConf.init({ path: 'proxy', conf }).then(() => {
const app = require('../app'); // models + createOidcClient now see merged conf
const server = http.createServer(app);
server.listen(port);
}).catch(err => { console.error('boot failed:', err); process.exit(1); });
Pattern B — gate an explicit require('../models')
const conf = require('@simpleworkjs/conf');
require('@simpleworkjs/bao-conf').init({ path: 'jump-host', conf }).then(() => {
require('../models'); // createOidcClient sees merged conf.oidc
const app = require('../app');
server.listen(webPort);
sshServer.start();
});
Values read at call time (e.g. conf.ldap.bindPassword inside a lookup) need no special handling — they see the overlay whenever it has resolved.
API Reference
init({ path, conf, addr?, token? }) → Promise<conf>
Fetch secret/data/<path>/conf and deep-merge it over conf in place. Fail-soft on error/404, and fail-soft on a missing VAULT_TOKEN (standalone Docker, bare metal, CI with no OpenBao sidecar): warns and leaves conf untouched so boot continues. Throws only if path/conf are omitted. The explicit get/set/request helpers still throw on a missing token.
get(path, opts?) → Promise<object|null>
Read a KV-v2 secret at secret/data/<path>. Returns the inner data object, or null if absent / on error.
set(path, data, opts?) → Promise<object>
Write a KV-v2 secret at secret/data/<path> (wrapped as { data }). Throws on non-2xx. Used by bootstraps that write generated creds into OpenBao.
request(method, vaultPath, body?, opts?) → Promise<Response>
Low-level OpenBao API request below /v1/. Returns the raw fetch Response. Used by brokers that mint scoped tokens or write policies.
configure({ addr?, token? }) → { addr, token }
Resolve and cache the connection config from options or env. Called implicitly; exported for explicit setup. Throws if no token is available.
Environment Variables
| Variable | Description | Default |
|---|---|---|
VAULT_ADDR |
OpenBao API URL | http://openbao:8200 |
VAULT_TOKEN |
OpenBao token (scoped — no root fallback) | required |
Examples
Bootstrap writing generated creds into OpenBao
const baoConf = require('@simpleworkjs/bao-conf');
// BOOTSTRAP_VAULT_TOKEN has write policy on secret/proxy/conf etc.
await baoConf.set('proxy/conf', { oauth: { clientId, clientSecret } });
A token/policy broker (server-side, scoped tokens)
const baoConf = require('@simpleworkjs/bao-conf');
await baoConf.request('PUT', `sys/policies/acl/user-${uid}`, {
policy: `path "secret/users/${uid}/*" { capabilities = ["create","read","update","delete","list"] }`
});
const res = await baoConf.request('POST', 'auth/token/create/sso-broker', { policy: `user-${uid}` });
const { auth } = await res.json();
// auth.client_token is the per-user token — inject it as X-Vault-Token on proxied requests.
Best Practices
- Mint scoped per-app tokens in setup/orchestration and pass them via
VAULT_TOKEN. Never propagate the root token to application containers. - Call
init()before any require-time capture of an overlaid secret. - Keep file-loaded config as a safe fallback —
init()is fail-soft by design. - Fail-soft for boot config, fail-loud for writes —
init/getresolve on error;setthrows. - Deep-merge, don't replace — partial overlays (just the secrets) work without re-stating the whole config in OpenBao.
Contributing
Contributions are welcome! Please see our GitHub repository for guidelines.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes and add tests
- Run tests:
npm test - Commit your changes
- Push to the branch
- Submit a pull request