# Add @suaveplan/api-plugin-vault — per-user vault routes wrapping @suaveplan/vault (3-way plug-and-play) **Package**: `@suaveplan/api-plugin-vault` (NEW, server-tier) **Status**: PROPOSED **Type**: new-package **Position**: depends on `add-api-plugin-auth` (+ optionally `add-api-plugin-user`) - **Spec delta**: ./specs/api-plugin-vault/spec.md ## Why Genesis already ships `@suaveplan/vault` — the universal encrypted-vault library (open/unlock/lock FSM, items + attachments, container backed by `DriveLike`). What's missing is the HTTP surface: operators building an app with `@suaveplan/api` want `/vault/items` routes that look up the authenticated principal, scope the vault to that user, and apply Better-Auth's session for authorisation. This plugin ships exactly that — three CRUD route groups (`items`, `attachments`, `shares` — items only for v1) plus a `vaultService` for downstream plugins. Third instance of the **3-way plug-and-play contract**: SurrealQL schema for vault ownership/sharing metadata, Zod schemas at every request boundary, OpenAPI routes registered through `definePluginRoutes`. The encryption / decryption stays inside `@suaveplan/vault`; this plugin is the HTTP wrapper + per-user scoping layer only. ## What changes Net-new package at `0.1.0`. ADDED: ### Plugin factory + descriptor - `createVaultPlugin(options): PluginDescriptor` — `definePlugin` consumer with `dependencies: ["auth"]` (user plugin optional). `options` Zod-validated. - `VaultPluginOptions` (Zod-validated): `{ routePrefix?: string (default "/vault"); storage: DriveLike; maxItemSizeBytes?: number (default 10 * 1024 * 1024); applySchemaOnInit?: boolean (default true); logger?: LoggerLike }`. `storage` is the `DriveLike` instance the vault library writes encrypted blobs to (operator supplies — could be `@suaveplan/storage-s3`-backed `@suaveplan/drive`, local fs, etc.). ### Routes (5) All built with `RouteBuilder`; all gated by `requireAuth()` upstream. Every route scopes the vault to `c.get("user")!.id`: - `GET /vault/items` — list items owned by the principal. Response: `VaultItemListResponseSchema` (paginated). - `POST /vault/items` — create a new item. Body: `CreateVaultItemBodySchema`. Response: `VaultItemResponseSchema`. - `GET /vault/items/:id` — fetch a single item. Response: `VaultItemResponseSchema`. 404 + `VAULT_ITEM_NOT_FOUND` when the id doesn't exist or isn't owned by the principal. - `PATCH /vault/items/:id` — update fields. Body: `UpdateVaultItemBodySchema` (strict mode). Response: `VaultItemResponseSchema`. - `DELETE /vault/items/:id` — remove. Response: `{ deleted: true }`. Owner-only or `VAULT_FORBIDDEN`. ### vaultService - `vaultService` registered via `ctx.registerService("vaultService", ...)`. Surface: `{ openForPrincipal(principal: Principal, password: string): Promise; listItemsForOwner(ownerId: string): Promise; close(handle: VaultHandle): Promise }`. - ServiceRegistry augmentation: `declare module "@suaveplan/api-plugin/service-registry" { interface ServiceRegistry { vaultService: VaultService } }`. - The principal's unlock password is NEVER stored — passed per request via a header (`X-Vault-Unlock`) or extracted from a separate per-user secret store. Default extractor: `headerExtractor("X-Vault-Unlock")` (operators override via `options.unlockExtractor`). ### .surql schema (plug-and-play point 1) - `./schema` exports `VAULT_OWNERSHIP_SCHEMA` (frozen string) — DDL for two tables: - `vault_item`: `id`, `ownerId: record` (record-link), `containerKey: string`, `title: string`, `kind: string`, `metadata: option`, `createdAt`, `updatedAt`. Indexed on `(ownerId, kind)` and `(ownerId, createdAt DESC)`. - `vault_share`: forward-declared for future sharing-route follow-up (`ownerId: record`, `granteeId: record`, `itemId: record`, `permissions: array`, `grantedAt`, `revokedAt: option`). - DDL uses `DEFINE TABLE OVERWRITE` for idempotency. Genesis-original (no upstream port; the vault library doesn't define ownership metadata — that's HTTP-layer concern). - `applyVaultSchema(adapter): Promise` convenience helper. `onInit` applies via `migrationRunner` (preferred) or `db` fallback. ### Zod schemas (plug-and-play point 2) — exposed at `./schemas/*` - `./schemas/vault-item` — `VaultItemSchema`, `VaultItemSummarySchema`, `VaultItemResponseSchema`, `VaultItemListResponseSchema`, `CreateVaultItemBodySchema`, `UpdateVaultItemBodySchema` (strict mode). - `./schemas/options` — `VaultPluginOptionsSchema`. ### Errors + observability - `VaultPluginError extends SuaveplanError` via `defineCodes("VAULT_PLUGIN", [...])`. Codes: `VAULT_PLUGIN_INVALID_INPUT`, `VAULT_PLUGIN_ITEM_NOT_FOUND`, `VAULT_PLUGIN_FORBIDDEN`, `VAULT_PLUGIN_UNLOCK_FAILED`, `VAULT_PLUGIN_ITEM_TOO_LARGE`, `VAULT_PLUGIN_SERVICE_NOT_AVAILABLE`, `VAULT_PLUGIN_SCHEMA_APPLY_FAILED`, `VAULT_PLUGIN_ROUTE_PREFIX_COLLISION`, `VAULT_PLUGIN_UNLOCK_HEADER_MISSING`. - OTel: counter `vault.items.created_total{outcome}`, counter `vault.items.read_total{outcome}`, counter `vault.items.updated_total{outcome}`, counter `vault.items.deleted_total{outcome}`, histogram `vault.item.size_bytes` (on create + update — distribution of item sizes). ### Subpath exports `.`, `./plugin/plugin`, `./routes/routes`, `./services/vault-service`, `./schemas/vault-item`, `./schemas/options`, `./schema/schema`, `./error-codes/error-codes`, `./otel/instruments`, `./types/types`. ## Out of scope - **Sharing / collaborator endpoints.** The `vault_share` table is forward-declared in the schema but no routes ship — follow-up `api-plugin-vault-sharing`. - **Attachments routes** (`/vault/items/:id/attachments`). Vault library supports attachments; HTTP routes are a follow-up `api-plugin-vault-attachments`. - **Vault discovery / cross-user search.** Per-user only in v1. - **Vault key escrow / recovery flows.** Operator concern; vault library has primitives, this plugin doesn't surface them. - **Multi-vault per user.** One vault per user in v1; multi-vault is a follow-up. - **WebDAV / SFTP gateways.** HTTP routes only. ## Source mapping This package is **genesis-original**. Design intent draws from: - The 3-way plug-and-play contract established by `add-api-plugin-auth`. - `@suaveplan/vault` shipped surface (`VaultManager`, item/attachment APIs). - `@suaveplan/api-plugin-auth/middleware/require-auth` + `Principal`. - The legacy `enterprise-api-plugin-vault.md` proposal's route-surface intent (verbatim: `/vault/items` CRUD). | Requirement | Source | |---|---| | `definePlugin` consumer w/ `dependencies: ["auth"]` | `@suaveplan/api-plugin` | | Per-user scoping via `c.get("user").id` | `@suaveplan/api-plugin-auth` (sibling) | | Vault encryption / FSM | `@suaveplan/vault` (shipped) | | `RouteBuilder` end-to-end Zod | `@suaveplan/api-openapi` (shipped) | | 3-way plug-and-play (`.surql` + Zod + OpenAPI) | genesis architectural principle | ## Impact ### Affected dependents None today — net-new. Future `api-plugin-vault-sharing` / `api-plugin-vault-attachments` follow-ups will depend on this. ### Catalog additions None — every dep already in catalogs.