Transport-agnostic authentication and authorization for TypeScript — JWT, cookies, typed principals, and fluent policy composition.
Authentication libraries are often tightly coupled to a specific HTTP framework. Swapping frameworks means rewriting auth middleware. Authorization is then bolted on separately through per-route guards or decorator magic.
@cleverbrush/auth is transport-agnostic: schemes receive a plain AuthenticationContext (headers + cookies + items map), not a raw HTTP request. The resulting Principal<T> is fully typed and can be used anywhere. Authorization composes through a fluent PolicyBuilder.
Principal<T> — .hasRole(), .hasClaim(), .isAuthenticated..requireRole() and .require(predicate).crypto.import { jwtScheme, signJwt } from '@cleverbrush/auth';
type UserClaims = { sub: string; role: string };
// Sign a token (for testing or token issuance)
const token = signJwt(
{ sub: 'user-1', role: 'admin' },
process.env.JWT_SECRET!
);
// Create the scheme
const scheme = jwtScheme<UserClaims>({
secret: process.env.JWT_SECRET!,
algorithms: ['HS256'], // default
issuer: 'https://my-app.com',
audience: 'my-api',
clockTolerance: 5, // seconds
mapClaims: claims => ({
sub: claims.sub as string,
role: claims.role as string
})
});
// Authenticate
const result = await scheme.authenticate({
headers: { authorization: `Bearer ${token}` },
cookies: {},
items: new Map()
});
if (result.succeeded) {
result.principal.value?.sub; // 'user-1'
result.principal.hasRole('admin'); // true
}Supported algorithms: HS256, HS384, HS512, RS256, RS384, RS512.
import { cookieScheme } from '@cleverbrush/auth';
type SessionData = { userId: string; role: string };
const scheme = cookieScheme<SessionData>({
cookieName: 'session',
validate: async (cookieValue) => {
// Look up session from your store, verify signature, etc.
const session = await sessionStore.get(cookieValue);
if (!session || session.expired) return null;
return { userId: session.userId, role: session.role };
}
});All schemes produce a Principal<T> — an immutable value object exposing the typed claims and role/claim helpers:
import { Principal } from '@cleverbrush/auth';
const principal: Principal<{ sub: string; role: string }> = result.principal;
principal.isAuthenticated; // true
principal.claims.sub; // 'user-1'
principal.hasRole('admin'); // true
principal.hasClaim('role', 'admin'); // true
// Anonymous sentinel
const anon = Principal.anonymous();
anon.isAuthenticated; // falseCompose policies with PolicyBuilder and evaluate them with AuthorizationService:
import { PolicyBuilder, AuthorizationService, requireRole } from '@cleverbrush/auth';
// Build a policy
const adminPolicy = new PolicyBuilder()
.requireRole('admin')
.require(p => p.hasClaim('verified', 'true'))
.build('admin-only');
const policies = new Map([['admin-only', adminPolicy]]);
const authz = new AuthorizationService(policies);
// Check by requirements array
const r1 = await authz.authorize(principal, [requireRole('admin')]);
// Check by named policy
const r2 = await authz.authorize(principal, 'admin-only');
if (!r2.allowed) {
console.log(r2.reason); // 'Forbidden' | 'Not authenticated'
}import { parseCookies, serializeCookie } from '@cleverbrush/auth';
// Parse Cookie header
const cookies = parseCookies('session=abc123; theme=dark');
// { session: 'abc123', theme: 'dark' }
// Build Set-Cookie header value
const header = serializeCookie('session', 'token-value', {
httpOnly: true,
secure: true,
sameSite: 'Lax',
maxAge: 3600 // seconds
});
// 'session=token-value; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax'import { createServer, endpoint } from '@cleverbrush/server';
import { jwtScheme } from '@cleverbrush/auth';
import { object, string } from '@cleverbrush/schema';
const UserPrincipal = object({ sub: string(), role: string() });
const server = createServer();
server
.useAuthentication({
defaultScheme: 'jwt',
schemes: [
jwtScheme({
secret: process.env.JWT_SECRET!,
mapClaims: c => ({ sub: c.sub as string, role: c.role as string })
})
]
})
.useAuthorization()
.handle(
endpoint.get('/api/admin').authorize(UserPrincipal, 'admin'),
({ principal }) => ({ greeting: `Hello ${principal.claims.sub}` })
);
await server.listen(3000);