tempest-react-sdk 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +2 -2
  2. package/bin/create-tempest-app.e2e.test.mjs +112 -0
  3. package/bin/create-tempest-app.mjs +69 -28
  4. package/bin/lib/alias/collect.mjs +20 -3
  5. package/bin/lib/alias/index.mjs +12 -1
  6. package/bin/lib/alias/index.test.mjs +24 -0
  7. package/bin/lib/component/generate.mjs +191 -0
  8. package/bin/lib/component/generate.test.mjs +336 -0
  9. package/bin/lib/css/analyze.test.mjs +21 -1
  10. package/bin/lib/css/collect.mjs +14 -0
  11. package/bin/lib/css/extract.mjs +63 -5
  12. package/bin/lib/css/extract.test.mjs +110 -1
  13. package/bin/lib/css/fix.e2e.test.mjs +50 -0
  14. package/bin/lib/css/index.mjs +6 -1
  15. package/bin/lib/fs/contain.mjs +50 -0
  16. package/bin/lib/icons/generate.mjs +28 -3
  17. package/bin/lib/icons/generate.test.mjs +57 -1
  18. package/bin/lib/openapi/escape.mjs +224 -0
  19. package/bin/lib/openapi/generate.encode.test.mjs +120 -0
  20. package/bin/lib/openapi/generate.hostile.test.mjs +621 -0
  21. package/bin/lib/openapi/generate.mjs +129 -18
  22. package/bin/lib/openapi/generate.test.mjs +7 -2
  23. package/bin/lib/openapi/schema-to-zod.mjs +43 -4
  24. package/bin/tempest-gen.test.mjs +188 -0
  25. package/bin/tempest.mjs +263 -6
  26. package/dist/auth/create-tempest-auth.cjs +1 -1
  27. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  28. package/dist/auth/create-tempest-auth.js +203 -58
  29. package/dist/auth/create-tempest-auth.js.map +1 -1
  30. package/dist/auth/refresh-queue.cjs +1 -1
  31. package/dist/auth/refresh-queue.cjs.map +1 -1
  32. package/dist/auth/refresh-queue.js +13 -6
  33. package/dist/auth/refresh-queue.js.map +1 -1
  34. package/dist/br/pix.cjs +1 -1
  35. package/dist/br/pix.cjs.map +1 -1
  36. package/dist/br/pix.js +85 -72
  37. package/dist/br/pix.js.map +1 -1
  38. package/dist/br.d.ts +19 -4
  39. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  40. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  41. package/dist/components/Chat/ChatAttachments.js +46 -38
  42. package/dist/components/Chat/ChatAttachments.js.map +1 -1
  43. package/dist/components/CodeBlock/CodeBlock.cjs +1 -1
  44. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  45. package/dist/components/CodeBlock/CodeBlock.js +1 -1
  46. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  47. package/dist/components/CodeBlock/tokenize.cjs +1 -1
  48. package/dist/components/CodeBlock/tokenize.cjs.map +1 -1
  49. package/dist/components/CodeBlock/tokenize.js +54 -25
  50. package/dist/components/CodeBlock/tokenize.js.map +1 -1
  51. package/dist/components/Markdown/markdown-parse.cjs +7 -5
  52. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  53. package/dist/components/Markdown/markdown-parse.js +171 -90
  54. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  55. package/dist/components/Toast/Toast.module.cjs +1 -1
  56. package/dist/components/Toast/Toast.module.cjs.map +1 -1
  57. package/dist/components/Toast/Toast.module.js +7 -3
  58. package/dist/components/Toast/Toast.module.js.map +1 -1
  59. package/dist/components/Toast/ToastProvider.cjs +1 -1
  60. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  61. package/dist/components/Toast/ToastProvider.js +77 -51
  62. package/dist/components/Toast/ToastProvider.js.map +1 -1
  63. package/dist/data/create-data-provider.cjs +1 -1
  64. package/dist/data/create-data-provider.cjs.map +1 -1
  65. package/dist/data/create-data-provider.js +32 -21
  66. package/dist/data/create-data-provider.js.map +1 -1
  67. package/dist/http/api-client.cjs +1 -1
  68. package/dist/http/api-client.cjs.map +1 -1
  69. package/dist/http/api-client.js +81 -73
  70. package/dist/http/api-client.js.map +1 -1
  71. package/dist/http/errors.cjs +1 -1
  72. package/dist/http/errors.cjs.map +1 -1
  73. package/dist/http/errors.js +39 -35
  74. package/dist/http/errors.js.map +1 -1
  75. package/dist/http/resumable-upload.cjs +1 -1
  76. package/dist/http/resumable-upload.cjs.map +1 -1
  77. package/dist/http/resumable-upload.js +141 -105
  78. package/dist/http/resumable-upload.js.map +1 -1
  79. package/dist/http/retry.cjs +1 -1
  80. package/dist/http/retry.cjs.map +1 -1
  81. package/dist/http/retry.js +5 -6
  82. package/dist/http/retry.js.map +1 -1
  83. package/dist/oauth/callback.cjs +2 -0
  84. package/dist/oauth/callback.cjs.map +1 -0
  85. package/dist/oauth/callback.js +42 -0
  86. package/dist/oauth/callback.js.map +1 -0
  87. package/dist/oauth/use-oauth-callback.cjs +1 -1
  88. package/dist/oauth/use-oauth-callback.cjs.map +1 -1
  89. package/dist/oauth/use-oauth-callback.js +13 -20
  90. package/dist/oauth/use-oauth-callback.js.map +1 -1
  91. package/dist/push/use-push-subscription.cjs +1 -1
  92. package/dist/push/use-push-subscription.cjs.map +1 -1
  93. package/dist/push/use-push-subscription.js +42 -42
  94. package/dist/push/use-push-subscription.js.map +1 -1
  95. package/dist/push/web-push-client.cjs +1 -1
  96. package/dist/push/web-push-client.cjs.map +1 -1
  97. package/dist/push/web-push-client.js +23 -3
  98. package/dist/push/web-push-client.js.map +1 -1
  99. package/dist/query/offline-persistence.cjs +1 -1
  100. package/dist/query/offline-persistence.cjs.map +1 -1
  101. package/dist/query/offline-persistence.js +68 -23
  102. package/dist/query/offline-persistence.js.map +1 -1
  103. package/dist/styles/component/Toast.css +6 -1
  104. package/dist/styles/feedback.css +6 -1
  105. package/dist/styles.css +1 -1
  106. package/dist/sw/background-sync.cjs +1 -1
  107. package/dist/sw/background-sync.cjs.map +1 -1
  108. package/dist/sw/background-sync.js +272 -60
  109. package/dist/sw/background-sync.js.map +1 -1
  110. package/dist/sw/cache-inspect.cjs.map +1 -1
  111. package/dist/sw/cache-inspect.js.map +1 -1
  112. package/dist/sw/cache.cjs +1 -1
  113. package/dist/sw/cache.cjs.map +1 -1
  114. package/dist/sw/cache.js +91 -64
  115. package/dist/sw/cache.js.map +1 -1
  116. package/dist/sw/credential-headers.cjs +2 -0
  117. package/dist/sw/credential-headers.cjs.map +1 -0
  118. package/dist/sw/credential-headers.js +17 -0
  119. package/dist/sw/credential-headers.js.map +1 -0
  120. package/dist/sw/session-data.cjs +2 -0
  121. package/dist/sw/session-data.cjs.map +1 -0
  122. package/dist/sw/session-data.js +17 -0
  123. package/dist/sw/session-data.js.map +1 -0
  124. package/dist/sw.cjs +1 -1
  125. package/dist/sw.d.ts +309 -23
  126. package/dist/sw.js +7 -5
  127. package/dist/telemetry/app-error-queue.cjs +1 -1
  128. package/dist/telemetry/app-error-queue.cjs.map +1 -1
  129. package/dist/telemetry/app-error-queue.js +19 -14
  130. package/dist/telemetry/app-error-queue.js.map +1 -1
  131. package/dist/telemetry/app-error-reporter.cjs +1 -1
  132. package/dist/telemetry/app-error-reporter.cjs.map +1 -1
  133. package/dist/telemetry/app-error-reporter.js +75 -34
  134. package/dist/telemetry/app-error-reporter.js.map +1 -1
  135. package/dist/telemetry/app-error-transport.cjs +1 -1
  136. package/dist/telemetry/app-error-transport.cjs.map +1 -1
  137. package/dist/telemetry/app-error-transport.js +16 -3
  138. package/dist/telemetry/app-error-transport.js.map +1 -1
  139. package/dist/tempest-react-sdk.cjs +1 -1
  140. package/dist/tempest-react-sdk.d.ts +1183 -64
  141. package/dist/tempest-react-sdk.js +160 -156
  142. package/dist/utils/csv.cjs +3 -3
  143. package/dist/utils/csv.cjs.map +1 -1
  144. package/dist/utils/csv.js +18 -14
  145. package/dist/utils/csv.js.map +1 -1
  146. package/dist/utils/owner-fingerprint.cjs +2 -0
  147. package/dist/utils/owner-fingerprint.cjs.map +1 -0
  148. package/dist/utils/owner-fingerprint.js +18 -0
  149. package/dist/utils/owner-fingerprint.js.map +1 -0
  150. package/dist/utils/safe-url.cjs +2 -0
  151. package/dist/utils/safe-url.cjs.map +1 -0
  152. package/dist/utils/safe-url.js +39 -0
  153. package/dist/utils/safe-url.js.map +1 -0
  154. package/dist/vite/tempest-csp.cjs +2 -2
  155. package/dist/vite/tempest-csp.cjs.map +1 -1
  156. package/dist/vite/tempest-csp.js +130 -64
  157. package/dist/vite/tempest-csp.js.map +1 -1
  158. package/dist/vite.d.ts +23 -5
  159. package/dist/webrtc/peer-link.cjs +1 -1
  160. package/dist/webrtc/peer-link.cjs.map +1 -1
  161. package/dist/webrtc/peer-link.js +2 -10
  162. package/dist/webrtc/peer-link.js.map +1 -1
  163. package/dist/webrtc/peer-mesh.cjs +1 -1
  164. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  165. package/dist/webrtc/peer-mesh.js +110 -72
  166. package/dist/webrtc/peer-mesh.js.map +1 -1
  167. package/package.json +1 -1
  168. package/template-pwa/src/pwa.tsx +22 -0
  169. package/template-pwa/src/sw.ts +27 -14
  170. package/dist/components/Markdown/markdown-url.cjs +0 -2
  171. package/dist/components/Markdown/markdown-url.cjs.map +0 -1
  172. package/dist/components/Markdown/markdown-url.js +0 -28
  173. package/dist/components/Markdown/markdown-url.js.map +0 -1
@@ -9,104 +9,108 @@ var e = class extends Error {
9
9
  constructor(e) {
10
10
  super(e.detail), this.name = "TempestApiError", this.status = e.status, this.detail = e.detail, this.code = e.code, this.requestId = e.requestId, this.fields = e.fields, this.body = e.body;
11
11
  }
12
+ }, t = class extends Error {
13
+ constructor() {
14
+ super("A sessão mudou antes de a operação terminar."), this.name = "SessionSupersededError";
15
+ }
12
16
  };
