4.5 KiB
auth-client
(no submodules)
Purpose
auth-client.ts ships a typed AuthClient class plus the createAuthClient(config) factory that wraps better-auth/client. The wrapper centralises three concerns the underlying client leaves to the caller: (1) consistent injection of an optional x-site-domain header across every request, used for multi-tenant routing on the auth server; (2) a structural surface that does not depend on the dynamic plugin generic graph emitted by better-auth (each method we call is typed as an optional callable so plugin presence is detected at runtime and reported as a no-op when absent rather than as a compile-time error); and (3) normalisation of the listSessions payload from native Date to ISO string for serialisation safety. Configuration is validated through Zod at the boundary and a ReactAuthConfigInvalidError is thrown with code REACT_AUTH.CONFIG_INVALID when the URL or site domain is malformed.
Features
AuthClientclass +createAuthClient(config)factory.- Optional
siteDomainpropagates as thex-site-domainheader on every call. - Optional
fetchinjection seam for testing (novi.mockrequired). - Zod-validated config; invalid input throws
ReactAuthConfigInvalidError. listSessions()normalisesDatefields to ISO strings.- Every method is plugin-aware: when the underlying client lacks the method it returns
undefined(or[]forlistSessions).
Usage: Basic
import { createAuthClient } from "@suaveplan/react-auth";
const client = createAuthClient({
baseURL: "https://auth.example.com",
siteDomain: "tenant-a",
});
await client.signIn({ email: "a@b.com", password: "secret" });
const session = await client.getSession();
Usage: Advanced
Inject a custom fetch (e.g. a recorder or a happy-path stub) without ever mocking the better-auth module:
import { createAuthClient } from "@suaveplan/react-auth";
const captured: Request[] = [];
const recordingFetch: typeof fetch = async (input, init) => {
const req = new Request(input, init);
captured.push(req);
return new Response(JSON.stringify({ data: null }), { status: 200 });
};
const client = createAuthClient({
baseURL: "https://auth.example.com",
siteDomain: "tenant-b",
fetch: recordingFetch,
});
await client.getSession();
expect(captured[0].headers.get("x-site-domain")).toBe("tenant-b");
API Reference
interface AuthClientConfig—baseURL: string(URL),siteDomain?: string,fetch?: typeof fetch.class AuthClientconstructor(config: AuthClientConfig)— validatesconfig, throwsReactAuthConfigInvalidErroron failure.signIn(credentials: LoginCredentials): Promise<unknown>signUp(data: RegisterData): Promise<unknown>signOut(): Promise<unknown>getSession(): Promise<unknown>forgotPassword(email: string): Promise<unknown>resetPassword(data: { token: string; password: string }): Promise<unknown>changePassword(data: ChangePasswordData): Promise<unknown>verifyEmail(token: string): Promise<unknown>resendVerificationEmail(email: string): Promise<unknown>updateProfile(data: UpdateProfileData): Promise<unknown>deleteAccount(password: string): Promise<unknown>listSessions(): Promise<Session[]>— Date fields ISO-normalised.revokeSession(sessionId: string): Promise<unknown>revokeAllSessions(): Promise<unknown>getRawClient(): BetterAuthClientLike— escape hatch for plugin-specific access.
function createAuthClient(config: AuthClientConfig): AuthClient— thin factory.
Implementation Notes
The client does not throw on missing plugin methods. The upstream package emitted a single @ts-expect-error to work around forgetPassword not being statically typed; the wrapper replaces that with a structural interface (BetterAuthClientLike) where every method is optional. This keeps the call sites compile-safe while also covering deployments where the auth server registers a different plugin set. listSessions returns [] when no data array is present so callers can render an empty state without branching on undefined. The x-site-domain header is computed per-call from this.config.siteDomain so that future hot-swaps of the client config (out of scope for Wave 1) automatically pick up the new value. Constructor-time validation uses Zod with z.string().url() for baseURL — failing fast catches misconfiguration during app boot rather than on first request.