Proposed owner: @askrjs/auth/server. This document specifies the contract for
askr-auth#66 and gates provider implementations. It is the implementation contract; implementation and provider qualification
are tracked separately. It makes no runtime or live-provider qualification claim. Auth#64–71, Server#76, Core#777, CLI#185 and Examples#32 are
all in the maintainer-approved 0.5.0 scope. Publication remains held for full
maintainer review.
Auth owns confidential OIDC exchange, token validation, transaction correlation,
sealed session state, atomic retirement and token renewal. Server owns the
reserved /auth routes, Web transport, CSRF, safe redirects and typed problem
responses. Core owns the minimal client identity cache, route policies,
hydration continuity, cross-tab invalidation and private data retirement.
Applications own login UI, durable users, account linking and authorization.
No vendor SDK is required. Static hosting without a server is unsupported.
The API block below records the exact transport-facing method signatures. One createProviderSession(options) creates an authority and
implements the existing AuthResolver.resolve. It also supplies:
login(request, {provider, returnTo, resource?, scopes?}): Promise<Response>;callback(request, {provider}): Promise<Response>;logout(request, {provider?, providerLogout?, returnTo?, revoke?}): Promise<Response>;token(request, provider, {resource?, scopes, signal?}): Promise<string>;finalize(request, response): Promise<Response>andclose(): Promise<void>.
origin is an explicitly configured canonical HTTPS origin with no credentials,
path, query or fragment. It is never inferred from Host or forwarded headers.
Each provider has a unique conservative path-segment id. Its redirect URI is
exactly ${origin}/auth/callback/${id}. An unsupported origin, duplicate provider,
unknown capability, invalid resource/scope policy, weakly encoded key or unsafe
post-logout destination fails at setup. Tests may inject fetch and a clock;
production transport still validates configured origins and endpoints. Synchronous
setup validates configuration, not a future discovery response: generic optional
providerLogout/revocation capabilities require their explicit endpoints. Presets
may declare their documented discovery operation, but a missing/malformed remote
endpoint fails safely as configuration before that operation, never as silent
local-only success or an interaction-required login/consent outcome.
resolve returns a constructed AuthContext, never the private session record
or provider token response. Its session is only {id, subject, expiresAt}.
Its public principal is a minimal identity keyed by issuer and subject, not
email and not arbitrary provider claims. The default browser snapshot contains
only authenticated, principal: {id, subject?}, tenant and normalized scopes;
session is omitted. AuthContext.scopes and snapshot scopes contain only the
current provider's confirmed identity/authentication scopes from the configured
identity policy (excluding offline_access); they never flatten API consent
grants, resource scopes or other providers. API authority is exclusively checked
by token(provider, {resource, scopes}). Equal scope names on different resources
cannot become interchangeable through requireScope or hydration. Provider mode
currently supports identity and those configured validated scopes only.
It does not resolve durable app users, roles or permissions; role/permission
predicates deny absent grants. Existing manual mode retains app-owned enriched
identity/policy. App-owned server policies/actions can consult their own domain
store. A generic Principal spread or an implicit serializer/enrichment hook is
not supported. A provider's
unsigned profile hint is not an authenticated claim. Optional server-only
onSignIn receives the validated principal plus a separately typed bounded
unsigned name hint. It lets applications persist first-login Apple profile data
without placing it in the public snapshot or treating it as verified identity.
The callback receives a signal and is fenced before/after await; app-owned side
effects must honor it. It cannot replace the principal, skip validation, or
return token/session state.
Provider definitions are branded, frozen shared-engine configuration. Only
createOidcProvider and the four presets produce valid definitions; copying or
spreading a definition loses its private policy ownership and is rejected at
setup. A private WeakMap binds each definition to issuer/claim/normalization
policies; no public callback can replace protocol validation. This policy
fingerprint is stable across workers, excludes secret values, and is checked
when loading a stored session; changed policy cannot reuse an old grant. Generic OIDC uses
one exact issuer; Google's two issuer spellings are preset-private and are
canonicalized to the configured HTTPS issuer for principal/cache identity.
Auth0 definitions without organization configuration reject unexpected org_id
or org_name claims. With an expected organization id/name, validate every
present corresponding claim; an org_name cannot be accepted based only on an
org_id match if its expected name was not configured. Require the expected name
when the provider returns one, rather than guessing it or treating an authorize
parameter as validation. Auth0's
organization id/name check, Google's hd check, and Apple's boolean normalization
and unsigned profile hint are private policies applied after the shared signed
ID-token validation. Keep generic low-level createOidcClient compatibility by
extending private helpers for response mode, secret factories, refresh, issuer
allowlists and claim checks; do not fork JWT validation or widen every caller's
trust. The factory is the demonstrated generic-OIDC entry, not an alias for a
preset. Trusted optional endpoint overrides are setup-only HTTPS URLs without
credentials/fragments; discovery issuer still must match. Provider fetches that
carry secrets/tokens reject redirects. Browser mocks intercept provider HTTP;
they do not add a production 'skip validation' switch.
Provider definitions are thin shared-engine configuration. They declare issuer,
client id/authentication, identity scopes, response mode, authorization extension
parameters and capabilities. Shared configuration includes explicit, bounded
issuer aliases/claim restrictions, resource-to-scope policy and endpoint options
where provider discovery does not advertise an operation. The default generic
OIDC validator remains exact-issuer. Google alone supplies its two documented
issuer spellings. Presets cannot replace signature/state/nonce validation.
Future client-authentication methods require a new discriminated method, not
an arbitrary string or a protocol-parameter override. A secret factory receives
only the operation signal and Unix-millisecond now; Apple's ES256 secret is
created server-side through the existing signer.
Login and successful callback return a 303 redirect. Logout without provider
continuation returns 204; an explicitly requested supported provider logout may
return 303. Each method stages its cookie changes in engine-owned request state.
The host MUST call finalize as the last boundary for every response, including
errors and provider-route responses. finalize clones headers when necessary,
is idempotent for the same request/response ownership, and rechecks captured
identity epoch, record revision and engine lifetime before attaching pending
cookies. It preserves unrelated application Set-Cookie values and removes only
engine-owned stale issuance. It never slides absolute session expiry.
ProviderSessionError has safe codes configuration, invalid-callback,
invalid-transaction, session-oversize, capacity, store-unavailable, provider-unavailable, and
closed. The message identifies corrective action without input values.
InteractionRequiredError has code: interaction-required, reason: login | consent, provider id, resource and required scopes. Server maps it to a 401
problem with those allowlisted fields. Configuration/capacity/store failures
remain distinct and are not mislabeled as login or consent. Invalid callback or
transaction is a bounded 400 failure with the transaction cookie cleared.
Never attach raw upstream response bodies, query/form input, tokens, secrets or
provider errors as an Error message, cause, enumerable property, log field or
problem detail. Internal diagnostics may record an operation, safe category and
HTTP status. Network/timeout/malformed upstream transport failures use a static
provider-unavailable error, distinct from store-unavailable; neither becomes
interaction-required. Caller cancellation uses a fixed-message AbortError.
Cancellation must settle callers promptly and cannot commit later.
The supplied request signal participates in provider fetch cancellation; joining
one renewal must not transfer ownership of the shared operation to a single
joining caller. close aborts this engine's pending work and forbids new work.
Closing a worker does not globally log out other workers using a shared store.
Deduplicated renewal belongs to the engine and current identity: each token
caller races its own request/option signal without aborting renewal for other
waiters. Identity retirement or engine closure cancels the shared operation.
The default is an AES-GCM encrypted __Host-askr-session cookie plus a bounded
in-process authority. It is NOT a stateless deployment mode. Cookie ciphertext
may contain the private envelope; page JavaScript never receives plaintext
tokens. In store mode, the cookie contains only an encrypted opaque locator and
identity epoch; the sealed private envelope lives in the configured store.
Both modes use the same state transitions and response finalizer.
Session keys are explicit base64url encodings of 32 cryptographically random
bytes, current first then previous keys. Use a new random 96-bit IV for every
AES-GCM seal, an explicit algorithm allow-list, a versioned envelope and AAD
binding its purpose, cookie name and configured application origin. Old keys
only decrypt; new writes use the current key. Tampered/unknown-key/expired
cookies resolve anonymously and cannot enroll their payload into authority.
The serialized cookie has a 3,800-byte UTF-8 budget including its attributes.
Oversize is a typed session-oversize error recommending a configured store,
not truncation or a fallback to unsigned storage. Session expiry defaults to
8 hours, measured from the successful code exchange start; cache hits and
renewal never move it forward. Session maxAgeSeconds is an integer 1..2,592,000 (30 days); leewaySeconds is
0..300 (default 30) and only shortens usable access-token life. maxEntries is an
integer 1..1,000,000. Secrets contain 1..4 keys. Transaction TTL is an integer
1..600. These are setup-validated bounds, not inferred entropy guarantees.
The authority retains the latest sealed payload, identity epoch, monotone revision and current refresh grant state, not just a set of revocation ids. Concurrent requests with an older cookie revision for the SAME active identity epoch can use the latest authority state. That prevents a second rotation after the first refresh has completed. A cookie from a different identity epoch never resolves to the replacement user's identity. A retired id/epoch is never reactivated by a cookie or a delayed request. The default capacity is 10,000 records; expired entries are pruned, but unexpired active records and retirement or consumed-transaction tombstones are not evicted to make room. Capacity fails closed. Restart/close loses the default authority: old cookies/transactions are then rejected, and users sign in again. Sharing encryption keys alone does not make multiple processes share this authority.
The new ProviderSessionStore is distinct from the existing read-only
SessionStore.get auth lookup API. It exposes read, compareAndSwap, and
withLock. Its opaque record contains revision, absolute expiry, retired flag
and engine-sealed value. CAS is atomic and linearizable, including creation
(expected revision null). Revisions only increase; retirement leaves a tombstone
until the maximum credential/transaction expiry plus accepted clock skew.
An old revision cannot recreate a retired or removed record. Store adapters
must not silently fall back to process memory after a connection failure.
The authority:<id> lock serializes final cached/renewed access-token validation
and synchronous delivery staging with logout and identity replacement. A store
read may capture a record before its promise resolves; a last read alone cannot
fence retirement. If retirement wins this lock, delivery fails. If delivery wins,
its validation and staging finish before retirement can proceed. Logout cannot
recall a token whose delivery already won this ordering.
withLock serializes the SAME refresh-token grant family across resources and
scope sets, while the acquisition promise cache deduplicates each exact token
key. The adapter MUST preserve exclusivity until the operation callback settles,
including the whole provider exchange. A lease that may expire while the
callback is still running does not implement this interface. Merely rejecting
withLock after the callback already performed exchange or CAS is insufficient.
Coordinate retries/worker failure without overlapping callback execution. CAS fences commits; CAS
alone does not stop two provider refresh requests or prevent token-family reuse.
If lock ownership or an exchange outcome becomes uncertain, fail closed and
require interactive recovery rather than retrying the old refresh token. No
built-in distributed adapter is claimed qualified in 0.5.0. Adapter tests must
run two independent engine instances sharing actual atomic test-store state.
| Deployment | Callback single use | Logout/replacement fencing | Refresh serialization | Restart continuity |
|---|---|---|---|---|
| Default one engine/process | Atomic in-process consume | Shared in-process epoch + CAS | In-process grant-family lock | No; old authority rejected |
| Multiple independent defaults, same keys | Not supported | Not shared | Not shared | Not supported |
| Shared atomic store and exclusive grant-family lock | Shared CAS | Shared epoch + CAS | Shared lock plus current payload | Yes, subject to store durability |
| Pure encrypted stateless cookie | Not offered | Cannot guarantee retirement | Cannot guarantee rotation ownership | Not a supported mode |
The finalizer prevents stale cookies attached after retirement; it cannot recall HTTP bytes already handed to the network. A previously emitted response may arrive late and overwrite the browser cookie. Authority must still reject its retired epoch, so it cannot resurrect identity. Do not claim wire-order control or that logout revokes an already-issued upstream access token. Local logout, provider logout and upstream revocation are different operations.
Each authorization stores random state, nonce, S256 verifier/challenge, provider and client/issuer, validated returnTo, requested resource/scopes, creation/expiry, an authority record id and the captured login/identity epoch. Default TTL is 300 seconds and maximum is 600. It uses an encrypted, Secure, HttpOnly provider- scoped transaction cookie, Path=/ and no Domain; its name has the __Host- prefix. A small unauthenticated session-authority envelope can establish the one app cookie on first login. That stable authority correlates pending flows across providers without introducing a second application session cookie.
Starting a newer login advances the authority's pending-login generation and retires its superseded transactions while keeping the current identity active until a replacement succeeds. Each transaction captures that generation. A successful callback atomically checks it and advances the identity epoch with the new account/token state. Concurrent older callbacks cannot replace the new identity. Logout retires the authority and all captured pending flows before revocation or a provider redirect. A form_post transaction carries the captured authority/epoch because the Lax application session cookie may be absent on the cross-site POST. Absence of that Lax cookie must not erase replacement fencing.
A valid encrypted transaction record is atomically consumed BEFORE any code
exchange, including provider denial and validation failure. Every callback
clears its transaction cookie; replaying a copied cookie is rejected by CAS,
including two callbacks arriving concurrently. Missing or undecryptable cookies
cannot initiate exchange. Validate method, bounded content type/body size,
duplicate protocol fields, a 32-KiB form-body budget, provider, state and expiry before using code/nonce. Denial/malformed provider callbacks cannot preserve a
usable transaction. If a transaction has insufficient remaining lifetime when
exchange completes, reject it; a slow exchange never extends its TTL.
Callbacks cannot take arbitrary redirect destinations or extension parameters.
The validated local returnTo may retain its query and fragment for the final
application redirect; neither becomes part of the provider redirect_uri.
Query-mode transactions use SameSite=Lax and accept GET only. Form-post
transactions use SameSite=None;Secure and accept only a bounded
application/x-www-form-urlencoded POST. Only that transaction cookie changes
SameSite; the application session stays Lax. Form_post callback is the narrow
exception to unsafe same-origin Origin enforcement, protected by its explicit
provider/method, encrypted browser correlation, single-use state and nonce.
No blanket CSRF exception for /auth, generic POST or every callback method.
If browser privacy policy blocks the correlation cookie, fail with an explicit
transaction error; never accept state without its browser-bound cookie.
All flows generate PKCE/state/nonce and exercise them with signed simulated providers. Provider-enforced PKCE is a separate capability that MUST be evidenced, not inferred because a server sent code_challenge. Apple's current public request/token documentation does not establish PKCE enforcement; record this as a live-qualification limitation, retaining state, nonce, confidential client authentication and one-use transaction protection. Do not claim PKCE validation by Apple without provider evidence or silently remove generated PKCE fields.
An API token request without resource selects the provider's sole explicitly
configured capabilities.resources target. Zero or multiple configured targets
produce a configuration error; scopes never infer a target and array order never
selects one. Canonicalize that resource before cache lookup, locking and every
interaction-required outcome. Login without a resource remains identity-only;
incremental API consent supplies an explicit resource.
The exact cache key includes configured provider id and a stable non-secret policy fingerprint (canonical issuer/aliases, client, organization/connection constraints, resources/scopes and private policy version), then issuer + client + subject/account + resource + canonical sorted unique scopes. The grant and any retained refresh token are separately bound to the authenticated identity and configured provider. Unknown resources, unsupported scopes or capabilities fail configuration validation; an allowed but not yet granted resource/scope set produces consent interaction. Identity scope tokens never satisfy an API token request. Do not return an ID token as an access token, broaden a cache key or reuse another account's grant. Token-type and expiry fields must validate before caching. Access expiry is exchange-start plus finite expires_in, with bounded leeway; cache hits never extend it.
Freshness checks run before and after awaited work, including consent exchange, provider discovery, JWKS lookup, secret factories, refresh and cookie sealing. A lost session epoch produces login interaction and cannot write refreshed data. Code exchange still requires a signed ID token and its original nonce. Refresh may omit an ID token and then retains the existing authenticated principal. If refresh returns one, validate its signature, issuer, audience/azp, subject and expiry against the same original account; normally its nonce is absent, and a present nonce must equal the original. A refreshed ID token cannot change the account or grant policy. Reuse shared private JWT validators, but never weaken code-exchange nonce requirements to accommodate refresh.
Provider invalid_grant/login_required/consent_required outcomes are mapped to safe typed interaction only; provider text is never exposed. Network/store errors are distinguishable from required user interaction. There are no silent iframe flows, automatic authorization retries or perpetual refresh loops.
Every valid logout locally retires the whole application identity. The
provider field only selects that identity's grant for optional operations; it
does not request a redirect. providerLogout:true separately requests supported
RP-initiated continuation AFTER local retirement; default false returns 204.
revoke:true separately revokes each unique selected refresh grant family (or
the provider's supported access-token revocation when no refresh grant exists).
With no selector, use the current identity provider and all its resource grants.
A configured list of providers does not imply linked identities: replacement
login retires the previous identity and its private grants; account linking is
out of scope. A foreign selector is a configuration error.
Google/Apple {provider, revoke:true} therefore means local logout + revocation
without an unsupported provider redirect. Unsupported providerLogout or
revocation requests fail validation before remote work; local-only logout
always remains available. A remote failure never reverses completed retirement. Post-logout redirect options are resolved only against a configured
same-origin path allow-list (default /); no arbitrary absolute callback or
returnTo is sent upstream. Local retirement survives upstream revocation failure.
All four presets remain pending live qualification under Auth#71 until provider-specific packed-version/commit/browser/SSR+SPA evidence is recorded. The table specifies implementation targets, not a present support claim.
| Preset | Callback/client authentication | Identity and API separation | Renewal | Logout/revocation |
|---|---|---|---|---|
| Auth0 | query; confidential secret | Exact tenant/custom-domain issuer; organization claim checked; explicit audience + scopes | Configured rotating refresh grant; serialize whole family | Explicit OIDC logout; refresh-token revocation |
| Microsoft | query; confidential Web client secret | Exact tenant GUID issuer only; Graph resource/scopes separate; no common/organizations/consumers authority | Replace stored refresh token after use; serialize whole grant | Tenant end_session_endpoint; no generic revocation promise |
| query; web client secret | Exactly two issuer spellings; enforce hd claim; identity scopes separate from configured API consent | offline first-consent behavior; missing refresh means explicit re-consent | Local logout only; token revocation | |
| Apple | form_post; server ES256 JWT secret | Services ID audience; unsigned first-login user hint separate; no API bearer capability | Account-validity check no more than once/day | Local logout only; token revocation |
Microsoft's documentation says replacement refresh tokens do not automatically revoke old ones; do not describe it as Auth0-style theft/reuse detection. Auth0's reuse interval can tolerate particular network races, but is not a distributed session/logout authority or a replacement for a shared refresh lock. Apple client-secret lifetime must stay within its provider limit, and the generated secret cache uses a short expiry; Apple private-key/profile handling remains server-side. Each preset has explicit setup and simulated-provider acceptance.
createAskrApp.auth is an exclusive union. Provider mode accepts providers,
session and explicit origin; it creates the one engine and resolver. Existing
manual mode retains {resolver, routes?, pages?}. Combining either mode's fields
is rejected at setup and in strict types. Standalone registerAuthRoutes remains
for existing email/password apps. Provider mode does not auto-register its JWT
cookie routes or compose two resolvers. Combining local credentials and provider
login in one session is not advertised until a shared credential entry contract
exists; applications may stay on existing manual auth meanwhile.
Server's ServerAuthContext extends AuthContext with a required non-enumerable
token(provider, options) bound to the exact Request. Provider mode delegates
to the engine; manual mode throws a safe configuration error. Authentication
JSON/hydration serializers never spread this object. Cookie-authenticated unsafe
requests require exact configured-origin Origin, with only the correlated
form_post exception above. /auth/session has no-store, explicit minimal JSON
and no tokens; /auth/logout is POST only. API/page/action errors pass through
the same safe interaction mapping and final response boundary.
Core's binding is owned by @askrjs/askr/router, avoiding an Auth → Core cycle.
It must supply the existing RouteAuthOptions and login/logout helpers, use the
SSR sanitized snapshot for first hydration and later client navigations, dedupe
GET /auth/session until explicit invalidation, and attach cancellation/owner
fences. After logout, interaction loss or cross-tab notification, invalidate
identity and private data BEFORE acquiring/rendering another identity. Retire
only provider-binding-owned data runtimes and use #754/#756 ownership guards;
never globally reset unrelated roots or reuse a disposed singleton. Boot can
allocate an owned runtime for the opted-in binding. A supplied runtime needs
explicit ownership validation and cannot silently be shared across unrelated
identities. The binding has no static provider tokens, secrets or server imports. Core's
own focused tests do not introduce a Core → Server dev dependency cycle; the
actual packed Core + Auth + Server SSR/SPA native acceptance runs downstream in
Examples32. Cross-site form_post proof uses owned TLS app/provider sites and
real browser cookie handling, not fabricated Cookie headers or mocked flags.
Cross-tab messages contain only an invalidation event, never identity, token,
session id or principal data. BroadcastChannel is primary; storage-event plus
pageshow/visibility session revalidation is the documented fallback. Receivers
clear private data and refetch the same server session. A received event is not
authentication evidence. currentAuth() consumers must update observably, and
binding-owned listeners/requests stop with their app lifetime.
The contract/source must be reviewed and committed before preset code. Engine RED→GREEN tests cover real signed artifacts, both callback methods, copied-cookie concurrent replay, denial/invalid-state consumption, expiry/key rotation/size, resource/account separation including equal scope spellings in two resources, identity-only snapshot scopes, whole-grant rotation, two-engine shared-store coordination, pending renewal vs logout/replacement, stale finalization and restart rejection. A delayed already-emitted cookie must fail authentication. Use exact secret/token canaries against response bodies, hydration, errors/logs and browser bundle contents. Carry server-relevant regressions from Auth65's removed tests before deleting them.
Server integration uses real createAskrApp and simulated provider HTTP. Native Chromium/Firefox/WebKit run SSR AND SPA for every provider, including genuinely cross-site form_post, no hydration refetch, denied policy, consent, renewal/loss, logout and cross-tab private data clearing. Packed normal installs use actual TS6/7 and Node floor/current, import server entries without window/document, reject removed browser/auth0 exports and SDK dependencies, and inspect actual client chunks for server modules/secrets. Normal npm ci and lifecycle final pack, fmt/check/coverage and existing performance gates remain unchanged.
Auth71 live execution is separate: missing credentials never blocks engine, presets, scaffolds, examples or simulated qualification. Record pending entries for missing credentials and exact required setup; never substitute simulated passes or invent provider evidence. A provider/pipeline change invalidates its prior live pass. Tokens/keys/user data are never stored in qualification records.
These are protocol/provider references, not claims that implementation exists: OIDC Core errata 2, OAuth Security BCP RFC9700, current browser-apps RFC10017, Auth0 rotation, Auth0 OIDC logout, Microsoft refresh, Google OIDC, Google Web OAuth, Apple authorization, Apple account verification, cookie attributes.
The atomic store, cookie budget, default TTL/capacity, strict response finalizer, minimal serialization and deployment limitations are explicit Askr design choices. They require the above implementation/native tests; references alone are not acceptance evidence.
// Public owner: @askrjs/auth/server. Helper shapes below are private unless exported.
import type { AuthContext, AuthResolver, Principal } from "@askrjs/auth";
interface ProviderTokenRequest {
readonly resource?: string;
readonly scopes: readonly string[];
readonly signal?: AbortSignal;
}
interface ProviderLoginRequest {
readonly provider: string;
readonly returnTo: string;
readonly resource?: string;
readonly scopes?: readonly string[];
}
interface ProviderLogoutRequest {
readonly provider?: string; // Optional grant selector; defaults to the current identity provider.
readonly providerLogout?: boolean; // Default false; independent of local retirement/revocation.
readonly returnTo?: string; // Only used for explicit providerLogout continuation.
readonly revoke?: boolean; // Default false; revoke each unique selected grant family.
}
type ProviderClientAuthentication = {
readonly method: "client_secret_post" | "client_secret_basic";
readonly secret:
| string
| ((context: {
readonly now: number;
readonly signal: AbortSignal;
}) => string | PromiseLike<string>);
};
interface ProviderCapabilities {
readonly renewal: "refresh" | "account-check" | "none";
readonly incrementalConsent: boolean;
readonly providerLogout: boolean;
readonly revocation: boolean;
readonly resources: readonly { readonly resource: string; readonly scopes: readonly string[] }[];
}
// Presets return this shared definition; provider-specific claim/parameter policies
// remain engine-owned, not user overrides of protocol-managed parameters.
declare const providerDefinitionBrand: unique symbol;
export interface ProviderDefinition {
readonly [providerDefinitionBrand]: true;
readonly id: string;
readonly issuer: string;
readonly clientId: string;
readonly clientAuthentication: ProviderClientAuthentication;
readonly responseMode: "query" | "form_post";
readonly identityScopes: readonly string[];
readonly capabilities: ProviderCapabilities;
readonly authorizationParams?: Readonly<Record<string, string>>;
}
export interface OidcProviderOptions {
readonly id: string;
readonly issuer: string; // One exact issuer; aliases are private preset policy only.
readonly clientId: string;
readonly clientAuthentication: ProviderClientAuthentication;
readonly identityScopes?: readonly string[];
readonly responseMode?: "query" | "form_post";
readonly capabilities?: Partial<ProviderCapabilities>;
readonly authorizationParams?: Readonly<Record<string, string>>;
readonly endpoints?: {
readonly authorization?: string;
readonly token?: string;
readonly jwks?: string;
readonly logout?: string;
readonly revocation?: string;
}; // Trusted explicit HTTPS URLs, no credentials/fragments; never request-supplied.
}
export declare function createOidcProvider(options: OidcProviderOptions): ProviderDefinition;
// Existing @askrjs/auth SessionStore remains the read-only auth lookup contract.
// This new owner is intentionally distinct: atomics/locking are mandatory.
export interface ProviderSessionRecord {
readonly revision: number;
readonly expiresAt: number;
readonly retired: boolean;
readonly value: string | null; // Engine-sealed opaque value, never a principal/token object.
}
export interface ProviderSessionStore {
read(id: string, options: { signal: AbortSignal }): Promise<ProviderSessionRecord | null>;
compareAndSwap(
id: string,
expectedRevision: number | null,
next: ProviderSessionRecord,
options: { signal: AbortSignal },
): Promise<boolean>;
withLock<T>(
key: string,
operation: () => Promise<T>,
options: { signal: AbortSignal },
): Promise<T>;
}
export interface ProviderSessionOptions {
readonly origin: string; // Explicit canonical HTTPS origin; never inferred from Host/forwarded headers.
readonly providers: readonly ProviderDefinition[];
readonly session: {
readonly secrets: readonly string[]; // Current then previous base64url-encoded 32-byte keys.
readonly store?: ProviderSessionStore; // Presence selects opaque-id cookie + shared sealed storage.
readonly maxAgeSeconds?: number; // Integer 1..2,592,000, default 28,800; absolute.
readonly leewaySeconds?: number; // Integer 0..300, default 30; cannot extend expiry.
readonly maxEntries?: number; // Integer 1..1,000,000, default 10,000; fail closed when full.
};
readonly onSignIn?: (context: {
readonly provider: string;
readonly principal: Readonly<Principal>;
readonly profileHint?: { readonly givenName?: string; readonly familyName?: string };
readonly signal: AbortSignal;
}) => void | PromiseLike<void>; // Server-only; profileHint is unsigned/untrusted, never identity.
readonly postLogoutRedirects?: readonly string[]; // Exact same-origin relative paths; default ["/"].
readonly transactionTtlSeconds?: number; // Default 300, maximum 600.
readonly fetch?: typeof fetch;
readonly clock?: () => number; // Unix milliseconds.
}
export interface ProviderSession extends AuthResolver {
resolve(request: Request, options?: { signal?: AbortSignal }): Promise<AuthContext>;
login(request: Request, options: ProviderLoginRequest): Promise<Response>;
callback(request: Request, options: { readonly provider: string }): Promise<Response>;
logout(request: Request, options?: ProviderLogoutRequest): Promise<Response>;
token(request: Request, provider: string, options: ProviderTokenRequest): Promise<string>;
finalize(request: Request, response: Response): Promise<Response>;
close(): Promise<void>;
}
export declare function createProviderSession(options: ProviderSessionOptions): ProviderSession;
export type ProviderSessionErrorCode =
| "configuration"
| "invalid-callback"
| "invalid-transaction"
| "session-oversize"
| "capacity"
| "store-unavailable"
| "provider-unavailable"
| "closed";
export declare class ProviderSessionError extends Error {
readonly code: ProviderSessionErrorCode;
constructor(code: ProviderSessionErrorCode); // Static safe message by code; no raw message/cause option.
}
export declare class InteractionRequiredError extends Error {
constructor(options: {
readonly reason: "login" | "consent";
readonly provider: string;
readonly resource?: string;
readonly scopes: readonly string[];
});
readonly code: "interaction-required";
readonly reason: "login" | "consent";
readonly provider: string;
readonly resource?: string;
readonly scopes: readonly string[];
}For reusable method arguments, provider capabilities and client authentication,
derive types from the retained ProviderSession and ProviderDefinition owners.
Preset options derive from each factory's Parameters<typeof createProvider>[0].
See the nine-name migration;
these helper shapes are not standalone public exports.