Charter · guide

A Vex policy from a charter

What a role may do with each table, resolved by Charter and handed to Vex as the policy it enforces.

Install it
pnpm add @niscorp/charter @niscorp/vex @niscorp/prism @niscorp/strata zod
Step 1 of 4

What the tables allow

For data, the list of what exists comes from Vex. scopeGrants takes table names and answers with every grant they can carry: a read, and three kinds of write, for each.

Charter does not make this list and does not know what a table is. It is handed the names.

What the tables allow
import { resolvePrincipal, resolveScoping } from '@niscorp/charter';
import { createScopePolicy, scopeGrants } from '@niscorp/vex';

const grants = scopeGrants(['shows', 'bookings']);
console.log(grants);
Step 2 of 4

A role’s data

A role’s data is a selection over those names, written like any other. A member reads shows, and may do anything with bookings.

scoping names how far the role reaches. Charter does not know what personal means. It only knows which role said it.

A role’s data
const charter = {
  member: { actions: ['seats'], data: ['shows.read', 'bookings.*'], scoping: 'personal' },
  usher: { actions: ['door.list'], data: ['*.read'] },
};
Step 3 of 4

What a granted read does

Which rows come back is not the charter’s. It is a rule on the table, kept by the app, with a version for each reach. At the personal reach a read of bookings is matched to the member asking. Listing a table here grants nothing: only the charter grants.

What a granted read does
const behaviors = {
  shows: { read: [] },
  bookings: {
    default: { read: [] },
    personal: { read: [{ match: 'member_id', to: 'memberId' }] },
  },
};
Step 4 of 4

Compile the policy

resolvePrincipal is asked for the data section, and answers with the grants the role selects. createScopePolicy takes those, the rules, and the reach, and makes the policy Vex enforces.

A member can read their own bookings. An usher can read every booking and write none: the usher’s policy has no write for bookings at all, so Vex refuses one.

Compile the policy
for (const role of ['member', 'usher']) {
  const granted = resolvePrincipal(charter, grants, [role], 'data');
  const policy = createScopePolicy(granted, behaviors, resolveScoping(charter, role));
  console.log(role, JSON.stringify(policy.entities['bookings']));
}
Then

Run it.

Every grant two tables can carry, then what each role’s policy says about bookings: the member’s read is matched to them and they may write, and the usher’s read is whole with no write in it.

Run it
npx tsx policy.mts
It prints
[
  'shows.read',
  'shows.write.insert',
  'shows.write.update',
  'shows.write.delete',
  'bookings.read',
  'bookings.write.insert',
  'bookings.write.update',
  'bookings.write.delete'
]
member {"read":[{"match":"member_id","to":"memberId"}],"insert":[],"update":[],"delete":[]}
usher {"read":[]}