Charter · guide

Roles that build on roles

One role extends another, one is cut out of another, and what a deny does and does not carry down.

Install it
pnpm add @niscorp/charter
Step 1 of 4

What exists

Charter is always handed the list of what exists. Here it is the ids of the screens the theatre’s app ships. An id with a dot in it, like me.tickets, is just a name: the dot is there so one pattern can select a family.

What exists
import { resolvePrincipal } from '@niscorp/charter';

const screens = [
  'welcome',
  'programme',
  'seats',
  'me.tickets',
  'me.pass',
  'door.list',
  'members',
  'takings',
  'settings',
];
Step 2 of 4

Extend a role

extends takes everything the named roles select. The usher has two screens. The manager gets those, and adds three by name.

Extend a role
const charter = {
  usher: ['programme', 'door.*'],
  manager: { extends: ['usher'], allow: ['members', 'takings', 'settings'] },
};
console.log('manager:', [...resolvePrincipal(charter, screens, ['manager'])]);
Step 3 of 4

A deny does not carry down

The trainee is an usher without the door list. The lead extends the trainee and allows the door list again.

That works: a deny belongs to the role it is written in, and a role that extends it may add the name back. The verifier, in the next guide, reports this as a warning, so it is always a choice somebody made.

A deny does not carry down
const withTrainee = {
  ...charter,
  trainee: { extends: ['usher'], deny: ['door.list'] },
  lead: { extends: ['trainee'], allow: ['door.list', 'members'] },
};
console.log('trainee:', [...resolvePrincipal(withTrainee, screens, ['trainee'])]);
console.log('lead:', [...resolvePrincipal(withTrainee, screens, ['lead'])]);
Step 4 of 4

Cut one role out of another

without takes away everything another role selects. It is how a plan or a switch is written: the money screens are a role, and the volunteer manager is a manager without them.

Cut one role out of another
const withMoney = {
  ...charter,
  money: ['takings'],
  volunteer: { extends: ['manager'], without: ['money'] },
};
console.log('volunteer:', [...resolvePrincipal(withMoney, screens, ['volunteer'])]);
Then

Run it.

The manager’s five screens, the trainee’s one, the lead’s three, and the volunteer manager with the takings cut out.

Run it
npx tsx roles.mts
It prints
manager: [ 'programme', 'door.list', 'members', 'takings', 'settings' ]
trainee: [ 'programme' ]
lead: [ 'programme', 'door.list', 'members' ]
volunteer: [ 'programme', 'door.list', 'members', 'settings' ]