@ontrails/permits
v0.2.4
Published
Scope-based authorization for Trails.
Readme
@ontrails/permits
Scope-based authorization for Trails.
The permits package owns adapter-agnostic auth resources, adapters, and helpers. Core executeTrail enforces trail permit declarations once a surface has resolved a permit into ctx.permit.
The core pattern
1. Declare permit requirements on trails
export const create = trail('gist.create', {
permit: { scopes: ['gist:write'] },
implementation: async (input, ctx) => {
// executeTrail enforces scopes before execution enters the implementation
return Result.ok(newGist);
},
});
export const search = trail('gist.search', {
permit: 'public',
implementation: async (input, ctx) => {
// No authentication required
return Result.ok(results);
},
});2. Resolve a permit at the surface
export const graph = topo('my-app', gistModule);
// Surface auth verifies credentials and passes { permit } into executeTrail.The execution pipeline reads each trail's permit field:
'public'orundefined— execution passes through{ scopes: [...] }— execution checks thatctx.permitcontains all required scopes
Permit enforcement is intrinsic to executeTrail; there is no opt-in layer to wire. The previously deprecated authLayer compatibility wrapper has been removed (TRL-475).
3. Bind an adapter at bootstrap
import { createJwtAdapter } from '@ontrails/permits/jwt';
const adapter = createJwtAdapter({
secret: process.env.JWT_SECRET,
issuer: 'https://auth.example.com',
audience: 'api.example.com',
});Auth adapters
An auth adapter authenticates requests and produces permits.
Built-in: JWT adapter
Verifies HS256-signed JWTs and extracts claims into permits:
import { createJwtAdapter } from '@ontrails/permits/jwt';
const adapter = createJwtAdapter({
secret: 'your-hmac-secret',
issuer: 'https://auth.example.com',
audience: 'api.example.com',
scopesClaim: 'scope',
rolesClaim: 'roles',
});@ontrails/permits/jwt is the canonical built-in JWT adapter subpath. The root package intentionally keeps convenience re-exports for interactive discovery, but generated code, docs, and adapter-specific examples should import JWT names from the subpath.
Custom adapters
Implement the AuthAdapter interface:
import type { AuthAdapter, PermitExtractionInput, Permit } from '@ontrails/permits';
const myAdapter: AuthAdapter = {
authenticate: async (input: PermitExtractionInput) => {
if (!input.bearerToken) return Result.ok(null);
const permit: Permit = {
id: 'user-42',
scopes: ['user:read', 'user:write'],
roles: ['admin'],
};
return Result.ok(permit);
},
};Permits and scopes
A Permit is the resolved identity and scopes from successful authentication:
interface Permit {
readonly id: string;
readonly scopes: readonly string[];
readonly roles?: readonly string[];
readonly tenantId?: string;
readonly metadata?: Readonly<Record<string, unknown>>;
}Access the permit in your implementation:
import { getPermit } from '@ontrails/permits';
const myTrail = trail('do.something', {
implementation: async (_input, ctx) => {
const permit = getPermit(ctx);
if (!permit) return Result.err(new Error('Not authenticated'));
return Result.ok({ userId: permit.id });
},
});Scopes follow the entity:action convention: user:read, gist:write, etc.
The auth.verify trail
An infrastructure trail that verifies bearer tokens and returns permits:
import { authVerify } from '@ontrails/permits';
// Returns { valid: true, permit: { id, scopes, roles } }
// or { valid: false, error: 'Token has expired', errorCode: 'expired_token' }Testing with mock permits
Use createTestPermit() and createPermitForTrail() in tests:
import {
createTestPermit,
createPermitForTrail,
} from '@ontrails/permits/testing';
const permit = createTestPermit({
id: 'user-123',
scopes: ['gist:read', 'gist:write'],
roles: ['editor'],
});
// Create a permit matching a trail's requirements
const trailPermit = createPermitForTrail(myTrail);
// { id: 'test-...', scopes: ['gist:write'] }Permit governance
Use validatePermits() to check trails against governance rules:
import { validatePermits } from '@ontrails/permits';
const diagnostics = validatePermits(app.list());Built-in rules:
destroyWithoutPermit— error if a destroy trail has no permitwriteWithoutPermit— warning if a write trail has no permitscopeNamingConsistency— warning if a scope doesn't followentity:actionorphanScopeDetection— warning if a scope appears in only one trail
Installation
These installation examples target Trails 0.2.1 on the normal npm release line.
bun add --exact @ontrails/[email protected] @ontrails/[email protected] zod