Files
skills/docs/ui/react/react-auth/src/client/auth-client.md
2026-05-29 08:20:56 +01:00

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

  • AuthClient class + createAuthClient(config) factory.
  • Optional siteDomain propagates as the x-site-domain header on every call.
  • Optional fetch injection seam for testing (no vi.mock required).
  • Zod-validated config; invalid input throws ReactAuthConfigInvalidError.
  • listSessions() normalises Date fields to ISO strings.
  • Every method is plugin-aware: when the underlying client lacks the method it returns undefined (or [] for listSessions).

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 AuthClient
    • constructor(config: AuthClientConfig) — validates config, throws ReactAuthConfigInvalidError on 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.