tempest-react-sdk 0.65.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/auth/create-tempest-auth.cjs +1 -1
  2. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  3. package/dist/auth/create-tempest-auth.js +31 -36
  4. package/dist/auth/create-tempest-auth.js.map +1 -1
  5. package/dist/http/api-client.cjs +1 -1
  6. package/dist/http/api-client.cjs.map +1 -1
  7. package/dist/http/api-client.js +87 -85
  8. package/dist/http/api-client.js.map +1 -1
  9. package/dist/http/credential-scope.cjs +2 -0
  10. package/dist/http/credential-scope.cjs.map +1 -0
  11. package/dist/http/credential-scope.js +23 -0
  12. package/dist/http/credential-scope.js.map +1 -0
  13. package/dist/http/resumable-upload.cjs +1 -1
  14. package/dist/http/resumable-upload.cjs.map +1 -1
  15. package/dist/http/resumable-upload.js +121 -120
  16. package/dist/http/resumable-upload.js.map +1 -1
  17. package/dist/http/upload-with-progress.cjs +1 -1
  18. package/dist/http/upload-with-progress.cjs.map +1 -1
  19. package/dist/http/upload-with-progress.js +35 -34
  20. package/dist/http/upload-with-progress.js.map +1 -1
  21. package/dist/tempest-react-sdk.cjs +1 -1
  22. package/dist/tempest-react-sdk.d.ts +217 -16
  23. package/dist/tempest-react-sdk.js +233 -231
  24. package/dist/utils/absent.cjs +2 -0
  25. package/dist/utils/absent.cjs.map +1 -0
  26. package/dist/utils/absent.js +12 -0
  27. package/dist/utils/absent.js.map +1 -0
  28. package/dist/utils/format.cjs +1 -1
  29. package/dist/utils/format.cjs.map +1 -1
  30. package/dist/utils/format.js +42 -37
  31. package/dist/utils/format.js.map +1 -1
  32. package/dist/utils/numbers.cjs +1 -1
  33. package/dist/utils/numbers.cjs.map +1 -1
  34. package/dist/utils/numbers.js +14 -12
  35. package/dist/utils/numbers.js.map +1 -1
  36. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- const e=require("../http/api-client.cjs"),t=require("./create-auth-store.cjs"),n=require("./refresh-queue.cjs");function r(e){let t=e??{};return{token:t.access_token,refreshToken:t.refresh_token}}function i(i){let{baseURL:a,prefix:o,loginPath:s=`/api/auth/login`,refreshPath:c=`/api/auth/refresh`,mePath:l,storeName:u=`tempest-auth`,storage:d=`local`,withCredentials:f=!1,fetcher:p,parseTokens:m=r,parseUser:h,refreshBody:g=e=>e?{refresh_token:e}:void 0,retry:_,redirectTo:v}=i,y=t.createAuthStore({name:u,storage:d}),b=`${u}-refresh`;function x(){return typeof window>`u`?null:d===`session`?window.sessionStorage:window.localStorage}function S(){return x()?.getItem(b)??null}function C(e){let t=x();t&&(e?t.setItem(b,e):t.removeItem(b))}let w=()=>y.getState(),T=()=>w().token,E=e.createApiClient({baseURL:a,prefix:o,withCredentials:f,fetcher:p});async function D(){if(!l)return w().user;let t=T(),n=await e.createApiClient({baseURL:a,prefix:o,withCredentials:f,fetcher:p,getToken:()=>t}).get(l);return w().setUser(n),n}async function O(e){let t=await E.post(s,{body:e}),{token:n,refreshToken:r}=m(t);w().setToken(n),C(r??null);let i=h?.(t);return i==null?D():(w().setUser(i),i)}function k(){w().logout(),C(null)}function A(){k(),v&&typeof window<`u`&&window.location.assign(v)}let j=n.createRefreshQueue(async()=>{let e=await E.post(c,{body:g(S())}),{token:t,refreshToken:n}=m(e);w().setToken(t),n&&C(n)});return{useAuthStore:y,api:e.createApiClient({baseURL:a,prefix:o,withCredentials:f,fetcher:p,getToken:T,refresh:j,retry:_,onUnauthorized:()=>A()}),login:O,logout:k,refresh:j,getToken:T}}exports.createTempestAuth=i;
