PermDock
Adapters

oRPC

permdock/orpc adds a request-scoped PermDock to oRPC context, a protect middleware fed by procedure input, and an oo.spec hook that emits OpenAPI 3.2 security for the generated document.

Status: planned Phase: 2

Purpose

oRPC is contract-first and generates OpenAPI natively, which makes it the RPC framework where the OpenAPI 3.2 story is most direct. permdock/orpc mirrors the tRPC adapter: ctx.permdock on every procedure, protect(permission, load) as a middleware after input validation, ORPCError denials carrying Problem Details, and an oo.spec(middleware, ...) hook so the security requirement is attached to the operation wherever the middleware is used.

API

// server/permdock.ts
import { os } from '@orpc/server'
import { oo } from '@orpc/openapi'
import { createPermDock } from 'permdock/orpc'
import { policy } from './policy'
import { permissions } from './permissions'

export const { permdock, protect, openapi } = createPermDock(policy, { subject: ({ context }) => context.user ?? null })

export const base = os.$context<Context>().use(permdock())          // context.permdock is the request-scoped PermDock

// server/routers/posts.ts
export const remove = base
  .route({ method: 'DELETE', path: '/posts/{id}' })
  .input(z.object({ id: z.string() }))
  .use(openapi.protect(permissions.post.delete, ({ input }) => loadPost(input.id)))   // protect + oo.spec in one
  .handler(async ({ context }) => deletePost(context.permdockData))

export const list = base
  .route({ method: 'GET', path: '/posts' })
  .handler(async ({ context }) => context.permdock.filter(permissions.post.read, await listPosts()))
ExportRole
permdock()Middleware. Resolves the subject once per request (memoised on the request context) and adds permdock to the context with oRPC's context inference.
protect(permission, load?)Middleware placed after .input(...). Loads and boundary-validates the resource, decides, continues with context.permdockData or throws ORPCError with code FORBIDDEN.
openapi.protect(permission, load?)protect wrapped with oo.spec(middleware, (spec) => ...) so the operation's security requirement and x-permdock-permissions extension are merged into the generated document automatically.
openapi.securitySchemes()Document-level securitySchemes (scopes from listPermissions, oauth2MetadataUrl, deviceAuthorization, deprecated) to pass to OpenAPIGenerator. Targets 3.2; 3.1 output uses the registered x-oai-* fallbacks and x-permdock-oauth2MetadataUrl (see OpenAPI registries).
permdockHandlerProcedure or Fetch handler for the AuthZEN-shaped decision endpoint.

Request lifecycle

  1. The oRPC handler (Fetch, Node, Next.js) creates the initial context with the authenticated user.
  2. permdock() resolves the subject once and adds the instance to the context.
  3. protect (or openapi.protect) runs after input validation: load, validate, decide, continue or throw.
  4. Handlers call context.permdock.assert(...), filter or where; thrown PermDock errors are converted to ORPCError by an interceptor the adapter provides.
  5. When OpenAPIGenerator runs, oo.spec contributions from every openapi.protect usage appear on their operations.

What it validates

  • Untrusted loader results and inputs used as resource data against the resource schema under validate: 'boundary', after oRPC's own input validation.
  • Decision-endpoint bodies against the AuthZEN evaluations schema.
  • Middleware order at the type level: protect requires permdock in the inferred context.

How denials surface

  • denied: ORPCError code FORBIDDEN, status 403, data carrying the Problem Details object. Through the OpenAPI handler the response body is application/problem+json.
  • approval-required: FORBIDDEN with data.type ending in /approval-required and data.token.
  • Boundary validation failure: BAD_REQUEST with issues.
  • Anonymous subject: UNAUTHORIZED.

Example app

apps/examples/orpc: oRPC on Hono with a posts contract, permdock() and openapi.protect on mutations, OpenAPIGenerator output checked into the example and validated by permdock openapi --check, a React client using permdock/react against the shared decision endpoint, and Vitest tests calling procedures directly and through the OpenAPI handler.

Open questions

  • Whether openapi.protect should be the only exported guard (always emitting spec) or stay separate from protect for apps that do not generate OpenAPI.
  • How context.permdockData interacts with oRPC's typed context merging in nested routers; the alternative is a protect(permission, load, handler) shape.
  • Whether the Problem Details body should be the ORPCError data payload or whether the OpenAPI handler should own the content type mapping.

On this page