@aooth/auth API Reference
Complete export reference for @aooth/auth. See the Auth Conceptual Guide for narrative documentation. Subpaths: ./redis, ./atscript-db, ./atscript-db/model.as, ./client (browser-safe), ./authz (authorization server — see the Authorization Server guide).
Classes
AuthCredential<TPayload extends object = object>
class AuthCredential<TPayload extends object = object> {
constructor(opts: AuthCredentialOptions<TPayload>);
issue(userId: string, options?: IssueOptions<TPayload>): Promise<IssueResult>;
validate(accessToken: string): Promise<AuthContext<TPayload> | null>;
refresh(refreshToken: string): Promise<IssueResult>;
revoke(token: string): Promise<void>;
revokeAllForUser(userId: string): Promise<number>;
listForUser(userId: string): Promise<AuthContext<TPayload>[]>;
listSessions(
userId: string,
opts?: { enrich?: SessionEnricher; kind?: string | string[] },
): Promise<SessionInfo[] | EnrichedSession[]>;
revokeSession(userId: string, sessionId: string): Promise<void>;
revokeOtherSessions(userId: string, keepSessionId: string): Promise<number>;
deriveStateKey(label?: string): Buffer; // HKDF-derived stable secret
}The orchestrator. Store-agnostic — accepts any CredentialStore (stateful or stateless). Refresh rotation modes are 'none' | 'always' | 'sliding' (default 'sliding'). On reuse-after-grace, calls onRotationReuse and revokes the compromised token family (or every session for the user with refresh.reuseResponse: 'user'). listSessions / revokeSession / revokeOtherSessions group the user's credentials into token families by sessionId (no-op [] / 0 on stores that can't enumerate) — see Sessions. deriveStateKey(label = "wf-state") HKDF-derives a stable secret from the configured auth secret — used by @aooth/auth-moost's WfTriggerProvider as the default encapsulated wf-state token secret so it survives restarts without a separate config. See Credentials & Sessions and Refresh & Rotation.
AuthCredentialOptions<TPayload>:
interface AuthCredentialOptions<TPayload extends object = object> {
store: CredentialStore<TPayload>;
method?: "session" | "token"; // default 'token'
accessTtl?: number; // default 1h; throws INVALID_CONFIG if ≤ 0
refresh?: RefreshConfig;
/**
* Raw-token denylist consulted on every `validate()`. Disjoint from the
* store's own `jti`-keyed denylist (used by stateless `revoke`/`update`/
* `consume`). Sharing one `DenylistStore` instance across both is safe —
* raw tokens and UUID jtis never collide.
*/
denylist?: DenylistStore;
maxConcurrent?: number;
onLimit?: "reject" | "evict-oldest"; // default 'reject'
trackLastSeen?: "refresh" | "validate" | false; // default false — see Sessions
clock?: Clock;
}CredentialStoreMemory<TPayload>
new CredentialStoreMemory<TPayload>();Reference stateful store. Holds Map<token, state> + Map<userId, Set<token>> for O(1) revokeAllForUser. See Stores.
CredentialStoreJwt<TPayload>
new CredentialStoreJwt<TPayload>(opts: {
algorithm?: 'HS256'|'HS384'|'HS512'|'RS256'|'RS384'|'RS512'|'ES256'|'ES384'|'ES512'|'EdDSA'
secret?: string | Uint8Array | CryptoKey
privateKey?: CryptoKey | Uint8Array
publicKey?: CryptoKey | Uint8Array
issuer?: string
audience?: string
denylist?: DenylistStore
clock?: Clock
})Stateless JWT store using jose. Default algorithm HS256. JWT verify pins algorithms: [this.algorithm] to defend against algorithm-confusion. Custom claim state carries iatMs/expMs at ms precision. Throws AuthError('INVALID_CONFIG') if keys missing. See Tokens (JWT).
CredentialStoreEncapsulated<TPayload>
new CredentialStoreEncapsulated<TPayload>(opts: {
secret: string | Buffer | Uint8Array // string → scrypt KDF; 32-byte Buffer/Uint8Array skips KDF
denylist?: DenylistStore
clock?: Clock
})AES-256-GCM token. Format: base64url(iv ‖ ciphertext ‖ authTag) of JSON.stringify({...state, jti}). Pass a 32-byte buffer to skip the fixed-salt KDF. See Tokens (JWT) and Stores.
DenylistStoreMemory
new DenylistStoreMemory();In-memory Map<jti, expiresAt> denylist with lazy expiry on has and explicit cleanup() sweep. Required by stateless stores if revoke / consume / update are used. See Stores.
Functions
generateMagicLinkToken
function generateMagicLinkToken(): string;32 bytes CSPRNG → base64url → 43 chars. URL-safe ([A-Za-z0-9_-] only). See Magic Links.
defaultClock
const defaultClock: Clock; // { now: () => Date.now() }The default Clock implementation injected into stores when none is supplied. Override for deterministic tests or skew-corrected clocks. See Credentials & Sessions.
Client
Exported from the browser-safe @aooth/auth/client subpath only (no jose / Node crypto). See Client (Browser Silent Refresh) for narrative.
createAuthedFetch
function createAuthedFetch<TFetch extends FetchFn = DefaultFetch>(
options?: CreateAuthedFetchOptions<TFetch>,
): TFetch;
interface CreateAuthedFetchOptions<TFetch extends FetchFn = DefaultFetch> {
refreshPath?: string; // default '/auth/refresh'
onLogout?: () => void; // fired once per failed refresh
fetch?: TFetch; // default: global fetch
refreshOn?: number[]; // default [401]
}A fetch wrapper for cookie-transport SPAs: forwards credentials: "include", single-flights POST {refreshPath} on a matching status (single refresh for N concurrent failures), retries the original request once on success, and calls onLogout() once on failure. Returns a function with the same signature as the wrapped fetch. See Client.
FetchFn / MinimalResponse / MinimalRequestInit / DefaultFetch
type FetchFn = (input: string | URL, init?: MinimalRequestInit) => Promise<MinimalResponse>;
interface MinimalResponse {
readonly ok: boolean;
readonly status: number;
}Structural types so the subpath needs no DOM lib. The real DOM fetch / Response satisfy them; DefaultFetch resolves to the ambient fetch type when the consumer's tsconfig includes the DOM lib, so createAuthedFetch() returns a real Response.
Types — Core
AuthContext<TPayload>
type AuthContext<TPayload extends object = object> = {
userId: string;
method: "session" | "token";
credentialId: string; // sha256(accessToken) — safe to log
sessionId?: string; // token-family id; falls back to credentialId for legacy tokens
expiresAt: number;
} & TPayload; // the credential's typed root fields surface FLAT — no `claims` containerThe "who is calling" object returned by validate. credentialId is a fingerprint, not the token — cannot be replayed. sessionId identifies the token family ("this device") — see Sessions. The credential's per-token payload TPayload (the typed root fields a consumer adds to their model — e.g. @arbac.attenuate.* fields read by @aooth/arbac-moost) is intersected in and read by name (ctx.tenantId, not ctx.claims.tenantId). See Credentials & Sessions.
CredentialMetadata
interface CredentialMetadata {
ip?: string;
userAgent?: string;
fingerprint?: string;
label?: string;
}Open to declaration merging — augment via declare module '@aooth/auth' { interface CredentialMetadata { ... } }. See Credentials & Sessions.
CredentialState
// The fixed ENVELOPE — no generic, no `claims`. A consumer's per-token payload
// is NOT a field here; it rides as additional typed root fields intersected
// via `CredentialState & TPayload` (the orchestrator + stores are generic over
// that payload).
interface CredentialState {
userId: string;
issuedAt: number;
expiresAt: number;
metadata?: CredentialMetadata;
kind?: "access" | "refresh";
parentCredentialId?: string;
rotatedAt?: number;
sessionId?: string; // token-family id, stable across rotation
lastSeenAt?: number; // activity time; only written under trackLastSeen
}The persisted envelope stores hold. sessionId / lastSeenAt back the Sessions APIs. See Stores.
IssueOptions<TPayload>
// The typed payload `TPayload` is spread FLAT alongside the framework
// hints; reserved keys `metadata`/`sessionId`/`ttl`/`expiresAt`/`kind` (and
// the envelope keys) must not be reused as payload field names.
type IssueOptions<TPayload extends object = object> = TPayload & {
metadata?: CredentialMetadata;
sessionId?: string; // omit → issue() mints a random opaque one
ttl?: number; // per-mint access TTL (ms, > 0) — overrides accessTtl; excl. with expiresAt
expiresAt?: number; // absolute access expiry instant (ms) — overrides ttl + accessTtl
kind?: string; // semantic credential kind ("cli-session" / "pat") → metadata.credentialKind
};Options for issue. Pass the credential's typed root fields directly (e.g. issue(userId, { tenantId: "t-1" })); supply sessionId to join an existing session family. ttl / expiresAt override the access-token lifetime for THIS mint only (refresh keeps refresh.ttl); kind labels the whole session family and feeds the listSessions({ kind }) filter. See Sessions and Credentials.
IssueResult
interface IssueResult {
accessToken: string;
refreshToken?: string;
accessExpiresAt: number;
refreshExpiresAt?: number;
}Returned by issue and refresh. refreshToken is present iff refresh: RefreshConfig is configured. See Credentials & Sessions.
RefreshConfig
interface RefreshConfig {
ttl: number;
rotation?: "none" | "always" | "sliding"; // default 'sliding'
rotationGraceMs?: number; // default 30_000 — sliding AND always
reuseResponse?: "session" | "user"; // default 'session'
onRotationReuse?: (state: CredentialState) => void;
}Rotation policy. Both 'sliding' (rolling expiry) and 'always' (fixed session ceiling) rotate every refresh, mark rotatedAt, and tolerate reuse within rotationGraceMs on stateful stores (store-backed, so the grace holds across instances); reuse after the grace fires the theft response. reuseResponse selects the blast radius: 'session' (default — the compromised token family via revokeSession) or 'user' (every session via revokeAllForUser). See Refresh & Rotation.
Types — Stores
CredentialStore<TPayload>
interface CredentialStore<TPayload extends object = object> {
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
consume(token: string): Promise<(CredentialState & TPayload) | null>; // single-use
update(token: string, state: CredentialState & TPayload): Promise<string>; // may return new token
revoke(token: string): Promise<void>;
revokeAllForUser(userId: string): Promise<number>;
listForUser?(userId: string): Promise<Array<CredentialState & TPayload & { token: string }>>;
touch?(token: string, at: number): Promise<void>; // backs trackLastSeen: 'validate'
listSessions?(userId: string): Promise<Array<CredentialState & TPayload & { token: string }>>; // optional native grouping
}The pluggable storage contract. Stateless stores throw STATELESS_OPERATION_UNSUPPORTED on consume/revoke/update unless a DenylistStore is configured. update MAY return a different token (callers MUST use the returned value). touch / listSessions are optional session capabilities — see Sessions. See Stores.
SessionInfo
interface SessionInfo {
sessionId: string;
userId: string;
createdAt: number; // origin credential's issuedAt
lastSeenAt?: number; // newest activity; falls back to createdAt
expiresAt: number; // live refresh token's expiry (or access)
current?: boolean; // set by the caller, not the store
metadata?: CredentialMetadata;
}One row per token family, returned by listSessions. See Sessions.
EnrichedSession
interface EnrichedSession extends SessionInfo {
device?: string;
browser?: string;
os?: string;
location?: string;
geo?: { country?: string; city?: string };
}A SessionInfo augmented by a SessionEnricher at read time. aooth ships no UA/geo derivation. See Sessions.
SessionEnricher
type SessionEnricher = (s: SessionInfo) => EnrichedSession | Promise<EnrichedSession>;Consumer-supplied read-time mapper passed to listSessions(userId, { enrich }). See Sessions.
DenylistStore
interface DenylistStore {
add(jti: string, expiresAt: number): Promise<void>;
has(jti: string): Promise<boolean>;
cleanup(): Promise<number>;
}Optional sidecar for stateless stores. See Stores.
Types — Transport
AuthEmailKind
type AuthEmailKind =
| "recovery.magicLink"
| "invite.magicLink"
| "mfa.code"
| "login.pincode"
| "recovery.pincode"
| "invite.pincode"
| "notifyNewDevice";See Email & SMS Senders.
AuthEmailEvent
interface AuthEmailEvent {
kind: AuthEmailKind;
recipient: string;
url?: string;
code?: string;
expiresAt: number;
username?: string;
metadata?: Record<string, unknown>;
}Payload handed to EmailSender.send. See Email & SMS Senders.
EmailSender
interface EmailSender {
send(event: AuthEmailEvent): Promise<void>;
}Interface only — consumers ship their SES/SendGrid/Twilio implementation. Workflows await the call; blocking transports must queue and return. See Email & SMS Senders.
AuthSmsKind
type AuthSmsKind = "login.pincode" | "recovery.pincode" | "invite.pincode";See Email & SMS Senders.
AuthSmsEvent
interface AuthSmsEvent {
kind: AuthSmsKind;
recipient: string;
code: string;
ttlMs: number;
userId?: string;
}See Email & SMS Senders.
SmsSender
interface SmsSender {
send(event: AuthSmsEvent): Promise<void>;
}Interface only — provider implementation is the consumer's responsibility. See Email & SMS Senders.
BuildMagicLinkUrl
type BuildMagicLinkUrl = (kind: AuthEmailKind, token: string, ctx?: { userId?: string }) => string;Consumer-owned URL builder fed to the magic-link outlet. The optional third { userId } arg is supplied for the invite.magicLink kind so the URL can carry the invitee id (read by AuthController.invitePostRedemption for the idempotent already-accepted envelope); recovery callers ignore it. See Magic Links.
Clock
interface Clock {
now(): number;
}Time abstraction injected into stores and AuthCredential. Replace for tests or skew-corrected clocks. See Credentials & Sessions.
Errors
AuthError
class AuthError extends Error {
readonly name: 'AuthError'
constructor(
public type: AuthErrorType,
message?: string,
public details?: Record<string, unknown>
)
}Single error class. See Errors.
AuthErrorType
type AuthErrorType =
| "INVALID_TOKEN"
| "TOKEN_EXPIRED"
| "TOKEN_REVOKED"
| "REFRESH_REUSE_DETECTED"
| "STATELESS_OPERATION_UNSUPPORTED"
| "MAX_CONCURRENT_REACHED"
| "INVALID_CONFIG";REFRESH_REUSE_DETECTED fires on 'always' reuse and 'sliding' reuse-after-grace; both trigger revokeAllForUser. See Errors and Refresh & Rotation.
Subpath: @aooth/auth/redis
import { CredentialStoreRedis, DenylistStoreRedis, RedisLike } from "@aooth/auth/redis";CredentialStoreRedis<TPayload>
new CredentialStoreRedis<TPayload>(opts: {
redis: RedisLike
prefix?: string // default 'aooth:cred'
clock?: Clock
})Stateful Redis-backed store. Keys: aooth:cred:t:<token> (JSON state with PX TTL), aooth:cred:u:<userId> (token SET). persist fails loud on dead credentials. See Stores.
DenylistStoreRedis
new DenylistStoreRedis(opts: { redis: RedisLike; prefix?: string })Backing key aooth:dl:<jti> with PX TTL. cleanup() is a no-op — Redis self-evicts. See Stores.
RedisLike
interface RedisLike {
set(key: string, value: string, mode?: "PX", ttlMs?: number): Promise<string | null>;
get(key: string): Promise<string | null>;
del(...keys: string[]): Promise<number>;
exists(key: string): Promise<number>;
/** `PEXPIRE key ttlMs` — ttl is **milliseconds**, not seconds. */
expire(key: string, ttlMs: number): Promise<number>;
sadd(key: string, ...members: string[]): Promise<number>;
srem(key: string, ...members: string[]): Promise<number>;
smembers(key: string): Promise<string[]>;
}Structural Redis interface — only the 8 methods the adapters use. Works with ioredis, redis@4, etc. See Stores.
Subpath: @aooth/auth/atscript-db
import {
CredentialStoreAtscriptDb,
AuthCredentialRow,
AuthCredentialTable,
// Durable authorization-server stores (multi-pod) — see below.
PendingAuthorizationStoreAtscriptDb,
PendingAuthorizationTable,
AuthCodeStoreAtscriptDb,
AuthCodeTable,
} from "@aooth/auth/atscript-db";
import { AoothAuthCredential, AoothCredentialMetadataBase } from "@aooth/auth/atscript-db/model.as";
import { AoothPendingAuthorization } from "@aooth/auth/atscript-db/pending-authorization";
import { AoothAuthCode } from "@aooth/auth/atscript-db/auth-code";CredentialStoreAtscriptDb<TPayload>
new CredentialStoreAtscriptDb<TPayload>(opts: {
table: AuthCredentialTable<TPayload>
metadataField?: string // name of your @aooth.auth.metadata-annotated column
})There is no clock option — time-sensitive bookkeeping (TTL checks, opportunistic GC) reads Date.now() directly.
metadataField is the name of the consumer-declared credential-metadata column: the fully-typed @db.json field on your extends AoothAuthCredential model, marked @aooth.auth.metadata and resolved at boot by getAoothCredentialMetadataSpec (threaded here as plain config — same pattern as UserStore.handleFields). Shape the column as AoothCredentialMetadataBase & { ...your keys } — the exported .as type single-sources the framework-written envelope keys (see Stores). The store maps the envelope's metadata through that column on every write/read. Absent → metadata is not persisted/read (memory/JWT stores are unaffected).
Single-table stateful store. revokeAllForUser uses deleteMany({ userId }) — one round trip. retrieve GCs expired rows opportunistically. See Stores.
PendingAuthorizationStoreAtscriptDb / AuthCodeStoreAtscriptDb
new PendingAuthorizationStoreAtscriptDb(opts: {
table: PendingAuthorizationTable; // db.getTable(AoothPendingAuthorization)
clock?: Clock; ttlMs?: number; // default 15 min
})
new AuthCodeStoreAtscriptDb(opts: {
table: AuthCodeTable; // db.getTable(AoothAuthCode)
clock?: Clock; ttlMs?: number; // default 60 s
})Durable (multi-pod) implementations of the two authorization-server stores from @aooth/auth/authz — drop-in under the same PENDING_AUTHORIZATION_STORE_TOKEN / AUTH_CODE_STORE_TOKEN. Back them with the raw .as models (@aooth/auth/atscript-db/pending-authorization + …/auth-code). The grant's tokenPolicy persists as a JSON string (its payload is an open record a closed @db.json would reject). AuthCodeStoreAtscriptDb.consume is an atomic check-and-delete — it reads the row, then deleteOne(code), and only the caller whose delete reports deletedCount === 1 wins the row (single-use under a concurrent double-redeem / back-button replay), so no version column is needed. Both models carry nullable resource (RFC 8707) — and pending also clientName (consent display) — so run your schema sync after upgrading: connector clients always send resource, and an un-synced column fails at the /authorize insert. See Authorization Server.
DynamicClientStoreAtscriptDb
new DynamicClientStoreAtscriptDb(opts: {
table: DynamicClientTable; // db.getTable(AoothDynamicClient)
clock?: Clock;
})Durable DynamicClientStore (RFC 7591 registrations) over the @aooth/auth/atscript-db/dynamic-client model (AoothDynamicClient, table aooth_dynamic_clients) — long-lived rows, since a connector caches its client_id across grants. redirectUris/grantTypes/responseTypes persist as JSON-string columns (the tokenPolicy precedent — engine-portable; matching happens in DynamicClientPolicy after parse). deleteUnusedBefore is one mass delete over createdAt < cutoff && lastUsedAt unset (never-used GC); touch is a portable findOne + replaceOne. See Wiring — MCP connectors.
AuthCredentialRow<TPayload>
type AuthCredentialRow<TPayload extends object = object> = {
token: string;
userId: string;
issuedAt: number;
expiresAt: number;
kind?: string;
parentCredentialId?: string;
rotatedAt?: number;
sessionId?: string;
lastSeenAt?: number;
} & TPayload; // consumer's typed columns persist flat (replaces the dropped `claims` blob)Plain TS mirror of AoothAuthCredential.as (envelope columns) intersected with the consumer's typed payload columns. There is no metadata envelope column — credential metadata is consumer-declared (a fully-typed @db.json field on the extending model, marked @aooth.auth.metadata) and mapped dynamically through the store's metadataField option. kind is a free-form string (the .as model intentionally does not narrow it) so the row can carry magic-link discriminators like 'magic.recovery' alongside the orchestrator's 'access' / 'refresh'. See Stores.
AuthCredentialTable<TPayload>
interface AuthCredentialTable<TPayload extends object = object> {
insertOne(row: AuthCredentialRow<TPayload>): Promise<{ insertedId: unknown }>;
findOne(query: { filter: Record<string, unknown> }): Promise<AuthCredentialRow<TPayload> | null>;
findMany(query: {
filter?: Record<string, unknown>;
controls?: Record<string, unknown>;
}): Promise<AuthCredentialRow<TPayload>[]>;
/** Replaces by PK on the row itself — no wrapper. */
replaceOne(
row: AuthCredentialRow<TPayload>,
): Promise<{ matchedCount: number; modifiedCount: number }>;
/** Deletes by PK value (the token string). */
deleteOne(idOrPk: unknown): Promise<{ deletedCount: number }>;
deleteMany(filter: Record<string, unknown>): Promise<{ deletedCount: number }>;
}Structural interface — the subset of AtscriptDbTable the adapter uses. Apps cast their db.getTable(AoothAuthCredential) to this type. See Stores.
Subpath: @aooth/auth/authz
The framework-agnostic authorization-server layer (aoothjs AS an OAuth/OIDC provider for its own clients). Narrative + wiring: Authorization Server. The moost HTTP endpoints live in @aooth/auth-moost.
IdTokenSigner
class IdTokenSigner {
constructor(opts: IdTokenSignerOptions);
readonly issuer: string; // canonical (trailing slash stripped)
readonly alg: IdTokenAlg; // 'RS256' | 'ES256'
readonly kid: string;
sign(claims: IdTokenClaims): Promise<string>; // a signed id_token JWS
jwks(): Promise<{ keys: JWK[] }>; // public half, for GET /auth/jwks
}Signs OIDC id_tokens (RS256/ES256) and publishes the matching JWKS. Holds one asymmetric keypair (privateKey PKCS8 PEM, publicKey SPKI PEM); keys are imported lazily + cached, so construction is cheap and synchronous. issuer is canonicalised once so iss / discovery issuer / derived endpoint URLs are byte-identical (a relying party compares for exact equality). Never used for the access token. IdTokenSignerOptions: { issuer, kid, privateKey, publicKey, alg?='RS256', ttlSec?=300, clock? }.
LoopbackClientPolicy
class LoopbackClientPolicy implements ClientRedirectPolicy {
resolveClient(args: { clientId?; redirectUri; scope? }): ResolvedClient;
}Tier-1 policy: accepts any redirect_uri whose host is a loopback literal (127.0.0.1 / [::1] / localhost) on any port (RFC 8252), rejects everything else. Public client (no client_id/secret); PKCE is the binding.
RegisteredClientPolicy
class RegisteredClientPolicy implements ClientRedirectPolicy {
constructor(opts: { clients: RegisteredClient[] });
resolveClient(args: { clientId?; redirectUri; scope? }): ResolvedClient;
authenticateClient(args: { clientId?; clientSecret? }): void; // confidential → constant-time secret check
}Tier-2 policy: a static registry of first-party clients. Authorizes the client + redirect_uri against the registered allowlist, resolves granted scope (requested ∩ allowed) and what the grant delivers (id_token/access_token, aud = client_id). An unregistered client or unlisted redirect throws AuthorizeError.
CompositeClientPolicy
class CompositeClientPolicy implements ClientRedirectPolicy {
constructor(opts: {
loopback: ClientRedirectPolicy;
registered?: ClientRedirectPolicy;
dynamic?: ClientRedirectPolicy;
});
}Runs the tiers side by side, dispatching on the presence and ownership of client_id: absent → loopback; present → registered when its hasClient() knows the id (static-first — a dynamic registration can never shadow a static client), else dynamic. authenticateClient routes through the same picker. At least one of registered/dynamic is required; with both, registered must implement hasClient (validated at construction). Each sub-policy still owns its own redirect validation.
DynamicClientPolicy
class DynamicClientPolicy implements ClientRedirectPolicy {
constructor(opts: {
store: DynamicClientStore;
tokenPolicy?: TokenPolicy; // default { kind: 'dynamic-session', ttl: 30d }
allowedScopes?: string[]; // server-side bound: granted = requested ∩ this ∩ registration scope
clock?: Clock;
});
resolveClient(args: { clientId?; redirectUri; scope? }): Promise<ResolvedClient>;
authenticateClient(args: { clientId?; clientSecret? }): Promise<void>; // existence re-check; PKCE binds
hasClient(clientId: string): Promise<boolean>;
}Policy for RFC 7591 dynamically-registered public clients (MCP connectors) — the dynamic slot of CompositeClientPolicy. https redirects are exact-matched; loopback entries are port-agnostic (RFC 8252). Mints an access token only — no id_token. A registration deleted/GC'd between authorize and redemption fails closed at /token. See Wiring — MCP connectors.
DynamicClientRegistration
class DynamicClientRegistration {
constructor(opts: {
store: DynamicClientStore;
maxClients?: number; // default 1000 — reject-when-full, never evicts
unusedClientTtlMs?: number; // lazy GC of never-used registrations; omit = off
guard?: (args: { metadata: NewDynamicClient }) => void | Promise<void>;
validation?: ClientRegistrationValidationOptions;
clock?: Clock;
});
register(body: unknown): Promise<DynamicClient>; // validate → guard → GC → cap → persist
}The RFC 7591 registration operation behind POST {issuer}/register — @aooth/auth-moost's endpoint is a thin HTTP adapter over register(), and non-moost servers call it directly. Throws ClientRegistrationError on invalid metadata; the guard throws one to reject with a custom reason (any other throw is a server fault). validateClientRegistration(body, opts?) is also exported standalone.
Discovery / challenge builders
function buildAuthorizationServerMetadata(
opts: BuildAuthorizationServerMetadataOptions,
): AuthorizationServerMetadata;
function buildProtectedResourceMetadata(
opts: BuildProtectedResourceMetadataOptions,
): ProtectedResourceMetadata;
function buildWwwAuthenticateBearerChallenge(opts?: WwwAuthenticateBearerChallengeOptions): string;
function canonicalizeIssuer(issuer: string): string; // trailing-slash strip (RFC 8414 exactness)Pure, framework-free builders for the RFC 8414 AS-metadata document, the RFC 9728 protected-resource document, and the RFC 6750 WWW-Authenticate: Bearer … header value (every value sanitized — control characters stripped, quotes escaped). Re-exported by @aooth/auth-moost; consumers use them to mount the root-level discovery forms a prefix-mounted controller can't serve. See Root-mounted discovery.
OidcClaimsResolver / NoopOidcClaimsResolver
abstract class OidcClaimsResolver {
abstract resolveClaims(
userId: string,
scope: string | undefined,
): Record<string, unknown> | Promise<Record<string, unknown>>;
}
class NoopOidcClaimsResolver extends OidcClaimsResolver {} // emits {} → sub-only id_tokenSupplies the profile claims (email/email_verified/name/…) for an id_token — the part that depends on the consumer's user shape. The registered claims (iss/aud/sub/iat/exp/nonce) are owned by the controller. Honour the granted scope with scopeGrants.
PendingAuthorizationStoreMemory / AuthCodeStoreMemory / DynamicClientStoreMemory
class PendingAuthorizationStoreMemory extends PendingAuthorizationStore {}
class AuthCodeStoreMemory extends AuthCodeStore {}
class DynamicClientStoreMemory extends DynamicClientStore {}In-memory implementations of the server-side stores (for tests / single-process). AuthCodeStore.consume() is single-use + atomic. DynamicClientStore is the long-lived RFC 7591 registration store (create/get/delete/count/touch/deleteUnusedBefore — GC targets never-used rows only). A multi-pod deployment swaps the durable …AtscriptDb adapters (see the @aooth/auth/atscript-db subpath) under the same DI tokens.
Functions
scopeGrants(scope: string | undefined, claim: string): boolean—truewhen the space-joinedscopegrantsclaim("email"/"profile"/ …).isLoopbackRedirectUri(uri: string): boolean— the host checkLoopbackClientPolicyuses.
Types
TokenPolicy—{ kind?, ttl?, payload? }. What the grant mints, decided by the policy (never the client request) and recorded at/authorizetime.payloadcarries@arbac.attenuate.*fields for a scoped token; omit for full authority.ResolvedClient— the policy's verdict:{ clientId?, clientName?, redirectUri, tokenPolicy, scope?, idToken?, accessToken?, audience? }.clientNameis untrusted display text for the consent prompt (rendered as text, paired with the validated redirect host).RegisteredClient—{ clientId, clientName?, redirectUris?, redirectPrefixes?, type?='public', clientSecret?, idToken?=true, accessToken?=false, scopes?, tokenPolicy? }.DynamicClient/NewDynamicClient— a stored RFC 7591 registration:{ clientId, clientName?, redirectUris, tokenEndpointAuthMethod: 'none', grantTypes, responseTypes, scope?, createdAt, lastUsedAt? }(lastUsedAtunset = never used, the GC target).ClientRegistrationValidationOptions— registration caps:{ maxRedirectUris?=5, maxRedirectUriLength?=512, maxClientNameLength?=128, maxScopeLength?=256, allowedScopes? }.IdTokenClaims—{ sub, aud, nonce?, ttlSec?, extra? }(theextramap is the resolver's profile claims).IdTokenAlg—'RS256' | 'ES256'.ClientRedirectPolicy— the policy interface (resolveClient+ optionalauthenticateClient+ optionalhasClient— the known-ness probeCompositeClientPolicydispatches by).PendingAuthorizationStore/AuthCodeStore/DynamicClientStore— the abstract store contracts. A pending-authorization record carries abindingsecret (the value of theaooth_authzbrowser-binding cookie), plusclientName?(consent display, snapshot at authorize) andresource?(RFC 8707, consistency-checked at/token); the auth-code record carriesresource?too. A custom durable store must round-tripbinding, or the consent gate's binding check fails closed. See Consent gate & browser binding.AuthorizationServerMetadata/ProtectedResourceMetadata(+ theBuild…Optionsinputs) andWwwAuthenticateBearerChallengeOptions— the builders' wire-document / option shapes.
Errors
AuthorizeError(code: AuthorizeErrorCode) —'invalid_request' | 'invalid_client' | 'invalid_grant' | 'invalid_redirect' | 'access_denied' | 'unauthorized_client' | 'invalid_target' | 'server_error'. Thrown by the policies; mapped to the OAuth error responses by the controller.invalid_targetis RFC 8707's code (repeated/oversizedresource, or a/token-leg mismatch).ClientRegistrationError(code: ClientRegistrationErrorCode) —'invalid_redirect_uri' | 'invalid_client_metadata', the RFC 7591 §3.2.2 vocabulary;messagebecomes the response'serror_description. Thrown byvalidateClientRegistration/DynamicClientRegistration.register(and by aguardto reject a registration).