npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@inixiative/permissions

v0.1.0

Published

Generic rebac/abac/rbac core — a permix wrapper + relationship-walking check engine, ORM-agnostic via an injected relation resolver

Readme

@inixiative/permissions

Generic rebac / abac / rbac core: a small permix wrapper plus a relationship-walking check engine, on top of @inixiative/json-rules. It is ORM-agnostic — the one app-specific bit (resolving which model a relation points at) is injected, so this package never imports a database layer. Record hydration stays in the app.

Permission algebra (ActionRule)

type ActionRule =
  | string                       // rbac: delegate to another action on the same model
  | { rel: string; action: string }  // rebac: walk a relation, then check `action` on the target
  | { self: string }             // record[field] === actor id
  | { rule: Condition }          // abac: a json-rules predicate over the record
  | { any: ActionRule[] }        // OR
  | { all: ActionRule[] }        // AND
  | null;                        // terminal deny

A RebacSchema is model → { actions: { name → ActionRule } }.

Writing a schema

Action names are yours — the engine imposes no vocabulary. The own/manage/read chain below is illustrative; use whatever your roles grant.

import { Operator } from '@inixiative/json-rules';
import type { RebacSchema } from '@inixiative/permissions';

const schema: RebacSchema = {
  organization: {
    actions: {
      own: null, //         terminal: granted only via permix (roles) or row rules
      manage: 'own', //     delegation chain — own ⊇ manage ⊇ read
      read: 'manage',
    },
  },
  membership: {
    actions: {
      read: { any: [{ self: 'userId' }, { rel: 'organization', action: 'read' }] },
      leave: { self: 'userId' }, // the actor's own row
      manage: { rel: 'organization', action: 'manage' },
    },
  },
  document: {
    actions: {
      read: {
        any: [
          { rule: { field: 'isPublic', operator: Operator.equals, value: true } }, // abac guard
          { rel: 'organization', action: 'read' }, // rebac: walk the relation
        ],
      },
      manage: { rel: 'organization', action: 'manage' },
    },
  },
};

rel accepts dot-paths for multi-hop walks — { rel: 'space.organization', action: 'own' } chains through record.space.organization, resolving each hop's model via the injected resolver.

Records may also carry a permissionRules JSON field (action → ActionRule); check merges a row rule additively (OR) with the schema rule, so per-record overrides can widen but never revoke. Validate tenant-authored overrides with actionRuleSchema before persisting them.

The check engine

import { createRebacCheck } from '@inixiative/permissions';

// inject the ORM-specific relation resolver (e.g. derived from a Prisma model map)
const check = createRebacCheck((model, segment) => relationTargets[model]?.[segment] ?? null);

check(permix, schema, 'membership', record, 'manage');
// string → permix grant or schema delegation; { rel } walks `record.organization`; { self }
// matches the actor; { rule } evals json-rules; any/all compose. Cycles throw, they don't loop.

createRebacCheck is generic over the model key (default string). Pass your model union and the schema, model arg, and resolver all enforce it — no casts for your own types:

type Model = 'organization' | 'membership' | 'document';
const check = createRebacCheck<Model>((model, segment) => relationTargets[model]?.[segment] ?? null);
// now `schema: RebacSchema<Model>`, `model: Model` — a typo'd model name is a compile error.

Cycle detection uses object identity (a WeakMap), not record.id — so id-less or id-colliding records never produce a false "cycle", and a genuine self/mutual delegation (read: 'read') throws instead of overflowing the stack.

The permix wrapper

import { createPermissions } from '@inixiative/permissions';

const permix = createPermissions<'read' | 'manage' | 'own'>();
await permix.setup([{ resource: 'organization', id: 'o1', actions: { own: true } }]);
permix.setUserId('u1');
permix.check('organization', 'own', 'o1'); // true

Holds the actor's role-derived grants (keyed by resource / resource:id), a superadmin bypass, and the actor id. App code populates it from roles/entitlements (hydration); the check engine reads it back through the PermixLike slice.

Hydration (Prisma example)

check walks relation fields on the record you pass in — it never queries. The robust pattern is to make under-hydration impossible by construction: recursively load every FK-backed to-one relation (the record's ownership closure) before checking, instead of maintaining per-action include shapes. Then a missing relation can only mean the FK is null — a real deny, not a loading artifact. createHydrator is that pattern as an injected-seam primitive: give it parents (the to-one, FK-owning relations for a model) and load (a single read — also where you wrap caching).

Don't hand-maintain the relation map — extract it from the generated Prisma client with @inixiative/prisma-map, and feed the same map to both the resolver and the hydrator's parents so they can't drift:

import { buildPrismaMapV7, type RelationField } from '@inixiative/prisma-map';
import { createHydrator, createRebacCheck, type ParentRelation } from '@inixiative/permissions';

const map = buildPrismaMapV7();

// the resolver is a one-line lookup in the map
const check = createRebacCheck((model, segment) => {
  const field = map[model]?.fields[segment];
  return field?.kind === 'object' ? field.type : null;
});

// to-one parents: relation fields whose FK lives on this model (fromFields is empty on back-relations)
const parents = (model: string): ParentRelation[] =>
  Object.entries(map[model]?.fields ?? {})
    .filter((e): e is [string, RelationField] => e[1].kind === 'object' && !e[1].isList && e[1].fromFields.length > 0)
    .map(([field, rel]) => ({ field, model: rel.type, fk: rel.fromFields[0] }));

// load is the cache seam: the hydrator de-dupes within one call, your wrapper caches across calls
const hydrate = createHydrator({
  parents,
  load: (model, id) => cached(`${model}:${id}`, () => db[model].findFirst({ where: { id } })),
});

const doc = await hydrate('Document', await db.document.findFirstOrThrow({ where: { id } }));
check(permix, schema, 'Document', doc, 'manage');

(buildPrismaMapV6 covers Prisma 6 clients. The map keys models by Prisma name — Document — so if your schema and delegates use accessor casing, lowerFirst/upperFirst at the boundary.)

This guarantees the vast majority of the path automatically; records are plain data, so nothing stops you from attaching extra relations or predicate fields by hand before checking, or special-casing relations that don't traverse cleanly (self-references, polymorphic joins) in the resolver or a custom hydration step.

Also exported

  • actionRuleSchema — recursive Zod validator for a serialized ActionRule (validate tenant/row overrides). Lives on its own subpath so the core stays zod-free; zod is an optional peer — install it only if you use this:

    import { actionRuleSchema } from '@inixiative/permissions/actionRuleSchema';

Boundary

This package owns the wrapping and checking. It does not own hydration — loading records, deriving the relation map, mapping roles → grants, or populating permix from a user graph all stay in the consuming app (e.g. @template/permissions injects its Prisma-derived resolveModel, app rebacSchema, and role mappings — including app-specific action sets like ownerActions). Action and role naming conventions also stay in the app — this core makes no assumptions about what actions or roles are called.

License

MIT