{"version":3,"file":"create-tempest-auth.cjs","names":[],"sources":["../../src/auth/create-tempest-auth.ts"],"sourcesContent":["import { createApiClient } from \"../http\";\nimport type { ApiClient, RetryOptions } from \"../http\";\nimport { createAuthStore } from \"./create-auth-store\";\nimport type { AuthState } from \"./create-auth-store\";\nimport { createRefreshQueue } from \"./refresh-queue\";\n\n/** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */\nexport interface TempestTokenResponse {\n    /** The bearer access token. */\n    access_token: string;\n    /** Token type — always `\"bearer\"` for the SDK. */\n    token_type?: string;\n    /** Optional refresh token (when the API returns it in the body, not a cookie). */\n    refresh_token?: string;\n}\n\nexport interface CreateTempestAuthOptions<TUser> {\n    /** Base URL of the API. Required. */\n    baseURL: string;\n    /**\n     * Path segment the API is mounted under, such as `\"/api\"`, forwarded to\n     * every client this preset builds — see the `prefix` option of\n     * `createApiClient`.\n     *\n     * Safe to combine with the default paths below: the prefix is applied at\n     * most once, so `\"/api/auth/login\"` under a `\"/api\"` prefix stays\n     * `\"/api/auth/login\"`.\n     */\n    prefix?: string;\n    /** Login route (`POST`). Default: `\"/api/auth/login\"`. */\n    loginPath?: string;\n    /** Refresh route (`POST`). Default: `\"/api/auth/refresh\"`. */\n    refreshPath?: string;\n    /** Optional current-user route (`GET`) called after login/refresh. */\n    mePath?: string;\n    /** Persist key for the store. Default: `\"tempest-auth\"`. */\n    storeName?: string;\n    /** Storage backend. Default: `\"local\"`. */\n    storage?: \"local\" | \"session\";\n    /** Send cookies (needed when the refresh token lives in an httpOnly cookie). */\n    withCredentials?: boolean;\n    /** Custom fetch implementation (testing / SSR). Defaults to `globalThis.fetch`. */\n    fetcher?: typeof fetch;\n    /**\n     * Extract tokens from a login/refresh response. Default reads\n     * `access_token` + `refresh_token`.\n     */\n    parseTokens?: (data: unknown) => { token: string; refreshToken?: string };\n    /** Pull the user out of the login response, when the API embeds it. */\n    parseUser?: (data: unknown) => TUser | null;\n    /**\n     * Build the refresh request body. Default sends `{ refresh_token }` when a\n     * refresh token is stored, else `undefined` (cookie-based refresh).\n     */\n    refreshBody?: (refreshToken: string | null) => unknown;\n    /**\n     * Retry policy for `api`, forwarded to {@link createApiClient}. Off by\n     * default; `true` enables the conservative built-in policy, which never\n     * replays a write.\n     */\n    retry?: boolean | RetryOptions;\n    /**\n     * Where to send the browser when the session ends — a hard navigation via\n     * `window.location.assign`, after the store is cleared.\n     *\n     * **Prefer leaving this unset.** `logout()` already clears the store, so a\n     * `<RouteGuard when={isAuthenticated} redirectTo=\"/login\">` wrapped around\n     * the protected area navigates on its own, which keeps the SPA alive and the\n     * router history intact. Reach for this only when the expiry can happen\n     * outside any guarded subtree and a full reload is acceptable.\n     */\n    redirectTo?: string;\n}\n\nexport interface TempestAuth<TUser, TCredentials> {\n    /** The persisted Zustand auth store hook (compatible with `<AuthGuard>`). */\n    useAuthStore: ReturnType<typeof createAuthStore<TUser>>;\n    /** A `createApiClient` wired with bearer auth + 401 → refresh → retry. */\n    api: ApiClient;\n    /** Authenticate, store the session, and resolve the user (or null). */\n    login: (credentials: TCredentials) => Promise<TUser | null>;\n    /** Clear the session (and the stored refresh token). */\n    logout: () => void;\n    /** Refresh the access token (deduplicated across concurrent callers). */\n    refresh: () => Promise<void>;\n    /** The current access token, or null. */\n    getToken: () => string | null;\n}\n\nfunction defaultParseTokens(data: unknown): { token: string; refreshToken?: string } {\n    const d = (data ?? {}) as TempestTokenResponse;\n    return { token: d.access_token, refreshToken: d.refresh_token };\n}\n\n/**\n * Turn-key auth preset wiring `createAuthStore` + `createRefreshQueue` +\n * `createApiClient` to the Tempest FastAPI SDK auth contract: login returns\n * `{ access_token, token_type }`, requests carry `Authorization: Bearer`, and a\n * `401` triggers a single deduplicated refresh + replay.\n *\n * The session is cleared whenever that path ends unauthorized anyway — the\n * refresh threw, or the replay came back `401` — so a refresh token that the\n * backend has revoked cannot leave the app holding a dead session. With\n * `redirectTo` set, the browser also leaves the page; without it, clearing the\n * store is enough for a `<RouteGuard>` to navigate on its own.\n *\n * @example\n * const auth = createTempestAuth<User, { email: string; password: string }>({\n *     baseURL: import.meta.env.VITE_API_URL,\n *     mePath: \"/api/auth/me\",\n * });\n *\n * await auth.login({ email, password });   // stores session, returns the user\n * const orders = await auth.api.get(\"/api/orders\");  // sends the bearer token\n * auth.logout();\n *\n * @param options - The auth configuration.\n * @returns The store hook, a wired API client, and login/logout/refresh helpers.\n */\nexport function createTempestAuth<TUser, TCredentials = { email: string; password: string }>(\n    options: CreateTempestAuthOptions<TUser>,\n): TempestAuth<TUser, TCredentials> {\n    const {\n        baseURL,\n        prefix,\n        loginPath = \"/api/auth/login\",\n        refreshPath = \"/api/auth/refresh\",\n        mePath,\n        storeName = \"tempest-auth\",\n        storage = \"local\",\n        withCredentials = false,\n        fetcher,\n        parseTokens = defaultParseTokens,\n        parseUser,\n        refreshBody = (rt) => (rt ? { refresh_token: rt } : undefined),\n        retry,\n        redirectTo,\n    } = options;\n\n    const useAuthStore = createAuthStore<TUser>({ name: storeName, storage });\n    const refreshKey = `${storeName}-refresh`;\n\n    function storageImpl(): Storage | null {\n        if (typeof window === \"undefined\") return null;\n        return storage === \"session\" ? window.sessionStorage : window.localStorage;\n    }\n    function readRefreshToken(): string | null {\n        return storageImpl()?.getItem(refreshKey) ?? null;\n    }\n    function writeRefreshToken(token: string | null): void {\n        const s = storageImpl();\n        if (!s) return;\n        if (token) s.setItem(refreshKey, token);\n        else s.removeItem(refreshKey);\n    }\n\n    const state = (): AuthState<TUser> => useAuthStore.getState();\n    const getToken = (): string | null => state().token;\n\n    /**\n     * Load the current user, without letting a `401` end the session.\n     *\n     * `skipAuthRetry` is what keeps this one client honest: the call is\n     * authenticated, but a `401` here surfaces instead of triggering a refresh\n     * and `onUnauthorized`. That was the behaviour of the throwaway client this\n     * used to build per invocation, and it is the reason the preset needed\n     * three clients before 0.66.0.\n     */\n    async function fetchUser(): Promise<TUser | null> {\n        if (!mePath) return state().user;\n        const user = await api.get<TUser>(mePath, { skipAuthRetry: true });\n        state().setUser(user);\n        return user;\n    }\n\n    async function login(credentials: TCredentials): Promise<TUser | null> {\n        const data = await api.post<unknown>(loginPath, {\n            body: credentials,\n            skipAuth: true,\n        });\n        const { token, refreshToken } = parseTokens(data);\n        state().setToken(token);\n        writeRefreshToken(refreshToken ?? null);\n        const embedded = parseUser?.(data);\n        if (embedded != null) {\n            state().setUser(embedded);\n            return embedded;\n        }\n        return fetchUser();\n    }\n\n    function logout(): void {\n        state().logout();\n        writeRefreshToken(null);\n    }\n\n    /**\n     * Clear the session and, when `redirectTo` is set, leave the page.\n     *\n     * Separate from `logout` so an explicit sign-out stays a pure state change:\n     * a caller that already navigates itself would otherwise get a second,\n     * competing navigation.\n     */\n    function endSession(): void {\n        logout();\n        if (redirectTo && typeof window !== \"undefined\") {\n            window.location.assign(redirectTo);\n        }\n    }\n\n    const refresh = createRefreshQueue(async () => {\n        const data = await api.post<unknown>(refreshPath, {\n            body: refreshBody(readRefreshToken()),\n            skipAuth: true,\n        });\n        const { token, refreshToken } = parseTokens(data);\n        state().setToken(token);\n        if (refreshToken) writeRefreshToken(refreshToken);\n    });\n\n    const api = createApiClient({\n        baseURL,\n        prefix,\n        withCredentials,\n        fetcher,\n        getToken,\n        refresh,\n        retry,\n        onUnauthorized: () => endSession(),\n    });\n\n    return { useAuthStore, api, login, logout, refresh, getToken };\n}\n"],"mappings":"gHAyFA,SAAS,EAAmB,EAAyD,CACjF,IAAM,EAAK,GAAQ,CAAC,EACpB,MAAO,CAAE,MAAO,EAAE,aAAc,aAAc,EAAE,aAAc,CAClE,CA2BA,SAAgB,EACZ,EACgC,CAChC,GAAM,CACF,UACA,SACA,YAAY,kBACZ,cAAc,oBACd,SACA,YAAY,eACZ,UAAU,QACV,kBAAkB,GAClB,UACA,cAAc,EACd,YACA,cAAe,GAAQ,EAAK,CAAE,cAAe,CAAG,EAAI,IAAA,GACpD,QACA,cACA,EAEE,EAAe,EAAA,gBAAuB,CAAE,KAAM,EAAW,SAAQ,CAAC,EAClE,EAAa,GAAG,EAAU,UAEhC,SAAS,GAA8B,CAEnC,OADI,OAAO,OAAW,IAAoB,KACnC,IAAY,UAAY,OAAO,eAAiB,OAAO,YAClE,CACA,SAAS,GAAkC,CACvC,OAAO,EAAY,CAAC,EAAE,QAAQ,CAAU,GAAK,IACjD,CACA,SAAS,EAAkB,EAA4B,CACnD,IAAM,EAAI,EAAY,EACjB,IACD,EAAO,EAAE,QAAQ,EAAY,CAAK,EACjC,EAAE,WAAW,CAAU,EAChC,CAEA,IAAM,MAAgC,EAAa,SAAS,EACtD,MAAgC,EAAM,CAAC,CAAC,MAW9C,eAAe,GAAmC,CAC9C,GAAI,CAAC,EAAQ,OAAO,EAAM,CAAC,CAAC,KAC5B,IAAM,EAAO,MAAM,EAAI,IAAW,EAAQ,CAAE,cAAe,EAAK,CAAC,EAEjE,OADA,EAAM,CAAC,CAAC,QAAQ,CAAI,EACb,CACX,CAEA,eAAe,EAAM,EAAkD,CACnE,IAAM,EAAO,MAAM,EAAI,KAAc,EAAW,CAC5C,KAAM,EACN,SAAU,EACd,CAAC,EACK,CAAE,QAAO,gBAAiB,EAAY,CAAI,EAChD,EAAM,CAAC,CAAC,SAAS,CAAK,EACtB,EAAkB,GAAgB,IAAI,EACtC,IAAM,EAAW,IAAY,CAAI,EAKjC,OAJI,GAAY,KAIT,EAAU,GAHb,EAAM,CAAC,CAAC,QAAQ,CAAQ,EACjB,EAGf,CAEA,SAAS,GAAe,CACpB,EAAM,CAAC,CAAC,OAAO,EACf,EAAkB,IAAI,CAC1B,CASA,SAAS,GAAmB,CACxB,EAAO,EACH,GAAc,OAAO,OAAW,KAChC,OAAO,SAAS,OAAO,CAAU,CAEzC,CAEA,IAAM,EAAU,EAAA,mBAAmB,SAAY,CAC3C,IAAM,EAAO,MAAM,EAAI,KAAc,EAAa,CAC9C,KAAM,EAAY,EAAiB,CAAC,EACpC,SAAU,EACd,CAAC,EACK,CAAE,QAAO,gBAAiB,EAAY,CAAI,EAChD,EAAM,CAAC,CAAC,SAAS,CAAK,EAClB,GAAc,EAAkB,CAAY,CACpD,CAAC,EAEK,EAAM,EAAA,gBAAgB,CACxB,UACA,SACA,kBACA,UACA,WACA,UACA,QACA,mBAAsB,EAAW,CACrC,CAAC,EAED,MAAO,CAAE,eAAc,MAAK,QAAO,SAAQ,UAAS,UAAS,CACjE"}