@morphixai/authz
v0.1.0
Published
Project-agnostic authorization engine — type policy AND instance relationship, plus resource portability. Knows nothing about auth, storage or transport.
Maintainers
Readme
@morphixai/authz
Project-agnostic authorization engine. Answers two questions and nothing else:
- can — may this actor perform this action on this resource type, given the principal's relationship to the target? (type policy AND instance relationship)
- portability — how does this resource type travel when its container is shared?
It knows nothing about authentication, storage, scope, or how a resource is
physically fetched. Everything project-specific enters through three injection
points: actors, policies, relationshipResolver.
Install
npm install @morphixai/authzQuick start
import { createPermissionEngine } from '@morphixai/authz';
const engine = createPermissionEngine({
actors: ['user', 'agent', 'share', 'anonymous'],
policies: {
'doc.page': {
actions: {
read: [
{ actor: 'user', relations: ['owns'] },
{ actor: 'share', relations: ['shared-with'] },
{ actor: 'anonymous', public: true }, // published → public read
],
write: [{ actor: 'user', relations: ['owns'] }],
},
portability: 'travels-as-is',
},
},
// instance-level authority — query your own ownership / share / membership data
relationshipResolver: async (principal, relation, target) => {
if (relation === 'owns') return db.pages.isOwnedBy(target, principal.id);
return false;
},
// optional: batch counterpart so filterAuthorized avoids N+1 on list endpoints
batchRelationshipResolver: async (principal, relation, targets) =>
db.pages.ownedSubset(targets, principal.id),
});Single decision
await engine.can({
principal: { id: 'u1' },
actor: 'user',
action: 'write',
resourceType: 'doc.page',
target: pageId,
});
// → { allow: boolean, reason?: string }List endpoints
await engine.filterAuthorized({
principal: { id: 'u1' },
actor: 'user',
action: 'read',
resourceType: 'doc.page',
targets: [a, b, c],
}); // → authorized subset, one batch passShare time
engine.portabilityOf('doc.page'); // → 'travels-as-is'
engine.evaluateShare('doc.page', { sameBoundary: true }); // → 'copy'evaluateShare maps portability to what should happen to the resource:
| Portability | sameBoundary: true | crossing out |
|---|---|---|
| travels-as-is | copy | copy |
| materialize-on-share | reference | materialize |
| never | skip | skip |
Model
- Grant binds an actor to the relations that satisfy it (or
public: truefor type-level public access). Multiple grants per actor are OR-ed. - Action is an open string —
read/write/delete/admin/ … — each with its own grant list. - Portability (
travels-as-is/materialize-on-share/never) is queried at share time viaevaluateShare(type, { sameBoundary }). It never affects allow/deny — a resource you cannot read is not made readable by being portable.
Design tenets
- Pure — the core does no IO; all IO lives in the injected resolver(s). Safe as the primary gate even where the datastore's own row security is bypassed.
- Fails closed — unknown resource types, actors and actions are denied.
- Declarative — a new resource type is one
Policyentry, not an engine change.
Subpath exports
| Entry | Contents |
|---|---|
| @morphixai/authz | createPermissionEngine + types (re-exports ./core) |
| @morphixai/authz/core | the platform-neutral core directly |
License
MIT
