@cool-ai/beach-auth-rbac
v0.1.3
Published
Beach-native RBAC reference Authoriser — reads CapabilityToken values from Principal.claims['capability-tokens'] and matches against the routing rule's requiresCapability. The default reference adapter for CAIB-138's authorisation primitive.
Readme
@cool-ai/beach-auth-rbac
Beach-native RBAC reference Authoriser — reads CapabilityToken values from Principal.claims['capability-tokens'] and matches against the routing rule's requiresCapability.
The default reference adapter for CAIB-138's authorisation primitive. In-process, no external service, no extra deployment.
Install
pnpm add @cool-ai/beach-auth-rbacUsage
import { EventRouter } from '@cool-ai/beach-core';
import { RBACAuthoriser } from '@cool-ai/beach-auth-rbac';
const router = new EventRouter({ authoriser: new RBACAuthoriser() });
router.loadRoutingConfig({
rules: [
{
source: 'inbound:chat',
eventType: 'project_update',
to: [{ handler: 'project-updater' }],
requiresCapability: 'project:update',
},
],
});The router invokes RBACAuthoriser.check(event, 'project:update') after the rule matches and before the handler runs. The check reads event.principal.claims['capability-tokens'] (an array of CapabilityToken values) and returns true if any token's capability field matches the requested capability.
If no matching token is found — or the principal carries no 'capability-tokens' claim, or the principal is undefined — the check returns false and the router emits a RouteAuthorisationDeniedEvent via routeEvent.
Where tokens come from
Tokens attach to a Principal at the inbound boundary. The canonical attachment key is 'capability-tokens' (exported as CAPABILITY_TOKENS_CLAIM_KEY from @cool-ai/beach-core):
import type { CapabilityToken, Principal } from '@cool-ai/beach-core';
import { CAPABILITY_TOKENS_CLAIM_KEY } from '@cool-ai/beach-core';
function principalFromGatewayHeaders(headers: Headers): Principal {
const userId = headers.get('X-User-Id')!;
const capabilities = (headers.get('X-User-Capabilities') ?? '').split(',').filter(Boolean);
const tokens: CapabilityToken[] = capabilities.map((capability) => ({
principalId: userId,
capability,
}));
return {
principalId: userId,
claims: { [CAPABILITY_TOKENS_CLAIM_KEY]: tokens },
};
}The pattern is documented in Beach behind an external auth gateway — Gate 2's worked example for the v1.0 readiness criteria.
Options
new RBACAuthoriser({
scope: 'session:abc123', // require every token to match this scope
anonymousPrincipal: 'deny', // default; 'allow-anonymous' is for tests only
});scope
When set, only tokens whose scope field matches the configured value satisfy a check. Consumers needing per-event scope (extracted from event.data) implement a custom Authoriser directly against the interface; the findCapabilityTokens helper from this package is reusable:
import type { Authoriser, RouteEvent } from '@cool-ai/beach-core';
import { findCapabilityTokens } from '@cool-ai/beach-auth-rbac';
const perEventScope: Authoriser = {
async check(event: RouteEvent, capability: string): Promise<boolean> {
if (!event.principal) return false;
const data = event.data as { domainId?: string } | null;
const scope = data?.domainId ? `domain:${data.domainId}` : undefined;
return findCapabilityTokens(event.principal, capability, scope).length > 0;
},
};anonymousPrincipal
Behaviour when event.principal is undefined. Default 'deny'. 'allow-anonymous' returns true regardless of capability — for tests where authorisation is not the unit under test. Do not ship 'allow-anonymous' to production.
Exports
| Export | Purpose |
|---|---|
| RBACAuthoriser | The class implementing Authoriser |
| RBACAuthoriserOptions | Construction options type |
| findCapabilityTokens(principal, capability, scope?) | Pure-function token lookup, reusable in custom Authoriser implementations |
| isCapabilityToken(value) | Runtime type guard for the CapabilityToken shape |
When to use a different Authoriser
The Authoriser interface from @cool-ai/beach-core is small (one method, two parameters). Consumers needing more than capability-token matching write their own implementation directly against the interface:
- Policy-as-code (Cerbos, OPA, AuthZed) — wrap the engine's check API. The
findCapabilityTokenshelper from this package is not used; the engine is the source of truth. - Relationship-based access control (OpenFGA) — pre-model the domain as a graph, translate capabilities to (relation, object) pairs inside the Authoriser implementation.
- Custom RBAC — when the token shape differs from
CapabilityToken, or when scope must be derived from event data, write a custom Authoriser. ThefindCapabilityTokenshelper is reusable when the token storage convention is preserved.
The Authoriser interface ships in @cool-ai/beach-core; this package is one reference implementation among N.
Related
@cool-ai/beach-coreREADME § Identity and authorisation — theAuthoriserinterface,CapabilityTokenshape,requiresCapabilityrouting-rule field.documentation/guides/identity-and-authorisation.md— how the Principal flows through events and the Authoriser is consulted at dispatch.documentation/guides/beach-behind-an-external-auth-gateway.md— Gate 2's worked example.documentation/dependencies/diy-rbac/— surface-read + compromise audit for this adapter.
Licence
Apache-2.0. Copyright 2026 John Andrew.
