Files
skills/docs/api-stack/api/api-plugin-vault
2026-05-29 08:20:56 +01:00
..
2026-05-29 08:20:56 +01:00
2026-05-29 08:20:56 +01:00

@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 — Stable enum of the 9 error codes raised by @suaveplan/api-plugin-vault. Codes are namespaced under the `VAULT_PLUGIN.…
    • error-codes — Stable enum of the 9 error codes raised by @suaveplan/api-plugin-vault. Codes are namespaced under the `VAULT_PLUGIN.…
  • otel — OpenTelemetry instrument bundle for the vault plugin: four counters and one histogram covering the full CRUD surface. E…
    • instruments — OpenTelemetry instrument bundle for the vault plugin: four counters and one histogram covering the full CRUD surface. E…
  • plugin — createVaultPlugin(options) is the factory that returns a typed PluginDescriptor<VaultPluginConfig> ready to registe…
    • plugin — createVaultPlugin(options) is the factory that returns a typed PluginDescriptor<VaultPluginConfig> ready to registe…
    • unlock — Tiny helper module that owns the "where does the vault unlock secret come from?" question. The plugin's default `unlock…
  • routes — Builds the 5 OpenAPI route entries that createVaultPlugin mounts on the host's Hono app: list, create, read, update,…
    • build-routes — Builds the 5 OpenAPI route entries that createVaultPlugin mounts on the host's Hono app: list, create, read, update,…
  • schema — Owns the SurrealQL DDL the vault plugin applies during onInit — the first leg of the 3-way plug-and-play contract. `V…
    • schema — Owns the SurrealQL DDL the vault plugin applies during onInit — the first leg of the 3-way plug-and-play contract. `V…
  • options — Zod schema validating the input options to createVaultPlugin. Every field has a Zod-derived default (or optional())…
  • vault-item — Zod schemas describing the wire shape of every body, parameter, and response handled by the 5 vault routes. Every schem…
  • registry-augmentation — Side-effect module that augments @suaveplan/api-plugin's ServiceRegistry interface with the vaultService slot. Wi…
  • vault-service — Canonical VaultService implementation registered by createVaultPlugin in the host's ServiceRegistry under the `va…
  • types — Public TypeScript surface for @suaveplan/api-plugin-vault. Every interface here is consumed by sibling modules (plugi…
    • types — Public TypeScript surface for @suaveplan/api-plugin-vault. Every interface here is consumed by sibling modules (plugi…

Install

bun add @suaveplan/api-plugin-vault

Quick start

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