tempest-react-sdk 0.42.0 → 0.42.1
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.
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +34 -30
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +58 -30
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.js +6 -6
- package/dist/icons/material-symbols.cjs +2 -0
- package/dist/icons/material-symbols.cjs.map +1 -0
- package/dist/icons/material-symbols.js +32 -0
- package/dist/icons/material-symbols.js.map +1 -0
- package/dist/icons.cjs +1 -1
- package/dist/icons.d.ts +69 -0
- package/dist/icons.js +2 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +72 -6
- package/dist/tempest-react-sdk.js +6 -6
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +1 -1
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/preprocess/pipeline.cjs +1 -1
- package/dist/vision/preprocess/pipeline.cjs.map +1 -1
- package/dist/vision/preprocess/pipeline.js +43 -28
- package/dist/vision/preprocess/pipeline.js.map +1 -1
- package/dist/vision.d.ts +6 -3
- 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,loginPath:o=`/api/auth/login`,refreshPath:s=`/api/auth/refresh`,mePath:c,storeName:l=`tempest-auth`,storage:u=`local`,withCredentials:d=!1,fetcher:f,parseTokens:p=r,parseUser:m,refreshBody:h=e=>e?{refresh_token:e}:void 0}=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,loginPath:o=`/api/auth/login`,refreshPath:s=`/api/auth/refresh`,mePath:c,storeName:l=`tempest-auth`,storage:u=`local`,withCredentials:d=!1,fetcher:f,parseTokens:p=r,parseUser:m,refreshBody:h=e=>e?{refresh_token:e}:void 0,retry:g,redirectTo:_}=i,v=t.createAuthStore({name:l,storage:u}),y=`${l}-refresh`;function b(){return typeof window>`u`?null:u===`session`?window.sessionStorage:window.localStorage}function x(){return b()?.getItem(y)??null}function S(e){let t=b();t&&(e?t.setItem(y,e):t.removeItem(y))}let C=()=>v.getState(),w=()=>C().token,T=e.createApiClient({baseURL:a,withCredentials:d,fetcher:f});async function E(){if(!c)return C().user;let t=w(),n=await e.createApiClient({baseURL:a,withCredentials:d,fetcher:f,getToken:()=>t}).get(c);return C().setUser(n),n}async function D(e){let t=await T.post(o,{body:e}),{token:n,refreshToken:r}=p(t);C().setToken(n),S(r??null);let i=m?.(t);return i==null?E():(C().setUser(i),i)}function O(){C().logout(),S(null)}function k(){O(),_&&typeof window<`u`&&window.location.assign(_)}let A=n.createRefreshQueue(async()=>{let e=await T.post(s,{body:h(x())}),{token:t,refreshToken:n}=p(e);C().setToken(t),n&&S(n)});return{useAuthStore:v,api:e.createApiClient({baseURL:a,withCredentials:d,fetcher:f,getToken:w,refresh:A,retry:g,onUnauthorized:()=>k()}),login:D,logout:O,refresh:A,getToken:w}}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 } 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 /** 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\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 + retry. Logout (or a failed\n * refresh) clears the session.\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 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 } = 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, 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 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 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 withCredentials,\n fetcher,\n getToken,\n refresh,\n onUnauthorized: () => logout(),\n });\n\n return { useAuthStore, api, login, logout, refresh, getToken };\n}\n"],"mappings":"gHA8DA,SAAS,EAAmB,EAAyD,CACjF,IAAM,EAAK,GAAQ,CAAC,EACpB,MAAO,CAAE,MAAO,EAAE,aAAc,aAAc,EAAE,aAAc,CAClE,CAsBA,SAAgB,EACZ,EACgC,CAChC,GAAM,CACF,UACA,YAAY,kBACZ,cAAc,oBACd,SACA,YAAY,eACZ,UAAU,QACV,kBAAkB,GAClB,UACA,cAAc,EACd,YACA,cAAe,GAAQ,EAAK,CAAE,cAAe,CAAG,EAAI,IAAA,IACpD,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,kBAAiB,SAAQ,CAAC,EAErE,eAAe,GAAmC,CAC9C,GAAI,CAAC,EAAQ,OAAO,EAAM,CAAC,CAAC,KAC5B,IAAM,EAAQ,EAAS,EACjB,EAAO,MAAM,EAAA,gBAAgB,CAC/B,UACA,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,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,EAWD,MAAO,CAAE,eAAc,IATX,EAAA,gBAAgB,CACxB,UACA,kBACA,UACA,WACA,UACA,mBAAsB,EAAO,CACjC,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 /** 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 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, 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 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 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":"gHA+EA,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,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,kBAAiB,SAAQ,CAAC,EAErE,eAAe,GAAmC,CAC9C,GAAI,CAAC,EAAQ,OAAO,EAAM,CAAC,CAAC,KAC5B,IAAM,EAAQ,EAAS,EACjB,EAAO,MAAM,EAAA,gBAAgB,CAC/B,UACA,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,EAYD,MAAO,CAAE,eAAc,IAVX,EAAA,gBAAgB,CACxB,UACA,kBACA,UACA,WACA,UACA,QACA,mBAAsB,EAAW,CACrC,CAEuB,EAAK,QAAO,SAAQ,UAAS,UAAS,CACjE"}
|
|
@@ -10,62 +10,66 @@ function r(e) {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
function i(i) {
|
|
13
|
-
let { baseURL: a, loginPath: o = "/api/auth/login", refreshPath: s = "/api/auth/refresh", mePath: c, storeName: l = "tempest-auth", storage: u = "local", withCredentials: d = !1, fetcher: f, parseTokens: p = r, parseUser: m, refreshBody: h = (e) => e ? { refresh_token: e } : void 0 } = i,
|
|
13
|
+
let { baseURL: a, loginPath: o = "/api/auth/login", refreshPath: s = "/api/auth/refresh", mePath: c, storeName: l = "tempest-auth", storage: u = "local", withCredentials: d = !1, fetcher: f, parseTokens: p = r, parseUser: m, refreshBody: h = (e) => e ? { refresh_token: e } : void 0, retry: g, redirectTo: _ } = i, v = t({
|
|
14
14
|
name: l,
|
|
15
15
|
storage: u
|
|
16
|
-
}),
|
|
17
|
-
function
|
|
16
|
+
}), y = `${l}-refresh`;
|
|
17
|
+
function b() {
|
|
18
18
|
return typeof window > "u" ? null : u === "session" ? window.sessionStorage : window.localStorage;
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
return
|
|
20
|
+
function x() {
|
|
21
|
+
return b()?.getItem(y) ?? null;
|
|
22
22
|
}
|
|
23
|
-
function
|
|
24
|
-
let t =
|
|
25
|
-
t && (e ? t.setItem(
|
|
23
|
+
function S(e) {
|
|
24
|
+
let t = b();
|
|
25
|
+
t && (e ? t.setItem(y, e) : t.removeItem(y));
|
|
26
26
|
}
|
|
27
|
-
let
|
|
27
|
+
let C = () => v.getState(), w = () => C().token, T = e({
|
|
28
28
|
baseURL: a,
|
|
29
29
|
withCredentials: d,
|
|
30
30
|
fetcher: f
|
|
31
31
|
});
|
|
32
|
-
async function
|
|
33
|
-
if (!c) return
|
|
34
|
-
let t =
|
|
32
|
+
async function E() {
|
|
33
|
+
if (!c) return C().user;
|
|
34
|
+
let t = w(), n = await e({
|
|
35
35
|
baseURL: a,
|
|
36
36
|
withCredentials: d,
|
|
37
37
|
fetcher: f,
|
|
38
38
|
getToken: () => t
|
|
39
39
|
}).get(c);
|
|
40
|
-
return
|
|
40
|
+
return C().setUser(n), n;
|
|
41
41
|
}
|
|
42
|
-
async function
|
|
43
|
-
let t = await
|
|
44
|
-
|
|
42
|
+
async function D(e) {
|
|
43
|
+
let t = await T.post(o, { body: e }), { token: n, refreshToken: r } = p(t);
|
|
44
|
+
C().setToken(n), S(r ?? null);
|
|
45
45
|
let i = m?.(t);
|
|
46
|
-
return i == null ?
|
|
46
|
+
return i == null ? E() : (C().setUser(i), i);
|
|
47
47
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
48
|
+
function O() {
|
|
49
|
+
C().logout(), S(null);
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
function k() {
|
|
52
|
+
O(), _ && typeof window < "u" && window.location.assign(_);
|
|
53
|
+
}
|
|
54
|
+
let A = n(async () => {
|
|
55
|
+
let e = await T.post(s, { body: h(x()) }), { token: t, refreshToken: n } = p(e);
|
|
56
|
+
C().setToken(t), n && S(n);
|
|
54
57
|
});
|
|
55
58
|
return {
|
|
56
|
-
useAuthStore:
|
|
59
|
+
useAuthStore: v,
|
|
57
60
|
api: e({
|
|
58
61
|
baseURL: a,
|
|
59
62
|
withCredentials: d,
|
|
60
63
|
fetcher: f,
|
|
61
|
-
getToken:
|
|
62
|
-
refresh:
|
|
63
|
-
|
|
64
|
+
getToken: w,
|
|
65
|
+
refresh: A,
|
|
66
|
+
retry: g,
|
|
67
|
+
onUnauthorized: () => k()
|
|
64
68
|
}),
|
|
65
|
-
login:
|
|
66
|
-
logout:
|
|
67
|
-
refresh:
|
|
68
|
-
getToken:
|
|
69
|
+
login: D,
|
|
70
|
+
logout: O,
|
|
71
|
+
refresh: A,
|
|
72
|
+
getToken: w
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
75
|
//#endregion
|
|
@@ -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 } 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 /** 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\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 + retry. Logout (or a failed\n * refresh) clears the session.\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 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 } = 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, 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 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 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 withCredentials,\n fetcher,\n getToken,\n refresh,\n onUnauthorized: () => logout(),\n });\n\n return { useAuthStore, api, login, logout, refresh, getToken };\n}\n"],"mappings":";;;;AA8DA,SAAS,EAAmB,GAAyD;CACjF,IAAM,IAAK,KAAQ,CAAC;CACpB,OAAO;EAAE,OAAO,EAAE;EAAc,cAAc,EAAE;CAAc;AAClE;AAsBA,SAAgB,EACZ,GACgC;CAChC,IAAM,EACF,YACA,eAAY,mBACZ,iBAAc,qBACd,WACA,eAAY,gBACZ,aAAU,SACV,qBAAkB,IAClB,YACA,iBAAc,GACd,cACA,kBAAe,MAAQ,IAAK,EAAE,eAAe,EAAG,IAAI,KAAA,MACpD,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;EAAiB;CAAQ,CAAC;CAErE,eAAe,IAAmC;EAC9C,IAAI,CAAC,GAAQ,OAAO,EAAM,CAAC,CAAC;EAC5B,IAAM,IAAQ,EAAS,GACjB,IAAO,MAAM,EAAgB;GAC/B;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;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;CAWD,OAAO;EAAE;EAAc,KATX,EAAgB;GACxB;GACA;GACA;GACA;GACA;GACA,sBAAsB,EAAO;EACjC,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 /** 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 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, 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 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 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":";;;;AA+EA,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,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;EAAiB;CAAQ,CAAC;CAErE,eAAe,IAAmC;EAC9C,IAAI,CAAC,GAAQ,OAAO,EAAM,CAAC,CAAC;EAC5B,IAAM,IAAQ,EAAS,GACjB,IAAO,MAAM,EAAgB;GAC/B;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;CAYD,OAAO;EAAE;EAAc,KAVX,EAAgB;GACxB;GACA;GACA;GACA;GACA;GACA;GACA,sBAAsB,EAAW;EACrC,CAEuB;EAAK;EAAO;EAAQ;EAAS;CAAS;AACjE"}
|
package/dist/http/api-client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/ids.cjs"),t=require("./errors.cjs");function n(e,t,n){let r=new URL(t,e.endsWith(`/`)?e:`${e}/`);if(n)for(let[e,t]of Object.entries(n))t!=null&&r.searchParams.set(e,String(t));return r.toString()}function
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./errors.cjs"),n=require("./retry.cjs");var r=new Set([`GET`,`HEAD`,`OPTIONS`]),i=new Set([0,408,425,429]);function a(e,n){return!r.has(n)||!(e instanceof t.TempestApiError)?!1:i.has(e.status)||e.status>=500}function o(e){return e?e===!0?{}:e:null}function s(e,t,n){let r=new URL(t,e.endsWith(`/`)?e:`${e}/`);if(n)for(let[e,t]of Object.entries(n))t!=null&&r.searchParams.set(e,String(t));return r.toString()}function c(e){return typeof FormData<`u`&&e instanceof FormData}async function l(e,n){let r;try{r=await e.clone().json()}catch{try{r=await e.text()}catch{r=null}}return new t.TempestApiError(t.buildApiError(e.status,r,e.headers,n))}function u(t){let r=t.fetcher??globalThis.fetch.bind(globalThis);function i(){let e=t.getToken?.();return e?{Authorization:`Bearer ${e}`}:{}}async function u(e,n,a){let{body:o,params:l,headers:u,...d}=n,f=c(o),p={...f?{}:{"Content-Type":`application/json`},...a?{"X-Request-ID":a}:{},...t.headers,...i(),...u},m={...d,headers:p,credentials:t.withCredentials?`include`:d.credentials,body:o==null?void 0:f?o:JSON.stringify(o)};return r(s(t.baseURL,e,l),m)}async function d(n,r){let i=t.requestId?t.requestId():e.randomId(),a=await u(n,r,i);if(a.status===401){if(t.refresh){try{await t.refresh(),a=await u(n,r,i)}catch{throw await t.onUnauthorized?.(a),await l(a,i)}a.status===401&&await t.onUnauthorized?.(a)}else await t.onUnauthorized?.(a)}if(!a.ok)throw await l(a,i);if(a.status!==204)return(a.headers.get(`content-type`)??``).includes(`application/json`)?await a.json():await a.text()}async function f(e,r={}){let i=o(t.retry);if(!i)return d(e,r);let s=(r.method??`GET`).toUpperCase();return n.retry(()=>d(e,r),{...i,shouldRetry:i.shouldRetry??(e=>a(e,s))})}async function p(e,t,n=`POST`){return f(e,{method:n,body:t})}return{request:f,get:(e,t)=>f(e,{...t,method:`GET`}),post:(e,t)=>f(e,{...t,method:`POST`}),put:(e,t)=>f(e,{...t,method:`PUT`}),patch:(e,t)=>f(e,{...t,method:`PATCH`}),delete:(e,t)=>f(e,{...t,method:`DELETE`}),upload:p}}exports.createApiClient=u;
|
|
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 three lines over the limit and\n * every one of them is a request-lifecycle concern the client cannot delegate: base\n * URL joining, the auth header, the retry loop, the timeout signal and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiError, TempestApiError } from \"./errors\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\nfunction buildUrl(baseURL: string, path: string, params?: RequestOptions[\"params\"]): string {\n const url = new URL(path, baseURL.endsWith(\"/\") ? baseURL : `${baseURL}/`);\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n return url.toString();\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\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`,\n * automatic refresh + retry on 401 when `refresh` is supplied, and uploads\n * via `FormData`. Throws an `ApiError` on non-2xx responses.\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, ...rest } = options;\n const isForm = isFormData(body);\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 init: RequestInit = {\n ...rest,\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 return fetcher(buildUrl(config.baseURL, path, params), init);\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n let response = await rawRequest(path, options, requestId);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await rawRequest(path, options, requestId);\n } catch {\n await config.onUnauthorized?.(response);\n throw await parseError(response, requestId);\n }\n } else {\n await config.onUnauthorized?.(response);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { 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 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 { buildApiError, TempestApiError } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\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 */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\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 RETRIABLE_STATUSES.has(error.status) || error.status >= 500;\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\nfunction buildUrl(baseURL: string, path: string, params?: RequestOptions[\"params\"]): string {\n const url = new URL(path, baseURL.endsWith(\"/\") ? baseURL : `${baseURL}/`);\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n return url.toString();\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\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 * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\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 * 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`/`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, ...rest } = options;\n const isForm = isFormData(body);\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 init: RequestInit = {\n ...rest,\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 return fetcher(buildUrl(config.baseURL, path, params), init);\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n let response = await rawRequest(path, options, requestId);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await rawRequest(path, options, requestId);\n } catch {\n await config.onUnauthorized?.(response);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await config.onUnauthorized?.(response);\n }\n } else {\n await config.onUnauthorized?.(response);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { 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 upload,\n };\n}\n"],"mappings":"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"}
|
package/dist/http/api-client.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import { randomId as e } from "../utils/ids.js";
|
|
2
2
|
import { TempestApiError as t, buildApiError as n } from "./errors.js";
|
|
3
|
+
import { retry as r } from "./retry.js";
|
|
3
4
|
//#region src/http/api-client.ts
|
|
4
|
-
|
|
5
|
+
var i = /* @__PURE__ */ new Set([
|
|
6
|
+
"GET",
|
|
7
|
+
"HEAD",
|
|
8
|
+
"OPTIONS"
|
|
9
|
+
]), a = /* @__PURE__ */ new Set([
|
|
10
|
+
0,
|
|
11
|
+
408,
|
|
12
|
+
425,
|
|
13
|
+
429
|
|
14
|
+
]);
|
|
15
|
+
function o(e, n) {
|
|
16
|
+
return !i.has(n) || !(e instanceof t) ? !1 : a.has(e.status) || e.status >= 500;
|
|
17
|
+
}
|
|
18
|
+
function s(e) {
|
|
19
|
+
return e ? e === !0 ? {} : e : null;
|
|
20
|
+
}
|
|
21
|
+
function c(e, t, n) {
|
|
5
22
|
let r = new URL(t, e.endsWith("/") ? e : `${e}/`);
|
|
6
23
|
if (n) for (let [e, t] of Object.entries(n)) t != null && r.searchParams.set(e, String(t));
|
|
7
24
|
return r.toString();
|
|
8
25
|
}
|
|
9
|
-
function
|
|
26
|
+
function l(e) {
|
|
10
27
|
return typeof FormData < "u" && e instanceof FormData;
|
|
11
28
|
}
|
|
12
|
-
async function
|
|
29
|
+
async function u(e, r) {
|
|
13
30
|
let i;
|
|
14
31
|
try {
|
|
15
32
|
i = await e.clone().json();
|
|
@@ -22,72 +39,83 @@ async function a(e, r) {
|
|
|
22
39
|
}
|
|
23
40
|
return new t(n(e.status, i, e.headers, r));
|
|
24
41
|
}
|
|
25
|
-
function
|
|
42
|
+
function d(t) {
|
|
26
43
|
let n = t.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
27
|
-
function
|
|
44
|
+
function i() {
|
|
28
45
|
let e = t.getToken?.();
|
|
29
46
|
return e ? { Authorization: `Bearer ${e}` } : {};
|
|
30
47
|
}
|
|
31
|
-
async function
|
|
32
|
-
let { body:
|
|
48
|
+
async function a(e, r, a) {
|
|
49
|
+
let { body: o, params: s, headers: u, ...d } = r, f = l(o), p = {
|
|
33
50
|
...f ? {} : { "Content-Type": "application/json" },
|
|
34
|
-
...
|
|
51
|
+
...a ? { "X-Request-ID": a } : {},
|
|
35
52
|
...t.headers,
|
|
36
|
-
...
|
|
53
|
+
...i(),
|
|
37
54
|
...u
|
|
38
55
|
}, m = {
|
|
39
56
|
...d,
|
|
40
57
|
headers: p,
|
|
41
58
|
credentials: t.withCredentials ? "include" : d.credentials,
|
|
42
|
-
body:
|
|
59
|
+
body: o == null ? void 0 : f ? o : JSON.stringify(o)
|
|
43
60
|
};
|
|
44
|
-
return n(
|
|
61
|
+
return n(c(t.baseURL, e, s), m);
|
|
45
62
|
}
|
|
46
|
-
async function
|
|
47
|
-
let i = t.requestId ? t.requestId() : e(), o = await
|
|
63
|
+
async function d(n, r) {
|
|
64
|
+
let i = t.requestId ? t.requestId() : e(), o = await a(n, r, i);
|
|
48
65
|
if (o.status === 401) {
|
|
49
|
-
if (t.refresh)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
if (t.refresh) {
|
|
67
|
+
try {
|
|
68
|
+
await t.refresh(), o = await a(n, r, i);
|
|
69
|
+
} catch {
|
|
70
|
+
throw await t.onUnauthorized?.(o), await u(o, i);
|
|
71
|
+
}
|
|
72
|
+
o.status === 401 && await t.onUnauthorized?.(o);
|
|
73
|
+
} else await t.onUnauthorized?.(o);
|
|
55
74
|
}
|
|
56
|
-
if (!o.ok) throw await
|
|
75
|
+
if (!o.ok) throw await u(o, i);
|
|
57
76
|
if (o.status !== 204) return (o.headers.get("content-type") ?? "").includes("application/json") ? await o.json() : await o.text();
|
|
58
77
|
}
|
|
59
|
-
async function
|
|
60
|
-
|
|
78
|
+
async function f(e, n = {}) {
|
|
79
|
+
let i = s(t.retry);
|
|
80
|
+
if (!i) return d(e, n);
|
|
81
|
+
let a = (n.method ?? "GET").toUpperCase();
|
|
82
|
+
return r(() => d(e, n), {
|
|
83
|
+
...i,
|
|
84
|
+
shouldRetry: i.shouldRetry ?? ((e) => o(e, a))
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function p(e, t, n = "POST") {
|
|
88
|
+
return f(e, {
|
|
61
89
|
method: n,
|
|
62
90
|
body: t
|
|
63
91
|
});
|
|
64
92
|
}
|
|
65
93
|
return {
|
|
66
|
-
request:
|
|
67
|
-
get: (e, t) =>
|
|
94
|
+
request: f,
|
|
95
|
+
get: (e, t) => f(e, {
|
|
68
96
|
...t,
|
|
69
97
|
method: "GET"
|
|
70
98
|
}),
|
|
71
|
-
post: (e, t) =>
|
|
99
|
+
post: (e, t) => f(e, {
|
|
72
100
|
...t,
|
|
73
101
|
method: "POST"
|
|
74
102
|
}),
|
|
75
|
-
put: (e, t) =>
|
|
103
|
+
put: (e, t) => f(e, {
|
|
76
104
|
...t,
|
|
77
105
|
method: "PUT"
|
|
78
106
|
}),
|
|
79
|
-
patch: (e, t) =>
|
|
107
|
+
patch: (e, t) => f(e, {
|
|
80
108
|
...t,
|
|
81
109
|
method: "PATCH"
|
|
82
110
|
}),
|
|
83
|
-
delete: (e, t) =>
|
|
111
|
+
delete: (e, t) => f(e, {
|
|
84
112
|
...t,
|
|
85
113
|
method: "DELETE"
|
|
86
114
|
}),
|
|
87
|
-
upload:
|
|
115
|
+
upload: p
|
|
88
116
|
};
|
|
89
117
|
}
|
|
90
118
|
//#endregion
|
|
91
|
-
export {
|
|
119
|
+
export { d as createApiClient };
|
|
92
120
|
|
|
93
121
|
//# sourceMappingURL=api-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is three lines over the limit and\n * every one of them is a request-lifecycle concern the client cannot delegate: base\n * URL joining, the auth header, the retry loop, the timeout signal and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiError, TempestApiError } from \"./errors\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\nfunction buildUrl(baseURL: string, path: string, params?: RequestOptions[\"params\"]): string {\n const url = new URL(path, baseURL.endsWith(\"/\") ? baseURL : `${baseURL}/`);\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n return url.toString();\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\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`,\n * automatic refresh + retry on 401 when `refresh` is supplied, and uploads\n * via `FormData`. Throws an `ApiError` on non-2xx responses.\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, ...rest } = options;\n const isForm = isFormData(body);\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 init: RequestInit = {\n ...rest,\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 return fetcher(buildUrl(config.baseURL, path, params), init);\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n let response = await rawRequest(path, options, requestId);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await rawRequest(path, options, requestId);\n } catch {\n await config.onUnauthorized?.(response);\n throw await parseError(response, requestId);\n }\n } else {\n await config.onUnauthorized?.(response);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { 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 upload,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.js","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 { buildApiError, TempestApiError } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\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 */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\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 RETRIABLE_STATUSES.has(error.status) || error.status >= 500;\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\nfunction buildUrl(baseURL: string, path: string, params?: RequestOptions[\"params\"]): string {\n const url = new URL(path, baseURL.endsWith(\"/\") ? baseURL : `${baseURL}/`);\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n return url.toString();\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\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 * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\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 * 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`/`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, ...rest } = options;\n const isForm = isFormData(body);\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 init: RequestInit = {\n ...rest,\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 return fetcher(buildUrl(config.baseURL, path, params), init);\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n let response = await rawRequest(path, options, requestId);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await rawRequest(path, options, requestId);\n } catch {\n await config.onUnauthorized?.(response);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await config.onUnauthorized?.(response);\n }\n } else {\n await config.onUnauthorized?.(response);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { 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 upload,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs"),t=require("./
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./retry.cjs"),n=require("./idempotency.cjs");var r=`1.0.0`,i=5242880;function a(e){let t=new TextEncoder().encode(e),n=``;for(let e of t)n+=String.fromCharCode(e);return btoa(n)}function o(e){if(!e)return null;let t=Object.entries(e).map(([e,t])=>`${e} ${a(t)}`);return t.length>0?t.join(`,`):null}function s(e,t){let n=t,r=typeof n.name==`string`?n.name:`blob`,i=typeof n.lastModified==`number`?n.lastModified:0;return`${e}|${r}|${t.size}|${t.type}|${i}`}function c(e=`tempest-upload:`){function t(){try{return typeof localStorage>`u`?null:localStorage}catch{return null}}return{get(n){let r=t()?.getItem(e+n);if(!r)return null;try{return JSON.parse(r)}catch{return null}},set(n,r){t()?.setItem(e+n,JSON.stringify(r))},delete(n){t()?.removeItem(e+n)}}}function l(t){return new Promise((n,r)=>{let i=new XMLHttpRequest;i.open(t.method,t.url),i.withCredentials=t.withCredentials;for(let[e,n]of Object.entries(t.headers))i.setRequestHeader(e,n);if(t.onProgress){let e=t.onProgress;i.upload.onprogress=t=>e(t.loaded)}i.onload=()=>n({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>r(new e.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),t.register(i),i.send(t.body)})}function u(e){let t=e.header(`Upload-Offset`);if(t===null)return null;let n=Number(t);return Number.isFinite(n)&&n>=0?n:null}function d(e){if(!e)return null;try{return JSON.parse(e)}catch{return e}}function f(t,n){let r=d(t.text),i=e.buildApiError(t.status,r,{get:t.header}),a=typeof r==`object`&&!!r&&(`detail`in r||`message`in r);return new e.TempestApiError({...i,detail:a?i.detail:n})}function p(e){let t=typeof window>`u`?void 0:window.location.href;try{return new URL(e,t).href}catch{return e}}function m(a){let{endpoint:d,file:m,chunkSize:h=i,metadata:g,headers:_={},getToken:v,withCredentials:y=!1,key:b=s(d,m),storage:x=c(),retry:S,onProgress:C,onStateChange:w}=a,T=`idle`,E=0,D=null,O=null,k=null,A=null,j=0;function M(e){T!==e&&(T=e,w?.(e))}function N(e){C?.({loaded:e,total:m.size,fraction:m.size===0?1:e/m.size,resumedFrom:j})}function P(){let e={..._,"Tus-Resumable":r},t=v?.();return t&&!(`Authorization`in e)&&(e.Authorization=`Bearer ${t}`),e}function F(e){A=e}async function I(){!x||!D||!O||await x.set(b,{url:D,offset:E,size:m.size,idempotencyKey:O,updatedAt:Date.now()})}async function L(t){let n=await l({method:`HEAD`,url:t,headers:P(),withCredentials:y,register:F});if(n.status===404||n.status===410)throw new e.TempestApiError({status:n.status,detail:`O upload expirou no servidor. Comece de novo.`});let r=u(n);if(r===null)throw f(n,`HEAD sem Upload-Offset.`);return r}async function R(){let e=x?await x.get(b):null;if(e&&e.size===m.size&&(O=e.idempotencyKey,e.url))try{return E=await L(e.url),D=e.url,e.url}catch{E=0}M(`creating`),O??=n.generateIdempotencyKey(),D=null,E=0,x&&await x.set(b,{url:``,offset:0,size:m.size,idempotencyKey:O,updatedAt:Date.now()});let t={...P(),"Upload-Length":String(m.size),"Idempotency-Key":O},r=o(g);r&&(t[`Upload-Metadata`]=r);let i=await l({method:`POST`,url:d,headers:t,withCredentials:y,register:F});if(i.status!==201)throw f(i,`Criação do upload recusada.`);let a=i.header(`Location`);if(!a)throw f(i,`Criação do upload sem cabeçalho Location.`);return D=p(a),await I(),D}async function z(e,t){if(t.needed&&(E=await L(e),t.needed=!1,N(E),await I(),E>=m.size))return;let n=Math.min(E+h,m.size),r=E,i=await l({method:`PATCH`,url:e,headers:{...P(),"Content-Type":`application/offset+octet-stream`,"Upload-Offset":String(r)},body:m.slice(r,n),withCredentials:y,onProgress:e=>N(Math.min(r+e,m.size)),register:F});if(i.status===409||i.status===412)throw t.needed=!0,f(i,`Offset divergente — o servidor já tinha esses bytes.`);if(i.status!==204&&i.status!==200)throw f(i,`Chunk recusado pelo servidor.`);E=u(i)??n,N(E),await I()}async function B(){k=null;let e=await R();j=E,M(`uploading`),N(E);let n={needed:!1};for(;E<m.size&&!k;)await t.retry(()=>z(e,n),{retries:5,...S,shouldRetry:(e,t)=>k||e instanceof DOMException&&e.name===`AbortError`?!1:(n.needed=!0,S?.shouldRetry?.(e,t)??!0)});return k===`pause`?(M(`paused`),null):k===`abort`?(M(`aborted`),null):(M(`done`),x&&await x.delete(b),{url:e,size:m.size})}async function V(){try{return await B()}catch(e){if(k!==null||e instanceof DOMException&&e.name===`AbortError`)return M(k===`abort`?`aborted`:`paused`),null;throw M(`error`),e}finally{A=null}}function H(e){k=e,A?.abort(),A=null}return{start:V,resume:V,pause:()=>H(`pause`),abort:async({discard:e=!1}={})=>{H(`abort`),M(`aborted`),e&&(D&&await l({method:`DELETE`,url:D,headers:P(),withCredentials:y,register:()=>void 0}).catch(()=>void 0),x&&await x.delete(b))},get state(){return T},get offset(){return E},get url(){return D},key:b}}exports.DEFAULT_CHUNK_SIZE=i,exports.TUS_VERSION=r,exports.createLocalUploadStorage=c,exports.createResumableUpload=m,exports.uploadFingerprint=s;
|
|
2
2
|
//# sourceMappingURL=resumable-upload.cjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TempestApiError as e, buildApiError as t } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { retry as n } from "./retry.js";
|
|
3
|
+
import { generateIdempotencyKey as r } from "./idempotency.js";
|
|
4
4
|
//#region src/http/resumable-upload.ts
|
|
5
5
|
var i = "1.0.0", a = 5242880;
|
|
6
6
|
function o(e) {
|
|
@@ -146,7 +146,7 @@ function h(t) {
|
|
|
146
146
|
} catch {
|
|
147
147
|
E = 0;
|
|
148
148
|
}
|
|
149
|
-
M("creating"), O ??=
|
|
149
|
+
M("creating"), O ??= r(), D = null, E = 0, x && await x.set(b, {
|
|
150
150
|
url: "",
|
|
151
151
|
offset: 0,
|
|
152
152
|
size: f.size,
|
|
@@ -157,8 +157,8 @@ function h(t) {
|
|
|
157
157
|
...P(),
|
|
158
158
|
"Upload-Length": String(f.size),
|
|
159
159
|
"Idempotency-Key": O
|
|
160
|
-
},
|
|
161
|
-
|
|
160
|
+
}, n = s(g);
|
|
161
|
+
n && (t["Upload-Metadata"] = n);
|
|
162
162
|
let i = await u({
|
|
163
163
|
method: "POST",
|
|
164
164
|
url: o,
|
|
@@ -195,7 +195,7 @@ function h(t) {
|
|
|
195
195
|
let e = await R();
|
|
196
196
|
j = E, M("uploading"), N(E);
|
|
197
197
|
let t = { needed: !1 };
|
|
198
|
-
for (; E < f.size && !k;) await
|
|
198
|
+
for (; E < f.size && !k;) await n(() => z(e, t), {
|
|
199
199
|
retries: 5,
|
|
200
200
|
...S,
|
|
201
201
|
shouldRetry: (e, n) => k || e instanceof DOMException && e.name === "AbortError" ? !1 : (t.needed = !0, S?.shouldRetry?.(e, n) ?? !0)
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=`circle-question-mark`,t={brush:`brush`,build:`wrench`,code:`code`,delivery_dining:`bike`,electrical_services:`plug-zap`,format_paint:`paint-roller`,gavel:`gavel`,handyman:`wrench`,hardware:`wrench`,key:`key`,lock:`lock`,mic:`mic`,palette:`palette`,pedal_bike:`bike`,plumbing:`shower-head`,router:`router`,settings:`settings`,shield:`shield`,smartphone:`smartphone`,tv:`tv`,two_wheeler:`bike`,warehouse:`warehouse`};function n(n,r=e){return n?t[n.trim().toLowerCase()]??r:r}exports.MATERIAL_SYMBOL_FALLBACK=e,exports.fromMaterialSymbol=n,exports.materialToLucide=t;
|
|
2
|
+
//# sourceMappingURL=material-symbols.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"material-symbols.cjs","names":[],"sources":["../../src/icons/material-symbols.ts"],"sourcesContent":["import type { IconName } from \"./generated/icon-name\";\n\n/**\n * The slug `fromMaterialSymbol` falls back to when a code is unknown.\n *\n * A neutral glyph rather than nothing: a category created in an admin with a\n * code this table has not learned yet still has to draw something, or it opens a\n * hole in the grid and the bug reads as a layout problem instead of a missing\n * mapping.\n *\n * Lucide's `circle-help` is a deprecated alias of this slug. `<Icon>` resolves\n * aliases, so both render — but the bridge emits the canonical name, since a\n * value that gets persisted should not be one lucide has already renamed.\n */\nexport const MATERIAL_SYMBOL_FALLBACK: IconName = \"circle-question-mark\";\n\n/**\n * Material Symbols code → lucide slug, for backends that store `icon_code`.\n *\n * Every Python backend we write stores a category icon as a Material Symbol\n * (`build`, `format_paint`, `electrical_services`) — the vocabulary Flutter,\n * Android and the administrative seeds already speak. The SDK speaks lucide in\n * kebab-case. The two lists do not meet, and the failure is nastier than an\n * empty screen: a handful of codes collide by accident, so roughly one row in\n * ten draws the right icon and the bug reads as \"some icons went missing\".\n *\n * **This is a seed table, not the full vocabulary.** Material Symbols ships\n * ~3600 names and almost none of them will ever appear in an `icon_code` of\n * ours. It grows on demand, one hand-written pair at a time — a map generated by\n * name heuristics gets it badly wrong, starting with `build`, which is a wrench\n * in Material Symbols and not anything to do with construction.\n *\n * **Approximations, deliberate and not one-to-one:**\n *\n * - `plumbing` → `shower-head`, because lucide has no pipe.\n * - `build`, `handyman` and `hardware` all land on `wrench` — Material Symbols\n * distinguishes the tools, lucide does not.\n * - `pedal_bike`, `two_wheeler` and `delivery_dining` all land on `bike`.\n *\n * The thirteen identity pairs are here on purpose. Their names happen to match\n * in both vocabularies, so they already render today — leaving them out would\n * send them to {@link MATERIAL_SYMBOL_FALLBACK} and make this bridge a\n * regression for exactly the codes that used to work.\n *\n * @see fromMaterialSymbol — the lookup you normally call.\n */\nexport const materialToLucide: Readonly<Record<string, IconName>> = {\n brush: \"brush\",\n build: \"wrench\",\n code: \"code\",\n delivery_dining: \"bike\",\n electrical_services: \"plug-zap\",\n format_paint: \"paint-roller\",\n gavel: \"gavel\",\n handyman: \"wrench\",\n hardware: \"wrench\",\n key: \"key\",\n lock: \"lock\",\n mic: \"mic\",\n palette: \"palette\",\n pedal_bike: \"bike\",\n plumbing: \"shower-head\",\n router: \"router\",\n settings: \"settings\",\n shield: \"shield\",\n smartphone: \"smartphone\",\n tv: \"tv\",\n two_wheeler: \"bike\",\n warehouse: \"warehouse\",\n};\n\n/**\n * Translate a Material Symbols code into a lucide slug, always returning one.\n *\n * Never returns `undefined`: an unknown code resolves to `fallback`, so a row\n * whose `icon_code` this table has not learned yet still renders. The input is\n * trimmed and lower-cased before lookup, because a seed written by hand is the\n * kind of source that carries stray whitespace and the odd capital.\n *\n * Nothing else in `/icons` imports this module, so an app that does not store\n * Material Symbols never pays for the table.\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code)} size={20} />\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code, \"folder\")} size={20} />\n *\n * @param code - A Material Symbols name, or `null`/`undefined` for a row that has none.\n * @param fallback - Slug to use when `code` is empty or unknown. Defaults to {@link MATERIAL_SYMBOL_FALLBACK}.\n * @returns A lucide slug `<Icon>` can render.\n */\nexport function fromMaterialSymbol(\n code: string | null | undefined,\n fallback: IconName = MATERIAL_SYMBOL_FALLBACK,\n): IconName {\n if (!code) return fallback;\n return materialToLucide[code.trim().toLowerCase()] ?? fallback;\n}\n"],"mappings":"AAcA,IAAa,EAAqC,uBAgCrC,EAAuD,CAChE,MAAO,QACP,MAAO,SACP,KAAM,OACN,gBAAiB,OACjB,oBAAqB,WACrB,aAAc,eACd,MAAO,QACP,SAAU,SACV,SAAU,SACV,IAAK,MACL,KAAM,OACN,IAAK,MACL,QAAS,UACT,WAAY,OACZ,SAAU,cACV,OAAQ,SACR,SAAU,WACV,OAAQ,SACR,WAAY,aACZ,GAAI,KACJ,YAAa,OACb,UAAW,WACf,EAuBA,SAAgB,EACZ,EACA,EAAqB,EACb,CAER,OADK,EACE,EAAiB,EAAK,KAAK,CAAC,CAAC,YAAY,IAAM,EADpC,CAEtB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/icons/material-symbols.ts
|
|
2
|
+
var e = "circle-question-mark", t = {
|
|
3
|
+
brush: "brush",
|
|
4
|
+
build: "wrench",
|
|
5
|
+
code: "code",
|
|
6
|
+
delivery_dining: "bike",
|
|
7
|
+
electrical_services: "plug-zap",
|
|
8
|
+
format_paint: "paint-roller",
|
|
9
|
+
gavel: "gavel",
|
|
10
|
+
handyman: "wrench",
|
|
11
|
+
hardware: "wrench",
|
|
12
|
+
key: "key",
|
|
13
|
+
lock: "lock",
|
|
14
|
+
mic: "mic",
|
|
15
|
+
palette: "palette",
|
|
16
|
+
pedal_bike: "bike",
|
|
17
|
+
plumbing: "shower-head",
|
|
18
|
+
router: "router",
|
|
19
|
+
settings: "settings",
|
|
20
|
+
shield: "shield",
|
|
21
|
+
smartphone: "smartphone",
|
|
22
|
+
tv: "tv",
|
|
23
|
+
two_wheeler: "bike",
|
|
24
|
+
warehouse: "warehouse"
|
|
25
|
+
};
|
|
26
|
+
function n(n, r = e) {
|
|
27
|
+
return n ? t[n.trim().toLowerCase()] ?? r : r;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { e as MATERIAL_SYMBOL_FALLBACK, n as fromMaterialSymbol, t as materialToLucide };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=material-symbols.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"material-symbols.js","names":[],"sources":["../../src/icons/material-symbols.ts"],"sourcesContent":["import type { IconName } from \"./generated/icon-name\";\n\n/**\n * The slug `fromMaterialSymbol` falls back to when a code is unknown.\n *\n * A neutral glyph rather than nothing: a category created in an admin with a\n * code this table has not learned yet still has to draw something, or it opens a\n * hole in the grid and the bug reads as a layout problem instead of a missing\n * mapping.\n *\n * Lucide's `circle-help` is a deprecated alias of this slug. `<Icon>` resolves\n * aliases, so both render — but the bridge emits the canonical name, since a\n * value that gets persisted should not be one lucide has already renamed.\n */\nexport const MATERIAL_SYMBOL_FALLBACK: IconName = \"circle-question-mark\";\n\n/**\n * Material Symbols code → lucide slug, for backends that store `icon_code`.\n *\n * Every Python backend we write stores a category icon as a Material Symbol\n * (`build`, `format_paint`, `electrical_services`) — the vocabulary Flutter,\n * Android and the administrative seeds already speak. The SDK speaks lucide in\n * kebab-case. The two lists do not meet, and the failure is nastier than an\n * empty screen: a handful of codes collide by accident, so roughly one row in\n * ten draws the right icon and the bug reads as \"some icons went missing\".\n *\n * **This is a seed table, not the full vocabulary.** Material Symbols ships\n * ~3600 names and almost none of them will ever appear in an `icon_code` of\n * ours. It grows on demand, one hand-written pair at a time — a map generated by\n * name heuristics gets it badly wrong, starting with `build`, which is a wrench\n * in Material Symbols and not anything to do with construction.\n *\n * **Approximations, deliberate and not one-to-one:**\n *\n * - `plumbing` → `shower-head`, because lucide has no pipe.\n * - `build`, `handyman` and `hardware` all land on `wrench` — Material Symbols\n * distinguishes the tools, lucide does not.\n * - `pedal_bike`, `two_wheeler` and `delivery_dining` all land on `bike`.\n *\n * The thirteen identity pairs are here on purpose. Their names happen to match\n * in both vocabularies, so they already render today — leaving them out would\n * send them to {@link MATERIAL_SYMBOL_FALLBACK} and make this bridge a\n * regression for exactly the codes that used to work.\n *\n * @see fromMaterialSymbol — the lookup you normally call.\n */\nexport const materialToLucide: Readonly<Record<string, IconName>> = {\n brush: \"brush\",\n build: \"wrench\",\n code: \"code\",\n delivery_dining: \"bike\",\n electrical_services: \"plug-zap\",\n format_paint: \"paint-roller\",\n gavel: \"gavel\",\n handyman: \"wrench\",\n hardware: \"wrench\",\n key: \"key\",\n lock: \"lock\",\n mic: \"mic\",\n palette: \"palette\",\n pedal_bike: \"bike\",\n plumbing: \"shower-head\",\n router: \"router\",\n settings: \"settings\",\n shield: \"shield\",\n smartphone: \"smartphone\",\n tv: \"tv\",\n two_wheeler: \"bike\",\n warehouse: \"warehouse\",\n};\n\n/**\n * Translate a Material Symbols code into a lucide slug, always returning one.\n *\n * Never returns `undefined`: an unknown code resolves to `fallback`, so a row\n * whose `icon_code` this table has not learned yet still renders. The input is\n * trimmed and lower-cased before lookup, because a seed written by hand is the\n * kind of source that carries stray whitespace and the odd capital.\n *\n * Nothing else in `/icons` imports this module, so an app that does not store\n * Material Symbols never pays for the table.\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code)} size={20} />\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code, \"folder\")} size={20} />\n *\n * @param code - A Material Symbols name, or `null`/`undefined` for a row that has none.\n * @param fallback - Slug to use when `code` is empty or unknown. Defaults to {@link MATERIAL_SYMBOL_FALLBACK}.\n * @returns A lucide slug `<Icon>` can render.\n */\nexport function fromMaterialSymbol(\n code: string | null | undefined,\n fallback: IconName = MATERIAL_SYMBOL_FALLBACK,\n): IconName {\n if (!code) return fallback;\n return materialToLucide[code.trim().toLowerCase()] ?? fallback;\n}\n"],"mappings":";AAcA,IAAa,IAAqC,wBAgCrC,IAAuD;CAChE,OAAO;CACP,OAAO;CACP,MAAM;CACN,iBAAiB;CACjB,qBAAqB;CACrB,cAAc;CACd,OAAO;CACP,UAAU;CACV,UAAU;CACV,KAAK;CACL,MAAM;CACN,KAAK;CACL,SAAS;CACT,YAAY;CACZ,UAAU;CACV,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,IAAI;CACJ,aAAa;CACb,WAAW;AACf;AAuBA,SAAgB,EACZ,GACA,IAAqB,GACb;CAER,OADK,IACE,EAAiB,EAAK,KAAK,CAAC,CAAC,YAAY,MAAM,IADpC;AAEtB"}
|
package/dist/icons.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./icons/generated/aliases.cjs"),t=require("./icons/generated/icon-names.cjs"),n=require("./icons/icon-context.cjs"),r=require("./icons/shard-cache.cjs"),i=require("./icons/use-icon.cjs"),a=require("./icons/Icon.cjs"),o=require("./icons/IconProvider.cjs"),s=require("./icons/is-icon-name.cjs");exports.Icon=a.Icon,exports.IconProvider=o.IconProvider,exports.createIconRegistry=n.createIconRegistry,exports.iconAliases=e.iconAliases,exports.iconNames=t.iconNames,exports.iconStatus=r.iconStatus,exports.isIconName=s.isIconName,exports.loadIcon=r.loadIcon,exports.peekIcon=r.peekIcon,exports.preloadIcons=r.preloadIcons,exports.resolveIconAlias=r.resolveIconAlias,exports.useIcon=i.useIcon;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./icons/generated/aliases.cjs"),t=require("./icons/generated/icon-names.cjs"),n=require("./icons/icon-context.cjs"),r=require("./icons/shard-cache.cjs"),i=require("./icons/use-icon.cjs"),a=require("./icons/Icon.cjs"),o=require("./icons/IconProvider.cjs"),s=require("./icons/is-icon-name.cjs"),c=require("./icons/material-symbols.cjs");exports.Icon=a.Icon,exports.IconProvider=o.IconProvider,exports.MATERIAL_SYMBOL_FALLBACK=c.MATERIAL_SYMBOL_FALLBACK,exports.createIconRegistry=n.createIconRegistry,exports.fromMaterialSymbol=c.fromMaterialSymbol,exports.iconAliases=e.iconAliases,exports.iconNames=t.iconNames,exports.iconStatus=r.iconStatus,exports.isIconName=s.isIconName,exports.loadIcon=r.loadIcon,exports.materialToLucide=c.materialToLucide,exports.peekIcon=r.peekIcon,exports.preloadIcons=r.preloadIcons,exports.resolveIconAlias=r.resolveIconAlias,exports.useIcon=i.useIcon;
|