Elysia
permdock/elysia derives a request-scoped PermDock into Elysia's context and provides protect as a typed beforeHandle hook that keeps validated route types.
Purpose
Elysia is Fetch-native and Bun-first, so the server kernel runs unchanged. permdock/elysia is a plugin that uses derive to attach permdock to the context and a protect hook for beforeHandle. The adapter is typed against Elysia's inferred route context so params and body validated with TypeBox or Zod keep their types inside loadData (permix's Elysia guard was typed against the raw Context, breaking with validated routes).
API
import { Elysia } from "elysia";
import { createPermDock } from "permdock/elysia";
import { policy } from "./policy";
import { permissions } from "./permissions";
export const { permdock, protect } = createPermDock(policy, {
subject: ({ store }) => store.user ?? null,
});
const app = new Elysia()
.use(permdock()) // ctx.permdock is the request-scoped PermDock
.delete(
"/posts/:id",
async ({ permdockData: post, set }) => {
await deletePost(post);
set.status = 204;
},
{
beforeHandle: protect(permissions.post.delete, ({ params }) =>
loadPost(params.id),
),
detail: openapi.security(permissions.post.delete), // @elysiajs/openapi
},
);| Export | Role |
|---|---|
permdock() | Plugin. derive calls subject(ctx) once per request and adds permdock to the context, typed with the policy's roles, plans and permissions in every later handler. Uses as: 'global' scoping so routes registered after .use see it. Each createPermDock result gets its own plugin seed, so two factories in one app are not deduplicated into one, while .use(permdock()) in a child app and its parent is. |
protect(permission, loadData?, options?) | beforeHandle hook. The tenant option ((ctx) => ctx.params?.org) is resolved for each protect. Loads data (ctx.permdockData), validates untrusted input, decides. On denial it returns a Response with Problem Details, which Elysia sends without running the handler. |
permdockHandler() | Plugin mounting the AuthZEN-shaped decision endpoint under a prefix. |
openapi | Kernel hook contract; openapi.security(permission) returns the detail fragment for @elysiajs/openapi and openapi.securitySchemes() the document-level components. |
Guarding a group
Elysia's guard and group apply hooks to many routes at once. protect composes with them for collection actions, and instance actions still take a per-route loader:
app.group("/admin", (admin) =>
admin
.guard({ beforeHandle: protect(permissions.admin.access) })
.get("/audit", listAudit)
.delete("/users/:id", removeUser, {
beforeHandle: protect(permissions.user.delete, ({ params }) =>
loadUser(params.id),
),
}),
);Because hooks run in registration order, the group-level protect decides first and the route-level one only runs for callers that already hold admin.access.
WebSockets
connection(ws, options?) opens a kernel Connection for an Elysia .ws socket (typed ElysiaSocket), memoised per socket, and closes the socket with 1008 and the Problem Details type when the connection aborts. Pass revocations to createPermDock.
app.ws("/ws", {
async open(ws) {
await connection(ws, { permission: permissions.project.read });
},
async message(ws, message) {
const conn = await connection(ws);
const decision = conn.check(permissions.post.update, message);
if (decision.outcome !== "granted") ws.send({ denied: decision.denials });
},
async close(ws) {
(await connection(ws)).close();
},
});Request lifecycle
permdock/elysia follows the shared adapter contract through the server kernel. What differs on Elysia:
- The plugin uses
derive, notresolve, so the subject is resolved once per request before route validation; a subject that depends on a header reads it in thesubjectresolver. protectruns inbeforeHandleafter Elysia's own schema validation and puts the loaded, validated row onpermdockData, so the handler does not run the loader again.- The plugin's global
onErrormaps PermDock errors thrown bypermdock.assert(...)to Problem Details. protectwithoutpermdock()fails at startup with a message naming both.- The decision event carries the permission, resource, subject, tenant and outcome, not the HTTP method or route; a sink that needs them reads its own request context.
The peer range is elysia >=1. Denials are the kernel's Problem Details responses.
Example app
apps/examples/elysia: Bun runtime, posts API with the plugin and protect, @elysiajs/openapi emitting securitySchemes, the decision endpoint, and bun test cases using app.handle(new Request(...)) asserting exact denial bodies.
The example serves app.fetch with @hono/node-server under Node. tests/integration/src/http/elysia.test.ts runs the testHttpAdapter scenarios through app.handle, including a prefixed sub-app with its own .use(permdock()). Return new Response(null, { status: 204 }) for an empty response: set.status = 204 with an undefined return makes Elysia build an invalid 204 with a body.
Related standards
Last updated on
Fastify
permdock/fastify registers the Fetch kernel as a Fastify plugin that decorates request.permdock and provides protect as a typed preHandler hook.
NestJS
permdock/nest provides a module, a guard and a Protect decorator so Nest controllers get a request-scoped PermDock on the Express or Fastify platform.