Files
skills/docs/api-stack/api/api-plugin-vault/src/plugin/unlock.md
2026-05-29 08:20:56 +01:00

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 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

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 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.