tempest-react-sdk 0.76.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/alias/collect.mjs +20 -3
- package/bin/lib/alias/index.mjs +12 -1
- package/bin/lib/alias/index.test.mjs +24 -0
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/css/analyze.test.mjs +21 -1
- package/bin/lib/css/collect.mjs +14 -0
- package/bin/lib/css/extract.mjs +63 -5
- package/bin/lib/css/extract.test.mjs +110 -1
- package/bin/lib/css/fix.e2e.test.mjs +50 -0
- package/bin/lib/css/index.mjs +6 -1
- package/bin/lib/fs/contain.mjs +50 -0
- package/bin/lib/icons/generate.mjs +28 -3
- package/bin/lib/icons/generate.test.mjs +57 -1
- package/bin/lib/openapi/escape.mjs +224 -0
- package/bin/lib/openapi/generate.encode.test.mjs +120 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +621 -0
- package/bin/lib/openapi/generate.mjs +129 -18
- package/bin/lib/openapi/generate.test.mjs +7 -2
- package/bin/lib/openapi/schema-to-zod.mjs +43 -4
- package/bin/tempest-gen.test.mjs +188 -0
- package/bin/tempest.mjs +263 -6
- 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 +203 -58
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +85 -72
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +19 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/CodeBlock/tokenize.cjs +1 -1
- package/dist/components/CodeBlock/tokenize.cjs.map +1 -1
- package/dist/components/CodeBlock/tokenize.js +54 -25
- package/dist/components/CodeBlock/tokenize.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +171 -90
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.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 +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +141 -105
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +5 -6
- package/dist/http/retry.js.map +1 -1
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +272 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +309 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1183 -64
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +18 -14
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/owner-fingerprint.cjs +2 -0
- package/dist/utils/owner-fingerprint.cjs.map +1 -0
- package/dist/utils/owner-fingerprint.js +18 -0
- package/dist/utils/owner-fingerprint.js.map +1 -0
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +130 -64
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +23 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **CSRF** is off unless you set `csrf`. With it, every unsafe request (anything\n * but `GET`/`HEAD`/`OPTIONS`/`TRACE`) echoes the `csrf_token` cookie in an\n * `X-CSRF-Token` header — the double-submit contract of `tempest-fastapi-sdk`'s\n * `CSRFMiddleware` — scoped to the same origins as the bearer token and sent on\n * `skipAuth` requests too. See {@link ApiClientConfig.csrf}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const callerHeaders = headers as Record<string, string> | undefined;\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...csrfHeaders({\n method: rest.method ?? \"GET\",\n url,\n reference: config.baseURL,\n csrf: config.csrf,\n trustedOrigins: config.trustedOrigins,\n headers: { ...config.headers, ...callerHeaders },\n }),\n ...callerHeaders,\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, init, requestId, method, skipAuth);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport {\n buildApiError,\n SessionSupersededError,\n TempestApiError,\n isRetriableStatus,\n} from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestAuthContext, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **CSRF** is off unless you set `csrf`. With it, every unsafe request (anything\n * but `GET`/`HEAD`/`OPTIONS`/`TRACE`) echoes the `csrf_token` cookie in an\n * `X-CSRF-Token` header — the double-submit contract of `tempest-fastapi-sdk`'s\n * `CSRFMiddleware` — scoped to the same origins as the bearer token and sent on\n * `skipAuth` requests too. See {@link ApiClientConfig.csrf}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const callerHeaders = headers as Record<string, string> | undefined;\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...csrfHeaders({\n method: rest.method ?? \"GET\",\n url,\n reference: config.baseURL,\n csrf: config.csrf,\n trustedOrigins: config.trustedOrigins,\n headers: { ...config.headers, ...callerHeaders },\n }),\n ...callerHeaders,\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n /**\n * Call `onUnauthorized`, keeping the request's own error unless the hook\n * says the attempt belongs to an ended session.\n *\n * @param response - The `401` that ended the attempt.\n * @param requestId - Correlation id for the log.\n * @param context - The credential and session the attempt left with.\n * @throws {SessionSupersededError} When `onUnauthorized` threw it.\n */\n async function notifyUnauthorized(\n response: Response,\n requestId: string,\n context: RequestAuthContext,\n ): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response, context);\n } catch (error) {\n if (error instanceof SessionSupersededError) throw error;\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n /**\n * The credential and session an attempt leaves with, read just before it is\n * sent.\n *\n * @param skipAuth - Whether the attempt goes out without credentials.\n * @returns The context handed to `refresh` and `onUnauthorized`.\n */\n function authContext(skipAuth: boolean): RequestAuthContext {\n return {\n token: skipAuth ? null : (config.getToken?.() ?? null),\n epoch: config.getSessionEpoch?.() ?? null,\n };\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n const sent = authContext(skipAuth);\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n let replayed: RequestAuthContext;\n try {\n await config.refresh(sent);\n replayed = authContext(skipAuth);\n response = await send(path, init, requestId, method, skipAuth);\n } catch (error) {\n if (error instanceof SessionSupersededError) throw error;\n await notifyUnauthorized(response, requestId, sent);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId, replayed);\n }\n } else {\n await notifyUnauthorized(response, requestId, sent);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
|
package/dist/http/api-client.js
CHANGED
|
@@ -3,29 +3,29 @@ import { buildApiUrl as t } from "./build-url.js";
|
|
|
3
3
|
import { isTrustedCredentialTarget as n, reportSuppressedCredential as r } from "./credential-scope.js";
|
|
4
4
|
import { csrfHeaders as i } from "./csrf.js";
|
|
5
5
|
import { decodeByContentType as a } from "./decode-response.js";
|
|
6
|
-
import {
|
|
7
|
-
import { retry as
|
|
8
|
-
import { withTimeout as
|
|
6
|
+
import { SessionSupersededError as o, TempestApiError as s, buildApiError as c, isRetriableStatus as l } from "./errors.js";
|
|
7
|
+
import { retry as u } from "./retry.js";
|
|
8
|
+
import { withTimeout as d } from "./timeout.js";
|
|
9
9
|
//#region src/http/api-client.ts
|
|
10
|
-
var
|
|
10
|
+
var f = /* @__PURE__ */ new Set([
|
|
11
11
|
"GET",
|
|
12
12
|
"HEAD",
|
|
13
13
|
"OPTIONS"
|
|
14
14
|
]);
|
|
15
|
-
function
|
|
16
|
-
return !
|
|
15
|
+
function p(e, t) {
|
|
16
|
+
return !f.has(t) || !(e instanceof s) ? !1 : l(e.status);
|
|
17
17
|
}
|
|
18
|
-
function
|
|
18
|
+
function m(e) {
|
|
19
19
|
return e ? e === !0 ? {} : e : null;
|
|
20
20
|
}
|
|
21
|
-
var
|
|
22
|
-
function
|
|
21
|
+
var h = 15e3, g = 3e5;
|
|
22
|
+
function _(e) {
|
|
23
23
|
return typeof FormData < "u" && e instanceof FormData;
|
|
24
24
|
}
|
|
25
|
-
function
|
|
25
|
+
function v(e) {
|
|
26
26
|
return Math.round(performance.now() - e);
|
|
27
27
|
}
|
|
28
|
-
async function
|
|
28
|
+
async function y(e, t) {
|
|
29
29
|
let n;
|
|
30
30
|
try {
|
|
31
31
|
n = await e.clone().json();
|
|
@@ -36,47 +36,47 @@ async function v(e, t) {
|
|
|
36
36
|
n = null;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
return new
|
|
39
|
+
return new s(c(e.status, n, e.headers, t));
|
|
40
40
|
}
|
|
41
|
-
function
|
|
42
|
-
let
|
|
43
|
-
function
|
|
41
|
+
function b(c) {
|
|
42
|
+
let l = c.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
43
|
+
function f(e, t) {
|
|
44
44
|
if (t) return {};
|
|
45
|
-
let i =
|
|
46
|
-
return i ? n(e,
|
|
45
|
+
let i = c.getToken?.();
|
|
46
|
+
return i ? n(e, c.baseURL, c.trustedOrigins) ? { Authorization: `Bearer ${i}` } : (r(e, c.baseURL), {}) : {};
|
|
47
47
|
}
|
|
48
|
-
async function
|
|
49
|
-
let { body:
|
|
50
|
-
prefix:
|
|
51
|
-
params:
|
|
48
|
+
async function b(e, n, r, a) {
|
|
49
|
+
let { body: o, params: u, headers: p, signal: m, timeout: v, ...y } = n, b = _(o), x = b ? c.uploadTimeout : c.timeout, S = v === void 0 ? x === void 0 ? b ? g : h : x : v, C = t(c.baseURL, e, {
|
|
50
|
+
prefix: c.prefix,
|
|
51
|
+
params: u
|
|
52
52
|
}), w = p, T = {
|
|
53
53
|
...b ? {} : { "Content-Type": "application/json" },
|
|
54
54
|
...r ? { "X-Request-ID": r } : {},
|
|
55
|
-
...
|
|
56
|
-
...
|
|
55
|
+
...c.headers,
|
|
56
|
+
...f(C, a),
|
|
57
57
|
...i({
|
|
58
58
|
method: y.method ?? "GET",
|
|
59
59
|
url: C,
|
|
60
|
-
reference:
|
|
61
|
-
csrf:
|
|
62
|
-
trustedOrigins:
|
|
60
|
+
reference: c.baseURL,
|
|
61
|
+
csrf: c.csrf,
|
|
62
|
+
trustedOrigins: c.trustedOrigins,
|
|
63
63
|
headers: {
|
|
64
|
-
...
|
|
64
|
+
...c.headers,
|
|
65
65
|
...w
|
|
66
66
|
}
|
|
67
67
|
}),
|
|
68
68
|
...w
|
|
69
|
-
}, E =
|
|
69
|
+
}, E = d(m, S), D = {
|
|
70
70
|
...y,
|
|
71
71
|
signal: E.signal,
|
|
72
72
|
headers: T,
|
|
73
|
-
credentials:
|
|
74
|
-
body:
|
|
73
|
+
credentials: c.withCredentials ? "include" : y.credentials,
|
|
74
|
+
body: o == null ? void 0 : b ? o : JSON.stringify(o)
|
|
75
75
|
};
|
|
76
76
|
try {
|
|
77
|
-
return await
|
|
77
|
+
return await l(C, D);
|
|
78
78
|
} catch (e) {
|
|
79
|
-
throw E.timedOut() ? new
|
|
79
|
+
throw E.timedOut() ? new s({
|
|
80
80
|
status: 0,
|
|
81
81
|
detail: `A requisição excedeu ${S}ms e foi abandonada.`
|
|
82
82
|
}) : e;
|
|
@@ -84,35 +84,36 @@ function y(s) {
|
|
|
84
84
|
E.dispose();
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
async function
|
|
88
|
-
let a =
|
|
89
|
-
if (!a) return
|
|
87
|
+
async function x(e, t, n, r, i) {
|
|
88
|
+
let a = c.logger;
|
|
89
|
+
if (!a) return b(e, t, n, i);
|
|
90
90
|
let o = performance.now();
|
|
91
91
|
try {
|
|
92
|
-
let s = await
|
|
92
|
+
let s = await b(e, t, n, i), c = {
|
|
93
93
|
requestId: n,
|
|
94
94
|
status: s.status,
|
|
95
|
-
ms:
|
|
95
|
+
ms: v(o)
|
|
96
96
|
}, l = `${r} ${e} → ${s.status}`;
|
|
97
97
|
return s.status >= 400 ? a.warn(l, c) : a.debug(l, c), s;
|
|
98
98
|
} catch (t) {
|
|
99
99
|
throw a.warn(`${r} ${e} → no response`, {
|
|
100
100
|
requestId: n,
|
|
101
|
-
ms:
|
|
101
|
+
ms: v(o),
|
|
102
102
|
error: t
|
|
103
103
|
}), t;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
async function
|
|
107
|
-
if (
|
|
108
|
-
|
|
106
|
+
async function S(e, t, n) {
|
|
107
|
+
if (c.onUnauthorized) {
|
|
108
|
+
c.logger?.warn("unauthorized — calling onUnauthorized", {
|
|
109
109
|
requestId: t,
|
|
110
110
|
status: e.status
|
|
111
111
|
});
|
|
112
112
|
try {
|
|
113
|
-
await
|
|
113
|
+
await c.onUnauthorized(e, n);
|
|
114
114
|
} catch (n) {
|
|
115
|
-
|
|
115
|
+
if (n instanceof o) throw n;
|
|
116
|
+
c.logger?.warn("onUnauthorized threw — keeping the original response error", {
|
|
116
117
|
requestId: t,
|
|
117
118
|
status: e.status,
|
|
118
119
|
error: n
|
|
@@ -120,68 +121,75 @@ function y(s) {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
function C(e) {
|
|
125
|
+
return {
|
|
126
|
+
token: e ? null : c.getToken?.() ?? null,
|
|
127
|
+
epoch: c.getSessionEpoch?.() ?? null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async function w(t, n, r) {
|
|
131
|
+
let { skipAuth: i = !1, skipAuthRetry: a = !1, ...s } = n, l = c.requestId ? c.requestId() : e(), u = (s.method ?? "GET").toUpperCase(), d = C(i), f = await x(t, s, l, u, i);
|
|
132
|
+
if (f.status === 401 && !i && !a) {
|
|
133
|
+
if (c.refresh) {
|
|
134
|
+
let e;
|
|
127
135
|
try {
|
|
128
|
-
await
|
|
129
|
-
} catch {
|
|
130
|
-
throw await
|
|
136
|
+
await c.refresh(d), e = C(i), f = await x(t, s, l, u, i);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
throw e instanceof o ? e : (await S(f, l, d), await y(f, l));
|
|
131
139
|
}
|
|
132
|
-
|
|
133
|
-
} else await
|
|
140
|
+
f.status === 401 && await S(f, l, e);
|
|
141
|
+
} else await S(f, l, d);
|
|
134
142
|
}
|
|
135
|
-
if (!
|
|
136
|
-
return r(
|
|
143
|
+
if (!f.ok) throw await y(f, l);
|
|
144
|
+
return r(f);
|
|
137
145
|
}
|
|
138
|
-
async function
|
|
139
|
-
let { retry: r, ...i } = t, a =
|
|
140
|
-
if (!a) return
|
|
146
|
+
async function T(e, t, n) {
|
|
147
|
+
let { retry: r, ...i } = t, a = m(r === void 0 ? c.retry : r);
|
|
148
|
+
if (!a) return w(e, i, n);
|
|
141
149
|
let o = (i.method ?? "GET").toUpperCase();
|
|
142
|
-
return
|
|
150
|
+
return u(() => w(e, i, n), {
|
|
143
151
|
...a,
|
|
144
|
-
shouldRetry: a.shouldRetry ?? ((e) =>
|
|
152
|
+
shouldRetry: a.shouldRetry ?? ((e) => p(e, o))
|
|
145
153
|
});
|
|
146
154
|
}
|
|
147
|
-
async function
|
|
148
|
-
return
|
|
155
|
+
async function E(e, t = {}) {
|
|
156
|
+
return T(e, t, a);
|
|
149
157
|
}
|
|
150
|
-
async function
|
|
151
|
-
return
|
|
158
|
+
async function D(e, t, n = "POST", r) {
|
|
159
|
+
return E(e, {
|
|
152
160
|
...r,
|
|
153
161
|
method: n,
|
|
154
162
|
body: t
|
|
155
163
|
});
|
|
156
164
|
}
|
|
157
165
|
return {
|
|
158
|
-
request:
|
|
159
|
-
get: (e, t) =>
|
|
166
|
+
request: E,
|
|
167
|
+
get: (e, t) => E(e, {
|
|
160
168
|
...t,
|
|
161
169
|
method: "GET"
|
|
162
170
|
}),
|
|
163
|
-
post: (e, t) =>
|
|
171
|
+
post: (e, t) => E(e, {
|
|
164
172
|
...t,
|
|
165
173
|
method: "POST"
|
|
166
174
|
}),
|
|
167
|
-
put: (e, t) =>
|
|
175
|
+
put: (e, t) => E(e, {
|
|
168
176
|
...t,
|
|
169
177
|
method: "PUT"
|
|
170
178
|
}),
|
|
171
|
-
patch: (e, t) =>
|
|
179
|
+
patch: (e, t) => E(e, {
|
|
172
180
|
...t,
|
|
173
181
|
method: "PATCH"
|
|
174
182
|
}),
|
|
175
|
-
delete: (e, t) =>
|
|
183
|
+
delete: (e, t) => E(e, {
|
|
176
184
|
...t,
|
|
177
185
|
method: "DELETE"
|
|
178
186
|
}),
|
|
179
|
-
blob: (e, t) =>
|
|
180
|
-
arrayBuffer: (e, t) =>
|
|
181
|
-
upload:
|
|
187
|
+
blob: (e, t) => T(e, { ...t }, (e) => e.blob()),
|
|
188
|
+
arrayBuffer: (e, t) => T(e, { ...t }, (e) => e.arrayBuffer()),
|
|
189
|
+
upload: D
|
|
182
190
|
};
|
|
183
191
|
}
|
|
184
192
|
//#endregion
|
|
185
|
-
export {
|
|
193
|
+
export { b as createApiClient };
|
|
186
194
|
|
|
187
195
|
//# 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 over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **CSRF** is off unless you set `csrf`. With it, every unsafe request (anything\n * but `GET`/`HEAD`/`OPTIONS`/`TRACE`) echoes the `csrf_token` cookie in an\n * `X-CSRF-Token` header — the double-submit contract of `tempest-fastapi-sdk`'s\n * `CSRFMiddleware` — scoped to the same origins as the bearer token and sent on\n * `skipAuth` requests too. See {@link ApiClientConfig.csrf}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const callerHeaders = headers as Record<string, string> | undefined;\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...csrfHeaders({\n method: rest.method ?? \"GET\",\n url,\n reference: config.baseURL,\n csrf: config.csrf,\n trustedOrigins: config.trustedOrigins,\n headers: { ...config.headers, ...callerHeaders },\n }),\n ...callerHeaders,\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, init, requestId, method, skipAuth);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":";;;;;;;;;AAyBA,IAAM,oBAA0C,IAAI,IAAI;CAAC;CAAO;CAAQ;AAAS,CAAC;AAoBlF,SAAS,EAAmB,GAAgB,GAAyB;CAGjE,OAFI,CAAC,EAAmB,IAAI,CAAM,KAC9B,EAAE,aAAiB,KAAyB,KACzC,EAAkB,EAAM,MAAM;AACzC;AAQA,SAAS,EAAa,GAAiE;CAEnF,OADK,IACE,MAAW,KAAO,CAAC,IAAI,IADV;AAExB;AAGA,IAAM,IAAkB,MAGlB,IAAyB;AAE/B,SAAS,EAAW,GAAiC;CACjD,OAAO,OAAO,WAAa,OAAe,aAAgB;AAC9D;AAaA,SAAS,EAAU,GAA2B;CAC1C,OAAO,KAAK,MAAM,YAAY,IAAI,IAAI,CAAS;AACnD;AAEA,eAAe,EAAW,GAAoB,GAAkD;CAC5F,IAAI;CACJ,IAAI;EACA,IAAO,MAAM,EAAS,MAAM,CAAC,CAAC,KAAK;CACvC,QAAQ;EACJ,IAAI;GACA,IAAO,MAAM,EAAS,KAAK;EAC/B,QAAQ;GACJ,IAAO;EACX;CACJ;CACA,OAAO,IAAI,EACP,EAAc,EAAS,QAAQ,GAAM,EAAS,SAAS,CAAa,CACxE;AACJ;AA0DA,SAAgB,EAAgB,GAAoC;CAChE,IAAM,IAAU,EAAO,WAAW,WAAW,MAAM,KAAK,UAAU;CAalE,SAAS,EAAY,GAAa,GAA2C;EACzE,IAAI,GAAU,OAAO,CAAC;EACtB,IAAM,IAAQ,EAAO,WAAW;EAMhC,OALK,IACA,EAA0B,GAAK,EAAO,SAAS,EAAO,cAAc,IAIlE,EAAE,eAAe,UAAU,IAAQ,KAHtC,EAA2B,GAAK,EAAO,OAAO,GACvC,CAAC,KAHO,CAAC;CAMxB;CAEA,eAAe,EACX,GACA,GACA,GACA,GACiB;EACjB,IAAM,EAAE,SAAM,WAAQ,YAAS,WAAQ,YAAS,GAAG,MAAS,GACtD,IAAS,EAAW,CAAI,GACxB,IAAa,IAAS,EAAO,gBAAgB,EAAO,SAEpD,IACF,MAAY,KAAA,IAAsB,MAAe,KAAA,IAFpC,IAAS,IAAyB,IAEc,IAArC,GAEtB,IAAM,EAAY,EAAO,SAAS,GAAM;GAAE,QAAQ,EAAO;GAAQ;EAAO,CAAC,GAEzE,IAAgB,GAChB,IAAuC;GACzC,GAAI,IAAS,CAAC,IAAI,EAAE,gBAAgB,mBAAmB;GACvD,GAAI,IAAY,EAAE,gBAAgB,EAAU,IAAI,CAAC;GACjD,GAAG,EAAO;GACV,GAAG,EAAY,GAAK,CAAQ;GAC5B,GAAG,EAAY;IACX,QAAQ,EAAK,UAAU;IACvB;IACA,WAAW,EAAO;IAClB,MAAM,EAAO;IACb,gBAAgB,EAAO;IACvB,SAAS;KAAE,GAAG,EAAO;KAAS,GAAG;IAAc;GACnD,CAAC;GACD,GAAG;EACP,GAEM,IAAQ,EAAY,GAAQ,CAAK,GACjC,IAAoB;GACtB,GAAG;GACH,QAAQ,EAAM;GACd,SAAS;GACT,aAAa,EAAO,kBAAkB,YAAY,EAAK;GACvD,MACI,KAA+B,OACzB,KAAA,IACA,IACG,IACD,KAAK,UAAU,CAAI;EACnC;EAEA,IAAI;GACA,OAAO,MAAM,EAAQ,GAAK,CAAI;EAClC,SAAS,GAAO;GAOZ,MANI,EAAM,SAAS,IACT,IAAI,EAAgB;IACtB,QAAQ;IACR,QAAQ,wBAAwB,EAAM;GAC1C,CAAC,IAEC;EACV,UAAU;GACN,EAAM,QAAQ;EAClB;CACJ;CAEA,eAAe,EACX,GACA,GACA,GACA,GACA,GACiB;EACjB,IAAM,IAAM,EAAO;EACnB,IAAI,CAAC,GAAK,OAAO,EAAW,GAAM,GAAS,GAAW,CAAQ;EAE9D,IAAM,IAAY,YAAY,IAAI;EAClC,IAAI;GACA,IAAM,IAAW,MAAM,EAAW,GAAM,GAAS,GAAW,CAAQ,GAC9D,IAAQ;IAAE;IAAW,QAAQ,EAAS;IAAQ,IAAI,EAAU,CAAS;GAAE,GACvE,IAAO,GAAG,EAAO,GAAG,EAAK,KAAK,EAAS;GAG7C,OAFI,EAAS,UAAU,MAAK,EAAI,KAAK,GAAM,CAAK,IAC3C,EAAI,MAAM,GAAM,CAAK,GACnB;EACX,SAAS,GAAO;GAMZ,MALA,EAAI,KAAK,GAAG,EAAO,GAAG,EAAK,iBAAiB;IACxC;IACA,IAAI,EAAU,CAAS;IACvB;GACJ,CAAC,GACK;EACV;CACJ;CAEA,eAAe,EAAmB,GAAoB,GAAkC;EAC/E,MAAO,gBACZ;KAAO,QAAQ,KAAK,yCAAyC;IACzD;IACA,QAAQ,EAAS;GACrB,CAAC;GACD,IAAI;IACA,MAAM,EAAO,eAAe,CAAQ;GACxC,SAAS,GAAO;IACZ,EAAO,QAAQ,KAAK,8DAA8D;KAC9E;KACA,QAAQ,EAAS;KACjB;IACJ,CAAC;GACL;EATC;CAUL;CAEA,eAAe,EACX,GACA,GACA,GACU;EACV,IAAM,EAAE,cAAW,IAAO,mBAAgB,IAAO,GAAG,MAAS,GACvD,IAAY,EAAO,YAAY,EAAO,UAAU,IAAI,EAAS,GAC7D,KAAU,EAAK,UAAU,MAAA,CAAO,YAAY,GAC9C,IAAW,MAAM,EAAK,GAAM,GAAM,GAAW,GAAQ,CAAQ;EAEjE,IAAI,EAAS,WAAW,OAAO,CAAC,KAAY,CAAC,GAAe;GACxD,IAAI,EAAO,SAAS;IAChB,IAAI;KAEA,AADA,MAAM,EAAO,QAAQ,GACrB,IAAW,MAAM,EAAK,GAAM,GAAM,GAAW,GAAQ,CAAQ;IACjE,QAAQ;KAEJ,MADA,MAAM,EAAmB,GAAU,CAAS,GACtC,MAAM,EAAW,GAAU,CAAS;IAC9C;IACA,AAAI,EAAS,WAAW,OACpB,MAAM,EAAmB,GAAU,CAAS;GAEpD,OACI,MAAM,EAAmB,GAAU,CAAS;EAEpD;EAEA,IAAI,CAAC,EAAS,IACV,MAAM,MAAM,EAAW,GAAU,CAAS;EAG9C,OAAO,EAAO,CAAQ;CAC1B;CAEA,eAAe,EACX,GACA,GACA,GACU;EACV,IAAM,EAAE,OAAO,GAAY,GAAG,MAAS,GACjC,IAAe,EAAa,MAAe,KAAA,IAAyB,EAAO,QAApB,CAAyB;EACtF,IAAI,CAAC,GAAc,OAAO,EAAW,GAAM,GAAM,CAAM;EAEvD,IAAM,KAAU,EAAK,UAAU,MAAA,CAAO,YAAY;EAClD,OAAO,QAAuB,EAAW,GAAM,GAAM,CAAM,GAAG;GAC1D,GAAG;GACH,aACI,EAAa,iBAAiB,MAAmB,EAAmB,GAAO,CAAM;EACzF,CAAC;CACL;CAEA,eAAe,EAAW,GAAc,IAA0B,CAAC,GAAe;EAC9E,OAAO,EAAO,GAAM,GAAS,CAAmB;CACpD;CAEA,eAAe,EACX,GACA,GACA,IAAmC,QACnC,GACU;EACV,OAAO,EAAW,GAAM;GAAE,GAAG;GAAS;GAAQ,MAAM;EAAS,CAAC;CAClE;CAEA,OAAO;EACH;EACA,MAAS,GAAc,MACnB,EAAW,GAAM;GAAE,GAAG;GAAS,QAAQ;EAAM,CAAC;EAClD,OAAU,GAAc,MACpB,EAAW,GAAM;GAAE,GAAG;GAAS,QAAQ;EAAO,CAAC;EACnD,MAAS,GAAc,MACnB,EAAW,GAAM;GAAE,GAAG;GAAS,QAAQ;EAAM,CAAC;EAClD,QAAW,GAAc,MACrB,EAAW,GAAM;GAAE,GAAG;GAAS,QAAQ;EAAQ,CAAC;EACpD,SAAY,GAAc,MACtB,EAAW,GAAM;GAAE,GAAG;GAAS,QAAQ;EAAS,CAAC;EACrD,OAAO,GAAc,MACjB,EAAU,GAAM,EAAE,GAAG,EAAQ,IAAI,MAAa,EAAS,KAAK,CAAC;EACjE,cAAc,GAAc,MACxB,EAAiB,GAAM,EAAE,GAAG,EAAQ,IAAI,MAAa,EAAS,YAAY,CAAC;EAC/E;CACJ;AACJ"}
|
|
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 { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport {\n buildApiError,\n SessionSupersededError,\n TempestApiError,\n isRetriableStatus,\n} from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestAuthContext, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **CSRF** is off unless you set `csrf`. With it, every unsafe request (anything\n * but `GET`/`HEAD`/`OPTIONS`/`TRACE`) echoes the `csrf_token` cookie in an\n * `X-CSRF-Token` header — the double-submit contract of `tempest-fastapi-sdk`'s\n * `CSRFMiddleware` — scoped to the same origins as the bearer token and sent on\n * `skipAuth` requests too. See {@link ApiClientConfig.csrf}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const callerHeaders = headers as Record<string, string> | undefined;\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...csrfHeaders({\n method: rest.method ?? \"GET\",\n url,\n reference: config.baseURL,\n csrf: config.csrf,\n trustedOrigins: config.trustedOrigins,\n headers: { ...config.headers, ...callerHeaders },\n }),\n ...callerHeaders,\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n /**\n * Call `onUnauthorized`, keeping the request's own error unless the hook\n * says the attempt belongs to an ended session.\n *\n * @param response - The `401` that ended the attempt.\n * @param requestId - Correlation id for the log.\n * @param context - The credential and session the attempt left with.\n * @throws {SessionSupersededError} When `onUnauthorized` threw it.\n */\n async function notifyUnauthorized(\n response: Response,\n requestId: string,\n context: RequestAuthContext,\n ): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response, context);\n } catch (error) {\n if (error instanceof SessionSupersededError) throw error;\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n /**\n * The credential and session an attempt leaves with, read just before it is\n * sent.\n *\n * @param skipAuth - Whether the attempt goes out without credentials.\n * @returns The context handed to `refresh` and `onUnauthorized`.\n */\n function authContext(skipAuth: boolean): RequestAuthContext {\n return {\n token: skipAuth ? null : (config.getToken?.() ?? null),\n epoch: config.getSessionEpoch?.() ?? null,\n };\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n const sent = authContext(skipAuth);\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n let replayed: RequestAuthContext;\n try {\n await config.refresh(sent);\n replayed = authContext(skipAuth);\n response = await send(path, init, requestId, method, skipAuth);\n } catch (error) {\n if (error instanceof SessionSupersededError) throw error;\n await notifyUnauthorized(response, requestId, sent);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId, replayed);\n }\n } else {\n await notifyUnauthorized(response, requestId, sent);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
|
package/dist/http/errors.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=class extends Error{status;detail;code;requestId;fields;body;constructor(e){super(e.detail),this.name=`TempestApiError`,this.status=e.status,this.detail=e.detail,this.code=e.code,this.requestId=e.requestId,this.fields=e.fields,this.body=e.body}};function
|
|
1
|
+
var e=class extends Error{status;detail;code;requestId;fields;body;constructor(e){super(e.detail),this.name=`TempestApiError`,this.status=e.status,this.detail=e.detail,this.code=e.code,this.requestId=e.requestId,this.fields=e.fields,this.body=e.body}},t=class extends Error{constructor(){super(`A sessão mudou antes de a operação terminar.`),this.name=`SessionSupersededError`}};function n(e){return typeof e==`object`&&!!e&&typeof e.status==`number`&&typeof e.detail==`string`}var r=new Set([`body`,`query`,`path`,`header`,`cookie`]);function i(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`||typeof e==`number`).filter((e,t)=>!(t===0&&r.has(String(e))));return t.length>0?t.join(`.`):void 0}var a=4;function o(e){if(!Array.isArray(e))return;let t={};for(let n of e){if(typeof n!=`object`||!n)continue;let e=n,r=i(e.loc);if(r===void 0||r in t)continue;let a=d(e.msg)??d(e.message);a!==void 0&&(t[r]=a)}return Object.keys(t).length>0?t:void 0}function s(e){return typeof e==`object`&&e&&!Array.isArray(e)?e:void 0}function c(e){if(e===null)return;let t=[s(e.detail)?.field,e.field,s(e.details)?.field];for(let e of t)if(typeof e==`string`&&e!==``)return e}function l(e,t){if(e===void 0||t===void 0)return e;let n=t.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),r=e.replace(RegExp(`\\s+for field '${n}'(?:\\s+in\\s+'[^']*')?\\.?\\s*$`),``).trim();return r===``?e:r}function u(e,t){let n=o(e?.detail);if(n!==void 0)return n;let r=c(e);if(r!==void 0&&t!==void 0)return{[r]:t}}function d(e,t=0){if(e!=null){if(typeof e==`string`)return e===``?void 0:e;if(typeof e==`number`||typeof e==`boolean`)return String(e);if(!(t>=a)){if(Array.isArray(e)){let n=e.map(e=>{let n=d(e,t+1);if(n===void 0)return;let r=typeof e==`object`&&e?i(e.loc):void 0;return r===void 0?n:`${r}: ${n}`}).filter(e=>e!==void 0);return n.length>0?n.join(`; `):void 0}if(typeof e==`object`){let n=e;return d(n.msg,t+1)??d(n.message,t+1)??d(n.detail,t+1)}}}}var f=new Set([0,408,425,429]);function p(e){return f.has(e)||e>=500}function m(e){return`Erro ${e}`}function h(e,t,n,r){let i=typeof t==`object`&&t?t:null,a=l(d(i?.detail)??d(i?.message),c(i)),o=a??m(e),s=typeof i?.code==`string`?i.code:void 0,f=typeof i?.details==`object`&&i.details!==null?i.details:null;return{status:e,detail:o,code:s,requestId:(typeof f?.request_id==`string`?f.request_id:void 0)??n?.get(`X-Request-ID`)??r??void 0??void 0,retryAfter:g(n?.get(`Retry-After`)),fields:u(i,a),body:t}}function g(e){if(!e)return;let t=e.trim();if(/^\d+$/.test(t))return Number(t);let n=Date.parse(t);if(!Number.isNaN(n))return Math.max(0,Math.round((n-Date.now())/1e3))}exports.SessionSupersededError=t,exports.TempestApiError=e,exports.buildApiError=h,exports.isApiError=n,exports.isRetriableStatus=p,exports.parseRetryAfter=g,exports.syntheticDetail=m;
|
|
2
2
|
//# sourceMappingURL=errors.cjs.map
|
package/dist/http/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.cjs","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import type { ApiError } from \"./types\";\n\n/**\n * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a\n * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope\n * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a\n * `requestId` for log correlation, while still being a real `Error` (stack\n * trace, `instanceof Error`).\n *\n * @example\n * try {\n * await api.post(\"/users\", { body });\n * } catch (err) {\n * if (isApiError(err) && err.code === \"EMAIL_TAKEN\") {\n * showFieldError(\"email\", err.detail);\n * }\n * }\n */\nexport class TempestApiError extends Error implements ApiError {\n readonly status: number;\n readonly detail: string;\n readonly code?: string;\n readonly requestId?: string;\n readonly fields?: Record<string, string>;\n readonly body?: unknown;\n\n constructor(init: ApiError) {\n super(init.detail);\n this.name = \"TempestApiError\";\n this.status = init.status;\n this.detail = init.detail;\n this.code = init.code;\n this.requestId = init.requestId;\n this.fields = init.fields;\n this.body = init.body;\n }\n}\n\n/**\n * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}\n * instances and plain objects carrying `status` + `detail`.\n *\n * @param error - The unknown value (typically a caught error).\n * @returns Whether `error` conforms to the `ApiError` contract.\n */\nexport function isApiError(error: unknown): error is ApiError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as ApiError).status === \"number\" &&\n typeof (error as ApiError).detail === \"string\"\n );\n}\n\n/**\n * Location prefixes FastAPI puts at the head of a validation error's `loc`,\n * naming the part of the request rather than the field. Dropped from the\n * rendered path, so `[\"body\", \"email\"]` reads as `email`.\n */\nconst LOC_ROOTS: ReadonlySet<string> = new Set([\"body\", \"query\", \"path\", \"header\", \"cookie\"]);\n\n/**\n * Render a FastAPI validation error's `loc` tuple as a dotted field path.\n *\n * @param loc - The raw `loc` value from one validation error entry.\n * @returns The dotted path (`\"items.0.price\"`), or undefined when `loc` carries\n * nothing addressable.\n */\nfunction formatLoc(loc: unknown): string | undefined {\n if (!Array.isArray(loc)) return undefined;\n const parts = loc\n .filter(\n (part): part is string | number => typeof part === \"string\" || typeof part === \"number\",\n )\n .filter((part, index) => !(index === 0 && LOC_ROOTS.has(String(part))));\n return parts.length > 0 ? parts.join(\".\") : undefined;\n}\n\n/**\n * How deep {@link normalizeDetail} follows a nested `detail` before giving up.\n *\n * A real envelope needs two or three levels: the list, an entry, the entry's\n * own `detail`. The cap exists because the body is untrusted input arriving on\n * the error path — a response nesting `{\"detail\":{\"detail\":…}}` twenty thousand\n * deep (a 220 KB body) overflowed the stack, and a `RangeError` thrown while\n * *building* the error is worse than the error: the caller's `catch` stops\n * receiving a `TempestApiError`, so `isApiError` is false, `describeApiError`\n * has nothing to read and the `401` handling never runs.\n */\nconst MAX_DETAIL_DEPTH = 4;\n\n/**\n * Pull field-level messages out of a validation `detail` **list**.\n *\n * FastAPI's `422` body is `detail: [{ loc, msg, type }]`, which is exactly what\n * a form needs and exactly what the flattened `detail` string destroys. Only the\n * top level is read: a validation error names one field per entry, and following\n * nesting here would invent paths the backend never sent.\n *\n * This is one of two ways a body names a field — see {@link collectFields}, which\n * is the entry point and falls back to the singular keys when the list names\n * nothing addressable.\n *\n * @param raw - The `detail` value from the error body.\n * @returns Field path to message, or undefined when the body is not a\n * validation list (or carries no entry naming a field).\n */\nfunction collectListFields(raw: unknown): Record<string, string> | undefined {\n if (!Array.isArray(raw)) return undefined;\n\n const fields: Record<string, string> = {};\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const field = formatLoc(record.loc);\n if (field === undefined || field in fields) continue;\n const message = normalizeDetail(record.msg) ?? normalizeDetail(record.message);\n if (message === undefined) continue;\n fields[field] = message;\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n}\n\n/**\n * Narrow a value to a plain object — a record, and not an array.\n *\n * Arrays are excluded because `typeof [] === \"object\"` while a list means\n * something else entirely here: FastAPI's `detail` list is read by\n * {@link collectListFields}, and reading `.field` off it would only ever be\n * undefined.\n *\n * @param value - The candidate value from the error body.\n * @returns The value as a record, or undefined when it is not a plain object.\n */\nfunction plainRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * Read the field a **singular** error envelope names.\n *\n * A backend built on `tempest-fastapi-sdk` never answers with FastAPI's `detail`\n * list once it owns the handler: it names the guilty field in a key beside the\n * message. Three shapes are seen in the wild, and they are read inner-out:\n *\n * 1. `detail.field` — the field sits in the same object as the message it\n * describes (`{ detail: { detail: \"Cidade não encontrada…\", field: \"city\" } }`),\n * so it is the least ambiguous claim about which message belongs to which input.\n * 2. `field` at the top level — a flattened `RequestValidationError`\n * (`{ detail: \"Value error, … for field 'phone' in 'body'\", field: \"phone\" }`)\n * makes the same claim one level out.\n * 3. `details.field` — `details` is the envelope's free-form context bag, not a\n * validation channel, and its `field` may be about something no input on screen\n * carries (an unknown sort column, say). It answers last for exactly that reason.\n *\n * `location` (`\"body -> phone\"`) is deliberately not parsed: it renders the same\n * path `field` already names, no observed envelope sends it without `field`, and\n * splitting an arrow-separated string would invent a path the backend never sent.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @returns The field name, or undefined when none of the three keys carried a\n * non-empty string — anything else is not usable as a key on `fields`.\n */\nfunction namedField(body: Record<string, unknown> | null): string | undefined {\n if (body === null) return undefined;\n const candidates: readonly unknown[] = [\n plainRecord(body.detail)?.field,\n body.field,\n plainRecord(body.details)?.field,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate !== \"\") return candidate;\n }\n return undefined;\n}\n\n/**\n * Drop the machine-readable tail a `tempest-fastapi-sdk` backend appends.\n *\n * That backend flattens a field error into\n * `\"CPF ou CNPJ inválido for field 'cpf_cnpj' in 'body'\"` — a finished sentence\n * in the app's language with an English clause glued to the end. The clause\n * carries nothing new: the same two values arrive as `field` and `location` on\n * the envelope, which is where `ApiError.fields` reads them from. Left in, it\n * reaches the user, and every consuming app grew its own regex to shave it off.\n *\n * The trim only fires when the field the tail names is the field the envelope\n * resolved to. That is what keeps it from deleting text it cannot account for:\n * a tail naming some other field is either a different envelope shape or a\n * sentence that genuinely reads that way, and both are left alone.\n *\n * @param message - The readable message, or undefined when the body had none.\n * @param field - The field name {@link namedField} resolved, if any.\n * @returns The message without the tail, or the message unchanged.\n */\nfunction trimFieldSuffix(\n message: string | undefined,\n field: string | undefined,\n): string | undefined {\n if (message === undefined || field === undefined) return message;\n const escaped = field.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const trimmed = message\n .replace(new RegExp(`\\\\s+for field '${escaped}'(?:\\\\s+in\\\\s+'[^']*')?\\\\.?\\\\s*$`), \"\")\n .trim();\n return trimmed === \"\" ? message : trimmed;\n}\n\n/**\n * Index the body's field-level messages, whichever envelope carried them.\n *\n * The single entry point behind `ApiError.fields`, and it applies two rules the\n * tests pin:\n *\n * - FastAPI's `detail` list wins whenever it names at least one addressable\n * field. A plain FastAPI app still answers a schema-level `422` with that list\n * even on a `tempest-fastapi-sdk` backend, so it stays authoritative.\n * - A named field with no readable message produces nothing. The only string\n * left at that point is the synthetic `Erro <status>`, and `{ phone: \"Erro 422\" }`\n * on an input is noise rather than an error message.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @param message - The readable message the same body produced, before the\n * synthetic fallback — the string that also becomes `ApiError.detail`.\n * @returns Field name to message, or undefined when nothing named a field.\n */\nfunction collectFields(\n body: Record<string, unknown> | null,\n message: string | undefined,\n): Record<string, string> | undefined {\n const listed = collectListFields(body?.detail);\n if (listed !== undefined) return listed;\n const field = namedField(body);\n if (field === undefined || message === undefined) return undefined;\n return { [field]: message };\n}\n\n/**\n * Collapse a backend `detail` of any shape into a single readable line.\n *\n * FastAPI answers a `422` with `detail` as a **list** of\n * `{ loc, msg, type }` entries, not a string. Passing that through `String()`\n * yields `\"[object Object]\"` — an error message that tells the user nothing and\n * hides which field failed. Each entry becomes `\"<field>: <msg>\"` and the\n * entries are joined with `\"; \"`; a nested object is read through its\n * `msg`/`message`/`detail` string.\n *\n * @param raw - The `detail` (or `message`) value from the error body.\n * @param depth - Current nesting level. Past {@link MAX_DETAIL_DEPTH} the value\n * is treated as unreadable instead of followed further.\n * @returns The rendered message, or undefined when nothing readable is there —\n * letting the caller fall back to the synthetic `Erro <status>`.\n */\nfunction normalizeDetail(raw: unknown, depth: number = 0): string | undefined {\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"string\") return raw === \"\" ? undefined : raw;\n if (typeof raw === \"number\" || typeof raw === \"boolean\") return String(raw);\n if (depth >= MAX_DETAIL_DEPTH) return undefined;\n\n if (Array.isArray(raw)) {\n const lines = raw\n .map((entry) => {\n const message = normalizeDetail(entry, depth + 1);\n if (message === undefined) return undefined;\n const field =\n typeof entry === \"object\" && entry !== null\n ? formatLoc((entry as Record<string, unknown>).loc)\n : undefined;\n return field === undefined ? message : `${field}: ${message}`;\n })\n .filter((line): line is string => line !== undefined);\n return lines.length > 0 ? lines.join(\"; \") : undefined;\n }\n\n if (typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n return (\n normalizeDetail(entry.msg, depth + 1) ??\n normalizeDetail(entry.message, depth + 1) ??\n normalizeDetail(entry.detail, depth + 1)\n );\n }\n\n return undefined;\n}\n\n/**\n * Statuses worth a second attempt, as a set for the sub-500 cases.\n *\n * A network failure (status `0`), a request timeout, a too-early replay, and a\n * rate limit — which usually carries the `Retry-After` the backoff honours.\n * Everything else below 500 is the server refusing on purpose.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * Whether an HTTP status describes a condition a replay can plausibly fix.\n *\n * The single owner of that decision. It used to be spelled out in three places —\n * the client's own policy, the react-query default and the bare `retry()` helper\n * — and they had already drifted: the query default was missing `425`, so the\n * same `425 Too Early` was replayed through `createApiClient({ retry: true })`\n * and not replayed through `useQuery`. Same app, same error, two behaviours, and\n * no test caught it because each file asserted against its own copy.\n *\n * Deliberately about the status and nothing else. Whether a *non*-API error is\n * worth replaying, and whether the request's method may be replayed at all, are\n * the caller's calls: {@link createApiClient} refuses a non-idempotent method,\n * while a bare `retry()` has no method to inspect.\n *\n * @example\n * await api.get(\"/report\", {\n * retry: { shouldRetry: (error) => isApiError(error) && isRetriableStatus(error.status) },\n * });\n *\n * @param status - The HTTP status, where `0` means the request never landed.\n * @returns Whether a retry is worth attempting.\n */\nexport function isRetriableStatus(status: number): boolean {\n return RETRIABLE_STATUSES.has(status) || status >= 500;\n}\n\n/**\n * Detail text synthesised when a response body carries none.\n *\n * Exported because {@link describeApiError} has to recognise it: a detail the\n * server never sent says strictly less than the caller's own fallback, so the\n * funnel drops it. Comparing against a copied literal would silently stop\n * matching the day this sentence is reworded — no type error, no failing test.\n *\n * @param status - The HTTP status of the error.\n * @returns The synthetic detail for that status.\n */\nexport function syntheticDetail(status: number): string {\n return `Erro ${status}`;\n}\n\n/**\n * Parse an error body + response into the Tempest {@link ApiError} envelope.\n *\n * Reads `detail`/`message`, the programmatic `code`, and the correlation id\n * from `details.request_id` (falling back to the `X-Request-ID` header, then\n * the id the client sent).\n *\n * A `422` from FastAPI carries `detail` as a list of `{ loc, msg, type }`\n * entries, so it is flattened to `\"<field>: <msg>; <field>: <msg>\"` instead of\n * being stringified into `\"[object Object]\"`, and the same entries are indexed\n * on `fields` (`{ email: \"Field required\" }`) for a form to consume without\n * parsing that line back apart. The untouched body stays on `body`.\n *\n * A backend that owns its handlers — every `tempest-fastapi-sdk` app — sends no\n * such list, and names the field in a key instead:\n *\n * ```json\n * { \"detail\": \"Value error, … for field 'phone' in 'body'\", \"field\": \"phone\" }\n * { \"detail\": { \"detail\": \"Cidade não encontrada…\", \"field\": \"city\" },\n * \"code\": \"VALIDATION_ERROR\", \"details\": { \"field\": \"city\" } }\n * ```\n *\n * Those are indexed too, keyed by the field the backend named and valued with the\n * same sentence that becomes `detail`. Precedence is the list first, then\n * `detail.field`, `field`, `details.field` — see {@link collectFields}.\n *\n * A flattened `detail` from the list is developer-facing: it carries the\n * backend's field paths and the validator's own wording. `describeApiError` knows\n * not to show it to a person when `fields` is set — which now also covers a\n * business error that named a field, whose `detail` was a finished sentence. That\n * sentence is not lost: it is on `fields`, attached to the input that failed.\n *\n * @param status - HTTP status code.\n * @param body - The parsed error body (object, string, or null).\n * @param headers - The response headers (for the `X-Request-ID` fallback).\n * @param sentRequestId - The id the client sent on the request, if any.\n * @returns A fully-populated `ApiError`.\n *\n * @tempest-limits param-count — the arguments are the response as it arrives\n * (`status`, `body`, `headers`) plus the id the request was sent with, and they are\n * passed at exactly three places, all of them a client's response path\n * (`createApiClient`, `uploadWithProgress`, `createResumableUpload`). Wrapping them\n * in an options object would name each argument twice at every call site to say\n * nothing new.\n */\nexport function buildApiError(\n status: number,\n body: unknown,\n headers?: Headers | { get(name: string): string | null },\n sentRequestId?: string,\n): ApiError {\n const obj =\n typeof body === \"object\" && body !== null ? (body as Record<string, unknown>) : null;\n const message = trimFieldSuffix(\n normalizeDetail(obj?.detail) ?? normalizeDetail(obj?.message),\n namedField(obj),\n );\n const detail = message ?? syntheticDetail(status);\n const code = typeof obj?.code === \"string\" ? obj.code : undefined;\n const details =\n typeof obj?.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n const requestId =\n (typeof details?.request_id === \"string\" ? details.request_id : undefined) ??\n headers?.get(\"X-Request-ID\") ??\n sentRequestId ??\n undefined;\n\n return {\n status,\n detail,\n code,\n requestId: requestId ?? undefined,\n retryAfter: parseRetryAfter(headers?.get(\"Retry-After\")),\n fields: collectFields(obj, message),\n body,\n };\n}\n\n/**\n * Parse a `Retry-After` header into seconds. Accepts a delta-seconds integer\n * (`\"120\"`) or an HTTP-date (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`).\n *\n * @param value - The raw header value, or null.\n * @returns The delay in seconds (>= 0), or undefined when absent/unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const when = Date.parse(trimmed);\n if (Number.isNaN(when)) return undefined;\n return Math.max(0, Math.round((when - Date.now()) / 1000));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import type { ApiError } from \"./types\";\n\n/**\n * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a\n * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope\n * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a\n * `requestId` for log correlation, while still being a real `Error` (stack\n * trace, `instanceof Error`).\n *\n * @example\n * try {\n * await api.post(\"/users\", { body });\n * } catch (err) {\n * if (isApiError(err) && err.code === \"EMAIL_TAKEN\") {\n * showFieldError(\"email\", err.detail);\n * }\n * }\n */\nexport class TempestApiError extends Error implements ApiError {\n readonly status: number;\n readonly detail: string;\n readonly code?: string;\n readonly requestId?: string;\n readonly fields?: Record<string, string>;\n readonly body?: unknown;\n\n constructor(init: ApiError) {\n super(init.detail);\n this.name = \"TempestApiError\";\n this.status = init.status;\n this.detail = init.detail;\n this.code = init.code;\n this.requestId = init.requestId;\n this.fields = init.fields;\n this.body = init.body;\n }\n}\n\n/**\n * Thrown when the session that started an operation is no longer the current\n * one by the time it settles: a `refresh` hook (rethrown by\n * {@link createApiClient}), or a `createTempestAuth` `login()` whose session\n * was replaced while it waited on the previous session's teardown.\n *\n * A refresh is a network round trip, and the session can end or change hands\n * during it: the user signs out, or signs out and another account signs in. The\n * tokens that refresh brings back belong to the session that ended, so they are\n * discarded instead of installed, and the request that was waiting on them is\n * not replayed — replaying it would send it as whoever is signed in now.\n *\n * This is deliberately not an unauthorized outcome. The client does **not** call\n * `onUnauthorized` for it: that callback ends the session, and the current\n * session — possibly a different account that just signed in — did nothing\n * wrong. Treat it like an aborted request: drop the result, show nothing.\n *\n * @example\n * try {\n * await api.get(\"/api/orders\");\n * } catch (err) {\n * if (err instanceof SessionSupersededError) return;\n * throw err;\n * }\n */\nexport class SessionSupersededError extends Error {\n constructor() {\n super(\"A sessão mudou antes de a operação terminar.\");\n this.name = \"SessionSupersededError\";\n }\n}\n\n/**\n * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}\n * instances and plain objects carrying `status` + `detail`.\n *\n * @param error - The unknown value (typically a caught error).\n * @returns Whether `error` conforms to the `ApiError` contract.\n */\nexport function isApiError(error: unknown): error is ApiError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as ApiError).status === \"number\" &&\n typeof (error as ApiError).detail === \"string\"\n );\n}\n\n/**\n * Location prefixes FastAPI puts at the head of a validation error's `loc`,\n * naming the part of the request rather than the field. Dropped from the\n * rendered path, so `[\"body\", \"email\"]` reads as `email`.\n */\nconst LOC_ROOTS: ReadonlySet<string> = new Set([\"body\", \"query\", \"path\", \"header\", \"cookie\"]);\n\n/**\n * Render a FastAPI validation error's `loc` tuple as a dotted field path.\n *\n * @param loc - The raw `loc` value from one validation error entry.\n * @returns The dotted path (`\"items.0.price\"`), or undefined when `loc` carries\n * nothing addressable.\n */\nfunction formatLoc(loc: unknown): string | undefined {\n if (!Array.isArray(loc)) return undefined;\n const parts = loc\n .filter(\n (part): part is string | number => typeof part === \"string\" || typeof part === \"number\",\n )\n .filter((part, index) => !(index === 0 && LOC_ROOTS.has(String(part))));\n return parts.length > 0 ? parts.join(\".\") : undefined;\n}\n\n/**\n * How deep {@link normalizeDetail} follows a nested `detail` before giving up.\n *\n * A real envelope needs two or three levels: the list, an entry, the entry's\n * own `detail`. The cap exists because the body is untrusted input arriving on\n * the error path — a response nesting `{\"detail\":{\"detail\":…}}` twenty thousand\n * deep (a 220 KB body) overflowed the stack, and a `RangeError` thrown while\n * *building* the error is worse than the error: the caller's `catch` stops\n * receiving a `TempestApiError`, so `isApiError` is false, `describeApiError`\n * has nothing to read and the `401` handling never runs.\n */\nconst MAX_DETAIL_DEPTH = 4;\n\n/**\n * Pull field-level messages out of a validation `detail` **list**.\n *\n * FastAPI's `422` body is `detail: [{ loc, msg, type }]`, which is exactly what\n * a form needs and exactly what the flattened `detail` string destroys. Only the\n * top level is read: a validation error names one field per entry, and following\n * nesting here would invent paths the backend never sent.\n *\n * This is one of two ways a body names a field — see {@link collectFields}, which\n * is the entry point and falls back to the singular keys when the list names\n * nothing addressable.\n *\n * @param raw - The `detail` value from the error body.\n * @returns Field path to message, or undefined when the body is not a\n * validation list (or carries no entry naming a field).\n */\nfunction collectListFields(raw: unknown): Record<string, string> | undefined {\n if (!Array.isArray(raw)) return undefined;\n\n const fields: Record<string, string> = {};\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const field = formatLoc(record.loc);\n if (field === undefined || field in fields) continue;\n const message = normalizeDetail(record.msg) ?? normalizeDetail(record.message);\n if (message === undefined) continue;\n fields[field] = message;\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n}\n\n/**\n * Narrow a value to a plain object — a record, and not an array.\n *\n * Arrays are excluded because `typeof [] === \"object\"` while a list means\n * something else entirely here: FastAPI's `detail` list is read by\n * {@link collectListFields}, and reading `.field` off it would only ever be\n * undefined.\n *\n * @param value - The candidate value from the error body.\n * @returns The value as a record, or undefined when it is not a plain object.\n */\nfunction plainRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * Read the field a **singular** error envelope names.\n *\n * A backend built on `tempest-fastapi-sdk` never answers with FastAPI's `detail`\n * list once it owns the handler: it names the guilty field in a key beside the\n * message. Three shapes are seen in the wild, and they are read inner-out:\n *\n * 1. `detail.field` — the field sits in the same object as the message it\n * describes (`{ detail: { detail: \"Cidade não encontrada…\", field: \"city\" } }`),\n * so it is the least ambiguous claim about which message belongs to which input.\n * 2. `field` at the top level — a flattened `RequestValidationError`\n * (`{ detail: \"Value error, … for field 'phone' in 'body'\", field: \"phone\" }`)\n * makes the same claim one level out.\n * 3. `details.field` — `details` is the envelope's free-form context bag, not a\n * validation channel, and its `field` may be about something no input on screen\n * carries (an unknown sort column, say). It answers last for exactly that reason.\n *\n * `location` (`\"body -> phone\"`) is deliberately not parsed: it renders the same\n * path `field` already names, no observed envelope sends it without `field`, and\n * splitting an arrow-separated string would invent a path the backend never sent.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @returns The field name, or undefined when none of the three keys carried a\n * non-empty string — anything else is not usable as a key on `fields`.\n */\nfunction namedField(body: Record<string, unknown> | null): string | undefined {\n if (body === null) return undefined;\n const candidates: readonly unknown[] = [\n plainRecord(body.detail)?.field,\n body.field,\n plainRecord(body.details)?.field,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate !== \"\") return candidate;\n }\n return undefined;\n}\n\n/**\n * Drop the machine-readable tail a `tempest-fastapi-sdk` backend appends.\n *\n * That backend flattens a field error into\n * `\"CPF ou CNPJ inválido for field 'cpf_cnpj' in 'body'\"` — a finished sentence\n * in the app's language with an English clause glued to the end. The clause\n * carries nothing new: the same two values arrive as `field` and `location` on\n * the envelope, which is where `ApiError.fields` reads them from. Left in, it\n * reaches the user, and every consuming app grew its own regex to shave it off.\n *\n * The trim only fires when the field the tail names is the field the envelope\n * resolved to. That is what keeps it from deleting text it cannot account for:\n * a tail naming some other field is either a different envelope shape or a\n * sentence that genuinely reads that way, and both are left alone.\n *\n * @param message - The readable message, or undefined when the body had none.\n * @param field - The field name {@link namedField} resolved, if any.\n * @returns The message without the tail, or the message unchanged.\n */\nfunction trimFieldSuffix(\n message: string | undefined,\n field: string | undefined,\n): string | undefined {\n if (message === undefined || field === undefined) return message;\n const escaped = field.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const trimmed = message\n .replace(new RegExp(`\\\\s+for field '${escaped}'(?:\\\\s+in\\\\s+'[^']*')?\\\\.?\\\\s*$`), \"\")\n .trim();\n return trimmed === \"\" ? message : trimmed;\n}\n\n/**\n * Index the body's field-level messages, whichever envelope carried them.\n *\n * The single entry point behind `ApiError.fields`, and it applies two rules the\n * tests pin:\n *\n * - FastAPI's `detail` list wins whenever it names at least one addressable\n * field. A plain FastAPI app still answers a schema-level `422` with that list\n * even on a `tempest-fastapi-sdk` backend, so it stays authoritative.\n * - A named field with no readable message produces nothing. The only string\n * left at that point is the synthetic `Erro <status>`, and `{ phone: \"Erro 422\" }`\n * on an input is noise rather than an error message.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @param message - The readable message the same body produced, before the\n * synthetic fallback — the string that also becomes `ApiError.detail`.\n * @returns Field name to message, or undefined when nothing named a field.\n */\nfunction collectFields(\n body: Record<string, unknown> | null,\n message: string | undefined,\n): Record<string, string> | undefined {\n const listed = collectListFields(body?.detail);\n if (listed !== undefined) return listed;\n const field = namedField(body);\n if (field === undefined || message === undefined) return undefined;\n return { [field]: message };\n}\n\n/**\n * Collapse a backend `detail` of any shape into a single readable line.\n *\n * FastAPI answers a `422` with `detail` as a **list** of\n * `{ loc, msg, type }` entries, not a string. Passing that through `String()`\n * yields `\"[object Object]\"` — an error message that tells the user nothing and\n * hides which field failed. Each entry becomes `\"<field>: <msg>\"` and the\n * entries are joined with `\"; \"`; a nested object is read through its\n * `msg`/`message`/`detail` string.\n *\n * @param raw - The `detail` (or `message`) value from the error body.\n * @param depth - Current nesting level. Past {@link MAX_DETAIL_DEPTH} the value\n * is treated as unreadable instead of followed further.\n * @returns The rendered message, or undefined when nothing readable is there —\n * letting the caller fall back to the synthetic `Erro <status>`.\n */\nfunction normalizeDetail(raw: unknown, depth: number = 0): string | undefined {\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"string\") return raw === \"\" ? undefined : raw;\n if (typeof raw === \"number\" || typeof raw === \"boolean\") return String(raw);\n if (depth >= MAX_DETAIL_DEPTH) return undefined;\n\n if (Array.isArray(raw)) {\n const lines = raw\n .map((entry) => {\n const message = normalizeDetail(entry, depth + 1);\n if (message === undefined) return undefined;\n const field =\n typeof entry === \"object\" && entry !== null\n ? formatLoc((entry as Record<string, unknown>).loc)\n : undefined;\n return field === undefined ? message : `${field}: ${message}`;\n })\n .filter((line): line is string => line !== undefined);\n return lines.length > 0 ? lines.join(\"; \") : undefined;\n }\n\n if (typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n return (\n normalizeDetail(entry.msg, depth + 1) ??\n normalizeDetail(entry.message, depth + 1) ??\n normalizeDetail(entry.detail, depth + 1)\n );\n }\n\n return undefined;\n}\n\n/**\n * Statuses worth a second attempt, as a set for the sub-500 cases.\n *\n * A network failure (status `0`), a request timeout, a too-early replay, and a\n * rate limit — which usually carries the `Retry-After` the backoff honours.\n * Everything else below 500 is the server refusing on purpose.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * Whether an HTTP status describes a condition a replay can plausibly fix.\n *\n * The single owner of that decision. It used to be spelled out in three places —\n * the client's own policy, the react-query default and the bare `retry()` helper\n * — and they had already drifted: the query default was missing `425`, so the\n * same `425 Too Early` was replayed through `createApiClient({ retry: true })`\n * and not replayed through `useQuery`. Same app, same error, two behaviours, and\n * no test caught it because each file asserted against its own copy.\n *\n * Deliberately about the status and nothing else. Whether a *non*-API error is\n * worth replaying, and whether the request's method may be replayed at all, are\n * the caller's calls: {@link createApiClient} refuses a non-idempotent method,\n * while a bare `retry()` has no method to inspect.\n *\n * @example\n * await api.get(\"/report\", {\n * retry: { shouldRetry: (error) => isApiError(error) && isRetriableStatus(error.status) },\n * });\n *\n * @param status - The HTTP status, where `0` means the request never landed.\n * @returns Whether a retry is worth attempting.\n */\nexport function isRetriableStatus(status: number): boolean {\n return RETRIABLE_STATUSES.has(status) || status >= 500;\n}\n\n/**\n * Detail text synthesised when a response body carries none.\n *\n * Exported because {@link describeApiError} has to recognise it: a detail the\n * server never sent says strictly less than the caller's own fallback, so the\n * funnel drops it. Comparing against a copied literal would silently stop\n * matching the day this sentence is reworded — no type error, no failing test.\n *\n * @param status - The HTTP status of the error.\n * @returns The synthetic detail for that status.\n */\nexport function syntheticDetail(status: number): string {\n return `Erro ${status}`;\n}\n\n/**\n * Parse an error body + response into the Tempest {@link ApiError} envelope.\n *\n * Reads `detail`/`message`, the programmatic `code`, and the correlation id\n * from `details.request_id` (falling back to the `X-Request-ID` header, then\n * the id the client sent).\n *\n * A `422` from FastAPI carries `detail` as a list of `{ loc, msg, type }`\n * entries, so it is flattened to `\"<field>: <msg>; <field>: <msg>\"` instead of\n * being stringified into `\"[object Object]\"`, and the same entries are indexed\n * on `fields` (`{ email: \"Field required\" }`) for a form to consume without\n * parsing that line back apart. The untouched body stays on `body`.\n *\n * A backend that owns its handlers — every `tempest-fastapi-sdk` app — sends no\n * such list, and names the field in a key instead:\n *\n * ```json\n * { \"detail\": \"Value error, … for field 'phone' in 'body'\", \"field\": \"phone\" }\n * { \"detail\": { \"detail\": \"Cidade não encontrada…\", \"field\": \"city\" },\n * \"code\": \"VALIDATION_ERROR\", \"details\": { \"field\": \"city\" } }\n * ```\n *\n * Those are indexed too, keyed by the field the backend named and valued with the\n * same sentence that becomes `detail`. Precedence is the list first, then\n * `detail.field`, `field`, `details.field` — see {@link collectFields}.\n *\n * A flattened `detail` from the list is developer-facing: it carries the\n * backend's field paths and the validator's own wording. `describeApiError` knows\n * not to show it to a person when `fields` is set — which now also covers a\n * business error that named a field, whose `detail` was a finished sentence. That\n * sentence is not lost: it is on `fields`, attached to the input that failed.\n *\n * @param status - HTTP status code.\n * @param body - The parsed error body (object, string, or null).\n * @param headers - The response headers (for the `X-Request-ID` fallback).\n * @param sentRequestId - The id the client sent on the request, if any.\n * @returns A fully-populated `ApiError`.\n *\n * @tempest-limits param-count — the arguments are the response as it arrives\n * (`status`, `body`, `headers`) plus the id the request was sent with, and they are\n * passed at exactly three places, all of them a client's response path\n * (`createApiClient`, `uploadWithProgress`, `createResumableUpload`). Wrapping them\n * in an options object would name each argument twice at every call site to say\n * nothing new.\n */\nexport function buildApiError(\n status: number,\n body: unknown,\n headers?: Headers | { get(name: string): string | null },\n sentRequestId?: string,\n): ApiError {\n const obj =\n typeof body === \"object\" && body !== null ? (body as Record<string, unknown>) : null;\n const message = trimFieldSuffix(\n normalizeDetail(obj?.detail) ?? normalizeDetail(obj?.message),\n namedField(obj),\n );\n const detail = message ?? syntheticDetail(status);\n const code = typeof obj?.code === \"string\" ? obj.code : undefined;\n const details =\n typeof obj?.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n const requestId =\n (typeof details?.request_id === \"string\" ? details.request_id : undefined) ??\n headers?.get(\"X-Request-ID\") ??\n sentRequestId ??\n undefined;\n\n return {\n status,\n detail,\n code,\n requestId: requestId ?? undefined,\n retryAfter: parseRetryAfter(headers?.get(\"Retry-After\")),\n fields: collectFields(obj, message),\n body,\n };\n}\n\n/**\n * Parse a `Retry-After` header into seconds. Accepts a delta-seconds integer\n * (`\"120\"`) or an HTTP-date (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`).\n *\n * @param value - The raw header value, or null.\n * @returns The delay in seconds (>= 0), or undefined when absent/unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const when = Date.parse(trimmed);\n if (Number.isNaN(when)) return undefined;\n return Math.max(0, Math.round((when - Date.now()) / 1000));\n}\n"],"mappings":"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"}
|