Skip to content

The service

VeguiPermsService is the application-facing API exported by the vperms package. It owns validation, permission resolution, inheritance, cycle protection and evaluation; the adapter only persists data.

import { VeguiPermsMemoryAdapter, VeguiPermsService } from "vperms";
const vperms = new VeguiPermsService({
adapter: new VeguiPermsMemoryAdapter(),
defaultParents: { global: ["everyone"] },
});
interface VeguiPermsServiceOptions {
adapter: VeguiPermsAdapter;
defaultParents?: DefaultParents;
}

defaultParents is validated with DefaultParentsSchema when the service is constructed. See Inheritance.

Method Returns Description
can(workspaceId, subject, permission) Promise<boolean> Whether the subject may perform the permission.
resolvePermissions(workspaceId, subject) Promise<ResolvedSubject> Full effective permission snapshot.
saveSubject(workspaceId, subject) Promise<Subject> Create or update a subject.
deleteSubject(workspaceId, subject) Promise<boolean> Delete a subject; true when it existed.
setPermission(workspaceId, subject, permission, value) Promise<PermissionGrant> Upsert an allow/deny grant.
unsetPermission(workspaceId, subject, permission) Promise<boolean> Remove a grant; true when it existed.

Every method takes the workspace id first and accepts a raw subject id or a Principal. All public inputs are validated with Zod; invalid input throws a ZodError.

await vperms.saveSubject("workspace", {
id: "user",
type: SubjectType.User,
parents: ["developers"],
});
await vperms.setPermission("workspace", "developers", "posts.read", true);
await vperms.can("workspace", "user", "posts.read"); // true
await vperms.resolvePermissions("workspace", "user");

can() returns false when the adapter has no record for the subject. resolvePermissions() throws SubjectNotFoundError in that case, because the snapshot needs a subject record. Direct grants short-circuit inheritance; then explicit, type-default and global-default layers are consulted in order.

The runtime helpers are shared by all framework integrations. They hydrate a request-scoped context exactly once per request.

import {
createAbility,
resolveRequestContext,
resolveSubjectId,
} from "vperms";
const ability = createAbility(vperms, "workspace", "user");
await ability.can("posts.read"); // memoized per permission

createAbility(service, workspaceId, subject) returns an Ability whose can(permission) memoizes the promise, so the same permission is never evaluated twice within a request.

resolveRequestContext performs the whole dance:

const context = await resolveRequestContext({
adapter,
service: vperms,
workspaceId: "workspace",
subject: await getCurrentUser(), // SubjectId | Principal | null
});
context.id; // resolved subject id ("anonymous" when subject was null)
context.subject; // Subject record, or undefined when not persisted
context.kind; // SubjectType, or undefined
context.ability; // Ability

When the resolver returns null/undefined, the anonymous subject is loaded — created on demand with SubjectType.Anon via ensureAnonymousSubject — so defaultParents.byType.anon applies automatically. ANONYMOUS_SUBJECT_ID is "anonymous".

The Ability interface is intentionally minimal:

interface Ability {
can(permission: string): Promise<boolean>;
}

Middleware, guards, decorators and permission-export handlers all reuse the same instance.