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.
pnpm add @niscorp/charter @niscorp/vex @niscorp/prism @niscorp/strata zodWhat 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.
import { resolvePrincipal, resolveScoping } from '@niscorp/charter';
import { createScopePolicy, scopeGrants } from '@niscorp/vex';
const grants = scopeGrants(['shows', 'bookings']);
console.log(grants);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.
const charter = {
member: { actions: ['seats'], data: ['shows.read', 'bookings.*'], scoping: 'personal' },
usher: { actions: ['door.list'], data: ['*.read'] },
};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.
const behaviors = {
shows: { read: [] },
bookings: {
default: { read: [] },
personal: { read: [{ match: 'member_id', to: 'memberId' }] },
},
};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.
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']));
}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.
npx tsx policy.mts[
'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":[]}