@samveys/auth

Session and API-token guards, OIDC sign-in with PKCE, policies

Version
1.0.3Nov 5, 2026
Install
pnpm add @samveys/auth

This list was read from version 1.0.2; run pnpm sync to refresh it.

Import
import { auth, authenticate, authOf, authorize, definePolicy, guarded, … } from '@samveys/auth'

Function (14)

auth Function

The `@samveys/auth` plugin. Registers the named middleware `auth` (the default guard) and `auth:<guard>` for each guard, plus the lazy `ctx.auth`. The session and OIDC guards need the `@samveys/session` plugin and its middleware before `auth` (`use: ['session', 'auth']`); without it they fail with SVS_E090. A token-only API does not need the session plugin. ```ts auth({ guards: { web: sessionGuard({ provider }), api: tokenGuard({ provider }) }, default: 'web' }) ```

function auth(options: AuthOptions): import("@samveys/core").Plugin<AuthOptions>;

Exported from packages/auth/src/plugin.ts

authenticate Function

Middleware that authenticates the request with `guard`. On success `ctx.auth` is set; otherwise the request ends with SVS_E093 (401). Put `session` before it when the guard reads the session. @param name Name shown in `ctx.auth.guard`. Default: the guard's kind.

function authenticate(guard: Guard, name?: string): Middleware;

Exported from packages/auth/src/middleware.ts

authOf Function

`ctx.auth`, or SVS_E093 when no `auth` middleware ran for this request.

function authOf(ctx: HttpContext): AuthContext;

Exported from packages/auth/src/errors.ts

authorize Function

Checks `policy[action]` for the caller of `ctx` and throws SVS_E094 (403) unless it returns `true`. No signed-in user is SVS_E093; an unknown action, a rule that returns anything but `true`, and a rule that throws all deny (a broken rule must never let a request through).

function authorize<P extends Policy>(ctx: HttpContext, policy: P, action: keyof P & string, resource?: unknown): Promise<void>;

Exported from packages/auth/src/policy.ts

definePolicy Function

Declares the rules for one kind of resource. A rule gets the signed-in user and the resource and returns `true` to allow. ```ts const postPolicy = definePolicy({ update: (user: User, post: Post) => user.id === post.userId }) ```

function definePolicy<const P extends Readonly<Record<string, PolicyRule>>>(rules: P): P;

Exported from packages/auth/src/policy.ts

guarded Function

Wraps a route handler so the policy check runs immediately before it, whatever middleware the route has: a route that forgot the `auth` middleware is still refused (SVS_E093), and a signed-in user the policy does not allow gets SVS_E094. The resource is loaded first, so a rule can compare it with the user. ```ts router.put('/posts/:id', guarded(postPolicy, 'update', (ctx) => posts.find(ctx.params.id), (ctx, post) => save(post))) ```

function guarded<P extends Policy, R, T, C extends HttpContext = HttpContext>(policy: P, action: keyof P & string, loadResource: (ctx: C) => R | Promise<R>, handler: (ctx: C, resource: R) => T | Promise<T>): (ctx: C) => Promise<T>;

Exported from packages/auth/src/policy.ts

oidcGuard Function

