first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# 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.