13
- function t(e) {
17
+ function n(e) {
14
18
  return typeof e == "object" && !!e && typeof e.status == "number" && typeof e.detail == "string";
15
19
  }
16
- var n = /* @__PURE__ */ new Set([
20
+ var r = /* @__PURE__ */ new Set([
17
21
  "body",
18
22
  "query",
19
23
  "path",
20
24
  "header",
21
25
  "cookie"
22
26
  ]);
23
- function r(e) {
27
+ function i(e) {
24
28
  if (!Array.isArray(e)) return;
25
- let t = e.filter((e) => typeof e == "string" || typeof e == "number").filter((e, t) => !(t === 0 && n.has(String(e))));
29
+ let t = e.filter((e) => typeof e == "string" || typeof e == "number").filter((e, t) => !(t === 0 && r.has(String(e))));
26
30
  return t.length > 0 ? t.join(".") : void 0;
27
31
  }
28
- var i = 4;
29
- function a(e) {
32
+ var a = 4;
33
+ function o(e) {
30
34
  if (!Array.isArray(e)) return;
31
35
  let t = {};
32
36
  for (let n of e) {
33
37
  if (typeof n != "object" || !n) continue;
34
- let e = n, i = r(e.loc);
35
- if (i === void 0 || i in t) continue;
36
- let a = u(e.msg) ?? u(e.message);
37
- a !== void 0 && (t[i] = a);
38
+ let e = n, r = i(e.loc);
39
+ if (r === void 0 || r in t) continue;
40
+ let a = d(e.msg) ?? d(e.message);
41
+ a !== void 0 && (t[r] = a);
38
42
  }
39
43
  return Object.keys(t).length > 0 ? t : void 0;
40
44
  }
41
- function o(e) {
45
+ function s(e) {
42
46
  return typeof e == "object" && e && !Array.isArray(e) ? e : void 0;
43
47
  }
44
- function s(e) {
48
+ function c(e) {
45
49
  if (e === null) return;
46
50
  let t = [
47
- o(e.detail)?.field,
51
+ s(e.detail)?.field,
48
52
  e.field,
49
- o(e.details)?.field
53
+ s(e.details)?.field
50
54
  ];
51
55
  for (let e of t) if (typeof e == "string" && e !== "") return e;
52
56
  }
53
- function c(e, t) {
57
+ function l(e, t) {
54
58
  if (e === void 0 || t === void 0) return e;
55
59
  let n = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), r = e.replace(RegExp(`\\s+for field '${n}'(?:\\s+in\\s+'[^']*')?\\.?\\s*$`), "").trim();
56
60
  return r === "" ? e : r;
57
61
  }
58
- function l(e, t) {
59
- let n = a(e?.detail);
62
+ function u(e, t) {
63
+ let n = o(e?.detail);
60
64
  if (n !== void 0) return n;
61
- let r = s(e);
65
+ let r = c(e);
62
66
  if (r !== void 0 && t !== void 0) return { [r]: t };
63
67
  }
64
- function u(e, t = 0) {
68
+ function d(e, t = 0) {
65
69
  if (e != null) {
66
70
  if (typeof e == "string") return e === "" ? void 0 : e;
67
71
  if (typeof e == "number" || typeof e == "boolean") return String(e);
68
- if (!(t >= i)) {
72
+ if (!(t >= a)) {
69
73
  if (Array.isArray(e)) {
70
74
  let n = e.map((e) => {
71
- let n = u(e, t + 1);
75
+ let n = d(e, t + 1);
72
76
  if (n === void 0) return;
73
- let i = typeof e == "object" && e ? r(e.loc) : void 0;
74
- return i === void 0 ? n : `${i}: ${n}`;
77
+ let r = typeof e == "object" && e ? i(e.loc) : void 0;
78
+ return r === void 0 ? n : `${r}: ${n}`;
75
79
  }).filter((e) => e !== void 0);
76
80
  return n.length > 0 ? n.join("; ") : void 0;
77
81
  }
78
82
  if (typeof e == "object") {
79
83
  let n = e;
80
- return u(n.msg, t + 1) ?? u(n.message, t + 1) ?? u(n.detail, t + 1);
84
+ return d(n.msg, t + 1) ?? d(n.message, t + 1) ?? d(n.detail, t + 1);
81
85
  }
82
86
  }
83
87
  }
84
88
  }
85
- var d = /* @__PURE__ */ new Set([
89
+ var f = /* @__PURE__ */ new Set([
86
90
  0,
87
91
  408,
88
92
  425,
89
93
  429
90
94
  ]);
91
- function f(e) {
92
- return d.has(e) || e >= 500;
93
- }
94
95
  function p(e) {
96
+ return f.has(e) || e >= 500;
97
+ }
98
+ function m(e) {
95
99
  return `Erro ${e}`;
96
100
  }
97
- function m(e, t, n, r) {
98
- let i = typeof t == "object" && t ? t : null, a = c(u(i?.detail) ?? u(i?.message), s(i)), o = a ?? p(e), d = typeof i?.code == "string" ? i.code : void 0, f = typeof i?.details == "object" && i.details !== null ? i.details : null;
101
+ function h(e, t, n, r) {
102
+ let i = typeof t == "object" && t ? t : null, a = l(d(i?.detail) ?? d(i?.message), c(i)), o = a ?? m(e), s = typeof i?.code == "string" ? i.code : void 0, f = typeof i?.details == "object" && i.details !== null ? i.details : null;
99
103
  return {
100
104
  status: e,
101
105
  detail: o,
102
- code: d,
106
+ code: s,
103
107
  requestId: (typeof f?.request_id == "string" ? f.request_id : void 0) ?? n?.get("X-Request-ID") ?? r ?? void 0 ?? void 0,
104
- retryAfter: h(n?.get("Retry-After")),
105
- fields: l(i, a),
108
+ retryAfter: g(n?.get("Retry-After")),
109
+ fields: u(i, a),
106
110
  body: t
107
111
  };
108
112
  }
109
- function h(e) {
113
+ function g(e) {
110
114
  if (!e) return;
111
115
  let t = e.trim();
112
116
  if (/^\d+$/.test(t)) return Number(t);
@@ -114,6 +118,6 @@ function h(e) {
114
118
  if (!Number.isNaN(n)) return Math.max(0, Math.round((n - Date.now()) / 1e3));
115
119
  }
116
120
  //#endregion
117
- export { e as TempestApiError, m as buildApiError, t as isApiError, f as isRetriableStatus, h as parseRetryAfter, p as syntheticDetail };
121
+ export { t as SessionSupersededError, e as TempestApiError, h as buildApiError, n as isApiError, p as isRetriableStatus, g as parseRetryAfter, m as syntheticDetail };
118
122
 
119
123
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import type { ApiError } from \"./types\";\n\n/**\n * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a\n * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope\n * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a\n * `requestId` for log correlation, while still being a real `Error` (stack\n * trace, `instanceof Error`).\n *\n * @example\n * try {\n * await api.post(\"/users\", { body });\n * } catch (err) {\n * if (isApiError(err) && err.code === \"EMAIL_TAKEN\") {\n * showFieldError(\"email\", err.detail);\n * }\n * }\n */\nexport class TempestApiError extends Error implements ApiError {\n readonly status: number;\n readonly detail: string;\n readonly code?: string;\n readonly requestId?: string;\n readonly fields?: Record<string, string>;\n readonly body?: unknown;\n\n constructor(init: ApiError) {\n super(init.detail);\n this.name = \"TempestApiError\";\n this.status = init.status;\n this.detail = init.detail;\n this.code = init.code;\n this.requestId = init.requestId;\n this.fields = init.fields;\n this.body = init.body;\n }\n}\n\n/**\n * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}\n * instances and plain objects carrying `status` + `detail`.\n *\n * @param error - The unknown value (typically a caught error).\n * @returns Whether `error` conforms to the `ApiError` contract.\n */\nexport function isApiError(error: unknown): error is ApiError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as ApiError).status === \"number\" &&\n typeof (error as ApiError).detail === \"string\"\n );\n}\n\n/**\n * Location prefixes FastAPI puts at the head of a validation error's `loc`,\n * naming the part of the request rather than the field. Dropped from the\n * rendered path, so `[\"body\", \"email\"]` reads as `email`.\n */\nconst LOC_ROOTS: ReadonlySet<string> = new Set([\"body\", \"query\", \"path\", \"header\", \"cookie\"]);\n\n/**\n * Render a FastAPI validation error's `loc` tuple as a dotted field path.\n *\n * @param loc - The raw `loc` value from one validation error entry.\n * @returns The dotted path (`\"items.0.price\"`), or undefined when `loc` carries\n * nothing addressable.\n */\nfunction formatLoc(loc: unknown): string | undefined {\n if (!Array.isArray(loc)) return undefined;\n const parts = loc\n .filter(\n (part): part is string | number => typeof part === \"string\" || typeof part === \"number\",\n )\n .filter((part, index) => !(index === 0 && LOC_ROOTS.has(String(part))));\n return parts.length > 0 ? parts.join(\".\") : undefined;\n}\n\n/**\n * How deep {@link normalizeDetail} follows a nested `detail` before giving up.\n *\n * A real envelope needs two or three levels: the list, an entry, the entry's\n * own `detail`. The cap exists because the body is untrusted input arriving on\n * the error path — a response nesting `{\"detail\":{\"detail\":…}}` twenty thousand\n * deep (a 220 KB body) overflowed the stack, and a `RangeError` thrown while\n * *building* the error is worse than the error: the caller's `catch` stops\n * receiving a `TempestApiError`, so `isApiError` is false, `describeApiError`\n * has nothing to read and the `401` handling never runs.\n */\nconst MAX_DETAIL_DEPTH = 4;\n\n/**\n * Pull field-level messages out of a validation `detail` **list**.\n *\n * FastAPI's `422` body is `detail: [{ loc, msg, type }]`, which is exactly what\n * a form needs and exactly what the flattened `detail` string destroys. Only the\n * top level is read: a validation error names one field per entry, and following\n * nesting here would invent paths the backend never sent.\n *\n * This is one of two ways a body names a field — see {@link collectFields}, which\n * is the entry point and falls back to the singular keys when the list names\n * nothing addressable.\n *\n * @param raw - The `detail` value from the error body.\n * @returns Field path to message, or undefined when the body is not a\n * validation list (or carries no entry naming a field).\n */\nfunction collectListFields(raw: unknown): Record<string, string> | undefined {\n if (!Array.isArray(raw)) return undefined;\n\n const fields: Record<string, string> = {};\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const field = formatLoc(record.loc);\n if (field === undefined || field in fields) continue;\n const message = normalizeDetail(record.msg) ?? normalizeDetail(record.message);\n if (message === undefined) continue;\n fields[field] = message;\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n}\n\n/**\n * Narrow a value to a plain object — a record, and not an array.\n *\n * Arrays are excluded because `typeof [] === \"object\"` while a list means\n * something else entirely here: FastAPI's `detail` list is read by\n * {@link collectListFields}, and reading `.field` off it would only ever be\n * undefined.\n *\n * @param value - The candidate value from the error body.\n * @returns The value as a record, or undefined when it is not a plain object.\n */\nfunction plainRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * Read the field a **singular** error envelope names.\n *\n * A backend built on `tempest-fastapi-sdk` never answers with FastAPI's `detail`\n * list once it owns the handler: it names the guilty field in a key beside the\n * message. Three shapes are seen in the wild, and they are read inner-out:\n *\n * 1. `detail.field` — the field sits in the same object as the message it\n * describes (`{ detail: { detail: \"Cidade não encontrada…\", field: \"city\" } }`),\n * so it is the least ambiguous claim about which message belongs to which input.\n * 2. `field` at the top level — a flattened `RequestValidationError`\n * (`{ detail: \"Value error, … for field 'phone' in 'body'\", field: \"phone\" }`)\n * makes the same claim one level out.\n * 3. `details.field` — `details` is the envelope's free-form context bag, not a\n * validation channel, and its `field` may be about something no input on screen\n * carries (an unknown sort column, say). It answers last for exactly that reason.\n *\n * `location` (`\"body -> phone\"`) is deliberately not parsed: it renders the same\n * path `field` already names, no observed envelope sends it without `field`, and\n * splitting an arrow-separated string would invent a path the backend never sent.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @returns The field name, or undefined when none of the three keys carried a\n * non-empty string — anything else is not usable as a key on `fields`.\n */\nfunction namedField(body: Record<string, unknown> | null): string | undefined {\n if (body === null) return undefined;\n const candidates: readonly unknown[] = [\n plainRecord(body.detail)?.field,\n body.field,\n plainRecord(body.details)?.field,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate !== \"\") return candidate;\n }\n return undefined;\n}\n\n/**\n * Drop the machine-readable tail a `tempest-fastapi-sdk` backend appends.\n *\n * That backend flattens a field error into\n * `\"CPF ou CNPJ inválido for field 'cpf_cnpj' in 'body'\"` — a finished sentence\n * in the app's language with an English clause glued to the end. The clause\n * carries nothing new: the same two values arrive as `field` and `location` on\n * the envelope, which is where `ApiError.fields` reads them from. Left in, it\n * reaches the user, and every consuming app grew its own regex to shave it off.\n *\n * The trim only fires when the field the tail names is the field the envelope\n * resolved to. That is what keeps it from deleting text it cannot account for:\n * a tail naming some other field is either a different envelope shape or a\n * sentence that genuinely reads that way, and both are left alone.\n *\n * @param message - The readable message, or undefined when the body had none.\n * @param field - The field name {@link namedField} resolved, if any.\n * @returns The message without the tail, or the message unchanged.\n */\nfunction trimFieldSuffix(\n message: string | undefined,\n field: string | undefined,\n): string | undefined {\n if (message === undefined || field === undefined) return message;\n const escaped = field.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const trimmed = message\n .replace(new RegExp(`\\\\s+for field '${escaped}'(?:\\\\s+in\\\\s+'[^']*')?\\\\.?\\\\s*$`), \"\")\n .trim();\n return trimmed === \"\" ? message : trimmed;\n}\n\n/**\n * Index the body's field-level messages, whichever envelope carried them.\n *\n * The single entry point behind `ApiError.fields`, and it applies two rules the\n * tests pin:\n *\n * - FastAPI's `detail` list wins whenever it names at least one addressable\n * field. A plain FastAPI app still answers a schema-level `422` with that list\n * even on a `tempest-fastapi-sdk` backend, so it stays authoritative.\n * - A named field with no readable message produces nothing. The only string\n * left at that point is the synthetic `Erro <status>`, and `{ phone: \"Erro 422\" }`\n * on an input is noise rather than an error message.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @param message - The readable message the same body produced, before the\n * synthetic fallback — the string that also becomes `ApiError.detail`.\n * @returns Field name to message, or undefined when nothing named a field.\n */\nfunction collectFields(\n body: Record<string, unknown> | null,\n message: string | undefined,\n): Record<string, string> | undefined {\n const listed = collectListFields(body?.detail);\n if (listed !== undefined) return listed;\n const field = namedField(body);\n if (field === undefined || message === undefined) return undefined;\n return { [field]: message };\n}\n\n/**\n * Collapse a backend `detail` of any shape into a single readable line.\n *\n * FastAPI answers a `422` with `detail` as a **list** of\n * `{ loc, msg, type }` entries, not a string. Passing that through `String()`\n * yields `\"[object Object]\"` — an error message that tells the user nothing and\n * hides which field failed. Each entry becomes `\"<field>: <msg>\"` and the\n * entries are joined with `\"; \"`; a nested object is read through its\n * `msg`/`message`/`detail` string.\n *\n * @param raw - The `detail` (or `message`) value from the error body.\n * @param depth - Current nesting level. Past {@link MAX_DETAIL_DEPTH} the value\n * is treated as unreadable instead of followed further.\n * @returns The rendered message, or undefined when nothing readable is there —\n * letting the caller fall back to the synthetic `Erro <status>`.\n */\nfunction normalizeDetail(raw: unknown, depth: number = 0): string | undefined {\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"string\") return raw === \"\" ? undefined : raw;\n if (typeof raw === \"number\" || typeof raw === \"boolean\") return String(raw);\n if (depth >= MAX_DETAIL_DEPTH) return undefined;\n\n if (Array.isArray(raw)) {\n const lines = raw\n .map((entry) => {\n const message = normalizeDetail(entry, depth + 1);\n if (message === undefined) return undefined;\n const field =\n typeof entry === \"object\" && entry !== null\n ? formatLoc((entry as Record<string, unknown>).loc)\n : undefined;\n return field === undefined ? message : `${field}: ${message}`;\n })\n .filter((line): line is string => line !== undefined);\n return lines.length > 0 ? lines.join(\"; \") : undefined;\n }\n\n if (typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n return (\n normalizeDetail(entry.msg, depth + 1) ??\n normalizeDetail(entry.message, depth + 1) ??\n normalizeDetail(entry.detail, depth + 1)\n );\n }\n\n return undefined;\n}\n\n/**\n * Statuses worth a second attempt, as a set for the sub-500 cases.\n *\n * A network failure (status `0`), a request timeout, a too-early replay, and a\n * rate limit — which usually carries the `Retry-After` the backoff honours.\n * Everything else below 500 is the server refusing on purpose.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * Whether an HTTP status describes a condition a replay can plausibly fix.\n *\n * The single owner of that decision. It used to be spelled out in three places —\n * the client's own policy, the react-query default and the bare `retry()` helper\n * — and they had already drifted: the query default was missing `425`, so the\n * same `425 Too Early` was replayed through `createApiClient({ retry: true })`\n * and not replayed through `useQuery`. Same app, same error, two behaviours, and\n * no test caught it because each file asserted against its own copy.\n *\n * Deliberately about the status and nothing else. Whether a *non*-API error is\n * worth replaying, and whether the request's method may be replayed at all, are\n * the caller's calls: {@link createApiClient} refuses a non-idempotent method,\n * while a bare `retry()` has no method to inspect.\n *\n * @example\n * await api.get(\"/report\", {\n * retry: { shouldRetry: (error) => isApiError(error) && isRetriableStatus(error.status) },\n * });\n *\n * @param status - The HTTP status, where `0` means the request never landed.\n * @returns Whether a retry is worth attempting.\n */\nexport function isRetriableStatus(status: number): boolean {\n return RETRIABLE_STATUSES.has(status) || status >= 500;\n}\n\n/**\n * Detail text synthesised when a response body carries none.\n *\n * Exported because {@link describeApiError} has to recognise it: a detail the\n * server never sent says strictly less than the caller's own fallback, so the\n * funnel drops it. Comparing against a copied literal would silently stop\n * matching the day this sentence is reworded — no type error, no failing test.\n *\n * @param status - The HTTP status of the error.\n * @returns The synthetic detail for that status.\n */\nexport function syntheticDetail(status: number): string {\n return `Erro ${status}`;\n}\n\n/**\n * Parse an error body + response into the Tempest {@link ApiError} envelope.\n *\n * Reads `detail`/`message`, the programmatic `code`, and the correlation id\n * from `details.request_id` (falling back to the `X-Request-ID` header, then\n * the id the client sent).\n *\n * A `422` from FastAPI carries `detail` as a list of `{ loc, msg, type }`\n * entries, so it is flattened to `\"<field>: <msg>; <field>: <msg>\"` instead of\n * being stringified into `\"[object Object]\"`, and the same entries are indexed\n * on `fields` (`{ email: \"Field required\" }`) for a form to consume without\n * parsing that line back apart. The untouched body stays on `body`.\n *\n * A backend that owns its handlers — every `tempest-fastapi-sdk` app — sends no\n * such list, and names the field in a key instead:\n *\n * ```json\n * { \"detail\": \"Value error, … for field 'phone' in 'body'\", \"field\": \"phone\" }\n * { \"detail\": { \"detail\": \"Cidade não encontrada…\", \"field\": \"city\" },\n * \"code\": \"VALIDATION_ERROR\", \"details\": { \"field\": \"city\" } }\n * ```\n *\n * Those are indexed too, keyed by the field the backend named and valued with the\n * same sentence that becomes `detail`. Precedence is the list first, then\n * `detail.field`, `field`, `details.field` — see {@link collectFields}.\n *\n * A flattened `detail` from the list is developer-facing: it carries the\n * backend's field paths and the validator's own wording. `describeApiError` knows\n * not to show it to a person when `fields` is set — which now also covers a\n * business error that named a field, whose `detail` was a finished sentence. That\n * sentence is not lost: it is on `fields`, attached to the input that failed.\n *\n * @param status - HTTP status code.\n * @param body - The parsed error body (object, string, or null).\n * @param headers - The response headers (for the `X-Request-ID` fallback).\n * @param sentRequestId - The id the client sent on the request, if any.\n * @returns A fully-populated `ApiError`.\n *\n * @tempest-limits param-count — the arguments are the response as it arrives\n * (`status`, `body`, `headers`) plus the id the request was sent with, and they are\n * passed at exactly three places, all of them a client's response path\n * (`createApiClient`, `uploadWithProgress`, `createResumableUpload`). Wrapping them\n * in an options object would name each argument twice at every call site to say\n * nothing new.\n */\nexport function buildApiError(\n status: number,\n body: unknown,\n headers?: Headers | { get(name: string): string | null },\n sentRequestId?: string,\n): ApiError {\n const obj =\n typeof body === \"object\" && body !== null ? (body as Record<string, unknown>) : null;\n const message = trimFieldSuffix(\n normalizeDetail(obj?.detail) ?? normalizeDetail(obj?.message),\n namedField(obj),\n );\n const detail = message ?? syntheticDetail(status);\n const code = typeof obj?.code === \"string\" ? obj.code : undefined;\n const details =\n typeof obj?.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n const requestId =\n (typeof details?.request_id === \"string\" ? details.request_id : undefined) ??\n headers?.get(\"X-Request-ID\") ??\n sentRequestId ??\n undefined;\n\n return {\n status,\n detail,\n code,\n requestId: requestId ?? undefined,\n retryAfter: parseRetryAfter(headers?.get(\"Retry-After\")),\n fields: collectFields(obj, message),\n body,\n };\n}\n\n/**\n * Parse a `Retry-After` header into seconds. Accepts a delta-seconds integer\n * (`\"120\"`) or an HTTP-date (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`).\n *\n * @param value - The raw header value, or null.\n * @returns The delay in seconds (>= 0), or undefined when absent/unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const when = Date.parse(trimmed);\n if (Number.isNaN(when)) return undefined;\n return Math.max(0, Math.round((when - Date.now()) / 1000));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import type { ApiError } from \"./types\";\n\n/**\n * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a\n * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope\n * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a\n * `requestId` for log correlation, while still being a real `Error` (stack\n * trace, `instanceof Error`).\n *\n * @example\n * try {\n * await api.post(\"/users\", { body });\n * } catch (err) {\n * if (isApiError(err) && err.code === \"EMAIL_TAKEN\") {\n * showFieldError(\"email\", err.detail);\n * }\n * }\n */\nexport class TempestApiError extends Error implements ApiError {\n readonly status: number;\n readonly detail: string;\n readonly code?: string;\n readonly requestId?: string;\n readonly fields?: Record<string, string>;\n readonly body?: unknown;\n\n constructor(init: ApiError) {\n super(init.detail);\n this.name = \"TempestApiError\";\n this.status = init.status;\n this.detail = init.detail;\n this.code = init.code;\n this.requestId = init.requestId;\n this.fields = init.fields;\n this.body = init.body;\n }\n}\n\n/**\n * Thrown when the session that started an operation is no longer the current\n * one by the time it settles: a `refresh` hook (rethrown by\n * {@link createApiClient}), or a `createTempestAuth` `login()` whose session\n * was replaced while it waited on the previous session's teardown.\n *\n * A refresh is a network round trip, and the session can end or change hands\n * during it: the user signs out, or signs out and another account signs in. The\n * tokens that refresh brings back belong to the session that ended, so they are\n * discarded instead of installed, and the request that was waiting on them is\n * not replayed — replaying it would send it as whoever is signed in now.\n *\n * This is deliberately not an unauthorized outcome. The client does **not** call\n * `onUnauthorized` for it: that callback ends the session, and the current\n * session — possibly a different account that just signed in — did nothing\n * wrong. Treat it like an aborted request: drop the result, show nothing.\n *\n * @example\n * try {\n * await api.get(\"/api/orders\");\n * } catch (err) {\n * if (err instanceof SessionSupersededError) return;\n * throw err;\n * }\n */\nexport class SessionSupersededError extends Error {\n constructor() {\n super(\"A sessão mudou antes de a operação terminar.\");\n this.name = \"SessionSupersededError\";\n }\n}\n\n/**\n * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}\n * instances and plain objects carrying `status` + `detail`.\n *\n * @param error - The unknown value (typically a caught error).\n * @returns Whether `error` conforms to the `ApiError` contract.\n */\nexport function isApiError(error: unknown): error is ApiError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as ApiError).status === \"number\" &&\n typeof (error as ApiError).detail === \"string\"\n );\n}\n\n/**\n * Location prefixes FastAPI puts at the head of a validation error's `loc`,\n * naming the part of the request rather than the field. Dropped from the\n * rendered path, so `[\"body\", \"email\"]` reads as `email`.\n */\nconst LOC_ROOTS: ReadonlySet<string> = new Set([\"body\", \"query\", \"path\", \"header\", \"cookie\"]);\n\n/**\n * Render a FastAPI validation error's `loc` tuple as a dotted field path.\n *\n * @param loc - The raw `loc` value from one validation error entry.\n * @returns The dotted path (`\"items.0.price\"`), or undefined when `loc` carries\n * nothing addressable.\n */\nfunction formatLoc(loc: unknown): string | undefined {\n if (!Array.isArray(loc)) return undefined;\n const parts = loc\n .filter(\n (part): part is string | number => typeof part === \"string\" || typeof part === \"number\",\n )\n .filter((part, index) => !(index === 0 && LOC_ROOTS.has(String(part))));\n return parts.length > 0 ? parts.join(\".\") : undefined;\n}\n\n/**\n * How deep {@link normalizeDetail} follows a nested `detail` before giving up.\n *\n * A real envelope needs two or three levels: the list, an entry, the entry's\n * own `detail`. The cap exists because the body is untrusted input arriving on\n * the error path — a response nesting `{\"detail\":{\"detail\":…}}` twenty thousand\n * deep (a 220 KB body) overflowed the stack, and a `RangeError` thrown while\n * *building* the error is worse than the error: the caller's `catch` stops\n * receiving a `TempestApiError`, so `isApiError` is false, `describeApiError`\n * has nothing to read and the `401` handling never runs.\n */\nconst MAX_DETAIL_DEPTH = 4;\n\n/**\n * Pull field-level messages out of a validation `detail` **list**.\n *\n * FastAPI's `422` body is `detail: [{ loc, msg, type }]`, which is exactly what\n * a form needs and exactly what the flattened `detail` string destroys. Only the\n * top level is read: a validation error names one field per entry, and following\n * nesting here would invent paths the backend never sent.\n *\n * This is one of two ways a body names a field — see {@link collectFields}, which\n * is the entry point and falls back to the singular keys when the list names\n * nothing addressable.\n *\n * @param raw - The `detail` value from the error body.\n * @returns Field path to message, or undefined when the body is not a\n * validation list (or carries no entry naming a field).\n */\nfunction collectListFields(raw: unknown): Record<string, string> | undefined {\n if (!Array.isArray(raw)) return undefined;\n\n const fields: Record<string, string> = {};\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const record = entry as Record<string, unknown>;\n const field = formatLoc(record.loc);\n if (field === undefined || field in fields) continue;\n const message = normalizeDetail(record.msg) ?? normalizeDetail(record.message);\n if (message === undefined) continue;\n fields[field] = message;\n }\n\n return Object.keys(fields).length > 0 ? fields : undefined;\n}\n\n/**\n * Narrow a value to a plain object — a record, and not an array.\n *\n * Arrays are excluded because `typeof [] === \"object\"` while a list means\n * something else entirely here: FastAPI's `detail` list is read by\n * {@link collectListFields}, and reading `.field` off it would only ever be\n * undefined.\n *\n * @param value - The candidate value from the error body.\n * @returns The value as a record, or undefined when it is not a plain object.\n */\nfunction plainRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * Read the field a **singular** error envelope names.\n *\n * A backend built on `tempest-fastapi-sdk` never answers with FastAPI's `detail`\n * list once it owns the handler: it names the guilty field in a key beside the\n * message. Three shapes are seen in the wild, and they are read inner-out:\n *\n * 1. `detail.field` — the field sits in the same object as the message it\n * describes (`{ detail: { detail: \"Cidade não encontrada…\", field: \"city\" } }`),\n * so it is the least ambiguous claim about which message belongs to which input.\n * 2. `field` at the top level — a flattened `RequestValidationError`\n * (`{ detail: \"Value error, … for field 'phone' in 'body'\", field: \"phone\" }`)\n * makes the same claim one level out.\n * 3. `details.field` — `details` is the envelope's free-form context bag, not a\n * validation channel, and its `field` may be about something no input on screen\n * carries (an unknown sort column, say). It answers last for exactly that reason.\n *\n * `location` (`\"body -> phone\"`) is deliberately not parsed: it renders the same\n * path `field` already names, no observed envelope sends it without `field`, and\n * splitting an arrow-separated string would invent a path the backend never sent.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @returns The field name, or undefined when none of the three keys carried a\n * non-empty string — anything else is not usable as a key on `fields`.\n */\nfunction namedField(body: Record<string, unknown> | null): string | undefined {\n if (body === null) return undefined;\n const candidates: readonly unknown[] = [\n plainRecord(body.detail)?.field,\n body.field,\n plainRecord(body.details)?.field,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate !== \"\") return candidate;\n }\n return undefined;\n}\n\n/**\n * Drop the machine-readable tail a `tempest-fastapi-sdk` backend appends.\n *\n * That backend flattens a field error into\n * `\"CPF ou CNPJ inválido for field 'cpf_cnpj' in 'body'\"` — a finished sentence\n * in the app's language with an English clause glued to the end. The clause\n * carries nothing new: the same two values arrive as `field` and `location` on\n * the envelope, which is where `ApiError.fields` reads them from. Left in, it\n * reaches the user, and every consuming app grew its own regex to shave it off.\n *\n * The trim only fires when the field the tail names is the field the envelope\n * resolved to. That is what keeps it from deleting text it cannot account for:\n * a tail naming some other field is either a different envelope shape or a\n * sentence that genuinely reads that way, and both are left alone.\n *\n * @param message - The readable message, or undefined when the body had none.\n * @param field - The field name {@link namedField} resolved, if any.\n * @returns The message without the tail, or the message unchanged.\n */\nfunction trimFieldSuffix(\n message: string | undefined,\n field: string | undefined,\n): string | undefined {\n if (message === undefined || field === undefined) return message;\n const escaped = field.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const trimmed = message\n .replace(new RegExp(`\\\\s+for field '${escaped}'(?:\\\\s+in\\\\s+'[^']*')?\\\\.?\\\\s*$`), \"\")\n .trim();\n return trimmed === \"\" ? message : trimmed;\n}\n\n/**\n * Index the body's field-level messages, whichever envelope carried them.\n *\n * The single entry point behind `ApiError.fields`, and it applies two rules the\n * tests pin:\n *\n * - FastAPI's `detail` list wins whenever it names at least one addressable\n * field. A plain FastAPI app still answers a schema-level `422` with that list\n * even on a `tempest-fastapi-sdk` backend, so it stays authoritative.\n * - A named field with no readable message produces nothing. The only string\n * left at that point is the synthetic `Erro <status>`, and `{ phone: \"Erro 422\" }`\n * on an input is noise rather than an error message.\n *\n * @param body - The parsed error body, or null when it was not an object.\n * @param message - The readable message the same body produced, before the\n * synthetic fallback — the string that also becomes `ApiError.detail`.\n * @returns Field name to message, or undefined when nothing named a field.\n */\nfunction collectFields(\n body: Record<string, unknown> | null,\n message: string | undefined,\n): Record<string, string> | undefined {\n const listed = collectListFields(body?.detail);\n if (listed !== undefined) return listed;\n const field = namedField(body);\n if (field === undefined || message === undefined) return undefined;\n return { [field]: message };\n}\n\n/**\n * Collapse a backend `detail` of any shape into a single readable line.\n *\n * FastAPI answers a `422` with `detail` as a **list** of\n * `{ loc, msg, type }` entries, not a string. Passing that through `String()`\n * yields `\"[object Object]\"` — an error message that tells the user nothing and\n * hides which field failed. Each entry becomes `\"<field>: <msg>\"` and the\n * entries are joined with `\"; \"`; a nested object is read through its\n * `msg`/`message`/`detail` string.\n *\n * @param raw - The `detail` (or `message`) value from the error body.\n * @param depth - Current nesting level. Past {@link MAX_DETAIL_DEPTH} the value\n * is treated as unreadable instead of followed further.\n * @returns The rendered message, or undefined when nothing readable is there —\n * letting the caller fall back to the synthetic `Erro <status>`.\n */\nfunction normalizeDetail(raw: unknown, depth: number = 0): string | undefined {\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"string\") return raw === \"\" ? undefined : raw;\n if (typeof raw === \"number\" || typeof raw === \"boolean\") return String(raw);\n if (depth >= MAX_DETAIL_DEPTH) return undefined;\n\n if (Array.isArray(raw)) {\n const lines = raw\n .map((entry) => {\n const message = normalizeDetail(entry, depth + 1);\n if (message === undefined) return undefined;\n const field =\n typeof entry === \"object\" && entry !== null\n ? formatLoc((entry as Record<string, unknown>).loc)\n : undefined;\n return field === undefined ? message : `${field}: ${message}`;\n })\n .filter((line): line is string => line !== undefined);\n return lines.length > 0 ? lines.join(\"; \") : undefined;\n }\n\n if (typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n return (\n normalizeDetail(entry.msg, depth + 1) ??\n normalizeDetail(entry.message, depth + 1) ??\n normalizeDetail(entry.detail, depth + 1)\n );\n }\n\n return undefined;\n}\n\n/**\n * Statuses worth a second attempt, as a set for the sub-500 cases.\n *\n * A network failure (status `0`), a request timeout, a too-early replay, and a\n * rate limit — which usually carries the `Retry-After` the backoff honours.\n * Everything else below 500 is the server refusing on purpose.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * Whether an HTTP status describes a condition a replay can plausibly fix.\n *\n * The single owner of that decision. It used to be spelled out in three places —\n * the client's own policy, the react-query default and the bare `retry()` helper\n * — and they had already drifted: the query default was missing `425`, so the\n * same `425 Too Early` was replayed through `createApiClient({ retry: true })`\n * and not replayed through `useQuery`. Same app, same error, two behaviours, and\n * no test caught it because each file asserted against its own copy.\n *\n * Deliberately about the status and nothing else. Whether a *non*-API error is\n * worth replaying, and whether the request's method may be replayed at all, are\n * the caller's calls: {@link createApiClient} refuses a non-idempotent method,\n * while a bare `retry()` has no method to inspect.\n *\n * @example\n * await api.get(\"/report\", {\n * retry: { shouldRetry: (error) => isApiError(error) && isRetriableStatus(error.status) },\n * });\n *\n * @param status - The HTTP status, where `0` means the request never landed.\n * @returns Whether a retry is worth attempting.\n */\nexport function isRetriableStatus(status: number): boolean {\n return RETRIABLE_STATUSES.has(status) || status >= 500;\n}\n\n/**\n * Detail text synthesised when a response body carries none.\n *\n * Exported because {@link describeApiError} has to recognise it: a detail the\n * server never sent says strictly less than the caller's own fallback, so the\n * funnel drops it. Comparing against a copied literal would silently stop\n * matching the day this sentence is reworded — no type error, no failing test.\n *\n * @param status - The HTTP status of the error.\n * @returns The synthetic detail for that status.\n */\nexport function syntheticDetail(status: number): string {\n return `Erro ${status}`;\n}\n\n/**\n * Parse an error body + response into the Tempest {@link ApiError} envelope.\n *\n * Reads `detail`/`message`, the programmatic `code`, and the correlation id\n * from `details.request_id` (falling back to the `X-Request-ID` header, then\n * the id the client sent).\n *\n * A `422` from FastAPI carries `detail` as a list of `{ loc, msg, type }`\n * entries, so it is flattened to `\"<field>: <msg>; <field>: <msg>\"` instead of\n * being stringified into `\"[object Object]\"`, and the same entries are indexed\n * on `fields` (`{ email: \"Field required\" }`) for a form to consume without\n * parsing that line back apart. The untouched body stays on `body`.\n *\n * A backend that owns its handlers — every `tempest-fastapi-sdk` app — sends no\n * such list, and names the field in a key instead:\n *\n * ```json\n * { \"detail\": \"Value error, … for field 'phone' in 'body'\", \"field\": \"phone\" }\n * { \"detail\": { \"detail\": \"Cidade não encontrada…\", \"field\": \"city\" },\n * \"code\": \"VALIDATION_ERROR\", \"details\": { \"field\": \"city\" } }\n * ```\n *\n * Those are indexed too, keyed by the field the backend named and valued with the\n * same sentence that becomes `detail`. Precedence is the list first, then\n * `detail.field`, `field`, `details.field` — see {@link collectFields}.\n *\n * A flattened `detail` from the list is developer-facing: it carries the\n * backend's field paths and the validator's own wording. `describeApiError` knows\n * not to show it to a person when `fields` is set — which now also covers a\n * business error that named a field, whose `detail` was a finished sentence. That\n * sentence is not lost: it is on `fields`, attached to the input that failed.\n *\n * @param status - HTTP status code.\n * @param body - The parsed error body (object, string, or null).\n * @param headers - The response headers (for the `X-Request-ID` fallback).\n * @param sentRequestId - The id the client sent on the request, if any.\n * @returns A fully-populated `ApiError`.\n *\n * @tempest-limits param-count — the arguments are the response as it arrives\n * (`status`, `body`, `headers`) plus the id the request was sent with, and they are\n * passed at exactly three places, all of them a client's response path\n * (`createApiClient`, `uploadWithProgress`, `createResumableUpload`). Wrapping them\n * in an options object would name each argument twice at every call site to say\n * nothing new.\n */\nexport function buildApiError(\n status: number,\n body: unknown,\n headers?: Headers | { get(name: string): string | null },\n sentRequestId?: string,\n): ApiError {\n const obj =\n typeof body === \"object\" && body !== null ? (body as Record<string, unknown>) : null;\n const message = trimFieldSuffix(\n normalizeDetail(obj?.detail) ?? normalizeDetail(obj?.message),\n namedField(obj),\n );\n const detail = message ?? syntheticDetail(status);\n const code = typeof obj?.code === \"string\" ? obj.code : undefined;\n const details =\n typeof obj?.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n const requestId =\n (typeof details?.request_id === \"string\" ? details.request_id : undefined) ??\n headers?.get(\"X-Request-ID\") ??\n sentRequestId ??\n undefined;\n\n return {\n status,\n detail,\n code,\n requestId: requestId ?? undefined,\n retryAfter: parseRetryAfter(headers?.get(\"Retry-After\")),\n fields: collectFields(obj, message),\n body,\n };\n}\n\n/**\n * Parse a `Retry-After` header into seconds. Accepts a delta-seconds integer\n * (`\"120\"`) or an HTTP-date (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`).\n *\n * @param value - The raw header value, or null.\n * @returns The delay in seconds (>= 0), or undefined when absent/unparseable.\n */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const when = Date.parse(trimmed);\n if (Number.isNaN(when)) return undefined;\n return Math.max(0, Math.round((when - Date.now()) / 1000));\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/base64.cjs"),t=require("./credential-scope.cjs"),n=require("./csrf.cjs"),r=require("./errors.cjs"),i=require("./retry.cjs"),a=require("./idempotency.cjs");var o=`1.0.0`,s=5242880;function c(t){return e.bytesToBase64(new TextEncoder().encode(t))}function l(e){if(!e)return null;let t=Object.entries(e).map(([e,t])=>`${e} ${c(t)}`);return t.length>0?t.join(`,`):null}function u(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 d(e=`tempest-upload:`){function t(){try{return typeof localStorage>`u`?null:localStorage}catch{return null}}return{get(n){let r=t()?.getItem(e+n);if(!r)return null;try{return JSON.parse(r)}catch{return null}},set(n,r){t()?.setItem(e+n,JSON.stringify(r))},delete(n){t()?.removeItem(e+n)}}}function f(e){return new Promise((t,n)=>{let i=new XMLHttpRequest;i.open(e.method,e.url),i.withCredentials=e.withCredentials;for(let[t,n]of Object.entries(e.headers))i.setRequestHeader(t,n);if(e.onProgress){let t=e.onProgress;i.upload.onprogress=e=>t(e.loaded)}i.onload=()=>t({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>n(new r.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>n(new DOMException(`Aborted`,`AbortError`)),e.register(i),i.send(e.body)})}function p(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 m(e){if(!e)return null;try{return JSON.parse(e)}catch{return e}}var h=new Set([409,412]);function g(e){return!r.isApiError(e)||h.has(e.status)?!0:r.isRetriableStatus(e.status)}function _(e,t){let n=m(e.text),i=r.buildApiError(e.status,n,{get:e.header}),a=typeof n==`object`&&!!n&&(`detail`in n||`message`in n);return new r.TempestApiError({...i,detail:a?i.detail:t})}function v(e){let t=typeof window>`u`?void 0:window.location.href;try{return new URL(e,t).href}catch{return e}}function y(e){let{endpoint:c,file:m,chunkSize:h=s,metadata:y,headers:b={},getToken:x,trustedOrigins:S,withCredentials:C=!1,csrf:w,key:T=u(c,m),storage:E=d(),retry:D,onProgress:O,onStateChange:k}=e,A=`idle`,j=0,M=null,N=null,P=null,F=null,I=0;function L(e){A!==e&&(A=e,k?.(e))}function R(e){O?.({loaded:e,total:m.size,fraction:m.size===0?1:e/m.size,resumedFrom:I})}function z(e,r){let i={...b,"Tus-Resumable":o},a=x?.();return a&&!(`Authorization`in i)&&(t.isTrustedCredentialTarget(e,c,S)?i.Authorization=`Bearer ${a}`:t.reportSuppressedCredential(e,c)),Object.assign(i,n.csrfHeaders({method:r,url:e,reference:c,csrf:w,trustedOrigins:S,headers:b})),i}function B(e){F=e}async function V(){E&&M&&N&&await E.set(T,{url:M,offset:j,size:m.size,idempotencyKey:N,updatedAt:Date.now()})}async function H(e){let t=await f({method:`HEAD`,url:e,headers:z(e,`HEAD`),withCredentials:C,register:B});if(t.status===404||t.status===410)throw new r.TempestApiError({status:t.status,detail:`O upload expirou no servidor. Comece de novo.`});let n=p(t);if(n===null)throw _(t,`HEAD sem Upload-Offset.`);return n}async function U(){let e=E?await E.get(T):null;if(e&&e.size===m.size&&(N=e.idempotencyKey,e.url))try{return j=await H(e.url),M=e.url,e.url}catch{j=0}L(`creating`),N??=a.generateIdempotencyKey(),M=null,j=0,E&&await E.set(T,{url:``,offset:0,size:m.size,idempotencyKey:N,updatedAt:Date.now()});let t={...z(c,`POST`),"Upload-Length":String(m.size),"Idempotency-Key":N},n=l(y);n&&(t[`Upload-Metadata`]=n);let r=await f({method:`POST`,url:c,headers:t,withCredentials:C,register:B});if(r.status!==201)throw _(r,`Criação do upload recusada.`);let i=r.header(`Location`);if(!i)throw _(r,`Criação do upload sem cabeçalho Location.`);return M=v(i),await V(),M}async function W(e,t){if(t.needed&&(j=await H(e),t.needed=!1,R(j),await V(),j>=m.size))return;let n=Math.min(j+h,m.size),r=j,i=await f({method:`PATCH`,url:e,headers:{...z(e,`PATCH`),"Content-Type":`application/offset+octet-stream`,"Upload-Offset":String(r)},body:m.slice(r,n),withCredentials:C,onProgress:e=>R(Math.min(r+e,m.size)),register:B});if(i.status===409||i.status===412)throw t.needed=!0,_(i,`Offset divergente — o servidor já tinha esses bytes.`);if(i.status!==204&&i.status!==200)throw _(i,`Chunk recusado pelo servidor.`);j=p(i)??n,R(j),await V()}async function G(){P=null;let e=await U();I=j,L(`uploading`),R(j);let t={needed:!1};for(;j<m.size&&!P;)await i.retry(()=>W(e,t),{retries:5,...D,shouldRetry:(e,n)=>{if(P||e instanceof DOMException&&e.name===`AbortError`)return!1;let r=D?.shouldRetry?.(e,n)??g(e);return r&&(t.needed=!0),r}});return P===`pause`?(L(`paused`),null):P===`abort`?(L(`aborted`),null):(L(`done`),E&&await E.delete(T),{url:e,size:m.size})}async function K(){try{return await G()}catch(e){if(P!==null||e instanceof DOMException&&e.name===`AbortError`)return L(P===`abort`?`aborted`:`paused`),null;throw L(`error`),e}finally{F=null}}function q(e){P=e,F?.abort(),F=null}return{start:K,resume:K,pause:()=>q(`pause`),abort:async({discard:e=!1}={})=>{q(`abort`),L(`aborted`),e&&(M&&await f({method:`DELETE`,url:M,headers:z(M,`DELETE`),withCredentials:C,register:()=>void 0}).catch(()=>void 0),E&&await E.delete(T))},get state(){return A},get offset(){return j},get url(){return M},key:T}}exports.DEFAULT_CHUNK_SIZE=s,exports.TUS_VERSION=o,exports.createLocalUploadStorage=d,exports.createResumableUpload=y,exports.uploadFingerprint=u;
1
+ const e=require("../utils/base64.cjs"),t=require("./credential-scope.cjs"),n=require("./csrf.cjs"),r=require("./errors.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(t){return e.bytesToBase64(new TextEncoder().encode(t))}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 _(e){return new Promise((t,n)=>{let i=new XMLHttpRequest;i.open(e.method,e.url),i.withCredentials=e.withCredentials;for(let[t,n]of Object.entries(e.headers))i.setRequestHeader(t,n);if(e.onProgress){let t=e.onProgress;i.upload.onprogress=e=>t(e.loaded)}i.onload=()=>t({status:i.status,text:i.responseText,header:e=>i.getResponseHeader(e)}),i.onerror=()=>n(new r.TempestApiError({status:0,detail:`Falha de rede no upload resumível.`})),i.onabort=()=>n(new DOMException(`Aborted`,`AbortError`)),e.register(i),i.send(e.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(e){return!r.isApiError(e)||b.has(e.status)?!0:r.isRetriableStatus(e.status)}function S(e,t){let n=y(e.text),i=r.buildApiError(e.status,n,{get:e.header}),a=typeof n==`object`&&!!n&&(`detail`in n||`message`in n);return new r.TempestApiError({...i,detail:a?i.detail:t})}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(e){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}=e,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,r){let i={...b,"Tus-Resumable":s},a=w?.();return a&&!(`Authorization`in i)&&(t.isTrustedCredentialTarget(e,u,E)?i.Authorization=`Bearer ${a}`:t.reportSuppressedCredential(e,u)),Object.assign(i,n.csrfHeaders({method:r,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(e){let t=await _({method:`HEAD`,url:e,headers:J(e,`HEAD`),withCredentials:D,register:Y});if(t.status===404||t.status===410)throw new r.TempestApiError({status:t.status,detail:`O upload expirou no servidor. Comece de novo.`});let n=v(t);if(n===null)throw S(t,`HEAD sem Upload-Offset.`);return n}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
@@ -1 +1 @@
1
- {"version":3,"file":"resumable-upload.cjs","names":[],"sources":["../../src/http/resumable-upload.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a resumable upload is one long-lived\n * state machine: chunk the file, negotiate the offset the server already has, upload\n * with retry and backoff, honour pause, resume and abort, and report progress\n * throughout. Every stage reads the same cursor and the same abort signal, and\n * createResumableUpload is the closure that owns them.\n */\nimport { bytesToBase64 } from \"@/utils/base64\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, isApiError, isRetriableStatus, TempestApiError } from \"./errors\";\nimport { generateIdempotencyKey } from \"./idempotency\";\nimport { retry, type RetryOptions } from \"./retry\";\n\n/** The tus protocol version this client speaks. */\nexport const TUS_VERSION = \"1.0.0\";\n\n/** Default chunk size: 5 MiB, the size most tus servers are tuned for. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;\n\n/**\n * Where a resumable upload is.\n *\n * `\"paused\"` and `\"aborted\"` are both \"not running\", but only `\"paused\"` keeps the\n * persisted offset — `abort({ discard: true })` throws it away.\n */\nexport type ResumableUploadState =\n \"idle\" | \"creating\" | \"uploading\" | \"paused\" | \"done\" | \"error\" | \"aborted\";\n\n/** Byte-level progress for a resumable upload. */\nexport interface ResumableUploadProgress {\n /** Bytes the server holds, including anything a resume skipped. */\n loaded: number;\n /** Total size of the file. */\n total: number;\n /** `loaded / total`, between 0 and 1. */\n fraction: number;\n /** Bytes already on the server when this run started. `0` on a fresh upload. */\n resumedFrom: number;\n}\n\n/** What has to survive a page reload for a resume to be possible. */\nexport interface ResumableUploadRecord {\n /** Upload URL the creation POST returned, absolute. */\n url: string;\n /** Last offset the server confirmed. */\n offset: number;\n /** File size, so a different file under the same key is not resumed into. */\n size: number;\n /** Idempotency key of the creation request, reused if creation is retried. */\n idempotencyKey: string;\n /** Epoch ms of the last write, so an app can sweep stale records. */\n updatedAt: number;\n}\n\n/**\n * Persistence for resume state. Sync or async — both are awaited.\n *\n * Implement it over anything: the default is `localStorage`, and\n * `createOfflineStore` from `@/offline` slots in when you already have a Dexie\n * database open.\n */\nexport interface ResumableUploadStorage {\n /** Read the record for `key`, or `null`. */\n get(key: string): Promise<ResumableUploadRecord | null> | ResumableUploadRecord | null;\n /** Write the record for `key`. */\n set(key: string, record: ResumableUploadRecord): Promise<void> | void;\n /** Forget the record for `key`. */\n delete(key: string): Promise<void> | void;\n}\n\n/** Options for {@link createResumableUpload}. */\nexport interface ResumableUploadOptions {\n /** tus creation endpoint, e.g. `\"/api/uploads\"`. */\n endpoint: string;\n /** The bytes to upload. A `File` also supplies the default resume key. */\n file: Blob | File;\n /** Bytes per `PATCH`. Default {@link DEFAULT_CHUNK_SIZE}. */\n chunkSize?: number;\n /** Sent as `Upload-Metadata` (base64-encoded values), e.g. `{ filename }`. */\n metadata?: Record<string, string>;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n /** Returns the current bearer token, read before each request. */\n getToken?: () => string | null | undefined;\n /**\n * Origins besides `endpoint`'s that may receive the `getToken` credential.\n *\n * Since 0.66.0 the bearer token is scoped to the origin of `endpoint`, and\n * the creation response's `Location` no longer decides where it goes. A\n * server that hands the upload to object storage on another host still\n * works — the `PATCH` requests simply carry no `Authorization`, which is\n * what a presigned storage URL expects anyway.\n *\n * List the storage origin here when it does need the API's token.\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Default `false`. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token on the creation `POST`, every chunk `PATCH` and the\n * discarding `DELETE` — never on the offset `HEAD`. Off by default; `true`\n * reads the `csrf_token` cookie into `X-CSRF-Token`, as `createApiClient`\n * does.\n *\n * Scoped like the bearer token, to `endpoint`'s origin plus\n * {@link trustedOrigins}: a `Location` pointing at object storage on\n * another host gets neither.\n */\n csrf?: boolean | CsrfOptions;\n /**\n * Resume key. Defaults to a fingerprint of endpoint + file name/size/mtime, so\n * picking the same file after a reload resumes instead of restarting.\n */\n key?: string;\n /**\n * Where to persist resume state. Defaults to `localStorage`. Pass `null` to\n * disable persistence — resume then only survives a network blip, not a reload.\n */\n storage?: ResumableUploadStorage | null;\n /** Backoff for a failed chunk. Forwarded to `retry`. Default 5 attempts. */\n retry?: RetryOptions;\n /** Called on every upload-progress tick and after every confirmed chunk. */\n onProgress?: (progress: ResumableUploadProgress) => void;\n /** Called whenever {@link ResumableUpload.state} changes. */\n onStateChange?: (state: ResumableUploadState) => void;\n}\n\n/** What a finished upload resolves with. */\nexport interface ResumableUploadResult {\n /** The tus upload URL — hand this to your API to link the stored file. */\n url: string;\n /** Total bytes uploaded. */\n size: number;\n}\n\n/** A resumable upload in progress. Build one with {@link createResumableUpload}. */\nexport interface ResumableUpload {\n /**\n * Create (or re-attach to) the upload and push chunks until it is complete.\n *\n * Resolves `null` when the run stopped because of `pause()` or `abort()` —\n * neither is a failure. Rejects with a `TempestApiError` when the server\n * refused and the retries ran out.\n */\n start(): Promise<ResumableUploadResult | null>;\n /** Stop after the in-flight chunk is dropped, keeping the resume point. */\n pause(): void;\n /** Continue from the server's offset. Same resolution contract as `start`. */\n resume(): Promise<ResumableUploadResult | null>;\n /**\n * Stop for good.\n *\n * @param options - `discard: true` also sends `DELETE` (tus termination) and\n * forgets the persisted record, so the next `start()` uploads from zero.\n */\n abort(options?: { discard?: boolean }): Promise<void>;\n /** Current state. */\n readonly state: ResumableUploadState;\n /** Bytes the server has confirmed. */\n readonly offset: number;\n /** The upload URL, once creation succeeded. */\n readonly url: string | null;\n /** The resume key in use. */\n readonly key: string;\n}\n\ninterface RawResponse {\n status: number;\n text: string;\n header(name: string): string | null;\n}\n\n/**\n * Encode a string as standard base64 (padded), UTF-8 first.\n *\n * `Upload-Metadata` carries base64 values precisely so a filename with accents\n * survives an HTTP header, so the UTF-8 step is not optional: `btoa` alone throws\n * on any code point above U+00FF. Only that step is specific here — the\n * bytes-to-text half is {@link bytesToBase64}.\n *\n * @param value - Text to encode.\n * @returns Padded base64.\n */\nfunction base64Utf8(value: string): string {\n return bytesToBase64(new TextEncoder().encode(value));\n}\n\n/**\n * Build the `Upload-Metadata` header value: comma-separated `key base64(value)`.\n *\n * @param metadata - Plain string map.\n * @returns The header value, or `null` when there is nothing to send.\n */\nfunction encodeMetadata(metadata: Record<string, string> | undefined): string | null {\n if (!metadata) return null;\n const parts = Object.entries(metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`);\n return parts.length > 0 ? parts.join(\",\") : null;\n}\n\n/**\n * A stable-enough identity for a file, used as the default resume key.\n *\n * Name + size + last-modified is what the tus reference clients fingerprint on:\n * it is cheap (hashing the bytes of a 400 MB recording is not) and it changes\n * whenever the file does, which is the property that matters — resuming into the\n * wrong file would corrupt it silently.\n *\n * @param endpoint - Creation endpoint, so the same file to two servers is two uploads.\n * @param file - The blob or file being uploaded.\n * @returns A key safe to use in `localStorage`.\n */\nexport function uploadFingerprint(endpoint: string, file: Blob | File): string {\n const named = file as File;\n const name = typeof named.name === \"string\" ? named.name : \"blob\";\n const modified = typeof named.lastModified === \"number\" ? named.lastModified : 0;\n return `${endpoint}|${name}|${file.size}|${file.type}|${modified}`;\n}\n\n/**\n * `localStorage`-backed resume storage — the default.\n *\n * `localStorage` and not IndexedDB on purpose. The record is four fields and a\n * URL; the requirement is only that it survives a reload, and pulling Dexie in for\n * that would put an IndexedDB dependency in the bundle of every app that uploads a\n * file. Apps that already have `createOfflineStore` open can pass their own\n * {@link ResumableUploadStorage} instead.\n *\n * @param prefix - Key prefix. Default `\"tempest-upload:\"`.\n * @returns A storage that no-ops when `localStorage` is unavailable.\n */\nexport function createLocalUploadStorage(prefix = \"tempest-upload:\"): ResumableUploadStorage {\n function backend(): Storage | null {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n }\n\n return {\n get(key) {\n const raw = backend()?.getItem(prefix + key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as ResumableUploadRecord;\n } catch {\n return null;\n }\n },\n set(key, record) {\n backend()?.setItem(prefix + key, JSON.stringify(record));\n },\n delete(key) {\n backend()?.removeItem(prefix + key);\n },\n };\n}\n\n/**\n * Send one request over `XMLHttpRequest`.\n *\n * `XMLHttpRequest` rather than `fetch` for the same reason `uploadWithProgress`\n * uses it — `fetch` still cannot report upload progress in any browser — plus one\n * more: tus answers every write with the new `Upload-Offset` in a **response\n * header**, and `uploadWithProgress` only hands back a parsed body, so it could\n * not be reused here.\n *\n * @param init - Method, URL, headers, optional body and progress callback.\n * @returns Status, raw text and a header reader.\n */\nfunction sendRequest(init: {\n method: \"POST\" | \"HEAD\" | \"PATCH\" | \"DELETE\";\n url: string;\n headers: Record<string, string>;\n body?: Blob;\n withCredentials: boolean;\n onProgress?: (loaded: number) => void;\n register: (xhr: XMLHttpRequest) => void;\n}): Promise<RawResponse> {\n return new Promise<RawResponse>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(init.method, init.url);\n xhr.withCredentials = init.withCredentials;\n for (const [name, value] of Object.entries(init.headers)) {\n xhr.setRequestHeader(name, value);\n }\n if (init.onProgress) {\n const report = init.onProgress;\n xhr.upload.onprogress = (event: ProgressEvent) => report(event.loaded);\n }\n xhr.onload = () =>\n resolve({\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n });\n xhr.onerror = () =>\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload resumível.\",\n }),\n );\n xhr.onabort = () => reject(new DOMException(\"Aborted\", \"AbortError\"));\n init.register(xhr);\n xhr.send(init.body);\n });\n}\n\nfunction parseOffset(response: RawResponse): number | null {\n const raw = response.header(\"Upload-Offset\");\n if (raw === null) return null;\n const value = Number(raw);\n return Number.isFinite(value) && value >= 0 ? value : null;\n}\n\n/**\n * Read an error body without assuming it is JSON.\n *\n * A tus proxy that rejects a chunk often answers with plain text or an HTML error\n * page, and `JSON.parse` throwing there would replace a useful status with a parse\n * error.\n *\n * @param text - Raw response text.\n * @returns The parsed object, the raw text, or `null` when the body was empty.\n */\nfunction parseErrorBody(text: string): unknown {\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/**\n * Turn a refused tus response into a `TempestApiError`.\n *\n * The fallback `detail` is used unless the server sent a real error envelope,\n * because `buildApiError`'s own fallback (`\"Erro 409\"`) says nothing about which\n * step of the protocol broke — and that is the whole diagnostic value here.\n *\n * @param response - The raw response that was not acceptable.\n * @param detail - Message to use when the body carries none.\n * @returns The error to throw.\n */\n/**\n * The two statuses a chunk retry fixes that the shared policy cannot know about.\n *\n * `409` and `412` are the offset-divergence answers, and they are the entire\n * reason `resync` exists: the next attempt re-reads the server's offset with\n * `HEAD` and writes from there. They are 4xx refusals a replay genuinely fixes,\n * which is the one thing {@link isRetriableStatus} has no way to tell — from\n * outside this protocol they look like any other deliberate rejection.\n */\nconst RESYNCABLE_STATUSES: ReadonlySet<number> = new Set([409, 412]);\n\n/**\n * Whether a chunk failure is worth another attempt.\n *\n * The default used to be `true` for everything, which cost five round trips\n * before surfacing an answer the first one already gave. Two groups matter here\n * and both are specific to the resume protocol:\n *\n * - **`409`/`412` retry**, even though the shared policy rejects 4xx: they mean\n * \"your offset is wrong\", and `resync` is how the next attempt fixes it.\n * - **`404`/`410` do not**, even though a lost resource can look transient.\n * `probe()` turns them into \"O upload expirou no servidor. Comece de novo.\" and\n * recreating the upload only happens in `ensureUpload`, at attach time — never\n * inside the chunk loop. So a retry here re-runs `HEAD` against a resource that\n * is gone, five times, and then reports the same thing with the backoff added\n * on top.\n *\n * Anything with no API shape still retries: a transport failure has no status to\n * judge, and losing a large upload to one dropped connection is the outcome this\n * whole module exists to avoid.\n *\n * @param error - Whatever the attempt threw.\n * @returns Whether the chunk loop should try again.\n */\nfunction isRetriableChunkFailure(error: unknown): boolean {\n if (!isApiError(error)) return true;\n if (RESYNCABLE_STATUSES.has(error.status)) return true;\n return isRetriableStatus(error.status);\n}\n\nfunction failed(response: RawResponse, detail: string): TempestApiError {\n const body = parseErrorBody(response.text);\n const envelope = buildApiError(response.status, body, { get: response.header });\n const hasDetail =\n typeof body === \"object\" && body !== null && (\"detail\" in body || \"message\" in body);\n return new TempestApiError({ ...envelope, detail: hasDetail ? envelope.detail : detail });\n}\n\n/**\n * Resolve a `Location` header against the page, so a relative upload URL works.\n *\n * tus servers are free to answer creation with either an absolute URL or a\n * path, and the spec does not prefer one — a client that only handles absolute\n * URLs breaks against half the implementations.\n *\n * @param value - The raw `Location` header.\n * @returns An absolute URL, or the input when there is no base to resolve against.\n */\nfunction resolveUploadUrl(value: string): string {\n const base = typeof window === \"undefined\" ? undefined : window.location.href;\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Chunked, resumable upload speaking the **tus 1.0.0** protocol (core plus the\n * *creation* and *termination* extensions).\n *\n * ## Why tus and not a bespoke scheme\n *\n * A resumable client whose wire format is undocumented cannot be integrated, and\n * inventing one means the backend is ours forever. tus is a published spec with\n * off-the-shelf servers (`tusd`, `tuspy`, `tus-node-server`), so a caller can point\n * this at something they did not write.\n *\n * ## What the backend must implement\n *\n * Every request carries `Tus-Resumable: 1.0.0`.\n *\n * | Step | Request | Expected response |\n * | --- | --- | --- |\n * | Create | `POST {endpoint}` + `Upload-Length`, `Upload-Metadata`, `Idempotency-Key` | `201` + `Location` (the upload URL, absolute or endpoint-relative) |\n * | Probe | `HEAD {uploadUrl}` | `200`/`204` + `Upload-Offset` |\n * | Write | `PATCH {uploadUrl}` + `Upload-Offset`, `Content-Type: application/offset+octet-stream`, chunk body | `204` + the new `Upload-Offset`; `409` when the offset does not match |\n * | Discard | `DELETE {uploadUrl}` | `204` |\n *\n * ## The failure that actually happens\n *\n * A chunk that the server stored but whose response never arrived. The client\n * cannot tell that from a chunk that was lost, and re-sending it blindly would\n * duplicate bytes. Two things prevent that:\n *\n * - **Writes are addressed, not appended.** Every `PATCH` states the offset it\n * writes at, so a retry after a lost response is asked to write bytes the server\n * already has and answers `409`. On any retry the client re-reads the truth with\n * `HEAD` first and continues from there.\n * - **Creation carries an `Idempotency-Key`** (from `generateIdempotencyKey`),\n * persisted before the first attempt and reused on retry. tus has no idempotent\n * creation of its own, so without this a lost `201` leaves an orphan upload on\n * the server. A backend that honours the header returns the same `Location`; one\n * that ignores it still works, it just keeps the orphan.\n *\n * @param options - Endpoint, file, and the knobs above.\n * @returns A handle with `start`/`pause`/`resume`/`abort` and live `state`/`offset`.\n *\n * @example\n * const upload = createResumableUpload({\n * endpoint: \"/api/uploads\",\n * file: recording,\n * metadata: { filename: \"nota.webm\", ticket: ticketId },\n * getToken: () => auth.getToken(),\n * onProgress: ({ fraction }) => setPercent(Math.round(fraction * 100)),\n * });\n *\n * const done = await upload.start();\n * if (done) await api.post(\"/api/tickets/1/audio\", { body: { url: done.url } });\n */\nexport function createResumableUpload(options: ResumableUploadOptions): ResumableUpload {\n const {\n endpoint,\n file,\n chunkSize = DEFAULT_CHUNK_SIZE,\n metadata,\n headers = {},\n getToken,\n trustedOrigins,\n withCredentials = false,\n csrf,\n key = uploadFingerprint(endpoint, file),\n storage = createLocalUploadStorage(),\n retry: retryOptions,\n onProgress,\n onStateChange,\n } = options;\n\n let state: ResumableUploadState = \"idle\";\n let offset = 0;\n let url: string | null = null;\n let idempotencyKey: string | null = null;\n let stopping: \"pause\" | \"abort\" | null = null;\n let inFlight: XMLHttpRequest | null = null;\n let resumedFrom = 0;\n\n function setState(next: ResumableUploadState): void {\n if (state === next) return;\n state = next;\n onStateChange?.(next);\n }\n\n function report(loaded: number): void {\n onProgress?.({\n loaded,\n total: file.size,\n fraction: file.size === 0 ? 1 : loaded / file.size,\n resumedFrom,\n });\n }\n\n /**\n * Headers every tus request carries, with the credential scoped to the\n * endpoint's origin.\n *\n * The target is a parameter because after creation it is not ours: tus\n * answers `POST {endpoint}` with a `Location` the server chooses, and the\n * spec allows an absolute URL on another host — handing the upload to\n * object storage is the ordinary deployment, not an attack. Before 0.66.0\n * the bearer token followed that header wherever it pointed, along with the\n * file bytes.\n *\n * The CSRF token follows the same scope, and only on the methods that\n * write — which is why the method is a parameter too.\n *\n * @param target - The URL this particular request goes to.\n * @param method - The HTTP method of this request.\n * @returns The headers to send.\n */\n function baseHeaders(target: string, method: string): Record<string, string> {\n const result: Record<string, string> = { ...headers, \"Tus-Resumable\": TUS_VERSION };\n const token = getToken?.();\n if (token && !(\"Authorization\" in result)) {\n if (isTrustedCredentialTarget(target, endpoint, trustedOrigins)) {\n result.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(target, endpoint);\n }\n }\n Object.assign(\n result,\n csrfHeaders({\n method,\n url: target,\n reference: endpoint,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n return result;\n }\n\n function register(xhr: XMLHttpRequest): void {\n inFlight = xhr;\n }\n\n async function persist(): Promise<void> {\n if (!storage || !url || !idempotencyKey) return;\n await storage.set(key, {\n url,\n offset,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n /**\n * Ask the server how much it holds. The only source of truth after any failure.\n */\n async function probe(target: string): Promise<number> {\n const response = await sendRequest({\n method: \"HEAD\",\n url: target,\n headers: baseHeaders(target, \"HEAD\"),\n withCredentials,\n register,\n });\n if (response.status === 404 || response.status === 410) {\n throw new TempestApiError({\n status: response.status,\n detail: \"O upload expirou no servidor. Comece de novo.\",\n });\n }\n const confirmed = parseOffset(response);\n if (confirmed === null) throw failed(response, \"HEAD sem Upload-Offset.\");\n return confirmed;\n }\n\n /**\n * Re-attach to a persisted upload, or create a new one.\n *\n * The persisted record is only trusted when the file size still matches, and the\n * offset it holds is re-checked with `HEAD` — the client's copy can be ahead of\n * the server's whenever the last response was lost.\n */\n async function ensureUpload(): Promise<string> {\n const stored = storage ? await storage.get(key) : null;\n if (stored && stored.size === file.size) {\n idempotencyKey = stored.idempotencyKey;\n if (stored.url) {\n try {\n offset = await probe(stored.url);\n url = stored.url;\n return stored.url;\n } catch {\n offset = 0;\n }\n }\n }\n\n setState(\"creating\");\n idempotencyKey ??= generateIdempotencyKey();\n url = null;\n offset = 0;\n if (storage) {\n await storage.set(key, {\n url: \"\",\n offset: 0,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n const creationHeaders: Record<string, string> = {\n ...baseHeaders(endpoint, \"POST\"),\n \"Upload-Length\": String(file.size),\n \"Idempotency-Key\": idempotencyKey,\n };\n const encoded = encodeMetadata(metadata);\n if (encoded) creationHeaders[\"Upload-Metadata\"] = encoded;\n\n const response = await sendRequest({\n method: \"POST\",\n url: endpoint,\n headers: creationHeaders,\n withCredentials,\n register,\n });\n if (response.status !== 201) throw failed(response, \"Criação do upload recusada.\");\n const locationHeader = response.header(\"Location\");\n if (!locationHeader) throw failed(response, \"Criação do upload sem cabeçalho Location.\");\n\n url = resolveUploadUrl(locationHeader);\n await persist();\n return url;\n }\n\n /** Push one chunk, resyncing the offset first when a previous attempt failed. */\n async function writeChunk(target: string, resync: { needed: boolean }): Promise<void> {\n if (resync.needed) {\n offset = await probe(target);\n resync.needed = false;\n report(offset);\n await persist();\n if (offset >= file.size) return;\n }\n\n const end = Math.min(offset + chunkSize, file.size);\n const from = offset;\n const response = await sendRequest({\n method: \"PATCH\",\n url: target,\n headers: {\n ...baseHeaders(target, \"PATCH\"),\n \"Content-Type\": \"application/offset+octet-stream\",\n \"Upload-Offset\": String(from),\n },\n body: file.slice(from, end),\n withCredentials,\n onProgress: (loaded) => report(Math.min(from + loaded, file.size)),\n register,\n });\n\n if (response.status === 409 || response.status === 412) {\n resync.needed = true;\n throw failed(response, \"Offset divergente — o servidor já tinha esses bytes.\");\n }\n if (response.status !== 204 && response.status !== 200) {\n throw failed(response, \"Chunk recusado pelo servidor.\");\n }\n\n offset = parseOffset(response) ?? end;\n report(offset);\n await persist();\n }\n\n /**\n * Drive the whole upload: attach or create, then chunk until complete.\n *\n * The `shouldRetry` predicate does double duty — besides deciding, it arms\n * `resync` so the next attempt re-reads the server's offset with `HEAD` before\n * writing. That is deliberate: it is the one place that sees *every* chunk\n * failure, whatever the cause, and after any failure the client's idea of the\n * offset is exactly what cannot be trusted.\n *\n * `resync` is armed only when the attempt is actually going to happen. A\n * caller's own `shouldRetry` still wins the decision, and still arms the\n * resync when it says yes — the flag describes what the *next* attempt must\n * do, so setting it for an attempt that never comes describes nothing.\n * {@link isRetriableChunkFailure} is the default, and it is where `409`/`412`\n * earn a retry the shared policy would refuse and `404`/`410` lose one it\n * would have granted.\n *\n * @returns The result, or `null` when `pause`/`abort` stopped the run.\n */\n async function run(): Promise<ResumableUploadResult | null> {\n stopping = null;\n const target = await ensureUpload();\n resumedFrom = offset;\n setState(\"uploading\");\n report(offset);\n\n const resync = { needed: false };\n while (offset < file.size) {\n if (stopping) break;\n await retry(() => writeChunk(target, resync), {\n retries: 5,\n ...retryOptions,\n shouldRetry: (error, attempt) => {\n if (stopping) return false;\n if (error instanceof DOMException && error.name === \"AbortError\") return false;\n const again =\n retryOptions?.shouldRetry?.(error, attempt) ??\n isRetriableChunkFailure(error);\n if (again) resync.needed = true;\n return again;\n },\n });\n }\n\n if (stopping === \"pause\") {\n setState(\"paused\");\n return null;\n }\n if (stopping === \"abort\") {\n setState(\"aborted\");\n return null;\n }\n\n setState(\"done\");\n if (storage) await storage.delete(key);\n return { url: target, size: file.size };\n }\n\n async function guarded(): Promise<ResumableUploadResult | null> {\n try {\n return await run();\n } catch (error) {\n if (\n stopping !== null ||\n (error instanceof DOMException && error.name === \"AbortError\")\n ) {\n setState(stopping === \"abort\" ? \"aborted\" : \"paused\");\n return null;\n }\n setState(\"error\");\n throw error;\n } finally {\n inFlight = null;\n }\n }\n\n function stop(reason: \"pause\" | \"abort\"): void {\n stopping = reason;\n inFlight?.abort();\n inFlight = null;\n }\n\n return {\n start: guarded,\n resume: guarded,\n pause: () => stop(\"pause\"),\n abort: async ({ discard = false } = {}) => {\n stop(\"abort\");\n setState(\"aborted\");\n if (!discard) return;\n if (url) {\n await sendRequest({\n method: \"DELETE\",\n url,\n headers: baseHeaders(url, \"DELETE\"),\n withCredentials,\n register: () => undefined,\n }).catch(() => undefined);\n }\n if (storage) await storage.delete(key);\n },\n get state() {\n return state;\n },\n get offset() {\n return offset;\n },\n get url() {\n return url;\n },\n key,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resumable-upload.cjs","names":[],"sources":["../../src/http/resumable-upload.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a resumable upload is one long-lived\n * state machine: chunk the file, negotiate the offset the server already has, upload\n * with retry and backoff, honour pause, resume and abort, and report progress\n * throughout. Every stage reads the same cursor and the same abort signal, and\n * createResumableUpload is the closure that owns them.\n */\nimport { ownerFingerprint } from \"@/utils/owner-fingerprint\";\nimport { bytesToBase64 } from \"@/utils/base64\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, isApiError, isRetriableStatus, TempestApiError } from \"./errors\";\nimport { generateIdempotencyKey } from \"./idempotency\";\nimport { retry, type RetryOptions } from \"./retry\";\n\n/** The tus protocol version this client speaks. */\nexport const TUS_VERSION = \"1.0.0\";\n\n/** Default chunk size: 5 MiB, the size most tus servers are tuned for. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;\n\n/**\n * Thrown by `start()`/`resume()` when the run can no longer go on as the\n * account that started it.\n *\n * - `\"principal\"` — `getPrincipal` names someone else than when the run\n * attached: the next chunk would carry the new account's token to the\n * previous account's upload URL, and the record would be stamped for the\n * wrong owner.\n * - `\"cleared\"` — `createLocalUploadStorage().clear()` ran for this storage's\n * prefix (typically in `onSessionEnd`): writing the record back would undo\n * the clear.\n *\n * Not a pause: the run cannot be resumed as the new account. The state moves\n * to `\"error\"`, and the previous owner's record is left as it was.\n */\nexport class ResumableUploadSupersededError extends Error {\n /** Why the run stopped. */\n readonly reason: \"principal\" | \"cleared\";\n\n /**\n * @param reason - Why the run stopped.\n */\n constructor(reason: \"principal\" | \"cleared\") {\n super(\n reason === \"principal\"\n ? \"O usuário mudou durante o upload resumível.\"\n : \"Os registros de upload resumível foram limpos durante o upload.\",\n );\n this.name = \"ResumableUploadSupersededError\";\n this.reason = reason;\n }\n}\n\n/**\n * How many times `clear()` ran per local-storage prefix, so a run can tell its\n * record was cleared under it — even through another\n * `createLocalUploadStorage()` object with the same prefix, which is what an\n * `onSessionEnd` builds.\n */\nconst clearGenerations = new Map<string, number>();\n\n/**\n * The clear generation reader of each storage built by\n * `createLocalUploadStorage`. A custom storage has none, and its runs only\n * stop on a principal change.\n */\nconst localGenerations = new WeakMap<ResumableUploadStorage, () => number>();\n\n/**\n * Whether a stored value has the shape of a {@link ResumableUploadRecord}.\n *\n * `clear()` matches keys by prefix, and a prefix with no delimiter (`\"app\"`)\n * also matches an app's own keys (`\"appSettings\"`). Changing how keys are\n * built would orphan every record already stored under a custom prefix, so\n * `clear()` removes only values this storage could have written.\n *\n * @param raw - The stored string.\n * @returns Whether it parses as a record.\n */\nfunction isUploadRecord(raw: string | null): boolean {\n if (!raw) return false;\n try {\n const value = JSON.parse(raw) as Partial<ResumableUploadRecord> | null;\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof value.url === \"string\" &&\n typeof value.offset === \"number\" &&\n typeof value.size === \"number\" &&\n typeof value.idempotencyKey === \"string\"\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Where a resumable upload is.\n *\n * `\"paused\"` and `\"aborted\"` are both \"not running\", but only `\"paused\"` keeps the\n * persisted offset — `abort({ discard: true })` throws it away.\n */\nexport type ResumableUploadState =\n \"idle\" | \"creating\" | \"uploading\" | \"paused\" | \"done\" | \"error\" | \"aborted\";\n\n/** Byte-level progress for a resumable upload. */\nexport interface ResumableUploadProgress {\n /** Bytes the server holds, including anything a resume skipped. */\n loaded: number;\n /** Total size of the file. */\n total: number;\n /** `loaded / total`, between 0 and 1. */\n fraction: number;\n /** Bytes already on the server when this run started. `0` on a fresh upload. */\n resumedFrom: number;\n}\n\n/** What has to survive a page reload for a resume to be possible. */\nexport interface ResumableUploadRecord {\n /** Upload URL the creation POST returned, absolute. */\n url: string;\n /** Last offset the server confirmed. */\n offset: number;\n /** File size, so a different file under the same key is not resumed into. */\n size: number;\n /** Idempotency key of the creation request, reused if creation is retried. */\n idempotencyKey: string;\n /** Epoch ms of the last write, so an app can sweep stale records. */\n updatedAt: number;\n /**\n * One-way fingerprint of the principal that created the upload (`\"p:<hex>\"`),\n * `null` for an anonymous one. Absent when the upload ran without\n * {@link ResumableUploadOptions.getPrincipal}, and on records written before\n * 0.78.0.\n */\n owner?: string | null;\n}\n\n/**\n * Persistence for resume state. Sync or async — both are awaited.\n *\n * Implement it over anything: the default is `localStorage`, and\n * `createOfflineStore` from `@/offline` slots in when you already have a Dexie\n * database open.\n */\nexport interface ResumableUploadStorage {\n /** Read the record for `key`, or `null`. */\n get(key: string): Promise<ResumableUploadRecord | null> | ResumableUploadRecord | null;\n /** Write the record for `key`. */\n set(key: string, record: ResumableUploadRecord): Promise<void> | void;\n /** Forget the record for `key`. */\n delete(key: string): Promise<void> | void;\n}\n\n/**\n * The default `localStorage` storage, which can also forget every record it\n * holds.\n */\nexport interface LocalUploadStorage extends ResumableUploadStorage {\n /**\n * Forget every record under this storage's prefix — the call for\n * `onSessionEnd`, so the next account on the browser does not find the\n * previous one's upload URLs. Synchronous; a no-op without `localStorage`.\n *\n * Only values shaped like a record are removed, so a prefix without a\n * delimiter does not take the app's own keys with it. A run still going on\n * this prefix stops before its next chunk with\n * {@link ResumableUploadSupersededError} (`\"cleared\"`) instead of writing\n * its record back.\n */\n clear(): void;\n}\n\n/** Options for {@link createResumableUpload}. */\nexport interface ResumableUploadOptions {\n /** tus creation endpoint, e.g. `\"/api/uploads\"`. */\n endpoint: string;\n /** The bytes to upload. A `File` also supplies the default resume key. */\n file: Blob | File;\n /** Bytes per `PATCH`. Default {@link DEFAULT_CHUNK_SIZE}. */\n chunkSize?: number;\n /** Sent as `Upload-Metadata` (base64-encoded values), e.g. `{ filename }`. */\n metadata?: Record<string, string>;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n /** Returns the current bearer token, read before each request. */\n getToken?: () => string | null | undefined;\n /**\n * Who is signed in — a user id, or `null`/`undefined` for nobody — read when\n * `start()`/`resume()` attaches to the upload. The same getter the error\n * reporter and `provideBackgroundSyncCredentials` take.\n *\n * The resume record is stamped with a one-way fingerprint of it, and a\n * record stamped by anyone else — another user, nobody, or no stamp at all\n * — is deleted instead of resumed: its upload URL is not probed, and its\n * `Idempotency-Key` is never replayed. Without it, the record is bound to\n * the file alone (endpoint, name, size, type, mtime), so after user A\n * signed out leaving an upload half-done, user B picking an identical file\n * sent `HEAD`/`PATCH` to A's upload URL with B's token, and on refusal\n * replayed A's `Idempotency-Key` with B's token (#479). Pass it on every\n * app with more than one account per browser, and also clear the storage\n * at session end (`createLocalUploadStorage().clear()` in `onSessionEnd`).\n *\n * It is read again before every chunk and every record write: when it\n * names someone else mid-run, the run stops with\n * {@link ResumableUploadSupersededError} (`\"principal\"`) before the new\n * account's token reaches the previous one's upload URL. Without it, call\n * `abort()` on the uploads in progress from `onSessionEnd`.\n */\n getPrincipal?: () => string | number | null | undefined;\n /**\n * Origins besides `endpoint`'s that may receive the `getToken` credential.\n *\n * Since 0.66.0 the bearer token is scoped to the origin of `endpoint`, and\n * the creation response's `Location` no longer decides where it goes. A\n * server that hands the upload to object storage on another host still\n * works — the `PATCH` requests simply carry no `Authorization`, which is\n * what a presigned storage URL expects anyway.\n *\n * List the storage origin here when it does need the API's token.\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Default `false`. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token on the creation `POST`, every chunk `PATCH` and the\n * discarding `DELETE` — never on the offset `HEAD`. Off by default; `true`\n * reads the `csrf_token` cookie into `X-CSRF-Token`, as `createApiClient`\n * does.\n *\n * Scoped like the bearer token, to `endpoint`'s origin plus\n * {@link trustedOrigins}: a `Location` pointing at object storage on\n * another host gets neither.\n */\n csrf?: boolean | CsrfOptions;\n /**\n * Resume key. Defaults to a fingerprint of endpoint + file name/size/mtime, so\n * picking the same file after a reload resumes instead of restarting.\n */\n key?: string;\n /**\n * Where to persist resume state. Defaults to `localStorage`. Pass `null` to\n * disable persistence — resume then only survives a network blip, not a reload.\n */\n storage?: ResumableUploadStorage | null;\n /** Backoff for a failed chunk. Forwarded to `retry`. Default 5 attempts. */\n retry?: RetryOptions;\n /** Called on every upload-progress tick and after every confirmed chunk. */\n onProgress?: (progress: ResumableUploadProgress) => void;\n /** Called whenever {@link ResumableUpload.state} changes. */\n onStateChange?: (state: ResumableUploadState) => void;\n}\n\n/** What a finished upload resolves with. */\nexport interface ResumableUploadResult {\n /** The tus upload URL — hand this to your API to link the stored file. */\n url: string;\n /** Total bytes uploaded. */\n size: number;\n}\n\n/** A resumable upload in progress. Build one with {@link createResumableUpload}. */\nexport interface ResumableUpload {\n /**\n * Create (or re-attach to) the upload and push chunks until it is complete.\n *\n * Resolves `null` when the run stopped because of `pause()` or `abort()` —\n * neither is a failure. Rejects with a `TempestApiError` when the server\n * refused and the retries ran out.\n */\n start(): Promise<ResumableUploadResult | null>;\n /** Stop after the in-flight chunk is dropped, keeping the resume point. */\n pause(): void;\n /** Continue from the server's offset. Same resolution contract as `start`. */\n resume(): Promise<ResumableUploadResult | null>;\n /**\n * Stop for good.\n *\n * @param options - `discard: true` also sends `DELETE` (tus termination) and\n * forgets the persisted record, so the next `start()` uploads from zero.\n */\n abort(options?: { discard?: boolean }): Promise<void>;\n /** Current state. */\n readonly state: ResumableUploadState;\n /** Bytes the server has confirmed. */\n readonly offset: number;\n /** The upload URL, once creation succeeded. */\n readonly url: string | null;\n /** The resume key in use. */\n readonly key: string;\n}\n\ninterface RawResponse {\n status: number;\n text: string;\n header(name: string): string | null;\n}\n\n/**\n * Encode a string as standard base64 (padded), UTF-8 first.\n *\n * `Upload-Metadata` carries base64 values precisely so a filename with accents\n * survives an HTTP header, so the UTF-8 step is not optional: `btoa` alone throws\n * on any code point above U+00FF. Only that step is specific here — the\n * bytes-to-text half is {@link bytesToBase64}.\n *\n * @param value - Text to encode.\n * @returns Padded base64.\n */\nfunction base64Utf8(value: string): string {\n return bytesToBase64(new TextEncoder().encode(value));\n}\n\n/**\n * Build the `Upload-Metadata` header value: comma-separated `key base64(value)`.\n *\n * @param metadata - Plain string map.\n * @returns The header value, or `null` when there is nothing to send.\n */\nfunction encodeMetadata(metadata: Record<string, string> | undefined): string | null {\n if (!metadata) return null;\n const parts = Object.entries(metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`);\n return parts.length > 0 ? parts.join(\",\") : null;\n}\n\n/**\n * A stable-enough identity for a file, used as the default resume key.\n *\n * Name + size + last-modified is what the tus reference clients fingerprint on:\n * it is cheap (hashing the bytes of a 400 MB recording is not) and it changes\n * whenever the file does, which is the property that matters — resuming into the\n * wrong file would corrupt it silently.\n *\n * @param endpoint - Creation endpoint, so the same file to two servers is two uploads.\n * @param file - The blob or file being uploaded.\n * @returns A key safe to use in `localStorage`.\n */\nexport function uploadFingerprint(endpoint: string, file: Blob | File): string {\n const named = file as File;\n const name = typeof named.name === \"string\" ? named.name : \"blob\";\n const modified = typeof named.lastModified === \"number\" ? named.lastModified : 0;\n return `${endpoint}|${name}|${file.size}|${file.type}|${modified}`;\n}\n\n/**\n * `localStorage`-backed resume storage — the default.\n *\n * `localStorage` and not IndexedDB on purpose. The record is four fields and a\n * URL; the requirement is only that it survives a reload, and pulling Dexie in for\n * that would put an IndexedDB dependency in the bundle of every app that uploads a\n * file. Apps that already have `createOfflineStore` open can pass their own\n * {@link ResumableUploadStorage} instead.\n *\n * `clear()` forgets every record under the prefix. Call it from\n * `onSessionEnd`: a record outlives the session that wrote it, and only\n * completing the upload or `abort({ discard: true })` removes it otherwise.\n *\n * @param prefix - Key prefix. Default `\"tempest-upload:\"`.\n * @returns A storage that no-ops when `localStorage` is unavailable.\n */\nexport function createLocalUploadStorage(prefix = \"tempest-upload:\"): LocalUploadStorage {\n function backend(): Storage | null {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n }\n\n const storage: LocalUploadStorage = {\n get(key) {\n const raw = backend()?.getItem(prefix + key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as ResumableUploadRecord;\n } catch {\n return null;\n }\n },\n set(key, record) {\n backend()?.setItem(prefix + key, JSON.stringify(record));\n },\n delete(key) {\n backend()?.removeItem(prefix + key);\n },\n clear() {\n clearGenerations.set(prefix, (clearGenerations.get(prefix) ?? 0) + 1);\n const store = backend();\n if (!store) return;\n const owned: string[] = [];\n for (let index = 0; index < store.length; index += 1) {\n const name = store.key(index);\n if (name?.startsWith(prefix) && isUploadRecord(store.getItem(name))) {\n owned.push(name);\n }\n }\n for (const name of owned) store.removeItem(name);\n },\n };\n localGenerations.set(storage, () => clearGenerations.get(prefix) ?? 0);\n return storage;\n}\n\n/**\n * Send one request over `XMLHttpRequest`.\n *\n * `XMLHttpRequest` rather than `fetch` for the same reason `uploadWithProgress`\n * uses it — `fetch` still cannot report upload progress in any browser — plus one\n * more: tus answers every write with the new `Upload-Offset` in a **response\n * header**, and `uploadWithProgress` only hands back a parsed body, so it could\n * not be reused here.\n *\n * @param init - Method, URL, headers, optional body and progress callback.\n * @returns Status, raw text and a header reader.\n */\nfunction sendRequest(init: {\n method: \"POST\" | \"HEAD\" | \"PATCH\" | \"DELETE\";\n url: string;\n headers: Record<string, string>;\n body?: Blob;\n withCredentials: boolean;\n onProgress?: (loaded: number) => void;\n register: (xhr: XMLHttpRequest) => void;\n}): Promise<RawResponse> {\n return new Promise<RawResponse>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(init.method, init.url);\n xhr.withCredentials = init.withCredentials;\n for (const [name, value] of Object.entries(init.headers)) {\n xhr.setRequestHeader(name, value);\n }\n if (init.onProgress) {\n const report = init.onProgress;\n xhr.upload.onprogress = (event: ProgressEvent) => report(event.loaded);\n }\n xhr.onload = () =>\n resolve({\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n });\n xhr.onerror = () =>\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload resumível.\",\n }),\n );\n xhr.onabort = () => reject(new DOMException(\"Aborted\", \"AbortError\"));\n init.register(xhr);\n xhr.send(init.body);\n });\n}\n\nfunction parseOffset(response: RawResponse): number | null {\n const raw = response.header(\"Upload-Offset\");\n if (raw === null) return null;\n const value = Number(raw);\n return Number.isFinite(value) && value >= 0 ? value : null;\n}\n\n/**\n * Read an error body without assuming it is JSON.\n *\n * A tus proxy that rejects a chunk often answers with plain text or an HTML error\n * page, and `JSON.parse` throwing there would replace a useful status with a parse\n * error.\n *\n * @param text - Raw response text.\n * @returns The parsed object, the raw text, or `null` when the body was empty.\n */\nfunction parseErrorBody(text: string): unknown {\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/**\n * Turn a refused tus response into a `TempestApiError`.\n *\n * The fallback `detail` is used unless the server sent a real error envelope,\n * because `buildApiError`'s own fallback (`\"Erro 409\"`) says nothing about which\n * step of the protocol broke — and that is the whole diagnostic value here.\n *\n * @param response - The raw response that was not acceptable.\n * @param detail - Message to use when the body carries none.\n * @returns The error to throw.\n */\n/**\n * The two statuses a chunk retry fixes that the shared policy cannot know about.\n *\n * `409` and `412` are the offset-divergence answers, and they are the entire\n * reason `resync` exists: the next attempt re-reads the server's offset with\n * `HEAD` and writes from there. They are 4xx refusals a replay genuinely fixes,\n * which is the one thing {@link isRetriableStatus} has no way to tell — from\n * outside this protocol they look like any other deliberate rejection.\n */\nconst RESYNCABLE_STATUSES: ReadonlySet<number> = new Set([409, 412]);\n\n/**\n * Whether a chunk failure is worth another attempt.\n *\n * The default used to be `true` for everything, which cost five round trips\n * before surfacing an answer the first one already gave. Two groups matter here\n * and both are specific to the resume protocol:\n *\n * - **`409`/`412` retry**, even though the shared policy rejects 4xx: they mean\n * \"your offset is wrong\", and `resync` is how the next attempt fixes it.\n * - **`404`/`410` do not**, even though a lost resource can look transient.\n * `probe()` turns them into \"O upload expirou no servidor. Comece de novo.\" and\n * recreating the upload only happens in `ensureUpload`, at attach time — never\n * inside the chunk loop. So a retry here re-runs `HEAD` against a resource that\n * is gone, five times, and then reports the same thing with the backoff added\n * on top.\n *\n * Anything with no API shape still retries: a transport failure has no status to\n * judge, and losing a large upload to one dropped connection is the outcome this\n * whole module exists to avoid.\n *\n * @param error - Whatever the attempt threw.\n * @returns Whether the chunk loop should try again.\n */\nfunction isRetriableChunkFailure(error: unknown): boolean {\n if (!isApiError(error)) return true;\n if (RESYNCABLE_STATUSES.has(error.status)) return true;\n return isRetriableStatus(error.status);\n}\n\nfunction failed(response: RawResponse, detail: string): TempestApiError {\n const body = parseErrorBody(response.text);\n const envelope = buildApiError(response.status, body, { get: response.header });\n const hasDetail =\n typeof body === \"object\" && body !== null && (\"detail\" in body || \"message\" in body);\n return new TempestApiError({ ...envelope, detail: hasDetail ? envelope.detail : detail });\n}\n\n/**\n * Resolve a `Location` header against the page, so a relative upload URL works.\n *\n * tus servers are free to answer creation with either an absolute URL or a\n * path, and the spec does not prefer one — a client that only handles absolute\n * URLs breaks against half the implementations.\n *\n * @param value - The raw `Location` header.\n * @returns An absolute URL, or the input when there is no base to resolve against.\n */\nfunction resolveUploadUrl(value: string): string {\n const base = typeof window === \"undefined\" ? undefined : window.location.href;\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Chunked, resumable upload speaking the **tus 1.0.0** protocol (core plus the\n * *creation* and *termination* extensions).\n *\n * ## Why tus and not a bespoke scheme\n *\n * A resumable client whose wire format is undocumented cannot be integrated, and\n * inventing one means the backend is ours forever. tus is a published spec with\n * off-the-shelf servers (`tusd`, `tuspy`, `tus-node-server`), so a caller can point\n * this at something they did not write.\n *\n * ## What the backend must implement\n *\n * Every request carries `Tus-Resumable: 1.0.0`.\n *\n * | Step | Request | Expected response |\n * | --- | --- | --- |\n * | Create | `POST {endpoint}` + `Upload-Length`, `Upload-Metadata`, `Idempotency-Key` | `201` + `Location` (the upload URL, absolute or endpoint-relative) |\n * | Probe | `HEAD {uploadUrl}` | `200`/`204` + `Upload-Offset` |\n * | Write | `PATCH {uploadUrl}` + `Upload-Offset`, `Content-Type: application/offset+octet-stream`, chunk body | `204` + the new `Upload-Offset`; `409` when the offset does not match |\n * | Discard | `DELETE {uploadUrl}` | `204` |\n *\n * ## The failure that actually happens\n *\n * A chunk that the server stored but whose response never arrived. The client\n * cannot tell that from a chunk that was lost, and re-sending it blindly would\n * duplicate bytes. Two things prevent that:\n *\n * - **Writes are addressed, not appended.** Every `PATCH` states the offset it\n * writes at, so a retry after a lost response is asked to write bytes the server\n * already has and answers `409`. On any retry the client re-reads the truth with\n * `HEAD` first and continues from there.\n * - **Creation carries an `Idempotency-Key`** (from `generateIdempotencyKey`),\n * persisted before the first attempt and reused on retry. tus has no idempotent\n * creation of its own, so without this a lost `201` leaves an orphan upload on\n * the server. A backend that honours the header returns the same `Location`; one\n * that ignores it still works, it just keeps the orphan.\n *\n * @param options - Endpoint, file, and the knobs above.\n * @returns A handle with `start`/`pause`/`resume`/`abort` and live `state`/`offset`.\n *\n * @example\n * const upload = createResumableUpload({\n * endpoint: \"/api/uploads\",\n * file: recording,\n * metadata: { filename: \"nota.webm\", ticket: ticketId },\n * getToken: () => auth.getToken(),\n * onProgress: ({ fraction }) => setPercent(Math.round(fraction * 100)),\n * });\n *\n * const done = await upload.start();\n * if (done) await api.post(\"/api/tickets/1/audio\", { body: { url: done.url } });\n */\nexport function createResumableUpload(options: ResumableUploadOptions): ResumableUpload {\n const {\n endpoint,\n file,\n chunkSize = DEFAULT_CHUNK_SIZE,\n metadata,\n headers = {},\n getToken,\n getPrincipal,\n trustedOrigins,\n withCredentials = false,\n csrf,\n key = uploadFingerprint(endpoint, file),\n storage = createLocalUploadStorage(),\n retry: retryOptions,\n onProgress,\n onStateChange,\n } = options;\n\n let state: ResumableUploadState = \"idle\";\n let offset = 0;\n let url: string | null = null;\n let idempotencyKey: string | null = null;\n let owner: string | null | undefined;\n let attached = false;\n const generationOf = storage ? localGenerations.get(storage) : undefined;\n let generation = generationOf?.() ?? 0;\n\n /**\n * Throw when the run can no longer go on as the account that attached it:\n * the principal changed, or the local storage was cleared under it.\n *\n * @throws {ResumableUploadSupersededError} In either case.\n */\n function assertCurrentOwner(): void {\n if (getPrincipal && ownerFingerprint(\"p\", getPrincipal()) !== owner) {\n throw new ResumableUploadSupersededError(\"principal\");\n }\n if (generationOf && generationOf() !== generation) {\n throw new ResumableUploadSupersededError(\"cleared\");\n }\n }\n let stopping: \"pause\" | \"abort\" | null = null;\n let inFlight: XMLHttpRequest | null = null;\n let resumedFrom = 0;\n\n function setState(next: ResumableUploadState): void {\n if (state === next) return;\n state = next;\n onStateChange?.(next);\n }\n\n function report(loaded: number): void {\n onProgress?.({\n loaded,\n total: file.size,\n fraction: file.size === 0 ? 1 : loaded / file.size,\n resumedFrom,\n });\n }\n\n /**\n * Headers every tus request carries, with the credential scoped to the\n * endpoint's origin.\n *\n * The target is a parameter because after creation it is not ours: tus\n * answers `POST {endpoint}` with a `Location` the server chooses, and the\n * spec allows an absolute URL on another host — handing the upload to\n * object storage is the ordinary deployment, not an attack. Before 0.66.0\n * the bearer token followed that header wherever it pointed, along with the\n * file bytes.\n *\n * The CSRF token follows the same scope, and only on the methods that\n * write — which is why the method is a parameter too.\n *\n * @param target - The URL this particular request goes to.\n * @param method - The HTTP method of this request.\n * @returns The headers to send.\n */\n function baseHeaders(target: string, method: string): Record<string, string> {\n const result: Record<string, string> = { ...headers, \"Tus-Resumable\": TUS_VERSION };\n const token = getToken?.();\n if (token && !(\"Authorization\" in result)) {\n if (isTrustedCredentialTarget(target, endpoint, trustedOrigins)) {\n result.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(target, endpoint);\n }\n }\n Object.assign(\n result,\n csrfHeaders({\n method,\n url: target,\n reference: endpoint,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n return result;\n }\n\n function register(xhr: XMLHttpRequest): void {\n inFlight = xhr;\n }\n\n async function persist(): Promise<void> {\n if (!storage || !url || !idempotencyKey) return;\n assertCurrentOwner();\n await storage.set(key, {\n url,\n offset,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n ...(owner === undefined ? {} : { owner }),\n });\n }\n\n /**\n * Ask the server how much it holds. The only source of truth after any failure.\n */\n async function probe(target: string): Promise<number> {\n const response = await sendRequest({\n method: \"HEAD\",\n url: target,\n headers: baseHeaders(target, \"HEAD\"),\n withCredentials,\n register,\n });\n if (response.status === 404 || response.status === 410) {\n throw new TempestApiError({\n status: response.status,\n detail: \"O upload expirou no servidor. Comece de novo.\",\n });\n }\n const confirmed = parseOffset(response);\n if (confirmed === null) throw failed(response, \"HEAD sem Upload-Offset.\");\n return confirmed;\n }\n\n /**\n * Re-attach to a persisted upload, or create a new one.\n *\n * The persisted record is only trusted when the file size still matches, and the\n * offset it holds is re-checked with `HEAD` — the client's copy can be ahead of\n * the server's whenever the last response was lost.\n *\n * With `getPrincipal`, it is also only trusted when it carries the current\n * principal's fingerprint; anyone else's is deleted, and the creation that\n * follows gets a fresh `Idempotency-Key`. The key this handle kept in memory\n * from an earlier run is reused only while the principal is the same, which\n * is what keeps creation idempotent across `resume()` without storage.\n */\n async function ensureUpload(): Promise<string> {\n const current = getPrincipal ? ownerFingerprint(\"p\", getPrincipal()) : undefined;\n if (attached && current !== owner) idempotencyKey = null;\n owner = current;\n attached = true;\n generation = generationOf?.() ?? 0;\n let stored = storage ? await storage.get(key) : null;\n if (stored && owner !== undefined && stored.owner !== owner) {\n await storage?.delete(key);\n stored = null;\n }\n if (stored && stored.size === file.size) {\n idempotencyKey = stored.idempotencyKey;\n if (stored.url) {\n try {\n offset = await probe(stored.url);\n url = stored.url;\n return stored.url;\n } catch {\n offset = 0;\n }\n }\n }\n\n setState(\"creating\");\n idempotencyKey ??= generateIdempotencyKey();\n url = null;\n offset = 0;\n if (storage) {\n await storage.set(key, {\n url: \"\",\n offset: 0,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n ...(owner === undefined ? {} : { owner }),\n });\n }\n\n const creationHeaders: Record<string, string> = {\n ...baseHeaders(endpoint, \"POST\"),\n \"Upload-Length\": String(file.size),\n \"Idempotency-Key\": idempotencyKey,\n };\n const encoded = encodeMetadata(metadata);\n if (encoded) creationHeaders[\"Upload-Metadata\"] = encoded;\n\n const response = await sendRequest({\n method: \"POST\",\n url: endpoint,\n headers: creationHeaders,\n withCredentials,\n register,\n });\n if (response.status !== 201) throw failed(response, \"Criação do upload recusada.\");\n const locationHeader = response.header(\"Location\");\n if (!locationHeader) throw failed(response, \"Criação do upload sem cabeçalho Location.\");\n\n url = resolveUploadUrl(locationHeader);\n await persist();\n return url;\n }\n\n /** Push one chunk, resyncing the offset first when a previous attempt failed. */\n async function writeChunk(target: string, resync: { needed: boolean }): Promise<void> {\n assertCurrentOwner();\n if (resync.needed) {\n offset = await probe(target);\n resync.needed = false;\n report(offset);\n await persist();\n if (offset >= file.size) return;\n }\n\n const end = Math.min(offset + chunkSize, file.size);\n const from = offset;\n const response = await sendRequest({\n method: \"PATCH\",\n url: target,\n headers: {\n ...baseHeaders(target, \"PATCH\"),\n \"Content-Type\": \"application/offset+octet-stream\",\n \"Upload-Offset\": String(from),\n },\n body: file.slice(from, end),\n withCredentials,\n onProgress: (loaded) => report(Math.min(from + loaded, file.size)),\n register,\n });\n\n if (response.status === 409 || response.status === 412) {\n resync.needed = true;\n throw failed(response, \"Offset divergente — o servidor já tinha esses bytes.\");\n }\n if (response.status !== 204 && response.status !== 200) {\n throw failed(response, \"Chunk recusado pelo servidor.\");\n }\n\n offset = parseOffset(response) ?? end;\n report(offset);\n await persist();\n }\n\n /**\n * Drive the whole upload: attach or create, then chunk until complete.\n *\n * The `shouldRetry` predicate does double duty — besides deciding, it arms\n * `resync` so the next attempt re-reads the server's offset with `HEAD` before\n * writing. That is deliberate: it is the one place that sees *every* chunk\n * failure, whatever the cause, and after any failure the client's idea of the\n * offset is exactly what cannot be trusted.\n *\n * `resync` is armed only when the attempt is actually going to happen. A\n * caller's own `shouldRetry` still wins the decision, and still arms the\n * resync when it says yes — the flag describes what the *next* attempt must\n * do, so setting it for an attempt that never comes describes nothing.\n * {@link isRetriableChunkFailure} is the default, and it is where `409`/`412`\n * earn a retry the shared policy would refuse and `404`/`410` lose one it\n * would have granted.\n *\n * @returns The result, or `null` when `pause`/`abort` stopped the run.\n */\n async function run(): Promise<ResumableUploadResult | null> {\n stopping = null;\n const target = await ensureUpload();\n resumedFrom = offset;\n setState(\"uploading\");\n report(offset);\n\n const resync = { needed: false };\n while (offset < file.size) {\n if (stopping) break;\n await retry(() => writeChunk(target, resync), {\n retries: 5,\n ...retryOptions,\n shouldRetry: (error, attempt) => {\n if (stopping) return false;\n if (error instanceof ResumableUploadSupersededError) return false;\n if (error instanceof DOMException && error.name === \"AbortError\") return false;\n const again =\n retryOptions?.shouldRetry?.(error, attempt) ??\n isRetriableChunkFailure(error);\n if (again) resync.needed = true;\n return again;\n },\n });\n }\n\n if (stopping === \"pause\") {\n setState(\"paused\");\n return null;\n }\n if (stopping === \"abort\") {\n setState(\"aborted\");\n return null;\n }\n\n setState(\"done\");\n if (storage) await storage.delete(key);\n return { url: target, size: file.size };\n }\n\n async function guarded(): Promise<ResumableUploadResult | null> {\n try {\n return await run();\n } catch (error) {\n if (\n stopping !== null ||\n (error instanceof DOMException && error.name === \"AbortError\")\n ) {\n setState(stopping === \"abort\" ? \"aborted\" : \"paused\");\n return null;\n }\n setState(\"error\");\n throw error;\n } finally {\n inFlight = null;\n }\n }\n\n function stop(reason: \"pause\" | \"abort\"): void {\n stopping = reason;\n inFlight?.abort();\n inFlight = null;\n }\n\n return {\n start: guarded,\n resume: guarded,\n pause: () => stop(\"pause\"),\n abort: async ({ discard = false } = {}) => {\n stop(\"abort\");\n setState(\"aborted\");\n if (!discard) return;\n if (url) {\n await sendRequest({\n method: \"DELETE\",\n url,\n headers: baseHeaders(url, \"DELETE\"),\n withCredentials,\n register: () => undefined,\n }).catch(() => undefined);\n }\n if (storage) await storage.delete(key);\n },\n get state() {\n return state;\n },\n get offset() {\n return offset;\n },\n get url() {\n return url;\n },\n key,\n };\n}\n"],"mappings":"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"}