Skip to content

Exporting resolved permissions

The vperms package ships a framework-independent permission export behavior, used by the Express, Hono and Nest integrations. It serves the JSON-safe ResolvedSubject of a target subject after authorizing the request.

Reading resolved permissions is itself protected by reserved permission nodes:

  • vperms.subject.me.permissions — every subject can read its own set. It is resolved as the lowest-priority grant, so an explicit deny overrides it.
  • vperms.subject.<subjectId>.permissions — required to read another subject’s set. Normal wildcards apply, e.g. vperms.subject.*.permissions.
import {
SELF_PERMISSIONS_PERMISSION,
subjectPermissionsPermission,
} from "vperms";
SELF_PERMISSIONS_PERMISSION; // "vperms.subject.me.permissions"
subjectPermissionsPermission("user"); // "vperms.subject.user.permissions"

To let a group read everyone’s permissions:

await vperms.setPermission("workspace", "admins", "vperms.subject.*.permissions", true);

The export path must contain exactly one :param segment, which identifies the target subject. parsePermissionsExportPath turns it into a matcher:

import { parsePermissionsExportPath } from "vperms";
const route = parsePermissionsExportPath("/subject/:subjectId");
route.base; // "subject"
route.routePattern; // "subject/:subjectId"
route.param; // "subjectId"
route.match("/subject/user"); // { subjectId: "user" }
route.match("/subject/a/b"); // null

The literal target "me" is treated as the current subject. Any other value is validated as a subject id.

exportResolvedSubject is the shared implementation:

import { exportResolvedSubject } from "vperms";
const resolved = await exportResolvedSubject({
service,
adapter,
workspaceId: "workspace",
currentSubjectId: context.id,
targetSubjectId: "user",
ability: context.ability,
});

It always authorizes before loading the target:

  1. Resolve the target (treating "me" as the current subject) and validate it.
  2. Require ability.can(...) for the matching reserved permission.
  3. Load the subject record and return its resolved permissions.

It throws typed errors so each integration can map them to an HTTP response:

Error Meaning HTTP
PermissionDeniedError The ability lacks the reserved node. 403
SubjectNotFoundError No record for the target subject. 404
InvalidSubjectIdError The target id is malformed. 400

Enable the endpoint by passing permissionsExport to the integration’s options. The route is disabled unless configured.

// Express
vpermsMiddleware({
adapter,
workspace: "workspace",
resolver: (req) => req.user?.id ?? null,
permissionsExport: { path: "/subject/:subjectId" },
});
// NestJS
VPermsModule.forRoot({
adapter,
workspace: "workspace",
resolver: (req) => req.user?.id ?? null,
permissionsExport: { path: "/subject/:subjectId" },
});

The result is a JSON ResolvedSubject that the client and React bindings can evaluate directly. See the per-framework pages for the exact behavior: