Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.1 KiB
@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.…
- error-codes — Stable enum of the 9 error codes raised by
- 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 typedPluginDescriptor<VaultPluginConfig>ready to registe… - routes — Builds the 5 OpenAPI route entries that
createVaultPluginmounts on the host's Hono app: list, create, read, update,…- build-routes — Builds the 5 OpenAPI route entries that
createVaultPluginmounts on the host's Hono app: list, create, read, update,…
- build-routes — Builds the 5 OpenAPI route entries that
- 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…
- schema — Owns the SurrealQL DDL the vault plugin applies during
- options — Zod schema validating the input options to
createVaultPlugin. Every field has a Zod-derived default (oroptional())… - 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'sServiceRegistryinterface with thevaultServiceslot. Wi… - vault-service — Canonical
VaultServiceimplementation registered bycreateVaultPluginin the host'sServiceRegistryunder 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…
- types — Public TypeScript surface for
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:
- Resolves
app,openAPIRegistry,migrationRunner, anddbfrom the hostServiceRegistry. - Applies
VAULT_OWNERSHIP_SCHEMA(thevault_item+vault_sharetables plus their indexes). - Constructs the
vaultServicebacked by the configuredstorage+serverPepper. - Registers
vaultServicein the typedServiceRegistry. - Mounts 5 OpenAPI routes via
definePluginRoutesso they appear in the unified/openapi.jsondoc with full Zod schemas — every route is gated byrequireAuth().
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
Authorizationheader. 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: falseto manage migrations externally and applyVAULT_OWNERSHIP_SCHEMAvia the operator's own runner.
Modules
error-codes/error-codes— 9 stableVAULT_PLUGIN_*codes.otel/instruments— 4 counters + 1 histogram.plugin/plugin—createVaultPluginfactory.plugin/unlock— header-based extractor.routes/build-routes— the 5 OpenAPI route entries.schemas/options— boundary schema.schemas/vault-item— wire schemas.schema/schema— SurrealQL DDL.services/vault-service—vaultServiceimpl wrapping@suaveplan/vault.services/registry-augmentation— typedServiceRegistryslot.types/types— public TS surface.