@cleverbrush/auth

Transport-agnostic authentication and authorization for TypeScript — JWT, cookies, typed principals, and fluent policy composition.

$npm install @cleverbrush/auth

Zero runtime dependencies. Integrates seamlessly with @cleverbrush/server via useAuthentication() / useAuthorization().

💡 Why @cleverbrush/auth?

The Problem

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.

The Solution

@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.

Key Features

  • JWT scheme — HS256/HS384/HS512 and RS256/RS384/RS512 with configurable issuer, audience, and clock tolerance.
  • Cookie scheme — delegates cookie validation to your session store.
  • Typed Principal<T> .hasRole(), .hasClaim(), .isAuthenticated.
  • Policy builder — compose requirements with .requireRole() and .require(predicate).
  • Zero runtime dependencies — uses only Node.js built-in crypto.

JWT Authentication

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.

Cookie Authentication

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 };
    }
});

Principal

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; // false

Authorization

Compose 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'
}

Cookie Utilities

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'

Integration with @cleverbrush/server

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);