# 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 a `VaultUnlockExtractor` that 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: reads `X-Vault-Unlock`. - `extractUnlock(c, extractor)` — invokes the extractor and converts `null` into a typed `VaultPluginError` so 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 ```ts import { defaultUnlockExtractor, extractUnlock, headerExtractor, } from "@suaveplan/api-plugin-vault"; const unlockSecret = extractUnlock(c, defaultUnlockExtractor); const handle = await vaultService.openForPrincipal(principal, unlockSecret); ``` ## Usage: Advanced ```ts // 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 `defaultUnlockExtractor` and the example custom extractors above only read; they do not emit. - `headerExtractor` validates the header value is a non-empty trimmed string. Whitespace-only values count as absent because operators occasionally inject `X-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.