1
+ const e=require("../http/api-client.cjs"),t=require("./create-auth-store.cjs"),n=require("./refresh-queue.cjs");function r(e){let t=e??{};return{token:t.access_token,refreshToken:t.refresh_token}}function i(i){let{baseURL:a,prefix:o,loginPath:s=`/api/auth/login`,refreshPath:c=`/api/auth/refresh`,mePath:l,storeName:u=`tempest-auth`,storage:d=`local`,withCredentials:f=!1,fetcher:p,parseTokens:m=r,parseUser:h,refreshBody:g=e=>e?{refresh_token:e}:void 0,retry:_,redirectTo:v}=i,y=t.createAuthStore({name:u,storage:d}),b=`${u}-refresh`;function x(){return typeof window>`u`?null:d===`session`?window.sessionStorage:window.localStorage}function S(){return x()?.getItem(b)??null}function C(e){let t=x();t&&(e?t.setItem(b,e):t.removeItem(b))}let w=()=>y.getState(),T=()=>w().token;async function E(){if(!l)return w().user;let e=await j.get(l,{skipAuthRetry:!0});return w().setUser(e),e}async function D(e){let t=await j.post(s,{body:e,skipAuth:!0}),{token:n,refreshToken:r}=m(t);w().setToken(n),C(r??null);let i=h?.(t);return i==null?E():(w().setUser(i),i)}function O(){w().logout(),C(null)}function k(){O(),v&&typeof window<`u`&&window.location.assign(v)}let A=n.createRefreshQueue(async()=>{let e=await j.post(c,{body:g(S()),skipAuth:!0}),{token:t,refreshToken:n}=m(e);w().setToken(t),n&&C(n)}),j=e.createApiClient({baseURL:a,prefix:o,withCredentials:f,fetcher:p,getToken:T,refresh:A,retry:_,onUnauthorized:()=>k()});return{useAuthStore:y,api:j,login:D,logout:O,refresh:A,getToken:T}}exports.createTempestAuth=i;
2
2
  //# sourceMappingURL=create-tempest-auth.cjs.map
@@ -1 +1 @@
1
- {"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 // Bare client (no auth/refresh) used for the login + refresh calls themselves.\n const bareApi = createApiClient({ baseURL, prefix, withCredentials, fetcher });\n\n async function fetchUser(): Promise<TUser | null> {\n if (!mePath) return state().user;\n const token = getToken();\n const user = await createApiClient({\n baseURL,\n prefix,\n withCredentials,\n fetcher,\n getToken: () => token,\n }).get<TUser>(mePath);\n state().setUser(user);\n return user;\n }\n\n async function login(credentials: TCredentials): Promise<TUser | null> {\n const data = await bareApi.post<unknown>(loginPath, { body: credentials });\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 bareApi.post<unknown>(refreshPath, {\n body: refreshBody(readRefreshToken()),\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,MAGxC,EAAU,EAAA,gBAAgB,CAAE,UAAS,SAAQ,kBAAiB,SAAQ,CAAC,EAE7E,eAAe,GAAmC,CAC9C,GAAI,CAAC,EAAQ,OAAO,EAAM,CAAC,CAAC,KAC5B,IAAM,EAAQ,EAAS,EACjB,EAAO,MAAM,EAAA,gBAAgB,CAC/B,UACA,SACA,kBACA,UACA,aAAgB,CACpB,CAAC,CAAC,CAAC,IAAW,CAAM,EAEpB,OADA,EAAM,CAAC,CAAC,QAAQ,CAAI,EACb,CACX,CAEA,eAAe,EAAM,EAAkD,CACnE,IAAM,EAAO,MAAM,EAAQ,KAAc,EAAW,CAAE,KAAM,CAAY,CAAC,EACnE,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,EAAQ,KAAc,EAAa,CAClD,KAAM,EAAY,EAAiB,CAAC,CACxC,CAAC,EACK,CAAE,QAAO,gBAAiB,EAAY,CAAI,EAChD,EAAM,CAAC,CAAC,SAAS,CAAK,EAClB,GAAc,EAAkB,CAAY,CACpD,CAAC,EAaD,MAAO,CAAE,eAAc,IAXX,EAAA,gBAAgB,CACxB,UACA,SACA,kBACA,UACA,WACA,UACA,QACA,mBAAsB,EAAW,CACrC,CAEuB,EAAK,QAAO,SAAQ,UAAS,UAAS,CACjE"}
1
+ {"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"}
@@ -24,54 +24,49 @@ function i(i) {
24
24
  let t = x();
25
25
  t && (e ? t.setItem(b, e) : t.removeItem(b));
26
26
  }
