tempest-react-sdk 0.80.0 → 0.81.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 +35 -4
- package/bin/lib/component/generate.test.mjs +13 -1
- package/bin/lib/openapi/generate.hostile.test.mjs +13 -1
- package/bin/lib/openapi/generate.test.mjs +13 -1
- 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 +172 -103
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/br/boleto.cjs +1 -1
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js +2 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/br.d.ts +22 -3
- package/dist/components/AuthForm/AuthFieldControl.cjs +2 -0
- package/dist/components/AuthForm/AuthFieldControl.cjs.map +1 -0
- package/dist/components/AuthForm/AuthFieldControl.js +85 -0
- package/dist/components/AuthForm/AuthFieldControl.js.map +1 -0
- package/dist/components/AuthForm/AuthForm.cjs +2 -0
- package/dist/components/AuthForm/AuthForm.cjs.map +1 -0
- package/dist/components/AuthForm/AuthForm.js +83 -0
- package/dist/components/AuthForm/AuthForm.js.map +1 -0
- package/dist/components/AuthForm/AuthForm.module.cjs +3 -0
- package/dist/components/AuthForm/AuthForm.module.cjs.map +1 -0
- package/dist/components/AuthForm/AuthForm.module.js +14 -0
- package/dist/components/AuthForm/AuthForm.module.js.map +1 -0
- package/dist/components/AuthForm/AuthLinks.cjs +2 -0
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -0
- package/dist/components/AuthForm/AuthLinks.js +29 -0
- package/dist/components/AuthForm/AuthLinks.js.map +1 -0
- package/dist/components/AuthForm/ForgotPasswordForm.cjs +2 -0
- package/dist/components/AuthForm/ForgotPasswordForm.cjs.map +1 -0
- package/dist/components/AuthForm/ForgotPasswordForm.js +32 -0
- package/dist/components/AuthForm/ForgotPasswordForm.js.map +1 -0
- package/dist/components/AuthForm/LoginForm.cjs +2 -0
- package/dist/components/AuthForm/LoginForm.cjs.map +1 -0
- package/dist/components/AuthForm/LoginForm.js +39 -0
- package/dist/components/AuthForm/LoginForm.js.map +1 -0
- package/dist/components/AuthForm/ResetPasswordForm.cjs +2 -0
- package/dist/components/AuthForm/ResetPasswordForm.cjs.map +1 -0
- package/dist/components/AuthForm/ResetPasswordForm.js +39 -0
- package/dist/components/AuthForm/ResetPasswordForm.js.map +1 -0
- package/dist/components/AuthForm/SignUpForm.cjs +2 -0
- package/dist/components/AuthForm/SignUpForm.cjs.map +1 -0
- package/dist/components/AuthForm/SignUpForm.js +32 -0
- package/dist/components/AuthForm/SignUpForm.js.map +1 -0
- package/dist/components/AuthForm/auth-defaults.cjs +2 -0
- package/dist/components/AuthForm/auth-defaults.cjs.map +1 -0
- package/dist/components/AuthForm/auth-defaults.js +54 -0
- package/dist/components/AuthForm/auth-defaults.js.map +1 -0
- package/dist/components/AuthForm/auth-schemas.cjs +2 -0
- package/dist/components/AuthForm/auth-schemas.cjs.map +1 -0
- package/dist/components/AuthForm/auth-schemas.js +26 -0
- package/dist/components/AuthForm/auth-schemas.js.map +1 -0
- package/dist/components/AuthPage/AuthPage.cjs +2 -0
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -0
- package/dist/components/AuthPage/AuthPage.js +91 -0
- package/dist/components/AuthPage/AuthPage.js.map +1 -0
- package/dist/components/AuthPage/AuthPage.module.cjs +3 -0
- package/dist/components/AuthPage/AuthPage.module.cjs.map +1 -0
- package/dist/components/AuthPage/AuthPage.module.js +24 -0
- package/dist/components/AuthPage/AuthPage.module.js.map +1 -0
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +7 -7
- package/dist/forms/index.js +7 -7
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +15 -15
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +103 -100
- package/dist/http/api-client.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 +97 -97
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +12 -12
- package/dist/http/use-describe-api-error.cjs +1 -1
- package/dist/http/use-describe-api-error.cjs.map +1 -1
- package/dist/http/use-describe-api-error.js +9 -9
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +78 -56
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/styles/AuthForm.css +2 -0
- package/dist/styles/AuthPage.css +2 -0
- package/dist/styles/component/AuthForm.css +9 -0
- package/dist/styles/component/AuthPage.css +29 -0
- package/dist/styles/forms.css +36 -0
- package/dist/styles/manifest.json +63 -0
- 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 +124 -106
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw.d.ts +49 -15
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +603 -18
- package/dist/tempest-react-sdk.js +373 -365
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +25 -25
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +7 -7
- package/package.json +1 -1
- package/template-pwa/src/sw.ts +10 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.cjs","names":["TempestApiError","isRetriableStatus","buildApiError","isTrustedCredentialTarget","buildApiUrl","csrfHeaders","withTimeout","SessionSupersededError","randomId","retryWithBackoff","decodeByContentType","require_ids","require_build_url","require_credential_scope","require_csrf","require_decode_response","require_errors","require_retry","require_timeout","IDEMPOTENT_METHODS","isRetriableFailure","method","error","resolveRetry","config","DEFAULT_TIMEOUT","DEFAULT_UPLOAD_TIMEOUT","isFormData","body","elapsedMs","startedAt","parseError","response","sentRequestId","createApiClient","fetcher","authHeaders","skipAuth","token","url","rawRequest","rest","options","isForm","configured","limit","timeout","path","callerHeaders","headers","finalHeaders","requestId","timed","signal","init","cause","send","log","entry","line","notifyUnauthorized","context","authContext","attempt","sent","skipAuthRetry","replayed","decode","run","perRequest","retryOptions","request","upload","formData"],"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 url: (path: string, params?: RequestOptions[\"params\"]) =>\n buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.cjs","names":["TempestApiError","isRetriableStatus","buildApiError","isTrustedCredentialTarget","buildApiUrl","csrfHeaders","withTimeout","SessionSupersededError","randomId","retryWithBackoff","decodeByContentType","require_errors","require_ids","require_build_url","require_credential_scope","require_csrf","require_decode_response","require_retry","require_timeout","IDEMPOTENT_METHODS","isRetriableFailure","method","error","resolveRetry","config","DEFAULT_TIMEOUT","DEFAULT_UPLOAD_TIMEOUT","isFormData","body","elapsedMs","startedAt","pathForLog","path","parseError","response","sentRequestId","createApiClient","fetcher","authHeaders","skipAuth","token","url","rawRequest","rest","options","isForm","configured","limit","timeout","callerHeaders","headers","finalHeaders","requestId","timed","signal","init","cause","send","log","target","entry","line","notifyUnauthorized","context","authContext","attempt","sent","skipAuthRetry","replayed","decode","run","perRequest","retryOptions","request","upload","formData"],"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\n/**\n * The request path as the log line shows it: no query, no fragment, no userinfo.\n *\n * The logger promises never to write the query string, and `params` was never\n * the only way one gets in: a call site can write `\"/files?access_token=…\"`\n * inline, and an absolute path — the signed upload endpoint or CDN URL that\n * `buildApiUrl` passes through untouched — carries its signature in the query\n * and may carry `user:password@` in the authority. Everything from the first\n * `?` or `#` is dropped and the userinfo of an absolute (or scheme-relative)\n * URL is removed; the host and the path stay, because they are what makes the\n * line useful. Pure string work, no `URL` round trip, so a path that would not\n * parse is still logged instead of throwing inside the logging branch.\n *\n * @param path - The path exactly as the call site passed it.\n * @returns The path without query, fragment or credentials.\n */\nfunction pathForLog(path: string): string {\n return path.replace(/[?#][^]*/, \"\").replace(/^([^/]*\\/\\/)[^/]*@/, \"$1\");\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 target = `${method} ${pathForLog(path)}`;\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 = `${target} → ${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(`${target} → 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 url: (path: string, params?: RequestOptions[\"params\"]) =>\n buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }),\n };\n}\n"],"mappings":"A,8OA8BA,IAAMmB,EAA0C,IAAI,IAAI,CAAC,MAAO,OAAQ,SAAS,CAAC,EAoBlF,SAASC,EAAmB,EAAgB,EAAyB,CAGjE,MAFI,CAACD,EAAmB,IAAIE,CAAM,GAC9B,EAAEC,aAAiBtB,EAAAA,iBAAyB,GACzCC,EAAAA,kBAAkBqB,EAAM,MAAM,CACzC,CAQA,SAASC,EAAa,EAAiE,CAEnF,OADKC,EACEA,IAAW,GAAO,CAAC,EAAIA,EADV,IAExB,CAGA,IAAMC,EAAkB,KAGlBC,EAAyB,IAE/B,SAASC,EAAW,EAAiC,CACjD,OAAO,OAAO,SAAa,KAAeC,aAAgB,QAC9D,CAaA,SAASC,EAAU,EAA2B,CAC1C,OAAO,KAAK,MAAM,YAAY,IAAI,EAAIC,CAAS,CACnD,CAkBA,SAASC,EAAW,EAAsB,CACtC,OAAOC,EAAK,QAAQ,WAAY,EAAE,CAAC,CAAC,QAAQ,qBAAsB,IAAI,CAC1E,CAEA,eAAeC,EAAW,EAAoB,EAAkD,CAC5F,IAAIL,EACJ,GAAI,CACA,EAAO,MAAMM,EAAS,MAAM,CAAC,CAAC,KAAK,CACvC,MAAQ,CACJ,GAAI,CACA,EAAO,MAAMA,EAAS,KAAK,CAC/B,MAAQ,CACJ,EAAO,IACX,CACJ,CACA,OAAO,IAAIlC,EAAAA,gBACPE,EAAAA,cAAcgC,EAAS,OAAQN,EAAMM,EAAS,QAASC,CAAa,CACxE,CACJ,CA0DA,SAAgBC,EAAgB,EAAoC,CAChE,IAAMC,EAAUb,EAAO,SAAW,WAAW,MAAM,KAAK,UAAU,EAalE,SAASc,EAAY,EAAa,EAA2C,CACzE,GAAIC,EAAU,MAAO,CAAC,EACtB,IAAMC,EAAQhB,EAAO,WAAW,EAMhC,OALKgB,EACArC,EAAAA,0BAA0BsC,EAAKjB,EAAO,QAASA,EAAO,cAAc,EAIlE,CAAE,cAAe,UAAUgB,GAAQ,GAHtC1B,EAAA,2BAA2B2B,EAAKjB,EAAO,OAAO,EACvC,CAAC,GAHO,CAAC,CAMxB,CAEA,eAAekB,EACX,EACA,EACA,EACA,EACiB,CACjB,GAAM,CAAE,OAAM,SAAQ,UAAS,SAAQ,UAAS,GAAGC,GAASC,EACtDC,EAASlB,EAAWC,CAAI,EACxBkB,EAAaD,EAASrB,EAAO,cAAgBA,EAAO,QAEpDuB,EACFC,IAAY,IAAA,GAAsBF,IAAe,IAAA,GAFpCD,EAASnB,EAAyBD,EAEcqB,EAArCE,EAEtBP,EAAMrC,EAAAA,YAAYoB,EAAO,QAASQ,EAAM,CAAE,OAAQR,EAAO,OAAQ,QAAO,CAAC,EAEzEyB,EAAgBC,EAChBC,EAAuC,CACzC,GAAIN,EAAS,CAAC,EAAI,CAAE,eAAgB,kBAAmB,EACvD,GAAIO,EAAY,CAAE,eAAgBA,CAAU,EAAI,CAAC,EACjD,GAAG5B,EAAO,QACV,GAAGc,EAAYG,EAAKF,CAAQ,EAC5B,GAAGlC,EAAAA,YAAY,CACX,OAAQsC,EAAK,QAAU,MACvB,MACA,UAAWnB,EAAO,QAClB,KAAMA,EAAO,KACb,eAAgBA,EAAO,eACvB,QAAS,CAAE,GAAGA,EAAO,QAAS,GAAGyB,CAAc,CACnD,CAAC,EACD,GAAGA,CACP,EAEMI,EAAQ/C,EAAAA,YAAYgD,EAAQP,CAAK,EACjCQ,EAAoB,CACtB,GAAGZ,EACH,OAAQU,EAAM,OACd,QAASF,EACT,YAAa3B,EAAO,gBAAkB,UAAYmB,EAAK,YACvD,KACIf,GAA+B,KACzB,IAAA,GACAiB,EACGjB,EACD,KAAK,UAAUA,CAAI,CACnC,EAEA,GAAI,CACA,OAAO,MAAMS,EAAQI,EAAKc,CAAI,CAClC,OAASC,EAAO,CAOZ,MANIH,EAAM,SAAS,EACT,IAAIrD,EAAAA,gBAAgB,CACtB,OAAQ,EACR,OAAQ,wBAAwB+C,EAAM,qBAC1C,CAAC,EAECS,CACV,QAAU,CACNH,EAAM,QAAQ,CAClB,CACJ,CAEA,eAAeI,EACX,EACA,EACA,EACA,EACA,EACiB,CACjB,IAAMC,EAAMlC,EAAO,OACnB,GAAI,CAACkC,EAAK,OAAOhB,EAAWV,EAAMY,EAASQ,EAAWb,CAAQ,EAE9D,IAAMoB,EAAS,GAAGtC,EAAO,GAAGU,EAAWC,CAAI,IACrCF,EAAY,YAAY,IAAI,EAClC,GAAI,CACA,IAAMI,EAAW,MAAMQ,EAAWV,EAAMY,EAASQ,EAAWb,CAAQ,EAC9DqB,EAAQ,CAAE,YAAW,OAAQ1B,EAAS,OAAQ,GAAIL,EAAUC,CAAS,CAAE,EACvE+B,EAAO,GAAGF,EAAO,KAAKzB,EAAS,SAGrC,OAFIA,EAAS,QAAU,IAAKwB,EAAI,KAAKG,EAAMD,CAAK,EAC3CF,EAAI,MAAMG,EAAMD,CAAK,EACnB1B,CACX,OAASZ,EAAO,CAMZ,MALAoC,EAAI,KAAK,GAAGC,EAAO,gBAAiB,CAChC,YACA,GAAI9B,EAAUC,CAAS,EACvB,OACJ,CAAC,EACKR,CACV,CACJ,CAWA,eAAewC,EACX,EACA,EACA,EACa,CACb,GAAKtC,EAAO,eACZ,GAAO,QAAQ,KAAK,wCAAyC,CACzD,YACA,OAAQU,EAAS,MACrB,CAAC,EACD,GAAI,CACA,MAAMV,EAAO,eAAeU,EAAU6B,CAAO,CACjD,OAASzC,EAAO,CACZ,GAAIA,aAAiBf,EAAAA,uBAAwB,MAAMe,EACnDE,EAAO,QAAQ,KAAK,6DAA8D,CAC9E,YACA,OAAQU,EAAS,OACjB,OACJ,CAAC,CACL,CAVC,CAWL,CASA,SAAS8B,EAAY,EAAuC,CACxD,MAAO,CACH,MAAOzB,EAAW,KAAQf,EAAO,WAAW,GAAK,KACjD,MAAOA,EAAO,kBAAkB,GAAK,IACzC,CACJ,CAEA,eAAeyC,EACX,EACA,EACA,EACU,CACV,GAAM,CAAE,WAAW,GAAO,gBAAgB,GAAO,GAAGV,GAASX,EACvDQ,EAAY5B,EAAO,UAAYA,EAAO,UAAU,EAAIhB,EAAAA,SAAS,EAC7Da,GAAUkC,EAAK,QAAU,MAAA,CAAO,YAAY,EAC5CW,EAAOF,EAAYzB,CAAQ,EAC7BL,EAAW,MAAMuB,EAAKzB,EAAMuB,EAAMH,EAAW/B,EAAQkB,CAAQ,EAEjE,GAAIL,EAAS,SAAW,KAAO,CAACK,GAAY,CAAC4B,EAAe,CACxD,GAAI3C,EAAO,QAAS,CAChB,IAAI4C,EACJ,GAAI,CACA,MAAM5C,EAAO,QAAQ0C,CAAI,EACzB,EAAWF,EAAYzB,CAAQ,EAC/B,EAAW,MAAMkB,EAAKzB,EAAMuB,EAAMH,EAAW/B,EAAQkB,CAAQ,CACjE,OAASjB,EAAO,CAGZ,MAFIA,aAAiBf,EAAAA,uBAA8Be,GACnD,MAAMwC,EAAmB5B,EAAUkB,EAAWc,CAAI,EAC5C,MAAMjC,EAAWC,EAAUkB,CAAS,EAC9C,CACIlB,EAAS,SAAW,KACpB,MAAM4B,EAAmB5B,EAAUkB,EAAWgB,CAAQ,CAE9D,MACI,MAAMN,EAAmB5B,EAAUkB,EAAWc,CAAI,CAE1D,CAEA,GAAI,CAAChC,EAAS,GACV,MAAM,MAAMD,EAAWC,EAAUkB,CAAS,EAG9C,OAAOiB,EAAOnC,CAAQ,CAC1B,CAEA,eAAeoC,EACX,EACA,EACA,EACU,CACV,GAAM,CAAE,MAAOC,EAAY,GAAG5B,GAASC,EACjC4B,EAAejD,EAAagD,IAAe,IAAA,GAAyB/C,EAAO,MAApB+C,CAAyB,EACtF,GAAI,CAACC,EAAc,OAAOP,EAAWjC,EAAMW,EAAM0B,CAAM,EAEvD,IAAMhD,GAAUsB,EAAK,QAAU,MAAA,CAAO,YAAY,EAClD,OAAOlC,EAAAA,UAAuBwD,EAAWjC,EAAMW,EAAM0B,CAAM,EAAG,CAC1D,GAAGG,EACH,YACIA,EAAa,cAAiB,GAAmBpD,EAAmBE,EAAOD,CAAM,EACzF,CAAC,CACL,CAEA,eAAeoD,EAAW,EAAc,EAA0B,CAAC,EAAe,CAC9E,OAAOH,EAAOtC,EAAMY,EAASlC,EAAAA,mBAAmB,CACpD,CAEA,eAAegE,EACX,EACA,EACA,EAAmC,OACnC,EACU,CACV,OAAOD,EAAWzC,EAAM,CAAE,GAAGY,EAAS,SAAQ,KAAM+B,CAAS,CAAC,CAClE,CAEA,MAAO,CACH,UACA,KAAS,EAAc,IACnBF,EAAWzC,EAAM,CAAE,GAAGY,EAAS,OAAQ,KAAM,CAAC,EAClD,MAAU,EAAc,IACpB6B,EAAWzC,EAAM,CAAE,GAAGY,EAAS,OAAQ,MAAO,CAAC,EACnD,KAAS,EAAc,IACnB6B,EAAWzC,EAAM,CAAE,GAAGY,EAAS,OAAQ,KAAM,CAAC,EAClD,OAAW,EAAc,IACrB6B,EAAWzC,EAAM,CAAE,GAAGY,EAAS,OAAQ,OAAQ,CAAC,EACpD,QAAY,EAAc,IACtB6B,EAAWzC,EAAM,CAAE,GAAGY,EAAS,OAAQ,QAAS,CAAC,EACrD,MAAO,EAAc,IACjB0B,EAAUtC,EAAM,CAAE,GAAGY,CAAQ,EAAI,GAAaV,EAAS,KAAK,CAAC,EACjE,aAAc,EAAc,IACxBoC,EAAiBtC,EAAM,CAAE,GAAGY,CAAQ,EAAI,GAAaV,EAAS,YAAY,CAAC,EAC/E,SACA,KAAM,EAAc,IAChB9B,EAAAA,YAAYoB,EAAO,QAASQ,EAAM,CAAE,OAAQR,EAAO,OAAQ,QAAO,CAAC,CAC3E,CACJ,C"}
|
package/dist/http/api-client.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { SessionSupersededError as e, TempestApiError as t, buildApiError as n, isRetriableStatus as r } from "./errors.js";
|
|
2
|
+
import { randomId as i } from "../utils/ids.js";
|
|
3
|
+
import { buildApiUrl as a } from "./build-url.js";
|
|
4
|
+
import { isTrustedCredentialTarget as o, reportSuppressedCredential as s } from "./credential-scope.js";
|
|
5
|
+
import { csrfHeaders as c } from "./csrf.js";
|
|
6
|
+
import { decodeByContentType as l } from "./decode-response.js";
|
|
7
7
|
import { retry as u } from "./retry.js";
|
|
8
8
|
import { withTimeout as d } from "./timeout.js";
|
|
9
9
|
//#region src/http/api-client.ts
|
|
@@ -12,8 +12,8 @@ var f = /* @__PURE__ */ new Set([
|
|
|
12
12
|
"HEAD",
|
|
13
13
|
"OPTIONS"
|
|
14
14
|
]);
|
|
15
|
-
function p(e,
|
|
16
|
-
return !f.has(
|
|
15
|
+
function p(e, n) {
|
|
16
|
+
return !f.has(n) || !(e instanceof t) ? !1 : r(e.status);
|
|
17
17
|
}
|
|
18
18
|
function m(e) {
|
|
19
19
|
return e ? e === !0 ? {} : e : null;
|
|
@@ -25,43 +25,46 @@ function _(e) {
|
|
|
25
25
|
function v(e) {
|
|
26
26
|
return Math.round(performance.now() - e);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
function y(e) {
|
|
29
|
+
return e.replace(/[?#][^]*/, "").replace(/^([^/]*\/\/)[^/]*@/, "$1");
|
|
30
|
+
}
|
|
31
|
+
async function b(e, r) {
|
|
32
|
+
let i;
|
|
30
33
|
try {
|
|
31
|
-
|
|
34
|
+
i = await e.clone().json();
|
|
32
35
|
} catch {
|
|
33
36
|
try {
|
|
34
|
-
|
|
37
|
+
i = await e.text();
|
|
35
38
|
} catch {
|
|
36
|
-
|
|
39
|
+
i = null;
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
return new
|
|
42
|
+
return new t(n(e.status, i, e.headers, r));
|
|
40
43
|
}
|
|
41
|
-
function
|
|
42
|
-
let
|
|
44
|
+
function x(n) {
|
|
45
|
+
let r = n.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
43
46
|
function f(e, t) {
|
|
44
47
|
if (t) return {};
|
|
45
|
-
let
|
|
46
|
-
return
|
|
48
|
+
let r = n.getToken?.();
|
|
49
|
+
return r ? o(e, n.baseURL, n.trustedOrigins) ? { Authorization: `Bearer ${r}` } : (s(e, n.baseURL), {}) : {};
|
|
47
50
|
}
|
|
48
|
-
async function
|
|
49
|
-
let { body:
|
|
50
|
-
prefix:
|
|
51
|
+
async function x(e, i, o, s) {
|
|
52
|
+
let { body: l, params: u, headers: p, signal: m, timeout: v, ...y } = i, b = _(l), x = b ? n.uploadTimeout : n.timeout, S = v === void 0 ? x === void 0 ? b ? g : h : x : v, C = a(n.baseURL, e, {
|
|
53
|
+
prefix: n.prefix,
|
|
51
54
|
params: u
|
|
52
55
|
}), w = p, T = {
|
|
53
56
|
...b ? {} : { "Content-Type": "application/json" },
|
|
54
|
-
...
|
|
55
|
-
...
|
|
56
|
-
...f(C,
|
|
57
|
-
...
|
|
57
|
+
...o ? { "X-Request-ID": o } : {},
|
|
58
|
+
...n.headers,
|
|
59
|
+
...f(C, s),
|
|
60
|
+
...c({
|
|
58
61
|
method: y.method ?? "GET",
|
|
59
62
|
url: C,
|
|
60
|
-
reference:
|
|
61
|
-
csrf:
|
|
62
|
-
trustedOrigins:
|
|
63
|
+
reference: n.baseURL,
|
|
64
|
+
csrf: n.csrf,
|
|
65
|
+
trustedOrigins: n.trustedOrigins,
|
|
63
66
|
headers: {
|
|
64
|
-
...
|
|
67
|
+
...n.headers,
|
|
65
68
|
...w
|
|
66
69
|
}
|
|
67
70
|
}),
|
|
@@ -70,13 +73,13 @@ function b(c) {
|
|
|
70
73
|
...y,
|
|
71
74
|
signal: E.signal,
|
|
72
75
|
headers: T,
|
|
73
|
-
credentials:
|
|
74
|
-
body:
|
|
76
|
+
credentials: n.withCredentials ? "include" : y.credentials,
|
|
77
|
+
body: l == null ? void 0 : b ? l : JSON.stringify(l)
|
|
75
78
|
};
|
|
76
79
|
try {
|
|
77
|
-
return await
|
|
80
|
+
return await r(C, D);
|
|
78
81
|
} catch (e) {
|
|
79
|
-
throw E.timedOut() ? new
|
|
82
|
+
throw E.timedOut() ? new t({
|
|
80
83
|
status: 0,
|
|
81
84
|
detail: `A requisição excedeu ${S}ms e foi abandonada.`
|
|
82
85
|
}) : e;
|
|
@@ -84,116 +87,116 @@ function b(c) {
|
|
|
84
87
|
E.dispose();
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
|
-
async function
|
|
88
|
-
let
|
|
89
|
-
if (!
|
|
90
|
-
let
|
|
90
|
+
async function S(e, t, r, i, a) {
|
|
91
|
+
let o = n.logger;
|
|
92
|
+
if (!o) return x(e, t, r, a);
|
|
93
|
+
let s = `${i} ${y(e)}`, c = performance.now();
|
|
91
94
|
try {
|
|
92
|
-
let
|
|
93
|
-
requestId:
|
|
94
|
-
status:
|
|
95
|
-
ms: v(
|
|
96
|
-
}, l = `${
|
|
97
|
-
return
|
|
98
|
-
} catch (
|
|
99
|
-
throw
|
|
100
|
-
requestId:
|
|
101
|
-
ms: v(
|
|
102
|
-
error:
|
|
103
|
-
}),
|
|
95
|
+
let n = await x(e, t, r, a), i = {
|
|
96
|
+
requestId: r,
|
|
97
|
+
status: n.status,
|
|
98
|
+
ms: v(c)
|
|
99
|
+
}, l = `${s} → ${n.status}`;
|
|
100
|
+
return n.status >= 400 ? o.warn(l, i) : o.debug(l, i), n;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
throw o.warn(`${s} → no response`, {
|
|
103
|
+
requestId: r,
|
|
104
|
+
ms: v(c),
|
|
105
|
+
error: e
|
|
106
|
+
}), e;
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
|
-
async function
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
requestId:
|
|
110
|
-
status:
|
|
109
|
+
async function C(t, r, i) {
|
|
110
|
+
if (n.onUnauthorized) {
|
|
111
|
+
n.logger?.warn("unauthorized — calling onUnauthorized", {
|
|
112
|
+
requestId: r,
|
|
113
|
+
status: t.status
|
|
111
114
|
});
|
|
112
115
|
try {
|
|
113
|
-
await
|
|
114
|
-
} catch (
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
requestId:
|
|
118
|
-
status:
|
|
119
|
-
error:
|
|
116
|
+
await n.onUnauthorized(t, i);
|
|
117
|
+
} catch (i) {
|
|
118
|
+
if (i instanceof e) throw i;
|
|
119
|
+
n.logger?.warn("onUnauthorized threw — keeping the original response error", {
|
|
120
|
+
requestId: r,
|
|
121
|
+
status: t.status,
|
|
122
|
+
error: i
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
function
|
|
127
|
+
function w(e) {
|
|
125
128
|
return {
|
|
126
|
-
token: e ? null :
|
|
127
|
-
epoch:
|
|
129
|
+
token: e ? null : n.getToken?.() ?? null,
|
|
130
|
+
epoch: n.getSessionEpoch?.() ?? null
|
|
128
131
|
};
|
|
129
132
|
}
|
|
130
|
-
async function
|
|
131
|
-
let { skipAuth:
|
|
132
|
-
if (f.status === 401 && !
|
|
133
|
-
if (
|
|
134
|
-
let
|
|
133
|
+
async function T(t, r, a) {
|
|
134
|
+
let { skipAuth: o = !1, skipAuthRetry: s = !1, ...c } = r, l = n.requestId ? n.requestId() : i(), u = (c.method ?? "GET").toUpperCase(), d = w(o), f = await S(t, c, l, u, o);
|
|
135
|
+
if (f.status === 401 && !o && !s) {
|
|
136
|
+
if (n.refresh) {
|
|
137
|
+
let r;
|
|
135
138
|
try {
|
|
136
|
-
await
|
|
137
|
-
} catch (
|
|
138
|
-
throw
|
|
139
|
+
await n.refresh(d), r = w(o), f = await S(t, c, l, u, o);
|
|
140
|
+
} catch (t) {
|
|
141
|
+
throw t instanceof e ? t : (await C(f, l, d), await b(f, l));
|
|
139
142
|
}
|
|
140
|
-
f.status === 401 && await
|
|
141
|
-
} else await
|
|
143
|
+
f.status === 401 && await C(f, l, r);
|
|
144
|
+
} else await C(f, l, d);
|
|
142
145
|
}
|
|
143
|
-
if (!f.ok) throw await
|
|
144
|
-
return
|
|
146
|
+
if (!f.ok) throw await b(f, l);
|
|
147
|
+
return a(f);
|
|
145
148
|
}
|
|
146
|
-
async function
|
|
147
|
-
let { retry:
|
|
148
|
-
if (!
|
|
149
|
-
let
|
|
150
|
-
return u(() =>
|
|
151
|
-
...
|
|
152
|
-
shouldRetry:
|
|
149
|
+
async function E(e, t, r) {
|
|
150
|
+
let { retry: i, ...a } = t, o = m(i === void 0 ? n.retry : i);
|
|
151
|
+
if (!o) return T(e, a, r);
|
|
152
|
+
let s = (a.method ?? "GET").toUpperCase();
|
|
153
|
+
return u(() => T(e, a, r), {
|
|
154
|
+
...o,
|
|
155
|
+
shouldRetry: o.shouldRetry ?? ((e) => p(e, s))
|
|
153
156
|
});
|
|
154
157
|
}
|
|
155
|
-
async function
|
|
156
|
-
return
|
|
158
|
+
async function D(e, t = {}) {
|
|
159
|
+
return E(e, t, l);
|
|
157
160
|
}
|
|
158
|
-
async function
|
|
159
|
-
return
|
|
161
|
+
async function O(e, t, n = "POST", r) {
|
|
162
|
+
return D(e, {
|
|
160
163
|
...r,
|
|
161
164
|
method: n,
|
|
162
165
|
body: t
|
|
163
166
|
});
|
|
164
167
|
}
|
|
165
168
|
return {
|
|
166
|
-
request:
|
|
167
|
-
get: (e, t) =>
|
|
169
|
+
request: D,
|
|
170
|
+
get: (e, t) => D(e, {
|
|
168
171
|
...t,
|
|
169
172
|
method: "GET"
|
|
170
173
|
}),
|
|
171
|
-
post: (e, t) =>
|
|
174
|
+
post: (e, t) => D(e, {
|
|
172
175
|
...t,
|
|
173
176
|
method: "POST"
|
|
174
177
|
}),
|
|
175
|
-
put: (e, t) =>
|
|
178
|
+
put: (e, t) => D(e, {
|
|
176
179
|
...t,
|
|
177
180
|
method: "PUT"
|
|
178
181
|
}),
|
|
179
|
-
patch: (e, t) =>
|
|
182
|
+
patch: (e, t) => D(e, {
|
|
180
183
|
...t,
|
|
181
184
|
method: "PATCH"
|
|
182
185
|
}),
|
|
183
|
-
delete: (e, t) =>
|
|
186
|
+
delete: (e, t) => D(e, {
|
|
184
187
|
...t,
|
|
185
188
|
method: "DELETE"
|
|
186
189
|
}),
|
|
187
|
-
blob: (e, t) =>
|
|
188
|
-
arrayBuffer: (e, t) =>
|
|
189
|
-
upload:
|
|
190
|
-
url: (e,
|
|
191
|
-
prefix:
|
|
192
|
-
params:
|
|
190
|
+
blob: (e, t) => E(e, { ...t }, (e) => e.blob()),
|
|
191
|
+
arrayBuffer: (e, t) => E(e, { ...t }, (e) => e.arrayBuffer()),
|
|
192
|
+
upload: O,
|
|
193
|
+
url: (e, t) => a(n.baseURL, e, {
|
|
194
|
+
prefix: n.prefix,
|
|
195
|
+
params: t
|
|
193
196
|
})
|
|
194
197
|
};
|
|
195
198
|
}
|
|
196
199
|
//#endregion
|
|
197
|
-
export {
|
|
200
|
+
export { x as createApiClient };
|
|
198
201
|
|
|
199
202
|
//# sourceMappingURL=api-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","names":["retryWithBackoff","IDEMPOTENT_METHODS","isRetriableFailure","method","error","TempestApiError","isRetriableStatus","resolveRetry","config","DEFAULT_TIMEOUT","DEFAULT_UPLOAD_TIMEOUT","isFormData","body","elapsedMs","startedAt","parseError","response","buildApiError","sentRequestId","createApiClient","fetcher","authHeaders","skipAuth","token","isTrustedCredentialTarget","url","reportSuppressedCredential","rawRequest","rest","options","isForm","configured","limit","timeout","buildApiUrl","path","callerHeaders","headers","finalHeaders","requestId","csrfHeaders","timed","withTimeout","signal","init","cause","send","log","entry","line","notifyUnauthorized","context","SessionSupersededError","authContext","attempt","randomId","sent","skipAuthRetry","replayed","decode","run","perRequest","retryOptions","retry","request","decodeByContentType","upload","formData"],"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 url: (path: string, params?: RequestOptions[\"params\"]) =>\n buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.js","names":["retryWithBackoff","IDEMPOTENT_METHODS","isRetriableFailure","method","error","TempestApiError","isRetriableStatus","resolveRetry","config","DEFAULT_TIMEOUT","DEFAULT_UPLOAD_TIMEOUT","isFormData","body","elapsedMs","startedAt","pathForLog","path","parseError","response","buildApiError","sentRequestId","createApiClient","fetcher","authHeaders","skipAuth","token","isTrustedCredentialTarget","url","reportSuppressedCredential","rawRequest","rest","options","isForm","configured","limit","timeout","buildApiUrl","callerHeaders","headers","finalHeaders","requestId","csrfHeaders","timed","withTimeout","signal","init","cause","send","log","target","entry","line","notifyUnauthorized","context","SessionSupersededError","authContext","attempt","randomId","sent","skipAuthRetry","replayed","decode","run","perRequest","retryOptions","retry","request","decodeByContentType","upload","formData"],"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\n/**\n * The request path as the log line shows it: no query, no fragment, no userinfo.\n *\n * The logger promises never to write the query string, and `params` was never\n * the only way one gets in: a call site can write `\"/files?access_token=…\"`\n * inline, and an absolute path — the signed upload endpoint or CDN URL that\n * `buildApiUrl` passes through untouched — carries its signature in the query\n * and may carry `user:password@` in the authority. Everything from the first\n * `?` or `#` is dropped and the userinfo of an absolute (or scheme-relative)\n * URL is removed; the host and the path stay, because they are what makes the\n * line useful. Pure string work, no `URL` round trip, so a path that would not\n * parse is still logged instead of throwing inside the logging branch.\n *\n * @param path - The path exactly as the call site passed it.\n * @returns The path without query, fragment or credentials.\n */\nfunction pathForLog(path: string): string {\n return path.replace(/[?#][^]*/, \"\").replace(/^([^/]*\\/\\/)[^/]*@/, \"$1\");\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 target = `${method} ${pathForLog(path)}`;\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 = `${target} → ${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(`${target} → 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 url: (path: string, params?: RequestOptions[\"params\"]) =>\n buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("
|
|
1
|
+
const e=require("./errors.cjs"),t=require("../utils/base64.cjs"),n=require("./credential-scope.cjs"),r=require("./csrf.cjs"),i=require("./retry.cjs"),a=require("../utils/owner-fingerprint.cjs"),o=require("./idempotency.cjs");var s=`1.0.0`,c=5242880,l=class extends Error{reason;constructor(e){super(e===`principal`?`O usuário mudou durante o upload resumível.`:`Os registros de upload resumível foram limpos durante o upload.`),this.name=`ResumableUploadSupersededError`,this.reason=e}},u=new Map,d=new WeakMap;function f(e){if(!e)return!1;try{let t=JSON.parse(e);return typeof t==`object`&&!!t&&typeof t.url==`string`&&typeof t.offset==`number`&&typeof t.size==`number`&&typeof t.idempotencyKey==`string`}catch{return!1}}function p(e){return t.bytesToBase64(new TextEncoder().encode(e))}function m(e){if(!e)return null;let t=Object.entries(e).map(([e,t])=>`${e} ${p(t)}`);return t.length>0?t.join(`,`):null}function h(e,t){let n=t,r=typeof n.name==`string`?n.name:`blob`,i=typeof n.lastModified==`number`?n.lastModified:0;return`${e}|${r}|${t.size}|${t.type}|${i}`}function g(e=`tempest-upload:`){function t(){try{return typeof localStorage>`u`?null:localStorage}catch{return null}}let n={get(n){let r=t()?.getItem(e+n);if(!r)return null;try{return JSON.parse(r)}catch{return null}},set(n,r){t()?.setItem(e+n,JSON.stringify(r))},delete(n){t()?.removeItem(e+n)},clear(){u.set(e,(u.get(e)??0)+1);let n=t();if(!n)return;let r=[];for(let t=0;t<n.length;t+=1){let i=n.key(t);i?.startsWith(e)&&f(n.getItem(i))&&r.push(i)}for(let e of r)n.removeItem(e)}};return d.set(n,()=>u.get(e)??0),n}function _(t){return new Promise((n,r)=>{let i=new XMLHttpRequest;i.open(t.method,t.url),i.withCredentials=t.withCredentials;for(let[e,n]of Object.entries(t.headers))i.setRequestHeader(e,n);if(t.onProgress){let e=t.onProgress;i.upload.onprogress=t=>e(t.loaded)}i.onload=()=>n({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>r(new e.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),t.register(i),i.send(t.body)})}function v(e){let t=e.header(`Upload-Offset`);if(t===null)return null;let n=Number(t);return Number.isFinite(n)&&n>=0?n:null}function y(e){if(!e)return null;try{return JSON.parse(e)}catch{return e}}var b=new Set([409,412]);function x(t){return!e.isApiError(t)||b.has(t.status)?!0:e.isRetriableStatus(t.status)}function S(t,n){let r=y(t.text),i=e.buildApiError(t.status,r,{get:t.header}),a=typeof r==`object`&&!!r&&(`detail`in r||`message`in r);return new e.TempestApiError({...i,detail:a?i.detail:n})}function C(e){let t=typeof window>`u`?void 0:window.location.href;try{return new URL(e,t).href}catch{return e}}function w(t){let{endpoint:u,file:f,chunkSize:p=c,metadata:y,headers:b={},getToken:w,getPrincipal:T,trustedOrigins:E,withCredentials:D=!1,csrf:O,key:k=h(u,f),storage:A=g(),retry:j,onProgress:M,onStateChange:N}=t,P=`idle`,F=0,I=null,L=null,R,z=!1,B=A?d.get(A):void 0,V=B?.()??0;function H(){if(T&&a.ownerFingerprint(`p`,T())!==R)throw new l(`principal`);if(B&&B()!==V)throw new l(`cleared`)}let U=null,W=null,G=0;function K(e){P!==e&&(P=e,N?.(e))}function q(e){M?.({loaded:e,total:f.size,fraction:f.size===0?1:e/f.size,resumedFrom:G})}function J(e,t){let i={...b,"Tus-Resumable":s},a=w?.();return a&&!(`Authorization`in i)&&(n.isTrustedCredentialTarget(e,u,E)?i.Authorization=`Bearer ${a}`:n.reportSuppressedCredential(e,u)),Object.assign(i,r.csrfHeaders({method:t,url:e,reference:u,csrf:O,trustedOrigins:E,headers:b})),i}function Y(e){W=e}async function X(){A&&I&&L&&(H(),await A.set(k,{url:I,offset:F,size:f.size,idempotencyKey:L,updatedAt:Date.now(),...R===void 0?{}:{owner:R}}))}async function Z(t){let n=await _({method:`HEAD`,url:t,headers:J(t,`HEAD`),withCredentials:D,register:Y});if(n.status===404||n.status===410)throw new e.TempestApiError({status:n.status,detail:`O upload expirou no servidor. Comece de novo.`});let r=v(n);if(r===null)throw S(n,`HEAD sem Upload-Offset.`);return r}async function ee(){let e=T?a.ownerFingerprint(`p`,T()):void 0;z&&e!==R&&(L=null),R=e,z=!0,V=B?.()??0;let t=A?await A.get(k):null;if(t&&R!==void 0&&t.owner!==R&&(await A?.delete(k),t=null),t&&t.size===f.size&&(L=t.idempotencyKey,t.url))try{return F=await Z(t.url),I=t.url,t.url}catch{F=0}K(`creating`),L??=o.generateIdempotencyKey(),I=null,F=0,A&&await A.set(k,{url:``,offset:0,size:f.size,idempotencyKey:L,updatedAt:Date.now(),...R===void 0?{}:{owner:R}});let n={...J(u,`POST`),"Upload-Length":String(f.size),"Idempotency-Key":L},r=m(y);r&&(n[`Upload-Metadata`]=r);let i=await _({method:`POST`,url:u,headers:n,withCredentials:D,register:Y});if(i.status!==201)throw S(i,`Criação do upload recusada.`);let s=i.header(`Location`);if(!s)throw S(i,`Criação do upload sem cabeçalho Location.`);return I=C(s),await X(),I}async function te(e,t){if(H(),t.needed&&(F=await Z(e),t.needed=!1,q(F),await X(),F>=f.size))return;let n=Math.min(F+p,f.size),r=F,i=await _({method:`PATCH`,url:e,headers:{...J(e,`PATCH`),"Content-Type":`application/offset+octet-stream`,"Upload-Offset":String(r)},body:f.slice(r,n),withCredentials:D,onProgress:e=>q(Math.min(r+e,f.size)),register:Y});if(i.status===409||i.status===412)throw t.needed=!0,S(i,`Offset divergente — o servidor já tinha esses bytes.`);if(i.status!==204&&i.status!==200)throw S(i,`Chunk recusado pelo servidor.`);F=v(i)??n,q(F),await X()}async function ne(){U=null;let e=await ee();G=F,K(`uploading`),q(F);let t={needed:!1};for(;F<f.size&&!U;)await i.retry(()=>te(e,t),{retries:5,...j,shouldRetry:(e,n)=>{if(U||e instanceof l||e instanceof DOMException&&e.name===`AbortError`)return!1;let r=j?.shouldRetry?.(e,n)??x(e);return r&&(t.needed=!0),r}});return U===`pause`?(K(`paused`),null):U===`abort`?(K(`aborted`),null):(K(`done`),A&&await A.delete(k),{url:e,size:f.size})}async function Q(){try{return await ne()}catch(e){if(U!==null||e instanceof DOMException&&e.name===`AbortError`)return K(U===`abort`?`aborted`:`paused`),null;throw K(`error`),e}finally{W=null}}function $(e){U=e,W?.abort(),W=null}return{start:Q,resume:Q,pause:()=>$(`pause`),abort:async({discard:e=!1}={})=>{$(`abort`),K(`aborted`),e&&(I&&await _({method:`DELETE`,url:I,headers:J(I,`DELETE`),withCredentials:D,register:()=>void 0}).catch(()=>void 0),A&&await A.delete(k))},get state(){return P},get offset(){return F},get url(){return I},key:k}}exports.DEFAULT_CHUNK_SIZE=c,exports.ResumableUploadSupersededError=l,exports.TUS_VERSION=s,exports.createLocalUploadStorage=g,exports.createResumableUpload=w,exports.uploadFingerprint=h;
|
|
2
2
|
//# sourceMappingURL=resumable-upload.cjs.map
|