Skip to content

Express

@vperms/express wires a VeguiPermsService into an Express application: it resolves the current subject on every request, exposes an ability on req, ships route guards and can serve the permissions export endpoint.

import express from "express";
import { vpermsMiddleware, hasPermission } from "@vperms/express";
import { VeguiPermsMemoryAdapter } from "vperms";
const app = express();
app.use(
vpermsMiddleware({
adapter: new VeguiPermsMemoryAdapter(),
workspace: "workspace",
resolver: (req) => req.session?.userId ?? null,
}),
);
app.get("/posts/:id", hasPermission("posts.read"), async (req, res) => {
res.json({ allowed: await req.ability.can("posts.write") });
});
interface VpermsMiddlewareOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string | WorkspaceResolver;
defaultParents?: DefaultParents;
permissionsExport?: { path: string };
}
type SubjectResolver =
(req: Request) => SubjectId | Principal | null | Promise<...>;
type WorkspaceResolver = (req: Request) => string | Promise<string>;
  • resolver returns the subject identity for the request. Returning null resolves the anonymous subject (anonymous, type anon).
  • workspace is a constant id or a function when the workspace depends on the request (host, tenant, path segment…).
  • permissionsExport.path enables the export route, e.g. "/vperms/subject/:subjectId".

On each request the middleware hydrates req.ability, req.subject and req.kind, then calls next(). Any thrown error is forwarded to next(error).

import { hasPermission, hasAnyPermission } from "@vperms/express";
app.get("/posts/:id", hasPermission("posts.read"), handler);
app.delete("/posts/:id", hasAnyPermission("admin", "posts.delete"), handler);

hasPermission requires all inputs, hasAnyPermission requires at least one. Permissions may also be functions of the request:

import type { PermissionBuilder } from "@vperms/express";
const ownsPost: PermissionBuilder = (req) => `posts.${req.params.id}.write`;
app.put("/posts/:id", hasPermission(ownsPost), handler);

Both guards respond 403 with res.sendStatus(403) when denied. If vpermsMiddleware was not registered first they throw a descriptive error instead of silently allowing the request.

The package augments the global Express Request:

declare global {
namespace Express {
interface Request {
ability: RequestAbility; // can(permission): Promise<boolean>
subject?: Subject;
kind?: SubjectType;
}
}
}

req.ability.can() is memoized per request and permission.

app.use(
vpermsMiddleware({
adapter,
workspace: "workspace",
resolver,
permissionsExport: { path: "/vperms/subject/:subjectId" },
}),
);

A matching GET is answered directly by the middleware:

Status Meaning
200 ResolvedSubject JSON
403 The caller cannot read the target permissions
404 The subject does not exist
400 The subject id is invalid

See Exporting resolved permissions for the authorization rules.

RequestAbility, vpermsMiddleware, hasPermission, hasAnyPermission, PermissionBuilder, PermissionInput, SubjectResolver, SubjectResolverResult, WorkspaceResolver, PermissionsExportOptions, VpermsMiddlewareOptions, ANONYMOUS_SUBJECT_ID.