Files
suaveplan/packages/api-stack/api/api-plugin-vault/src/routes/build-routes.md
2026-05-27 23:29:53 +01:00

4.1 KiB

routes/build-routes

(no submodules)

Purpose

Builds the 5 OpenAPI route entries that createVaultPlugin mounts on the host's Hono app: list, create, read, update, delete. Each entry is a PluginRouteEntry — a RouteConfig plus a handler — with full Zod inference through the RouteBuilder chain. The OpenAPI document and the runtime routing surface cannot drift because both are produced from the same builder output.

Features

  • Five routes — GET /vault/items, POST /vault/items, GET /vault/items/:id, PATCH /vault/items/:id, DELETE /vault/items/:id — each tagged vault for grouped doc display.
  • Per-handler upstream requireAuth() invocation — every route is gated; anonymous calls produce AUTH_UNAUTHENTICATED (401) via the auth plugin's middleware.
  • Per-request open/close lifecycle: every route opens the principal's vault via vaultService.openForPrincipal, performs the operation, and closes the handle in finally so the vault is locked again before the response returns.
  • Anti-enumeration policy baked in: any reference to an item id that does not exist OR is owned by another principal raises VAULT_PLUGIN_ITEM_NOT_FOUND (404) — NEVER 403. This prevents an attacker from probing the existence of item ids owned by other principals.
  • Size enforcement at the handler boundary, not at the Zod schema, so the error message can include the exact byte count and the configured maxItemSizeBytes.
  • 4 OTel counters + 1 histogram emitted per operation (success + failure outcomes).

Usage: Basic

import { buildVaultRoutes } from "@suaveplan/api-plugin-vault";

const entries = buildVaultRoutes(vaultService, config);
definePluginRoutes(entries, {
    app,
    registry: openAPIRegistry,
    pluginName: "vault",
});

Usage: Advanced

// Drop one of the 5 routes by filtering before registration — operators
// running a read-only mirror can elide the mutation routes.
const entries = buildVaultRoutes(vaultService, config).filter((e) =>
    e.route.method === "get"
);
definePluginRoutes(entries, { app, registry, pluginName: "vault" });

API Reference

buildVaultRoutes(vaultService, config): PluginRouteEntry[]

Returns 5 entries in registration order: list, create, get, update, delete. Every entry has:

  • route — a built RouteConfig carrying the full Zod schema chain.
  • handler — async function taking a Hono Context and returning Promise<Response>.

The handler wraps the actual operation in a requireAuth() gate; the auth plugin's Principal is read from c.get("user").

Implementation Notes

  • The RouteBuilder .params(VaultItemParamsSchema) registers the {id} URL parameter on the OpenAPI doc using the OpenAPI convention ({id}); Hono's runtime router consumes the same builder output, so the path matches both surfaces.
  • The handler explicitly normalises the wire envelope: every success response is { success: true, data } (list also includes meta: { total }). The shared error envelope is contributed by the StandardResponses block from @suaveplan/api-openapi — handlers never construct an error envelope themselves; they throw a VaultPluginError and let the host's error-handler middleware serialise it.
  • Buffer.byteLength(body, "utf8") is used for size enforcement so the byte count matches what an HTTP client would actually transmit (rather than the JavaScript string length which counts UTF-16 code units).
  • The defensive null-check on c.get("user") inside withOpenVault exists because requireAuth() runs in a separate middleware step and TypeScript cannot narrow across the await boundary. Production cannot reach the branch (requireAuth has already thrown), but the branch is reachable from unit tests that bypass the guard.
  • Coverage note: the requireAuthThen response === undefined branch exists for TypeScript narrowing; the unit tests assert it raises VAULT_PLUGIN_FORBIDDEN by injecting a stubbed requireAuth that returns without invoking next.