@joyful-tools/access
v0.0.2
Published
Type-safe access control for dynamic permission sets.
Readme
@joyful/access
Type-safe access control for dynamic permission sets.
Declare your permissions once as resource:action strings with implication
rules, then check them against whatever permission list a user actually has — no
fixed roles required.
Installation
# npm
npm install @joyful-tools/access
# pnpm
pnpm add @joyful-tools/access
# bun
bun add @joyful-tools/access
# npm (from JSR)
npx jsr add @joyful/access
# pnpm 10.9+ (from JSR)
pnpm add jsr:@joyful/access
# yarn 4.9+ (from JSR)
yarn add jsr:@joyful/access
# deno
deno add jsr:@joyful/accessThe examples below use
@joyful/access(the JSR name) in imports. If you installed from npm, use@joyful-tools/accessinstead.
Why
- Dynamic permission sets, not fixed roles. Checks run against the raw string list a subject has (e.g. loaded from a database). Unknown or stale entries are ignored.
- Type-safe. A strict permission union is derived from your definition, so
checks only accept real permissions, while the granted list stays loosely
typed as
string[]. - Tiny surface. Define,
with,has,assert. That's it.
Usage
import { AccessControl } from "@joyful/access";
const ac = new AccessControl({
member: {
read: {},
edit: { implies: ["member:read"] },
},
invite: {
read: {},
edit: { implies: ["invite:read"] },
},
});
// user.permissions might be: ["member:edit", "invite:read", "old:garbage"]
const ctx = ac.with(user.permissions, { bypassIf: user.isSuperAdmin });
ctx.has("member:read"); // true (granted via the member:edit implication)
ctx.has("invite:edit"); // false
// pass an array for a logical AND
ctx.has(["member:read", "invite:read"]); // true only if both are granted
// bypassIf at the check level too (e.g. owners)
ctx.has("member:edit", { bypassIf: user.role === "owner" });
// assert is the Result version
yield* ctx.assert("member:edit"); // short circuits in Result.runbypassIf works at two levels: on with it bypasses every check on the context
(e.g. a super-admin — the permissions are not even expanded), and on
has/assert it bypasses just that one call. A check passes if either level
bypasses.
implies is type-checked against the permissions declared in the same call, so
a typo is a compile error (and also throws at construction time).
A granted resource:* entry grants every action of that resource (and
everything those actions imply). It is a grant-side convenience — queries are
always concrete permissions:
ac.with(["member:*"]).has("member:edit"); // trueAPI
new AccessControl(definition)- compile a definition.AccessControl.with(permissions, options?)- bind a subject's permissions into anAccessContext.options.bypassIfbypasses every check on the context.AccessContext.has(permissions, options?)-trueif every permission is granted (AND); pass a single permission or an array.options.bypassIfforces success.AccessContext.assert(permissions, options?)-Result<void, ForbiddenError>for the same check.ForbiddenError- error carrying the missingpermission.Permission<T>/InferPermission<AC>- derive the permission union at the type level.
License
MIT
