tRPC
permdock/trpc adds a request-scoped PermDock to tRPC context and a protect middleware that loads the resource from procedure input, with a trpc-to-openapi hook for OpenAPI security.
Status: planned Phase: 2
Purpose
tRPC procedures have no HTTP surface of their own, so the server kernel is used for subject resolution and decision mapping while denials become TRPCError values. permdock/trpc gives you ctx.permdock on every procedure and protect(permission, load) as a middleware whose loader receives the parsed input, so the same typed reference guards a mutation and documents it. When trpc-to-openapi exposes procedures as REST, the adapter's hook emits OpenAPI 3.2 security for them.
API
// server/permdock.ts
import { initTRPC } from '@trpc/server'
import { createPermDock } from 'permdock/trpc'
import { policy } from './policy'
import { permissions } from './permissions'
export const { permdock, protect, openapi } = createPermDock(policy, { subject: (opts) => opts.ctx.user ?? null })
const t = initTRPC.context<Context>().create()
export const router = t.router
export const procedure = t.procedure.use(permdock()) // ctx.permdock is the request-scoped PermDock
// server/routers/posts.ts
export const postsRouter = router({
list: procedure.query(async ({ ctx }) => ctx.permdock.filter(permissions.post.read, await listPosts())),
remove: procedure
.input(z.object({ id: z.string() }))
.use(protect(permissions.post.delete, ({ input }) => loadPost(input.id)))
.meta(openapi.security(permissions.post.delete)) // trpc-to-openapi: { openapi: { protect: true, ... } }
.mutation(async ({ ctx }) => deletePost(ctx.permdockData)),
})| Export | Role |
|---|---|
permdock() | Middleware. Calls subject(opts) once per request (memoised on ctx so nested routers share the instance) and extends ctx with permdock. Typed through tRPC's middleware context inference, no manual context augmentation. |
protect(permission, load?) | Middleware placed after .input(...). load receives input, ctx; the result is validated at the boundary when the loader is untrusted and passed on as ctx.permdockData. Denials throw TRPCError with code FORBIDDEN and the Problem Details body as cause. |
openapi.security(permission) | Returns the meta fragment for trpc-to-openapi: protect: true, securitySchemes scope names, and x-permdock-permissions. openapi.securitySchemes() feeds generateOpenApiDocument. |
permdockHandler | Procedure (or standalone Fetch handler) implementing the AuthZEN-shaped decision endpoint for permdock/react clients that share the tRPC server. |
Request lifecycle
- The tRPC adapter (Fetch, Express, Next.js) builds
ctxwith the authenticated user. permdock()resolves the subject once and adds the instance toctx.protectruns after input parsing: load, validate, decide, continue withctx.permdockDataor throw.- Procedures call
ctx.permdock.assert(...),filterorwherefor checks that need procedure-local data; thrown PermDock errors are mapped toTRPCErrorby anerrorFormatterhelper so the client receives the Problem Details fields underdata. on('decision')events carry the procedure path and type.
What it validates
- Loader results marked untrusted and inputs used as resource data against the resource schema under
validate: 'boundary', after tRPC's own input parser. - Decision-endpoint bodies against the AuthZEN
evaluationsschema. - Middleware order:
protectbeforepermdock()is a type error becausectx.permdockis absent from the inferred context.
How denials surface
denied:TRPCErrorcodeFORBIDDEN, message from the first denial reason,causecarrying the Problem Details object (permission,denials,alternatives). Overtrpc-to-openapithe HTTP response is403 application/problem+json.approval-required:TRPCErrorcodeFORBIDDENwithcause.typeending in/approval-requiredandcause.token.- Boundary validation failure:
TRPCErrorcodeBAD_REQUESTwithissues. - Anonymous subject:
UNAUTHORIZEDwhen the procedure requires one.
Example app
apps/examples/trpc: tRPC v11 with the Fetch adapter on Hono, a posts router using permdock() and protect, trpc-to-openapi generating a 3.2 document with securitySchemes from the hook, a React client using permdock/react against the shared decision endpoint, and Vitest tests calling procedures through createCaller and asserting TRPCError codes and cause bodies.
Related standards
- OpenAPI 3.2 through
trpc-to-openapi. - Problem Details for the REST surface and the
causeshape. - AuthZEN for the decision endpoint.
Open questions
- Whether
ctx.permdockDatais the right hand-off orprotectshould accept the procedure resolver directly (protect(permission, load, resolver)), avoiding a context key. - tRPC version floor: the plan targets v11;
trpc-to-openapicompatibility with the newest middleware typing needs a test intests/types. - Whether the decision endpoint should be a tRPC procedure (typed client, batched by tRPC) or the plain AuthZEN Fetch handler (shared with non-tRPC clients). Current lean: both, with the procedure delegating to the handler.
Terminal (your own CLI)
permdock/terminal puts permission checks inside command-line tools you build with commander, citty, oclif, yargs, clack or Ink, with verified subjects from device flow, keychain, env or CI tokens, sysexits exit codes and Problem Details on --json.
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.