27
- let w = () => y.getState(), T = () => w().token, E = e({
28
- baseURL: a,
29
- prefix: o,
30
- withCredentials: f,
31
- fetcher: p
32
- });
33
- async function D() {
27
+ let w = () => y.getState(), T = () => w().token;
28
+ async function E() {
34
29
  if (!l) return w().user;
35
- let t = T(), n = await e({
36
- baseURL: a,
37
- prefix: o,
38
- withCredentials: f,
39
- fetcher: p,
40
- getToken: () => t
41
- }).get(l);
42
- return w().setUser(n), n;
30
+ let e = await j.get(l, { skipAuthRetry: !0 });
31
+ return w().setUser(e), e;
43
32
  }
44
- async function O(e) {
45
- let t = await E.post(s, { body: e }), { token: n, refreshToken: r } = m(t);
33
+ async function D(e) {
34
+ let t = await j.post(s, {
35
+ body: e,
36
+ skipAuth: !0
37
+ }), { token: n, refreshToken: r } = m(t);
46
38
  w().setToken(n), C(r ?? null);
47
39
  let i = h?.(t);
48
- return i == null ? D() : (w().setUser(i), i);
40
+ return i == null ? E() : (w().setUser(i), i);
49
41
  }
50
- function k() {
42
+ function O() {
51
43
  w().logout(), C(null);
52
44
  }
53
- function A() {
54
- k(), v && typeof window < "u" && window.location.assign(v);
45
+ function k() {
46
+ O(), v && typeof window < "u" && window.location.assign(v);
55
47
  }
56
- let j = n(async () => {
57
- let e = await E.post(c, { body: g(S()) }), { token: t, refreshToken: n } = m(e);
48
+ let A = n(async () => {
49
+ let e = await j.post(c, {
50
+ body: g(S()),
51
+ skipAuth: !0
52
+ }), { token: t, refreshToken: n } = m(e);
58
53
  w().setToken(t), n && C(n);
54
+ }), j = e({
55
+ baseURL: a,
56
+ prefix: o,
57
+ withCredentials: f,
58
+ fetcher: p,
59
+ getToken: T,
60
+ refresh: A,
61
+ retry: _,
62
+ onUnauthorized: () => k()
59
63
  });
60
64
  return {
61
65
  useAuthStore: y,
62
- api: e({
63
- baseURL: a,
64
- prefix: o,
65
- withCredentials: f,
66
- fetcher: p,
67
- getToken: T,
68
- refresh: j,
69
- retry: _,
70
- onUnauthorized: () => A()
71
- }),
72
- login: O,
73
- logout: k,
74
- refresh: j,
66
+ api: j,
67
+ login: D,
68
+ logout: O,
69
+ refresh: A,
75
70
  getToken: T
76
71
  };
77
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-tempest-auth.js","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 // Bare client (no auth/refresh) used for the login + refresh calls themselves.\n const bareApi = createApiClient({ baseURL, prefix, withCredentials, fetcher });\n\n async function fetchUser(): Promise<TUser | null> {\n if (!mePath) return state().user;\n const token = getToken();\n const user = await createApiClient({\n baseURL,\n prefix,\n withCredentials,\n fetcher,\n getToken: () => token,\n }).get<TUser>(mePath);\n state().setUser(user);\n return user;\n }\n\n async function login(credentials: TCredentials): Promise<TUser | null> {\n const data = await bareApi.post<unknown>(loginPath, { body: credentials });\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 bareApi.post<unknown>(refreshPath, {\n body: refreshBody(readRefreshToken()),\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":";;;;AAyFA,SAAS,EAAmB,GAAyD;CACjF,IAAM,IAAK,KAAQ,CAAC;CACpB,OAAO;EAAE,OAAO,EAAE;EAAc,cAAc,EAAE;CAAc;AAClE;AA2BA,SAAgB,EACZ,GACgC;CAChC,IAAM,EACF,YACA,WACA,eAAY,mBACZ,iBAAc,qBACd,WACA,eAAY,gBACZ,aAAU,SACV,qBAAkB,IAClB,YACA,iBAAc,GACd,cACA,kBAAe,MAAQ,IAAK,EAAE,eAAe,EAAG,IAAI,KAAA,GACpD,UACA,kBACA,GAEE,IAAe,EAAuB;EAAE,MAAM;EAAW;CAAQ,CAAC,GAClE,IAAa,GAAG,EAAU;CAEhC,SAAS,IAA8B;EAEnC,OADI,OAAO,SAAW,MAAoB,OACnC,MAAY,YAAY,OAAO,iBAAiB,OAAO;CAClE;CACA,SAAS,IAAkC;EACvC,OAAO,EAAY,CAAC,EAAE,QAAQ,CAAU,KAAK;CACjD;CACA,SAAS,EAAkB,GAA4B;EACnD,IAAM,IAAI,EAAY;EACjB,MACD,IAAO,EAAE,QAAQ,GAAY,CAAK,IACjC,EAAE,WAAW,CAAU;CAChC;CAEA,IAAM,UAAgC,EAAa,SAAS,GACtD,UAAgC,EAAM,CAAC,CAAC,OAGxC,IAAU,EAAgB;EAAE;EAAS;EAAQ;EAAiB;CAAQ,CAAC;CAE7E,eAAe,IAAmC;EAC9C,IAAI,CAAC,GAAQ,OAAO,EAAM,CAAC,CAAC;EAC5B,IAAM,IAAQ,EAAS,GACjB,IAAO,MAAM,EAAgB;GAC/B;GACA;GACA;GACA;GACA,gBAAgB;EACpB,CAAC,CAAC,CAAC,IAAW,CAAM;EAEpB,OADA,EAAM,CAAC,CAAC,QAAQ,CAAI,GACb;CACX;CAEA,eAAe,EAAM,GAAkD;EACnE,IAAM,IAAO,MAAM,EAAQ,KAAc,GAAW,EAAE,MAAM,EAAY,CAAC,GACnE,EAAE,UAAO,oBAAiB,EAAY,CAAI;EAEhD,AADA,EAAM,CAAC,CAAC,SAAS,CAAK,GACtB,EAAkB,KAAgB,IAAI;EACtC,IAAM,IAAW,IAAY,CAAI;EAKjC,OAJI,KAAY,OAIT,EAAU,KAHb,EAAM,CAAC,CAAC,QAAQ,CAAQ,GACjB;CAGf;CAEA,SAAS,IAAe;EAEpB,AADA,EAAM,CAAC,CAAC,OAAO,GACf,EAAkB,IAAI;CAC1B;CASA,SAAS,IAAmB;EAExB,AADA,EAAO,GACH,KAAc,OAAO,SAAW,OAChC,OAAO,SAAS,OAAO,CAAU;CAEzC;CAEA,IAAM,IAAU,EAAmB,YAAY;EAC3C,IAAM,IAAO,MAAM,EAAQ,KAAc,GAAa,EAClD,MAAM,EAAY,EAAiB,CAAC,EACxC,CAAC,GACK,EAAE,UAAO,oBAAiB,EAAY,CAAI;EAEhD,AADA,EAAM,CAAC,CAAC,SAAS,CAAK,GAClB,KAAc,EAAkB,CAAY;CACpD,CAAC;CAaD,OAAO;EAAE;EAAc,KAXX,EAAgB;GACxB;GACA;GACA;GACA;GACA;GACA;GACA;GACA,sBAAsB,EAAW;EACrC,CAEuB;EAAK;EAAO;EAAQ;EAAS;CAAS;AACjE"}
1
+ {"version":3,"file":"create-tempest-auth.js","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":";;;;AAyFA,SAAS,EAAmB,GAAyD;CACjF,IAAM,IAAK,KAAQ,CAAC;CACpB,OAAO;EAAE,OAAO,EAAE;EAAc,cAAc,EAAE;CAAc;AAClE;AA2BA,SAAgB,EACZ,GACgC;CAChC,IAAM,EACF,YACA,WACA,eAAY,mBACZ,iBAAc,qBACd,WACA,eAAY,gBACZ,aAAU,SACV,qBAAkB,IAClB,YACA,iBAAc,GACd,cACA,kBAAe,MAAQ,IAAK,EAAE,eAAe,EAAG,IAAI,KAAA,GACpD,UACA,kBACA,GAEE,IAAe,EAAuB;EAAE,MAAM;EAAW;CAAQ,CAAC,GAClE,IAAa,GAAG,EAAU;CAEhC,SAAS,IAA8B;EAEnC,OADI,OAAO,SAAW,MAAoB,OACnC,MAAY,YAAY,OAAO,iBAAiB,OAAO;CAClE;CACA,SAAS,IAAkC;EACvC,OAAO,EAAY,CAAC,EAAE,QAAQ,CAAU,KAAK;CACjD;CACA,SAAS,EAAkB,GAA4B;EACnD,IAAM,IAAI,EAAY;EACjB,MACD,IAAO,EAAE,QAAQ,GAAY,CAAK,IACjC,EAAE,WAAW,CAAU;CAChC;CAEA,IAAM,UAAgC,EAAa,SAAS,GACtD,UAAgC,EAAM,CAAC,CAAC;CAW9C,eAAe,IAAmC;EAC9C,IAAI,CAAC,GAAQ,OAAO,EAAM,CAAC,CAAC;EAC5B,IAAM,IAAO,MAAM,EAAI,IAAW,GAAQ,EAAE,eAAe,GAAK,CAAC;EAEjE,OADA,EAAM,CAAC,CAAC,QAAQ,CAAI,GACb;CACX;CAEA,eAAe,EAAM,GAAkD;EACnE,IAAM,IAAO,MAAM,EAAI,KAAc,GAAW;GAC5C,MAAM;GACN,UAAU;EACd,CAAC,GACK,EAAE,UAAO,oBAAiB,EAAY,CAAI;EAEhD,AADA,EAAM,CAAC,CAAC,SAAS,CAAK,GACtB,EAAkB,KAAgB,IAAI;EACtC,IAAM,IAAW,IAAY,CAAI;EAKjC,OAJI,KAAY,OAIT,EAAU,KAHb,EAAM,CAAC,CAAC,QAAQ,CAAQ,GACjB;CAGf;CAEA,SAAS,IAAe;EAEpB,AADA,EAAM,CAAC,CAAC,OAAO,GACf,EAAkB,IAAI;CAC1B;CASA,SAAS,IAAmB;EAExB,AADA,EAAO,GACH,KAAc,OAAO,SAAW,OAChC,OAAO,SAAS,OAAO,CAAU;CAEzC;CAEA,IAAM,IAAU,EAAmB,YAAY;EAC3C,IAAM,IAAO,MAAM,EAAI,KAAc,GAAa;GAC9C,MAAM,EAAY,EAAiB,CAAC;GACpC,UAAU;EACd,CAAC,GACK,EAAE,UAAO,oBAAiB,EAAY,CAAI;EAEhD,AADA,EAAM,CAAC,CAAC,SAAS,CAAK,GAClB,KAAc,EAAkB,CAAY;CACpD,CAAC,GAEK,IAAM,EAAgB;EACxB;EACA;EACA;EACA;EACA;EACA;EACA;EACA,sBAAsB,EAAW;CACrC,CAAC;CAED,OAAO;EAAE;EAAc;EAAK;EAAO;EAAQ;EAAS;CAAS;AACjE"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./decode-response.cjs"),r=require("./errors.cjs"),i=require("./retry.cjs"),a=require("./timeout.cjs");var o=new Set([`GET`,`HEAD`,`OPTIONS`]);function s(e,t){return!o.has(t)||!(e instanceof r.TempestApiError)?!1:r.isRetriableStatus(e.status)}function c(e){return e?e===!0?{}:e:null}var l=15e3,u=3e5;function d(e){return typeof FormData<`u`&&e instanceof FormData}function f(e){return Math.round(performance.now()-e)}async function p(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new r.TempestApiError(r.buildApiError(e.status,n,e.headers,t))}function m(o){let m=o.fetcher??globalThis.fetch.bind(globalThis);function h(){let e=o.getToken?.();return e?{Authorization:`Bearer ${e}`}:{}}async function g(e,n,i){let{body:s,params:c,headers:f,signal:p,timeout:g,..._}=n,v=d(s),y=v?o.uploadTimeout:o.timeout,b=g===void 0?y===void 0?v?u:l:y:g,x={...v?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...o.headers,...h(),...f},S=a.withTimeout(p,b),C={..._,signal:S.signal,headers:x,credentials:o.withCredentials?`include`:_.credentials,body:s==null?void 0:v?s:JSON.stringify(s)},w=t.buildApiUrl(o.baseURL,e,{prefix:o.prefix,params:c});try{return await m(w,C)}catch(e){throw S.timedOut()?new r.TempestApiError({status:0,detail:`A requisição excedeu ${b}ms e foi abandonada.`}):e}finally{S.dispose()}}async function _(e,t,n,r){let i=o.logger;if(!i)return g(e,t,n);let a=performance.now();try{let o=await g(e,t,n),s={requestId:n,status:o.status,ms:f(a)},c=`${r} ${e} → ${o.status}`;return o.status>=400?i.warn(c,s):i.debug(c,s),o}catch(t){throw i.warn(`${r} ${e} → no response`,{requestId:n,ms:f(a),error:t}),t}}async function v(e,t){if(o.onUnauthorized){o.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await o.onUnauthorized(e)}catch(n){o.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function y(t,n,r){let i=o.requestId?o.requestId():e.randomId(),a=(n.method??`GET`).toUpperCase(),s=await _(t,n,i,a);if(s.status===401){if(o.refresh){try{await o.refresh(),s=await _(t,n,i,a)}catch{throw await v(s,i),await p(s,i)}s.status===401&&await v(s,i)}else await v(s,i)}if(!s.ok)throw await p(s,i);return r(s)}async function b(e,t,n){let r=c(o.retry);if(!r)return y(e,t,n);let a=(t.method??`GET`).toUpperCase();return i.retry(()=>y(e,t,n),{...r,shouldRetry:r.shouldRetry??(e=>s(e,a))})}async function x(e,t={}){return b(e,t,n.decodeByContentType)}async function S(e,t,n=`POST`,r){return x(e,{...r,method:n,body:t})}return{request:x,get:(e,t)=>x(e,{...t,method:`GET`}),post:(e,t)=>x(e,{...t,method:`POST`}),put:(e,t)=>x(e,{...t,method:`PUT`}),patch:(e,t)=>x(e,{...t,method:`PATCH`}),delete:(e,t)=>x(e,{...t,method:`DELETE`}),blob:(e,t)=>b(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>b(e,{...t},e=>e.arrayBuffer()),upload:S}}exports.createApiClient=m;
1
+ const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./decode-response.cjs"),i=require("./errors.cjs"),a=require("./retry.cjs"),o=require("./timeout.cjs");var s=new Set([`GET`,`HEAD`,`OPTIONS`]);function c(e,t){return!s.has(t)||!(e instanceof i.TempestApiError)?!1:i.isRetriableStatus(e.status)}function l(e){return e?e===!0?{}:e:null}var u=15e3,d=3e5;function f(e){return typeof FormData<`u`&&e instanceof FormData}function p(e){return Math.round(performance.now()-e)}async function m(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new i.TempestApiError(i.buildApiError(e.status,n,e.headers,t))}function h(s){let h=s.fetcher??globalThis.fetch.bind(globalThis);function g(e,t){if(t)return{};let r=s.getToken?.();return r?n.isTrustedCredentialTarget(e,s.baseURL,s.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,s.baseURL),{}):{}}async function _(e,n,r,a){let{body:c,params:l,headers:p,signal:m,timeout:_,...v}=n,y=f(c),b=y?s.uploadTimeout:s.timeout,x=_===void 0?b===void 0?y?d:u:b:_,S=t.buildApiUrl(s.baseURL,e,{prefix:s.prefix,params:l}),C={...y?{}:{"Content-Type":`application/json`},...r?{"X-Request-ID":r}:{},...s.headers,...g(S,a),...p},w=o.withTimeout(m,x),T={...v,signal:w.signal,headers:C,credentials:s.withCredentials?`include`:v.credentials,body:c==null?void 0:y?c:JSON.stringify(c)};try{return await h(S,T)}catch(e){throw w.timedOut()?new i.TempestApiError({status:0,detail:`A requisição excedeu ${x}ms e foi abandonada.`}):e}finally{w.dispose()}}async function v(e,t,n,r,i){let a=s.logger;if(!a)return _(e,t,n,i);let o=performance.now();try{let s=await _(e,t,n,i),c={requestId:n,status:s.status,ms:p(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:p(o),error:t}),t}}async function y(e,t){if(s.onUnauthorized){s.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await s.onUnauthorized(e)}catch(n){s.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function b(t,n,r){let{skipAuth:i=!1,skipAuthRetry:a=!1,...o}=n,c=s.requestId?s.requestId():e.randomId(),l=(o.method??`GET`).toUpperCase(),u=await v(t,o,c,l,i);if(u.status===401&&!i&&!a){if(s.refresh){try{await s.refresh(),u=await v(t,o,c,l,i)}catch{throw await y(u,c),await m(u,c)}u.status===401&&await y(u,c)}else await y(u,c)}if(!u.ok)throw await m(u,c);return r(u)}async function x(e,t,n){let{retry:r,...i}=t,o=l(r===void 0?s.retry:r);if(!o)return b(e,i,n);let u=(i.method??`GET`).toUpperCase();return a.retry(()=>b(e,i,n),{...o,shouldRetry:o.shouldRetry??(e=>c(e,u))})}async function S(e,t={}){return x(e,t,r.decodeByContentType)}async function C(e,t,n=`POST`,r){return S(e,{...r,method:n,body:t})}return{request:S,get:(e,t)=>S(e,{...t,method:`GET`}),post:(e,t)=>S(e,{...t,method:`POST`}),put:(e,t)=>S(e,{...t,method:`PUT`}),patch:(e,t)=>S(e,{...t,method:`PATCH`}),delete:(e,t)=>S(e,{...t,method:`DELETE`}),blob:(e,t)=>x(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>x(e,{...t},e=>e.arrayBuffer()),upload:C}}exports.createApiClient=h;
2
2
  //# sourceMappingURL=api-client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options, decode);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, init, requestId, method, skipAuth);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
@@ -1,29 +1,30 @@
1
1
  import { randomId as e } from "../utils/ids.js";
2
2
  import { buildApiUrl as t } from "./build-url.js";
3
- import { decodeByContentType as n } from "./decode-response.js";
4
- import { TempestApiError as r, buildApiError as i, isRetriableStatus as a } from "./errors.js";
5
- import { retry as o } from "./retry.js";
6
- import { withTimeout as s } from "./timeout.js";
3
+ import { isTrustedCredentialTarget as n, reportSuppressedCredential as r } from "./credential-scope.js";
4
+ import { decodeByContentType as i } from "./decode-response.js";
5
+ import { TempestApiError as a, buildApiError as o, isRetriableStatus as s } from "./errors.js";
6
+ import { retry as c } from "./retry.js";
7
+ import { withTimeout as l } from "./timeout.js";
7
8
  //#region src/http/api-client.ts
8
- var c = /* @__PURE__ */ new Set([
9
+ var u = /* @__PURE__ */ new Set([
9
10
  "GET",
10
11
  "HEAD",
11
12
  "OPTIONS"
12
13
  ]);
13
- function l(e, t) {
14
- return !c.has(t) || !(e instanceof r) ? !1 : a(e.status);
14
+ function d(e, t) {
15
+ return !u.has(t) || !(e instanceof a) ? !1 : s(e.status);
15
16
  }
16
- function u(e) {
17
+ function f(e) {
17
18
  return e ? e === !0 ? {} : e : null;
18
19
  }
19
- var d = 15e3, f = 3e5;
20
- function p(e) {
20
+ var p = 15e3, m = 3e5;
21
+ function h(e) {
21
22
  return typeof FormData < "u" && e instanceof FormData;
22
23
  }
23
- function m(e) {
24
+ function g(e) {
24
25
  return Math.round(performance.now() - e);
25
26
  }
26
- async function h(e, t) {
27
+ async function _(e, t) {
27
28
  let n;
28
29
  try {
29
30
  n = await e.clone().json();
@@ -34,71 +35,72 @@ async function h(e, t) {
34
35
  n = null;
35
36
  }
36
37
  }
37
- return new r(i(e.status, n, e.headers, t));
38
+ return new a(o(e.status, n, e.headers, t));
38
39
  }
39
- function g(i) {
40
- let a = i.fetcher ?? globalThis.fetch.bind(globalThis);
41
- function c() {
42
- let e = i.getToken?.();
43
- return e ? { Authorization: `Bearer ${e}` } : {};
40
+ function v(o) {
41
+ let s = o.fetcher ?? globalThis.fetch.bind(globalThis);
42
+ function u(e, t) {
43
+ if (t) return {};
44
+ let i = o.getToken?.();
45
+ return i ? n(e, o.baseURL, o.trustedOrigins) ? { Authorization: `Bearer ${i}` } : (r(e, o.baseURL), {}) : {};
44
46
  }
45
- async function g(e, n, o) {
46
- let { body: l, params: u, headers: m, signal: h, timeout: g, ..._ } = n, v = p(l), y = v ? i.uploadTimeout : i.timeout, b = g === void 0 ? y === void 0 ? v ? f : d : y : g, x = {
47
- ...v ? {} : { "Content-Type": "application/json" },
48
- ...o ? { "X-Request-ID": o } : {},
49
- ...i.headers,
50
- ...c(),
51
- ...m
52
- }, S = s(h, b), C = {
53
- ..._,
54
- signal: S.signal,
55
- headers: x,
56
- credentials: i.withCredentials ? "include" : _.credentials,
57
- body: l == null ? void 0 : v ? l : JSON.stringify(l)
58
- }, w = t(i.baseURL, e, {
59
- prefix: i.prefix,
60
- params: u
61
- });
47
+ async function v(e, n, r, i) {
48
+ let { body: c, params: d, headers: f, signal: g, timeout: _, ...v } = n, y = h(c), b = y ? o.uploadTimeout : o.timeout, x = _ === void 0 ? b === void 0 ? y ? m : p : b : _, S = t(o.baseURL, e, {
49
+ prefix: o.prefix,
50
+ params: d
51
+ }), C = {
52
+ ...y ? {} : { "Content-Type": "application/json" },
53
+ ...r ? { "X-Request-ID": r } : {},
54
+ ...o.headers,
55
+ ...u(S, i),
56
+ ...f
57
+ }, w = l(g, x), T = {
58
+ ...v,
59
+ signal: w.signal,
60
+ headers: C,
61
+ credentials: o.withCredentials ? "include" : v.credentials,
62
+ body: c == null ? void 0 : y ? c : JSON.stringify(c)
63
+ };
62
64
  try {
63
- return await a(w, C);
65
+ return await s(S, T);
64
66
  } catch (e) {
65
- throw S.timedOut() ? new r({
67
+ throw w.timedOut() ? new a({
66
68
  status: 0,
67
- detail: `A requisição excedeu ${b}ms e foi abandonada.`
69
+ detail: `A requisição excedeu ${x}ms e foi abandonada.`
68
70
  }) : e;
69
71
  } finally {
70
- S.dispose();
72
+ w.dispose();
71
73
  }
72
74
  }
73
- async function _(e, t, n, r) {
74
- let a = i.logger;
75
- if (!a) return g(e, t, n);
76
- let o = performance.now();
75
+ async function y(e, t, n, r, i) {
76
+ let a = o.logger;
77
+ if (!a) return v(e, t, n, i);
78
+ let s = performance.now();
77
79
  try {
78
- let i = await g(e, t, n), s = {
80
+ let o = await v(e, t, n, i), c = {
79
81
  requestId: n,
80
- status: i.status,
81
- ms: m(o)
82
- }, c = `${r} ${e} → ${i.status}`;
83
- return i.status >= 400 ? a.warn(c, s) : a.debug(c, s), i;
82
+ status: o.status,
83
+ ms: g(s)
84
+ }, l = `${r} ${e} → ${o.status}`;
85
+ return o.status >= 400 ? a.warn(l, c) : a.debug(l, c), o;
84
86
  } catch (t) {
85
87
  throw a.warn(`${r} ${e} → no response`, {
86
88
  requestId: n,
87
- ms: m(o),
89
+ ms: g(s),
88
90
  error: t
89
91
  }), t;
90
92
  }
91
93
  }
92
- async function v(e, t) {
93
- if (i.onUnauthorized) {
94
- i.logger?.warn("unauthorized — calling onUnauthorized", {
94
+ async function b(e, t) {
95
+ if (o.onUnauthorized) {
96
+ o.logger?.warn("unauthorized — calling onUnauthorized", {
95
97
  requestId: t,
96
98
  status: e.status
97
99
  });
98
100
  try {
99
- await i.onUnauthorized(e);
101
+ await o.onUnauthorized(e);
100
102
  } catch (n) {
101
- i.logger?.warn("onUnauthorized threw — keeping the original response error", {
103
+ o.logger?.warn("onUnauthorized threw — keeping the original response error", {
102
104
  requestId: t,
103
105
  status: e.status,
104
106
  error: n
@@ -106,68 +108,68 @@ function g(i) {
106
108
  }
107
109
  }
108
110
  }
109
- async function y(t, n, r) {
110
- let a = i.requestId ? i.requestId() : e(), o = (n.method ?? "GET").toUpperCase(), s = await _(t, n, a, o);
111
- if (s.status === 401) {
112
- if (i.refresh) {
111
+ async function x(t, n, r) {
112
+ let { skipAuth: i = !1, skipAuthRetry: a = !1, ...s } = n, c = o.requestId ? o.requestId() : e(), l = (s.method ?? "GET").toUpperCase(), u = await y(t, s, c, l, i);
113
+ if (u.status === 401 && !i && !a) {
114
+ if (o.refresh) {
113
115
  try {
114
- await i.refresh(), s = await _(t, n, a, o);
116
+ await o.refresh(), u = await y(t, s, c, l, i);
115
117
  } catch {
116
- throw await v(s, a), await h(s, a);
118
+ throw await b(u, c), await _(u, c);
117
119
  }
118
- s.status === 401 && await v(s, a);
119
- } else await v(s, a);
120
+ u.status === 401 && await b(u, c);
121
+ } else await b(u, c);
120
122
  }
121
- if (!s.ok) throw await h(s, a);
122
- return r(s);
123
+ if (!u.ok) throw await _(u, c);
124
+ return r(u);
123
125
  }
124
- async function b(e, t, n) {
125
- let r = u(i.retry);
126
- if (!r) return y(e, t, n);
127
- let a = (t.method ?? "GET").toUpperCase();
128
- return o(() => y(e, t, n), {
129
- ...r,
130
- shouldRetry: r.shouldRetry ?? ((e) => l(e, a))
126
+ async function S(e, t, n) {
127
+ let { retry: r, ...i } = t, a = f(r === void 0 ? o.retry : r);
128
+ if (!a) return x(e, i, n);
129
+ let s = (i.method ?? "GET").toUpperCase();
130
+ return c(() => x(e, i, n), {
131
+ ...a,
132
+ shouldRetry: a.shouldRetry ?? ((e) => d(e, s))
131
133
  });
132
134
  }
133
- async function x(e, t = {}) {
134
- return b(e, t, n);
135
+ async function C(e, t = {}) {
136
+ return S(e, t, i);
135
137
  }
136
- async function S(e, t, n = "POST", r) {
137
- return x(e, {
138
+ async function w(e, t, n = "POST", r) {
139
+ return C(e, {
138
140
  ...r,
139
141
  method: n,
140
142
  body: t
141
143
  });
142
144
  }
143
145
  return {
144
- request: x,
145
- get: (e, t) => x(e, {
146
+ request: C,
147
+ get: (e, t) => C(e, {
146
148
  ...t,
147
149
  method: "GET"
148
150
  }),
149
- post: (e, t) => x(e, {
151
+ post: (e, t) => C(e, {
150
152
  ...t,
151
153
  method: "POST"
152
154
  }),
153
- put: (e, t) => x(e, {
155
+ put: (e, t) => C(e, {
154
156
  ...t,
155
157
  method: "PUT"
156
158
  }),
157
- patch: (e, t) => x(e, {
159
+ patch: (e, t) => C(e, {
158
160
  ...t,
159
161
  method: "PATCH"
160
162
  }),
161
- delete: (e, t) => x(e, {
163
+ delete: (e, t) => C(e, {
162
164
  ...t,
163
165
  method: "DELETE"
164
166
  }),
165
- blob: (e, t) => b(e, { ...t }, (e) => e.blob()),
166
- arrayBuffer: (e, t) => b(e, { ...t }, (e) => e.arrayBuffer()),
167
- upload: S
167
+ blob: (e, t) => S(e, { ...t }, (e) => e.blob()),
168
+ arrayBuffer: (e, t) => S(e, { ...t }, (e) => e.arrayBuffer()),
169
+ upload: w
168
170
  };
169
171
  }
170
172
  //#endregion
171
- export { g as createApiClient };
173
+ export { v as createApiClient };
172
174
 
173
175
  //# sourceMappingURL=api-client.js.map