3.5 KiB
plugin/unlock
Purpose
Tiny helper module that owns the "where does the vault unlock secret
come from?" question. The plugin's default unlockExtractor reads the
X-Vault-Unlock request header, but operators with JWT-bound secrets,
cookie-bound secrets, or password-derived flows override the extractor
when constructing the plugin. extractUnlock(c, extractor) is the
canonical call site invoked by every mutating route — it raises a typed
VaultPluginError(VAULT_PLUGIN_UNLOCK_HEADER_MISSING) (400) when the
extractor returns null.
Features
headerExtractor(headerName)— factory returning aVaultUnlockExtractorthat reads the named header. Header lookup is case-insensitive per RFC 7230.defaultUnlockExtractor— the canonical default used when the plugin's options do not specify one: readsX-Vault-Unlock.extractUnlock(c, extractor)— invokes the extractor and convertsnullinto a typedVaultPluginErrorso the host error handler surfaces a clean 400 response.- Extracted secrets are returned to the caller; the module DOES NOT log or persist them. The §13a security audit explicitly forbids logging the unlock secret.
Usage: Basic
import {
defaultUnlockExtractor,
extractUnlock,
headerExtractor,
} from "@suaveplan/api-plugin-vault";
const unlockSecret = extractUnlock(c, defaultUnlockExtractor);
const handle = await vaultService.openForPrincipal(principal, unlockSecret);
Usage: Advanced
// JWT-bound unlock: extract a hashed password from a custom claim.
const jwtExtractor: VaultUnlockExtractor = (c) => {
const auth = c.req.header("Authorization");
if (!auth?.startsWith("Bearer ")) return null;
const token = auth.slice(7);
try {
const payload = JSON.parse(
Buffer.from(token.split(".")[1] ?? "", "base64").toString("utf8")
) as { vaultSecret?: string };
return payload.vaultSecret ?? null;
} catch {
return null;
}
};
const plugin = createVaultPlugin({
storage,
unlockExtractor: jwtExtractor,
});
API Reference
headerExtractor(headerName: string): VaultUnlockExtractor
Returns an extractor that reads headerName from the request. Trims
the value; returns null if the header is absent or empty after
trimming.
defaultUnlockExtractor: VaultUnlockExtractor
Pre-built extractor reading X-Vault-Unlock.
extractUnlock(c: Context, extractor: VaultUnlockExtractor): string
Calls the extractor; raises VaultPluginError(VAULT_PLUGIN_UNLOCK_HEADER_MISSING)
(400) when it returns null. The returned string is the unlock secret
and MUST be passed directly to vaultService.openForPrincipal.
Implementation Notes
- The unlock secret is NEVER logged. Both
defaultUnlockExtractorand the example custom extractors above only read; they do not emit. headerExtractorvalidates the header value is a non-empty trimmed string. Whitespace-only values count as absent because operators occasionally injectX-Vault-Unlock:placeholders during testing — trimming catches those.- The header name is forwarded verbatim to Hono's
c.req.header(headerName); Hono performs case-insensitive matching per the HTTP spec, so callers can supply any casing. - Returning the secret as a plain string (rather than wrapping it in a
SecureBuffer) is a deliberate trade-off: the unlock secret has the same lifetime as the request handler, and adding a heap-allocated wrapper imposes per-request allocator pressure without preventing a memory dump from disclosing the value.