167 lines
9.1 KiB
Markdown
167 lines
9.1 KiB
Markdown
# @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`.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [error-codes](./src/error-codes/error-codes.md) — Stable enum of the 9 error codes raised by `@suaveplan/api-plugin-vault`. Codes are namespaced under the `VAULT_PLUGIN.…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Stable enum of the 9 error codes raised by `@suaveplan/api-plugin-vault`. Codes are namespaced under the `VAULT_PLUGIN.…
|
|
- [otel](./src/otel/instruments.md) — OpenTelemetry instrument bundle for the vault plugin: four counters and one histogram covering the full CRUD surface. E…
|
|
- [instruments](./src/otel/instruments.md) — OpenTelemetry instrument bundle for the vault plugin: four counters and one histogram covering the full CRUD surface. E…
|
|
- [plugin](./src/plugin/plugin.md) — `createVaultPlugin(options)` is the factory that returns a typed `PluginDescriptor<VaultPluginConfig>` ready to registe…
|
|
- [plugin](./src/plugin/plugin.md) — `createVaultPlugin(options)` is the factory that returns a typed `PluginDescriptor<VaultPluginConfig>` ready to registe…
|
|
- [unlock](./src/plugin/unlock.md) — Tiny helper module that owns the "where does the vault unlock secret come from?" question. The plugin's default `unlock…
|
|
- [routes](./src/routes/build-routes.md) — Builds the 5 OpenAPI route entries that `createVaultPlugin` mounts on the host's Hono app: list, create, read, update,…
|
|
- [build-routes](./src/routes/build-routes.md) — Builds the 5 OpenAPI route entries that `createVaultPlugin` mounts on the host's Hono app: list, create, read, update,…
|
|
- [schema](./src/schema/schema.md) — Owns the SurrealQL DDL the vault plugin applies during `onInit` — the first leg of the 3-way plug-and-play contract. `V…
|
|
- [schema](./src/schema/schema.md) — Owns the SurrealQL DDL the vault plugin applies during `onInit` — the first leg of the 3-way plug-and-play contract. `V…
|
|
- [options](./src/schemas/options.md) — Zod schema validating the input options to `createVaultPlugin`. Every field has a Zod-derived default (or `optional()`)…
|
|
- [vault-item](./src/schemas/vault-item.md) — Zod schemas describing the wire shape of every body, parameter, and response handled by the 5 vault routes. Every schem…
|
|
- [registry-augmentation](./src/services/registry-augmentation.md) — Side-effect module that augments `@suaveplan/api-plugin`'s `ServiceRegistry` interface with the `vaultService` slot. Wi…
|
|
- [vault-service](./src/services/vault-service.md) — Canonical `VaultService` implementation registered by `createVaultPlugin` in the host's `ServiceRegistry` under the `va…
|
|
- [types](./src/types/types.md) — Public TypeScript surface for `@suaveplan/api-plugin-vault`. Every interface here is consumed by sibling modules (plugi…
|
|
- [types](./src/types/types.md) — Public TypeScript surface for `@suaveplan/api-plugin-vault`. Every interface here is consumed by sibling modules (plugi…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Install
|
|
|
|
```bash
|
|
bun add @suaveplan/api-plugin-vault
|
|
```
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
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
|
|
|
|
- [`error-codes/error-codes`](./src/error-codes/error-codes.md) — 9
|
|
stable `VAULT_PLUGIN_*` codes.
|
|
- [`otel/instruments`](./src/otel/instruments.md) — 4 counters + 1
|
|
histogram.
|
|
- [`plugin/plugin`](./src/plugin/plugin.md) — `createVaultPlugin`
|
|
factory.
|
|
- [`plugin/unlock`](./src/plugin/unlock.md) — header-based extractor.
|
|
- [`routes/build-routes`](./src/routes/build-routes.md) — the 5
|
|
OpenAPI route entries.
|
|
- [`schemas/options`](./src/schemas/options.md) — boundary schema.
|
|
- [`schemas/vault-item`](./src/schemas/vault-item.md) — wire schemas.
|
|
- [`schema/schema`](./src/schema/schema.md) — SurrealQL DDL.
|
|
- [`services/vault-service`](./src/services/vault-service.md) —
|
|
`vaultService` impl wrapping `@suaveplan/vault`.
|
|
- [`services/registry-augmentation`](./src/services/registry-augmentation.md)
|
|
— typed `ServiceRegistry` slot.
|
|
- [`types/types`](./src/types/types.md) — public TS surface.
|
|
|