@molecule/app-auth
v1.1.0
Published
Client-side authentication interface for molecule.dev
Maintainers
Readme
@molecule/app-auth
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Client-side authentication interface for molecule.dev.
Provides a unified auth API that works across different auth strategies
(JWT, session, OAuth, etc.): createJWTAuthClient builds a fetch-based
client; setClient/getClient bond it; login/logout/register/
isAuthenticated/getUser delegate to the bonded client. Framework
bindings (e.g. AuthProvider/useAuth in @molecule/app-react) wrap the
same client.
Quick Start
import { createJWTAuthClient, setClient } from '@molecule/app-auth'
const client = createJWTAuthClient({ baseURL: '/api' })
setClient(client)
await client.initialize() // restore the session from the httpOnly cookie
// anywhere in the app
import { getUser, isAuthenticated, login, logout } from '@molecule/app-auth'
await login({ email, password })
if (isAuthenticated()) console.log(getUser()?.email)Type
core
Installation
npm install @molecule/app-auth @molecule/app-bond @molecule/app-i18n @molecule/app-loggerAPI
Interfaces
AuthClient
Auth client interface that all auth bond packages must implement. Provides login/logout/register flows, token management, profile updates, and auth state subscription.
interface AuthClient<T = UserProfile> {
/**
* Returns the current authentication state snapshot.
*/
getState(): AuthState<T>
/**
* Returns whether the user is currently authenticated.
*/
isAuthenticated(): boolean
/**
* Gets the current user.
*/
getUser(): T | null
/**
* Updates the cached user object (state + persistent storage) without
* hitting the network. Intended for local refreshes after a per-app
* mutation (e.g., the user just PATCHed their own profile and the
* server returned the canonical row). Does NOT change tokens.
*/
setUser(user: T | null): void
/**
* Gets the current access token.
*/
getAccessToken(): string | null
/**
* Stores the access token in the configured token storage adapter (in-memory
* by default). Use this to seed the token after an out-of-band exchange (e.g.
* the OAuth code→token redirect) instead of writing to `localStorage` directly,
* which would violate the in-memory-default storage contract and make the bearer
* token JS-readable (XSS-exfiltratable). Pass `null` to clear it.
*/
setAccessToken(token: string | null): void
/**
* Gets the refresh token.
*/
getRefreshToken(): string | null
/**
* Logs in with credentials.
*/
login(credentials: LoginCredentials): Promise<AuthResult<T>>
/**
* Logs out the current user.
*/
logout(): Promise<void>
/**
* Registers a new user.
*/
register(data: RegisterData): Promise<AuthResult<T>>
/**
* Refreshes the access token.
*/
refresh(): Promise<AuthResult<T>>
/**
* Requests a password reset.
*/
requestPasswordReset(data: PasswordResetRequest): Promise<void>
/**
* Confirms a password reset.
*/
confirmPasswordReset(data: PasswordResetConfirm): Promise<void>
/**
* Updates the current user's profile.
*/
updateProfile(data: Partial<T>): Promise<T>
/**
* Changes the current user's password.
*/
changePassword(oldPassword: string, newPassword: string): Promise<void>
/**
* Initializes auth state (e.g., from stored tokens).
*/
initialize(): Promise<void>
/**
* Subscribes to auth state changes.
*/
subscribe(callback: (state: AuthState<T>) => void): () => void
/**
* Subscribes to auth state changes (alias for subscribe).
*/
onAuthChange(callback: (state: AuthState<T>) => void): () => void
/**
* Gets the current access token (alias for getAccessToken).
*/
getToken?(): string | null
/**
* Adds an auth event listener.
*/
addEventListener(listener: AuthEventListener): () => void
/**
* Destroys the auth client.
*/
destroy(): void
}AuthClientConfig
Configuration options for creating an auth client, including API endpoints, storage strategy, token refresh, and OAuth providers.
interface AuthClientConfig {
/**
* API base URL.
*/
baseURL?: string
/**
* Login endpoint.
*/
loginEndpoint?: string
/**
* Logout endpoint.
*/
logoutEndpoint?: string
/**
* Register endpoint.
*/
registerEndpoint?: string
/**
* Refresh endpoint.
*/
refreshEndpoint?: string
/**
* User profile endpoint.
*/
profileEndpoint?: string
/**
* Current-user endpoint for session restore (default `/users/me`). Called by
* `initialize()` with credentials to re-establish the session from the
* httpOnly cookie after a full page load — the only way to stay logged in when
* the bearer token is held in memory (the secure default; a localStorage copy
* is XSS-exfiltratable). Must return the user (as `user` or `props`).
*/
currentUserEndpoint?: string
/**
* Storage key prefix.
*/
storagePrefix?: string
/**
* Token storage type or custom storage adapter.
* - 'memory': In-memory storage (lost on page refresh)
* - StorageAdapter: Custom storage implementation (e.g., from `@molecule/app-storage`)
*
* @example
* ```typescript
* // Use in-memory storage
* storage: 'memory'
*
* // Use custom storage from `@molecule/app-storage`
* import { getProvider } from '@molecule/app-storage'
* const storageProvider = getProvider()
* storage: {
* getItem: (key) => storageProvider.get(key),
* setItem: (key, value) => storageProvider.set(key, value),
* removeItem: (key) => storageProvider.remove(key),
* }
* ```
*/
storage?: 'memory' | StorageAdapter
/**
* Decide whether a user returned by the cookie-restore probe
* (`currentUserEndpoint`) constitutes a signed-in CLIENT session. Return
* `false` to reject the restore: the client stays unauthenticated and the
* `mol_auth` presence hint is cleared so later loads skip the probe (the
* httpOnly session cookie is untouched and keeps authenticating API calls).
* Use this when the server also issues sessions that must never appear
* signed-in client-side — e.g. an anonymous/guest session whose user row
* (`isAnonymous`) would otherwise hydrate as a logged-in user. Defaults to
* accepting every restored user.
* @param user - The user returned by the restore probe.
* @returns Whether to establish the client session from this user.
*/
shouldRestoreUser?: (user: UserProfile) => boolean
/**
* Auto refresh tokens before expiry.
*/
autoRefresh?: boolean
/**
* Refresh tokens this many seconds before expiry.
*/
refreshBuffer?: number
/**
* Forgot password / password reset request endpoint.
*/
forgotPasswordEndpoint?: string
/**
* Password reset confirmation endpoint.
*/
resetPasswordEndpoint?: string
/**
* Change password endpoint.
*/
changePasswordEndpoint?: string
/**
* Available OAuth providers (e.g., ['github', 'google', 'gitlab']).
*/
oauthProviders?: string[]
/**
* OAuth endpoint path (default: '/oauth').
*/
oauthEndpoint?: string
}AuthResult
Successful authentication result containing the user, access/refresh tokens, and expiration.
interface AuthResult<T = UserProfile> {
/**
* Authenticated user.
*/
user: T
/**
* Access token (for token-based auth).
*/
accessToken?: string
/**
* Refresh token (for token-based auth).
*/
refreshToken?: string
/**
* Token expiration time (Unix timestamp).
*/
expiresAt?: number
/**
* Set (with HTTP 206) when the account has two-factor auth enabled and the
* login did not include a valid `twoFactorToken`. No tokens or user are
* issued until login is retried with the TOTP — callers must check this
* flag BEFORE treating the result as a successful authentication.
*/
twoFactorRequired?: boolean
}AuthState
Reactive authentication state snapshot (initialized, authenticated, user, loading, and error).
interface AuthState<T = UserProfile> {
/**
* Whether auth state has been initialized.
*/
initialized: boolean
/**
* Whether the user is authenticated.
*/
authenticated: boolean
/**
* Current user (if authenticated).
*/
user: T | null
/**
* Whether an auth operation is in progress.
*/
loading: boolean
/**
* Last auth error (if any).
*/
error: string | null
}LoginCredentials
Email/password login credentials with optional "remember me" flag.
interface LoginCredentials {
/**
* Email or username.
*/
email: string
/**
* Password.
*/
password: string
/**
* Whether to remember the session.
*/
remember?: boolean
/**
* Time-based one-time password (TOTP) from the user's authenticator app.
* Required when the account has two-factor auth enabled — the server
* answers `{ twoFactorRequired: true }` (HTTP 206) until a valid token
* is supplied.
*/
twoFactorToken?: string
}PasswordResetConfirm
Password reset confirmation data.
interface PasswordResetConfirm {
/**
* Reset token.
*/
token: string
/**
* New password.
*/
password: string
}PasswordResetRequest
Password reset request data.
interface PasswordResetRequest {
/**
* Email address.
*/
email: string
}RegisterData
New user registration payload (email, password, optional name and metadata).
interface RegisterData {
/**
* Email address.
*/
email: string
/**
* Password.
*/
password: string
/**
* Display name.
*/
name?: string
/**
* Additional registration fields.
*/
metadata?: Record<string, unknown>
}StorageAdapter
Minimal key-value storage adapter used by the auth client for persisting tokens and user data.
interface StorageAdapter {
getItem(key: string): string | null
setItem(key: string, value: string): void
removeItem(key: string): void
}TokenStorage
Internal token storage interface for reading and writing access/refresh tokens and user profile data.
interface TokenStorage {
getAccessToken(): string | null
setAccessToken(token: string | null): void
getRefreshToken(): string | null
setRefreshToken(token: string | null): void
getUser<T = UserProfile>(): T | null
setUser<T = UserProfile>(user: T | null): void
clear(): void
}UserProfile
User profile information.
interface UserProfile {
/**
* Unique user identifier.
*/
id: string
/**
* User's email address.
*/
email?: string
/**
* User's display name.
*/
name?: string
/**
* User's avatar/profile image URL.
*/
avatar?: string
/**
* User's roles or permissions.
*/
roles?: string[]
/**
* Additional metadata.
*/
metadata?: Record<string, unknown>
}Types
AuthEvent
Auth event types.
type AuthEvent =
| { type: 'login'; user: UserProfile }
| { type: 'logout' }
| { type: 'register'; user: UserProfile }
| { type: 'refresh' }
| { type: 'error'; error: string }AuthEventListener
Callback invoked when an authentication event occurs (login, logout, etc.).
type AuthEventListener = (event: AuthEvent) => voidFunctions
createJWTAuthClient(config)
Creates a simple JWT-based auth client.
This is a basic implementation that can be extended or replaced with more sophisticated auth providers.
function createJWTAuthClient(config?: AuthClientConfig): AuthClient<T>config— Auth client configuration including endpoints, storage, and refresh settings.
Returns: A fully configured AuthClient instance.
createTokenStorage(storage, prefix)
Creates a token storage implementation backed by either in-memory
storage or a custom StorageAdapter.
function createTokenStorage(storage?: 'memory' | StorageAdapter, prefix?: string): TokenStoragestorage—'memory'for in-memory storage (lost on refresh), or aStorageAdapterfor persistent storage.prefix— Key prefix for storage items (default:'molecule:auth:').
Returns: A TokenStorage instance.
getClient()
Retrieves the bonded auth client, throwing if none is configured.
function getClient(): AuthClient<T>Returns: The bonded auth client.
getTokenExpiration(token)
Returns the expiration timestamp of a JWT token in milliseconds.
function getTokenExpiration(token: string): number | nulltoken— The raw JWT string.
Returns: The expiration time in ms since epoch, or null if the token has no exp claim.
getUser()
Returns the current user profile, or null if not authenticated.
function getUser(): T | nullReturns: The user profile, or null if not authenticated.
hasClient()
Checks whether an auth client is currently bonded.
function hasClient(): booleanReturns: true if an auth client is bonded.
isAuthenticated()
Checks if the current user is authenticated.
function isAuthenticated(): booleanReturns: true if the user has an active session.
isTokenExpired(token, bufferSeconds)
Checks if a JWT token is expired (or will expire within the buffer window).
function isTokenExpired(token: string, bufferSeconds?: number): booleantoken— The raw JWT string.bufferSeconds— Seconds before actual expiry to consider the token expired (default: 0).
Returns: true if the token is expired or lacks an exp claim.
login(credentials)
Logs in with the given credentials (email/username + password).
function login(credentials: LoginCredentials): Promise<AuthResult<UserProfile>>credentials— Email/username and password.
Returns: The auth result containing tokens and user profile.
logout()
Logs out the current user, clearing tokens and session state.
function logout(): Promise<void>Returns: A promise that resolves when logout completes.
parseJWT(token)
Parses a JWT token payload without verification (base64-decodes the payload segment).
function parseJWT(token: string): T | nulltoken— The raw JWT string (header.payload.signature).
Returns: The decoded payload object, or null if parsing fails.
register(data)
Registers a new user account.
function register(data: RegisterData): Promise<AuthResult<UserProfile>>data— Registration data (email, password, name, etc.).
Returns: The auth result containing tokens and user profile.
setClient(client)
Registers an auth client as the active singleton. Called by bond packages during application startup.
function setClient(client: AuthClient<UserProfile>): voidclient— The auth client implementation to bond.
Injection Notes
Requirements
Peer dependencies:
@molecule/app-bond^1.0.1@molecule/app-i18n^1.0.1@molecule/app-logger^1.0.1
Runtime Dependencies
@molecule/app-bond@molecule/app-i18n@molecule/app-loggerA page reload "logging the user out" is the config default, not a bug. Token storage defaults to
'memory'(the secure default — a localStorage bearer token is XSS-exfiltratable). Staying signed in across reloads works by callingclient.initialize()once at startup: it re-fetchescurrentUserEndpoint(default/users/me) with the httpOnly cookie the API set at login. Wireinitialize(); do NOT "fix" reload-logout by copying tokens into localStorage.Endpoint defaults must match your API's real routes (
/auth/login,/auth/register,/auth/refresh,/users/logout,/users/me, …). Align them viacreateJWTAuthClient({ baseURL, ...endpoints })— never by hand-editing fetch calls in components.Client-side auth state is UX only — the server enforces authorization on every request; hiding a screen is not protection.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- [ ] Logging in with VALID credentials on the real login screen resolves
login()and flips AuthState toauthenticated: truewithgetUser()returning the actual UserProfile — the UI shows that user's ownemail/name/avatar, never a placeholder or a different account. - [ ] A WRONG password shows a visible error (AuthState
erroris set and the promise rejects) and establishes NO session:isAuthenticated()stays false and no protected screen becomes reachable. - [ ] Registering through the sign-up form calls
register(), creates the account, and lands authenticated (authenticated: true,userpopulated) on the app's post-signup screen — not back on the login form. If the flow shows an email-verification / "check your inbox" step, it must be genuinely backed (this client'sregister()returns an authenticated session directly and exposes no verify method) — never a dead screen the auth contract cannot advance past. - [ ] Logout calls
logout()and clears the session (authenticated: false,user: null, tokens cleared); afterward every protected screen redirects back to login. - [ ] Visiting a protected route while logged OUT redirects to login (the
guard reads
isAuthenticated()), and after authenticating you land back on the originally-requested screen rather than a generic home. - [ ] The session survives a FULL page reload: after login, hard-reload →
initialize()restores it from the httpOnly cookie (viacurrentUserEndpoint, default/users/me) and you stay signed in — you are NOT bounced to login. (The in-memory bearer token is dropped by design; the cookie restore is what keeps you in — do not "fix" this by writing the token to localStorage.) - [ ] Password reset round-trips:
requestPasswordReset({ email })thenconfirmPasswordReset({ token, password })with the emailed token, after whichlogin()with the NEW password succeeds and the OLD password no longer works. - [ ] Authorization holds: a signed-in user only ever reads/edits their OWN
record (
getUser()is the caller's profile, never another user's); the bearer token lives in memory only — it is never written to localStorage / sessionStorage where another script could read it; and an expired/invalid token fails closed — the app re-authenticates (refresh()orlogout()), never silently serving another user's data.
Translations
Translation strings are provided by @molecule/app-locales-auth.
