first commit
This commit is contained in:
93
docs/api-stack/api/api-plugin-vault/src/plugin/plugin.md
Normal file
93
docs/api-stack/api/api-plugin-vault/src/plugin/plugin.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# plugin/plugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`createVaultPlugin(options)` is the factory that returns a typed
|
||||
`PluginDescriptor<VaultPluginConfig>` ready to register with the host's
|
||||
`PluginRegistry`. Third instance of the 3-way plug-and-play contract:
|
||||
the plugin owns its `.surql` DDL, its Zod schemas, and its OpenAPI
|
||||
routes, and applies all three during `onInit`. Depends on
|
||||
`api-plugin-auth` so every route's principal scoping has a typed
|
||||
upstream supplier.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated options via `VaultPluginOptionsSchema` — invalid input
|
||||
raises `VaultPluginError(VAULT_PLUGIN_INVALID_INPUT)` carrying the
|
||||
`ZodError`.
|
||||
- Declares `dependencies: ["auth"]` so the registry refuses to load
|
||||
the plugin without `api-plugin-auth` registered upstream.
|
||||
- 6-step `onInit` lifecycle: resolve services → apply schema →
|
||||
construct `vaultService` → register `vaultService` → register routes
|
||||
→ (no global middleware — `requireAuth()` is per-route).
|
||||
- `routePrefix`-aware route mounting — paths are rebuilt at the prefix
|
||||
the operator chose, both in the served Hono app and in the
|
||||
`OpenAPIRegistry` snapshot. Default `/vault`.
|
||||
- Collision detection — pre-existing routes under the configured
|
||||
prefix raise `VAULT_PLUGIN_ROUTE_PREFIX_COLLISION`.
|
||||
- Graceful degradation — when `openAPIRegistry` is absent, routes
|
||||
mount on the Hono app directly (no doc); when neither
|
||||
`migrationRunner` nor `db` is available with `applySchemaOnInit: true`,
|
||||
init fails fast with `VAULT_PLUGIN_SERVICE_NOT_AVAILABLE`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { defineApp } from "@suaveplan/api";
|
||||
import { createAuthPlugin } from "@suaveplan/api-plugin-auth";
|
||||
import { createVaultPlugin } from "@suaveplan/api-plugin-vault";
|
||||
|
||||
const app = await defineApp()
|
||||
.name("my-api")
|
||||
.use(createAuthPlugin({ betterAuth: { /* ... */ } }))
|
||||
.use(createVaultPlugin({ storage: driveAdapter }))
|
||||
.start();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const plugin = createVaultPlugin({
|
||||
storage: driveAdapter,
|
||||
routePrefix: "/v1/vault",
|
||||
maxItemSizeBytes: 4 * 1024 * 1024,
|
||||
serverPepper: process.env["VAULT_SERVER_PEPPER"]!,
|
||||
unlockExtractor: (c) =>
|
||||
c.req.header("X-Custom-Vault-Unlock") ?? null,
|
||||
applySchemaOnInit: false, // operator runs migrations externally
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createVaultPlugin(options: VaultPluginOptions): PluginDescriptor<VaultPluginConfig>`
|
||||
|
||||
Returns a `definePlugin` descriptor. `name: "vault"`, `version: "0.1.0"`,
|
||||
`dependencies: ["auth"]`.
|
||||
|
||||
The `defaults` field carries every resolved option after Zod
|
||||
validation — the registry passes that to `onInit` via `ctx.getOwnConfig()`.
|
||||
|
||||
`onInit(ctx)` resolves services, applies the schema, constructs the
|
||||
service, registers it, and registers the 5 routes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plugin DOES NOT install global middleware. `requireAuth()` from
|
||||
`api-plugin-auth` is applied per-route inside `buildVaultRoutes` so
|
||||
the public `/openapi.json` doc only carries vault routes for the
|
||||
authenticated scope and operators can leave the vault prefix
|
||||
out of public-facing API documentation tooling.
|
||||
- The `rewritePath` helper preserves `{id}` segments verbatim — when
|
||||
the operator chooses `/v1/vault`, the route path
|
||||
`/vault/items/{id}` becomes `/v1/vault/items/{id}`.
|
||||
- The collision check inspects every previously registered route whose
|
||||
path starts with the configured prefix — operators wiring multiple
|
||||
vault plugin instances under different prefixes are unaffected.
|
||||
- The plugin does not provide an `onClose` hook because the
|
||||
`vaultService` is stateless across requests — every handle is
|
||||
opened + closed within a single request lifecycle.
|
||||
98
docs/api-stack/api/api-plugin-vault/src/plugin/unlock.md
Normal file
98
docs/api-stack/api/api-plugin-vault/src/plugin/unlock.md
Normal 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.
|
||||
Reference in New Issue
Block a user