Initial Commit
This commit is contained in:
97
openspec/archive/2026-05-18-add-api-plugin-vault/proposal.md
Normal file
97
openspec/archive/2026-05-18-add-api-plugin-vault/proposal.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 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<VaultPluginConfig>` — `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<VaultHandle>; listItemsForOwner(ownerId: string): Promise<VaultItemSummary[]>; close(handle: VaultHandle): Promise<void> }`.
|
||||
- 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<user>` (record-link), `containerKey: string`, `title: string`, `kind: string`, `metadata: option<object>`, `createdAt`, `updatedAt`. Indexed on `(ownerId, kind)` and `(ownerId, createdAt DESC)`.
|
||||
- `vault_share`: forward-declared for future sharing-route follow-up (`ownerId: record<user>`, `granteeId: record<user>`, `itemId: record<vault_item>`, `permissions: array<string>`, `grantedAt`, `revokedAt: option<datetime>`).
|
||||
- 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<void>` 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<VaultPluginErrorCode>` 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.
|
||||
Reference in New Issue
Block a user