Sign-in with an OpenID Connect provider through the authorization code flow with PKCE (S256). `redirectToProvider()` sends the browser to the provider with a fresh `state`, `nonce` and PKCE challenge, all remembered in the session. `handleCallback()` checks `state`, exchanges the code (sending the PKCE verifier), validates the ID token — signature (RS256 or ES256 from the provider's JWKS), `iss`, `aud`, `exp`, `iat`, `nonce` — maps the claims to a user and signs the user in with a new session id. Anything wrong is SVS_E095, with the precise reason in the error's `meta.reason` for logs. Needs the `session` middleware on both routes. Works with any compliant provider (Google Workspace, Entra ID, Keycloak): only the issuer URL differs.

function oidcGuard<U extends AuthUser = AuthUser>(options: OidcGuardOptions<U>): OidcGuard<U>;

Exported from packages/auth/src/oidc-guard.ts

ormUserProvider Function

A {@link UserProvider} over an `@samveys/orm` model: `findById` is `Model.find(id)`. Token and OIDC flows hand ids over as strings; the ORM casts them to the key column. ```ts const users = ormUserProvider(User) ```

function ormUserProvider<U extends AuthUser = AuthUser>(model: FindableModel, options?: OrmUserProviderOptions<U>): UserProvider<U>;

Exported from packages/auth/src/providers.ts

passwordAuth Function

Password sign-in with Argon2id from core. Hashes are upgraded on login when the cost parameters changed. The same error is thrown for an unknown email and a wrong password.

function passwordAuth<U extends PasswordUser>(options: PasswordAuthOptions<U>): {

Exported from packages/auth/src/password.ts

passwordResets Function

Password reset tokens: 32 random bytes shown once (in the mail link), stored as SHA-256, valid once and for `ttlMs`. Creating a new token invalidates older ones for the same email.

function passwordResets(options: PasswordResetOptions): {

Exported from packages/auth/src/password.ts

requireAbility Function

Middleware that refuses (SVS_E094, 403) unless `ctx.auth.can(ability)`. Runs after an authenticating middleware; with none it answers SVS_E093 instead, so a misordered route fails closed.

function requireAbility(ability: string): Middleware;

Exported from packages/auth/src/middleware.ts

sessionGuard Function

Guard for browser users: the signed-in user's id lives in the server-side session. Needs the `session` middleware before `auth`. `login()` regenerates the session id, so an id an attacker planted before sign-in is worthless afterwards (session fixation). A session whose user was deleted counts as signed out.

function sessionGuard<U extends AuthUser = AuthUser>(options: SessionGuardOptions<U>): SessionGuard<U>;

Exported from packages/auth/src/session-guard.ts

tokenGuard Function

Guard for API clients: `Authorization: Bearer svs_<id>.<secret>`. Tokens live in `samveys_api_tokens(id, user_id, name, token_hash, abilities, last_used_at, expires_at, created_at)`, created on first use. Only a SHA-256 of the secret is stored, so a leaked table cannot be replayed, and the hash is compared in constant time. Unknown, malformed, expired and orphaned tokens are all the same answer (unauthenticated), so a client cannot tell them apart.

function tokenGuard<U extends AuthUser = AuthUser>(options: TokenGuardOptions<U>): TokenGuard<U>;

Exported from packages/auth/src/token-guard.ts

writeAuthScaffold Function

Writes the auth scaffolding under `rootDir`; refuses to overwrite any existing file (SVS_E099).

function writeAuthScaffold(rootDir: string): readonly string[];

Exported from packages/auth/src/make-auth.ts

Value (1)

AUTH_FILES Value

Files written by `svs make:auth`: a login form, a reset-request form and a reset form, the controller with routes, and a migration. The forms meet WCAG 2.2 SC 3.3.8 "Accessible Authentication (Minimum)": no cognitive test, and paste and password managers keep working — so: - real `<label for>` on every field; - `autocomplete="username"` / `"current-password"` / `"new-password"` so managers fill them; - `type="email"` and `type="password"` (no custom widgets); - no `onpaste`, `oncopy` or `ondrop` handlers, no `maxlength` on passwords; - a "show password" button instead of a confirm-password field.

const AUTH_FILES: Readonly<Record<string, string>>;

Exported from packages/auth/src/make-auth.ts

Interface (24)

ApiTokenInfo Interface

Metadata of the API token that authenticated a request. The secret is never available again.

interface ApiTokenInfo {
    readonly id: string;
    readonly name: string;
    readonly abilities: readonly string[];
    readonly expiresAt: number | undefined;
}

Exported from packages/auth/src/contracts.ts

AuthContext Interface

`ctx.auth`: the authenticated caller of this request.

interface AuthContext {
    readonly user: AuthUser;
    /** The guard name from the plugin config, e.g. `web` or `api`. */
    readonly guard: string;
    /** Present when the request was authenticated by an API token. */
    readonly token?: ApiTokenInfo;
    /** True when the token (or, for session users, the `abilities` function of the guard) grants `ability`. `*` grants all. */
    can(ability: string): boolean;
}

Exported from packages/auth/src/contracts.ts

AuthOptions Interface

No documentation comment.

interface AuthOptions {
    /** Named guards. Each is available as the middleware `auth:<name>`. */
    readonly guards: Readonly<Record<string, Guard>>;
    /** The guard behind the plain `auth` middleware. Must be a key of `guards`. */
    readonly default: string;
}

Exported from packages/auth/src/contracts.ts

AuthUser Interface

The smallest user the framework needs. Your user model has more fields; policies see all of them.

interface AuthUser {
    readonly id: string | number;
}

Exported from packages/auth/src/contracts.ts

CreatedToken Interface

No documentation comment.

interface CreatedToken {
    /** `svs_<id>.<secret>`. Shown once: only a SHA-256 of the secret is stored. */
    readonly plainText: string;
    readonly token: ApiTokenInfo;
}

Exported from packages/auth/src/contracts.ts

CreateTokenOptions Interface

No documentation comment.

interface CreateTokenOptions {
    /** A label for the token list, e.g. `ci-deploy`. */
    readonly name: string;
    /** What the token may do, e.g. `['server:update']`. Use `['*']` for everything. */
    readonly abilities: readonly string[];
    /** Epoch milliseconds, or a Date. Default: never expires. */
    readonly expiresAt?: number | Date;
}

Exported from packages/auth/src/contracts.ts

FindableModel Interface

The part of an `@samveys/orm` model class the provider uses: `Model.find(id)`.

interface FindableModel {
    find(id: string | number | bigint): Promise<unknown>;
}

Exported from packages/auth/src/providers.ts

Guard Interface

Something that can identify the caller.

interface Guard {
    readonly kind: 'session' | 'token' | 'oidc' | (string & {});
    /** `undefined` means "not authenticated": the middleware then answers SVS_E093. */
    authenticate(ctx: HttpContext): Promise<GuardResult | undefined>;
    /** Called by the plugin at boot so a guard can use the app's database without being given one. */
    attach?(dependencies: GuardDependencies): void;
}

Exported from packages/auth/src/contracts.ts

GuardDependencies Interface

No documentation comment.

interface GuardDependencies {
    /** The app's `'db'` binding. */
    readonly database: () => Database;

Exported from packages/auth/src/contracts.ts

GuardResult Interface

What a guard returns for an authenticated request.

interface GuardResult {
    readonly user: AuthUser;
    readonly abilities: readonly string[];
    readonly token?: ApiTokenInfo;
}

Exported from packages/auth/src/contracts.ts

IdTokenClaims Interface

Claims of a validated OIDC ID token.

interface IdTokenClaims {
    readonly iss: string;
    readonly sub: string;
    readonly aud: string | readonly string[];
    readonly exp: number;
    readonly iat?: number;
    readonly nonce?: string;
    readonly email?: string;
    readonly email_verified?: boolean;
    readonly name?: string;
    readonly [claim: string]: unknown;
}

Exported from packages/auth/src/contracts.ts

OidcCallbackResult Interface

No documentation comment.

interface OidcCallbackResult<U extends AuthUser = AuthUser> {
    readonly user: U;
    readonly claims: IdTokenClaims;
}

Exported from packages/auth/src/contracts.ts

OidcGuard Interface

No documentation comment.

interface OidcGuard<U extends AuthUser = AuthUser> extends Guard {
    readonly kind: 'oidc';
    /** 302 to the provider's authorization endpoint, after storing `state`, `nonce` and the PKCE verifier in the session. */
    redirectToProvider(ctx: HttpContext): Promise<Response>;
    /** Validates the callback and signs the user in (new session id). Any failure is SVS_E095. */
    handleCallback(ctx: HttpContext): Promise<OidcCallbackResult<U>>;
    logout(ctx: HttpContext): Promise<void>;
}

Exported from packages/auth/src/contracts.ts

OidcGuardOptions Interface

No documentation comment.

interface OidcGuardOptions<U extends AuthUser = AuthUser> {
    /** The issuer URL; `/.well-known/openid-configuration` is appended. Must match the `issuer` in the discovery document exactly. */
    readonly issuer: string;
    readonly clientId: string;
    readonly clientSecret?: string;
    readonly redirectUri: string;
    /** Default `['openid', 'profile', 'email']`. */
    readonly scopes?: readonly string[];
    /** Needs `findOrCreateFromClaims`. */
    readonly provider: UserProvider<U> & Required<Pick<UserProvider<U>, 'findOrCreateFromClaims'>>;
  …
Show the whole declaration (16 lines)
interface OidcGuardOptions<U extends AuthUser = AuthUser> {
    /** The issuer URL; `/.well-known/openid-configuration` is appended. Must match the `issuer` in the discovery document exactly. */
    readonly issuer: string;
    readonly clientId: string;
    readonly clientSecret?: string;
    readonly redirectUri: string;
    /** Default `['openid', 'profile', 'email']`. */
    readonly scopes?: readonly string[];
    /** Needs `findOrCreateFromClaims`. */
    readonly provider: UserProvider<U> & Required<Pick<UserProvider<U>, 'findOrCreateFromClaims'>>;
    /** Signs the user in after the callback. Default: a session guard over the same provider. */
    readonly sessionGuard?: SessionGuard<U>;
    /** For tests or a proxy. Default `globalThis.fetch`. */
    readonly fetch?: typeof fetch;
    /** Clock in epoch milliseconds, for tests. */
    readonly now?: () => number;

Exported from packages/auth/src/contracts.ts

OrmUserProviderOptions Interface

No documentation comment.

interface OrmUserProviderOptions<U extends AuthUser> {
    readonly findByCredentials?: UserProvider<U>['findByCredentials'];
    /** Required for OIDC sign-in: look the user up by `claims.sub` or `claims.email`, creating it on first sign-in. */
    readonly findOrCreateFromClaims?: (claims: IdTokenClaims) => Promise<U>;

Exported from packages/auth/src/providers.ts

PasswordAuthOptions Interface

No documentation comment.

interface PasswordAuthOptions<U extends PasswordUser> {
    readonly provider: PasswordProvider<U>;
    readonly session: SessionGuard<U>;
    /** Default: core's Argon2id hasher. */
    readonly hasher?: Hasher;
}

Exported from packages/auth/src/password.ts

PasswordProvider Interface

No documentation comment.

interface PasswordProvider<U extends PasswordUser> {
    findByEmail(email: string): Promise<U | undefined | null>;
    /** Called when the stored hash used weaker parameters than today's: store the new one. */
    updatePasswordHash(user: U, hash: string): Promise<void>;
}

Exported from packages/auth/src/password.ts

PasswordResetOptions Interface

No documentation comment.

interface PasswordResetOptions {
    readonly database: () => Database;

Exported from packages/auth/src/password.ts

PasswordUser Interface

A user with a stored Argon2id password hash.

interface PasswordUser extends AuthUser {
    readonly email: string;
    readonly passwordHash: string;
}

Exported from packages/auth/src/password.ts

SessionGuard Interface

No documentation comment.

interface SessionGuard<U extends AuthUser = AuthUser> extends Guard {
    readonly kind: 'session';
    /** Stores the user id in the session and regenerates the session id (stops session fixation). */
    login(ctx: HttpContext, user: U): Promise<void>;
    /** Invalidates the session. */
    logout(ctx: HttpContext): Promise<void>;
}

Exported from packages/auth/src/contracts.ts

SessionGuardOptions Interface

No documentation comment.

interface SessionGuardOptions<U extends AuthUser = AuthUser> {
    readonly provider: UserProvider<U>;
    /** Session key for the user id. Default `auth.user_id`. */
    readonly sessionKey?: string;
    /** Abilities of a signed-in user, for `ctx.auth.can()` and `requireAbility()`. Default: none (fail closed). */
    readonly abilities?: (user: U) => readonly string[];

Exported from packages/auth/src/contracts.ts

TokenGuard Interface

No documentation comment.

interface TokenGuard<U extends AuthUser = AuthUser> extends Guard {
    readonly kind: 'token';
    createToken(user: U, options: CreateTokenOptions): Promise<CreatedToken>;
    /** Deletes one token by id. Returns whether it existed. */
    revokeToken(id: string): Promise<boolean>;
    /** Deletes every token of a user. Returns how many. */
    revokeAllTokens(user: AuthUser): Promise<number>;
}

Exported from packages/auth/src/contracts.ts

TokenGuardOptions Interface

No documentation comment.

interface TokenGuardOptions<U extends AuthUser = AuthUser> {
    readonly provider: UserProvider<U>;
    /** Default: the app's `'db'` binding. */
    readonly database?: DatabaseSource;
    /** Default `samveys_api_tokens`. */
    readonly table?: string;
    /** Clock in epoch milliseconds, for tests. */
    readonly now?: () => number;

Exported from packages/auth/src/contracts.ts

UserProvider Interface

Where users come from.

interface UserProvider<U extends AuthUser = AuthUser> {
    /** `undefined` (or `null`) when the user no longer exists: the request is then unauthenticated. */
    findById(id: string | number): Promise<U | undefined | null>;
    /** For password sign-in forms. Not used by the built-in guards. */
    findByCredentials?(input: Readonly<Record<string, unknown>>): Promise<U | undefined | null>;
    /** OIDC: map validated claims to a user, creating one on first sign-in. */
    findOrCreateFromClaims?(claims: IdTokenClaims): Promise<U>;
}

Exported from packages/auth/src/contracts.ts

Type (3)

DatabaseSource Type

A database connection, or a function that returns one (the plugin supplies `container.make('db')`).

type DatabaseSource = Database | (() => Database);

Exported from packages/auth/src/contracts.ts

Policy Type

A set of rules by action name, made with `definePolicy()`.

type Policy = Readonly<Record<string, PolicyRule>>;

Exported from packages/auth/src/contracts.ts

PolicyRule Type

One rule of a policy. Returns exactly `true` to allow; anything else denies.

type PolicyRule<U = any, R = any> = (user: U, resource: R) => boolean | Promise<boolean>;

Exported from packages/auth/src/contracts.ts