@simpleworkjs/bao-conf

OpenBao / Vault KV-v2 secrets overlay for @simpleworkjs/conf

npm version Tests License: MIT

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 fallbackinit() is fail-soft by design.
  • Fail-soft for boot config, fail-loud for writesinit/get resolve on error; set throws.
  • 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.

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make your changes and add tests
  4. Run tests: npm test
  5. Commit your changes
  6. Push to the branch
  7. Submit a pull request