{"version":3,"file":"upload-with-progress.cjs","names":[],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n    /** Bytes already uploaded. */\n    loaded: number;\n    /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n    total: number;\n    /** Fraction between 0 and 1, or null when total is unknown. */\n    fraction: number | null;\n    lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n    url: string;\n    body: FormData | Blob | File;\n    method?: \"POST\" | \"PUT\" | \"PATCH\";\n    headers?: Record<string, string>;\n    /** Returns the current bearer token. */\n    getToken?: () => string | null | undefined;\n    /**\n     * Origin the `getToken` credential belongs to, normally the API's.\n     *\n     * Unlike `createApiClient`, this helper has no base URL to infer a scope\n     * from — the caller names the destination on every call. Set this when the\n     * `url` can come from somewhere other than your own code (a signed URL the\n     * API handed back, a value from a response body) and the token must not\n     * follow it off-origin. Left unset, the header goes wherever `url` points,\n     * which is the behaviour every version before 0.66.0 had.\n     */\n    credentialOrigin?: string;\n    /**\n     * Extra origins allowed to receive the credential, on top of\n     * {@link credentialOrigin}. Ignored when that one is unset.\n     */\n    trustedOrigins?: readonly string[];\n    /** Send cookies. Defaults to false. */\n    withCredentials?: boolean;\n    /** Called on every `progress` event from the XHR upload channel. */\n    onProgress?: (event: UploadProgressEvent) => void;\n    /** Abort the request. */\n    signal?: AbortSignal;\n    /** Override the JSON parser. Defaults to `JSON.parse`. */\n    parser?: (raw: string) => unknown;\n    /**\n     * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n     * Defaults to a generated id. Return an empty string to disable.\n     */\n    requestId?: () => string;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n    if (!raw) return null;\n    try {\n        return JSON.parse(raw);\n    } catch {\n        return raw;\n    }\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n    const {\n        url,\n        body,\n        method = \"POST\",\n        headers = {},\n        getToken,\n        credentialOrigin,\n        trustedOrigins,\n        withCredentials = false,\n        onProgress,\n        signal,\n        parser = JSON.parse,\n        requestId,\n    } = options;\n\n    return new Promise<T>((resolve, reject) => {\n        if (signal?.aborted) {\n            reject(new DOMException(\"Aborted\", \"AbortError\"));\n            return;\n        }\n\n        const xhr = new XMLHttpRequest();\n        xhr.open(method, url);\n        xhr.withCredentials = withCredentials;\n\n        const token = getToken?.();\n        const sentRequestId = requestId ? requestId() : randomId();\n        const finalHeaders: Record<string, string> = { ...headers };\n        if (token && !(\"Authorization\" in finalHeaders)) {\n            if (\n                credentialOrigin === undefined ||\n                isTrustedCredentialTarget(url, credentialOrigin, trustedOrigins)\n            ) {\n                finalHeaders.Authorization = `Bearer ${token}`;\n            } else {\n                reportSuppressedCredential(url, credentialOrigin);\n            }\n        }\n        if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n            finalHeaders[\"X-Request-ID\"] = sentRequestId;\n        }\n        for (const [key, value] of Object.entries(finalHeaders)) {\n            xhr.setRequestHeader(key, value);\n        }\n\n        if (onProgress) {\n            xhr.upload.onprogress = (event) => {\n                onProgress({\n                    loaded: event.loaded,\n                    total: event.total,\n                    fraction: event.lengthComputable ? event.loaded / event.total : null,\n                    lengthComputable: event.lengthComputable,\n                });\n            };\n        }\n\n        function handleAbort(): void {\n            xhr.abort();\n        }\n        signal?.addEventListener(\"abort\", handleAbort);\n\n        xhr.onload = () => {\n            signal?.removeEventListener(\"abort\", handleAbort);\n            const isSuccess = xhr.status >= 200 && xhr.status < 300;\n            const contentType = xhr.getResponseHeader(\"content-type\") ?? \"\";\n\n            if (!isSuccess) {\n                const errorBody = parseErrorBody(xhr.responseText);\n                reject(\n                    new TempestApiError(\n                        buildApiError(\n                            xhr.status,\n                            errorBody,\n                            { get: (name) => xhr.getResponseHeader(name) },\n                            sentRequestId,\n                        ),\n                    ),\n                );\n                return;\n            }\n\n            if (xhr.status === 204 || !xhr.responseText) {\n                resolve(undefined as T);\n                return;\n            }\n\n            if (contentType.includes(\"application/json\")) {\n                try {\n                    resolve(parser(xhr.responseText) as T);\n                } catch (err) {\n                    reject(err);\n                }\n            } else {\n                resolve(xhr.responseText as unknown as T);\n            }\n        };\n\n        xhr.onerror = () => {\n            signal?.removeEventListener(\"abort\", handleAbort);\n            reject(\n                new TempestApiError({\n                    status: 0,\n                    detail: \"Falha de rede no upload.\",\n                    requestId: sentRequestId || undefined,\n                }),\n            );\n        };\n\n        xhr.onabort = () => {\n            signal?.removeEventListener(\"abort\", handleAbort);\n            reject(new DOMException(\"Aborted\", \"AbortError\"));\n        };\n\n        xhr.send(body);\n    });\n}\n"],"mappings":"kGA0DA,SAAS,EAAe,EAAsB,CAC1C,GAAI,CAAC,EAAK,OAAO,KACjB,GAAI,CACA,OAAO,KAAK,MAAM,CAAG,CACzB,MAAQ,CACJ,OAAO,CACX,CACJ,CAWA,SAAgB,EAAgC,EAAgD,CAC5F,GAAM,CACF,MACA,OACA,SAAS,OACT,UAAU,CAAC,EACX,WACA,mBACA,iBACA,kBAAkB,GAClB,aACA,SACA,SAAS,KAAK,MACd,aACA,EAEJ,OAAO,IAAI,SAAY,EAAS,IAAW,CACvC,GAAI,GAAQ,QAAS,CACjB,EAAO,IAAI,aAAa,UAAW,YAAY,CAAC,EAChD,MACJ,CAEA,IAAM,EAAM,IAAI,eAChB,EAAI,KAAK,EAAQ,CAAG,EACpB,EAAI,gBAAkB,EAEtB,IAAM,EAAQ,IAAW,EACnB,EAAgB,EAAY,EAAU,EAAI,EAAA,SAAS,EACnD,EAAuC,CAAE,GAAG,CAAQ,EACtD,GAAS,EAAE,kBAAmB,KAE1B,IAAqB,IAAA,IACrB,EAAA,0BAA0B,EAAK,EAAkB,CAAc,EAE/D,EAAa,cAAgB,UAAU,IAEvC,EAAA,2BAA2B,EAAK,CAAgB,GAGpD,GAAiB,EAAE,iBAAkB,KACrC,EAAa,gBAAkB,GAEnC,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAY,EAClD,EAAI,iBAAiB,EAAK,CAAK,EAG/B,IACA,EAAI,OAAO,WAAc,GAAU,CAC/B,EAAW,CACP,OAAQ,EAAM,OACd,MAAO,EAAM,MACb,SAAU,EAAM,iBAAmB,EAAM,OAAS,EAAM,MAAQ,KAChE,iBAAkB,EAAM,gBAC5B,CAAC,CACL,GAGJ,SAAS,GAAoB,CACzB,EAAI,MAAM,CACd,CACA,GAAQ,iBAAiB,QAAS,CAAW,EAE7C,EAAI,WAAe,CACf,GAAQ,oBAAoB,QAAS,CAAW,EAChD,IAAM,EAAY,EAAI,QAAU,KAAO,EAAI,OAAS,IAC9C,EAAc,EAAI,kBAAkB,cAAc,GAAK,GAE7D,GAAI,CAAC,EAAW,CACZ,IAAM,EAAY,EAAe,EAAI,YAAY,EACjD,EACI,IAAI,EAAA,gBACA,EAAA,cACI,EAAI,OACJ,EACA,CAAE,IAAM,GAAS,EAAI,kBAAkB,CAAI,CAAE,EAC7C,CACJ,CACJ,CACJ,EACA,MACJ,CAEA,GAAI,EAAI,SAAW,KAAO,CAAC,EAAI,aAAc,CACzC,EAAQ,IAAA,EAAc,EACtB,MACJ,CAEA,GAAI,EAAY,SAAS,kBAAkB,EACvC,GAAI,CACA,EAAQ,EAAO,EAAI,YAAY,CAAM,CACzC,OAAS,EAAK,CACV,EAAO,CAAG,CACd,MAEA,EAAQ,EAAI,YAA4B,CAEhD,EAEA,EAAI,YAAgB,CAChB,GAAQ,oBAAoB,QAAS,CAAW,EAChD,EACI,IAAI,EAAA,gBAAgB,CAChB,OAAQ,EACR,OAAQ,2BACR,UAAW,GAAiB,IAAA,EAChC,CAAC,CACL,CACJ,EAEA,EAAI,YAAgB,CAChB,GAAQ,oBAAoB,QAAS,CAAW,EAChD,EAAO,IAAI,aAAa,UAAW,YAAY,CAAC,CACpD,EAEA,EAAI,KAAK,CAAI,CACjB,CAAC,CACL"}