Files
suaveplan/openspec/archive/2026-05-18-add-api-plugin-vault/proposal.md
2026-05-27 23:29:53 +01:00

7.1 KiB

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.