# @suaveplan/api-plugin-vault - Per-user vault HTTP plugin for @suaveplan/api. Wraps @suaveplan/vault as a @suaveplan/api-plugin definePlugin descriptor, applies a SurrealQL ownership schema (vault_item + vault_share) at boot via @suaveplan/api-migrations, mounts 5 OpenAPI routes through @suaveplan/api-openapi (CRUD over /vault/items), registers a typed vaultService in the host ServiceRegistry, and scopes every operation to c.get('user').id supplied by @suaveplan/api-plugin-auth. Third instance of the 3-way plug-and-play contract (.surql DDL + Zod boundary schemas + OpenAPI route registration). Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin owns its `.surql` DDL, its Zod boundary schemas, and its OpenAPI route definitions — all wired into the host's `PluginRegistry`, `MigrationRunner`, and `OpenAPIRegistry` from a single `definePlugin` descriptor. Wraps `@suaveplan/vault` for the encrypted container and scopes every operation to `c.get("user").id` supplied by `@suaveplan/api-plugin-auth`. ## Documentation tree - [error-codes](./src/error-codes/error-codes.md) — Stable enum of the 9 error codes raised by `@suaveplan/api-plugin-vault`. Codes are namespaced under the `VAULT_PLUGIN.… - [error-codes](./src/error-codes/error-codes.md) — Stable enum of the 9 error codes raised by `@suaveplan/api-plugin-vault`. Codes are namespaced under the `VAULT_PLUGIN.… - [otel](./src/otel/instruments.md) — OpenTelemetry instrument bundle for the vault plugin: four counters and one histogram covering the full CRUD surface. E… - [instruments](./src/otel/instruments.md) — OpenTelemetry instrument bundle for the vault plugin: four counters and one histogram covering the full CRUD surface. E… - [plugin](./src/plugin/plugin.md) — `createVaultPlugin(options)` is the factory that returns a typed `PluginDescriptor` ready to registe… - [plugin](./src/plugin/plugin.md) — `createVaultPlugin(options)` is the factory that returns a typed `PluginDescriptor` ready to registe… - [unlock](./src/plugin/unlock.md) — Tiny helper module that owns the "where does the vault unlock secret come from?" question. The plugin's default `unlock… - [routes](./src/routes/build-routes.md) — Builds the 5 OpenAPI route entries that `createVaultPlugin` mounts on the host's Hono app: list, create, read, update,… - [build-routes](./src/routes/build-routes.md) — Builds the 5 OpenAPI route entries that `createVaultPlugin` mounts on the host's Hono app: list, create, read, update,… - [schema](./src/schema/schema.md) — Owns the SurrealQL DDL the vault plugin applies during `onInit` — the first leg of the 3-way plug-and-play contract. `V… - [schema](./src/schema/schema.md) — Owns the SurrealQL DDL the vault plugin applies during `onInit` — the first leg of the 3-way plug-and-play contract. `V… - [options](./src/schemas/options.md) — Zod schema validating the input options to `createVaultPlugin`. Every field has a Zod-derived default (or `optional()`)… - [vault-item](./src/schemas/vault-item.md) — Zod schemas describing the wire shape of every body, parameter, and response handled by the 5 vault routes. Every schem… - [registry-augmentation](./src/services/registry-augmentation.md) — Side-effect module that augments `@suaveplan/api-plugin`'s `ServiceRegistry` interface with the `vaultService` slot. Wi… - [vault-service](./src/services/vault-service.md) — Canonical `VaultService` implementation registered by `createVaultPlugin` in the host's `ServiceRegistry` under the `va… - [types](./src/types/types.md) — Public TypeScript surface for `@suaveplan/api-plugin-vault`. Every interface here is consumed by sibling modules (plugi… - [types](./src/types/types.md) — Public TypeScript surface for `@suaveplan/api-plugin-vault`. Every interface here is consumed by sibling modules (plugi… ## Install ```bash bun add @suaveplan/api-plugin-vault ``` ## Quick start ```ts import { defineApp } from "@suaveplan/api"; import { createAuthPlugin } from "@suaveplan/api-plugin-auth"; import { createVaultPlugin } from "@suaveplan/api-plugin-vault"; const driveAdapter = /* @suaveplan/storage-s3-backed @suaveplan/drive, etc */; const app = await defineApp() .name("my-api") .use( createAuthPlugin({ betterAuth: { /* ... Better-Auth options ... */ }, }) ) .use( createVaultPlugin({ storage: driveAdapter, serverPepper: process.env["VAULT_SERVER_PEPPER"]!, }) ) .start(); ``` `createVaultPlugin` returns a typed `PluginDescriptor` declaring `dependencies: ["auth"]`. The host's `PluginRegistry` calls `onInit(ctx)` which: 1. Resolves `app`, `openAPIRegistry`, `migrationRunner`, and `db` from the host `ServiceRegistry`. 2. Applies `VAULT_OWNERSHIP_SCHEMA` (the `vault_item` + `vault_share` tables plus their indexes). 3. Constructs the `vaultService` backed by the configured `storage` + `serverPepper`. 4. Registers `vaultService` in the typed `ServiceRegistry`. 5. Mounts 5 OpenAPI routes via `definePluginRoutes` so they appear in the unified `/openapi.json` doc with full Zod schemas — every route is gated by `requireAuth()`. ## The 3-way plug-and-play contract | Surface | Implementation | |---|---| | `.surql` DDL | `VAULT_OWNERSHIP_SCHEMA` defines `vault_item` and `vault_share` plus three indexes; applied via `MigrationRunner` (preferred) or direct `db.execute` fallback | | Zod schemas | `CreateVaultItemBodySchema`, `UpdateVaultItemBodySchema` (strict), `VaultItemResponseSchema`, `VaultItemListResponseSchema`, etc. | | OpenAPI routes | `buildVaultRoutes(vaultService, config)` returns 5 `RouteBuilder` entries; `definePluginRoutes` writes both Hono app AND `OpenAPIRegistry` | ## Routes | Method | Path | Description | |---|---|---| | `GET` | `/vault/items` | List items owned by the principal | | `POST` | `/vault/items` | Create a new item | | `GET` | `/vault/items/{id}` | Fetch a single item | | `PATCH` | `/vault/items/{id}` | Update fields (strict-mode body) | | `DELETE` | `/vault/items/{id}` | Remove an item | Every route requires `requireAuth()` upstream. Foreign-item references return `404 VAULT_PLUGIN_ITEM_NOT_FOUND` (NEVER 403) — see the "Cross-user isolation" section. ## X-Vault-Unlock header semantics The plugin's default `unlockExtractor` reads the per-request unlock secret from the `X-Vault-Unlock` header. The secret is forwarded to `@suaveplan/vault`'s 2SKD pipeline along with the principal's id (used as the Bitwarden-style "secret key") and a HKDF-derived per-principal salt. The plugin NEVER logs the secret and NEVER persists it. Alternative extractors (operators wire one via the `unlockExtractor` option): - **JWT-bound** — extract a claim from a JWT in the `Authorization` header. Useful when the client already holds a session-bound JWT with vault-key claims. - **Cookie-bound** — extract from a session cookie set during sign-in. - **Derived from password** — use the principal's authentication password (already submitted at sign-in) as the unlock secret; the caller does not need to send a second header at all. This requires the auth plugin to expose the post-auth password to the request context. ## Cross-user isolation (security note) Items owned by another principal return `404 VAULT_PLUGIN_ITEM_NOT_FOUND` — never `403`. Surfacing `403` would let an attacker enumerate item ids that exist for other principals (a known weakness in many CRUD plugins). The plugin's behaviour is deliberate; never override this in a wrapper without auditing the consequences. ## Schema application `VAULT_OWNERSHIP_SCHEMA` defines two tables. The `vault_item` table is the active surface; `vault_share` is forward-declared so the future `api-plugin-vault-sharing` follow-up can register routes without requiring a database migration. Operators can: - accept the default (`applySchemaOnInit: true`) — schema applies on every cold start, idempotently; - set `applySchemaOnInit: false` to manage migrations externally and apply `VAULT_OWNERSHIP_SCHEMA` via the operator's own runner. ## Modules - [`error-codes/error-codes`](./src/error-codes/error-codes.md) — 9 stable `VAULT_PLUGIN_*` codes. - [`otel/instruments`](./src/otel/instruments.md) — 4 counters + 1 histogram. - [`plugin/plugin`](./src/plugin/plugin.md) — `createVaultPlugin` factory. - [`plugin/unlock`](./src/plugin/unlock.md) — header-based extractor. - [`routes/build-routes`](./src/routes/build-routes.md) — the 5 OpenAPI route entries. - [`schemas/options`](./src/schemas/options.md) — boundary schema. - [`schemas/vault-item`](./src/schemas/vault-item.md) — wire schemas. - [`schema/schema`](./src/schema/schema.md) — SurrealQL DDL. - [`services/vault-service`](./src/services/vault-service.md) — `vaultService` impl wrapping `@suaveplan/vault`. - [`services/registry-augmentation`](./src/services/registry-augmentation.md) — typed `ServiceRegistry` slot. - [`types/types`](./src/types/types.md) — public TS surface.