boondmanager-mcp-server 2.15.2 → 2.17.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 (272) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +16 -2
  3. package/README.md +60 -15
  4. package/dist/config/access-policy.d.ts.map +1 -1
  5. package/dist/config/access-policy.js +6 -18
  6. package/dist/config/access-policy.js.map +1 -1
  7. package/dist/config/dictionary-overrides.d.ts.map +1 -1
  8. package/dist/config/dictionary-overrides.js +2 -12
  9. package/dist/config/dictionary-overrides.js.map +1 -1
  10. package/dist/config/env.d.ts +63 -0
  11. package/dist/config/env.d.ts.map +1 -0
  12. package/dist/config/env.js +116 -0
  13. package/dist/config/env.js.map +1 -0
  14. package/dist/config/profiles.d.ts +22 -8
  15. package/dist/config/profiles.d.ts.map +1 -1
  16. package/dist/config/profiles.js +48 -4
  17. package/dist/config/profiles.js.map +1 -1
  18. package/dist/constants.d.ts +13 -2
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +25 -1
  21. package/dist/constants.js.map +1 -1
  22. package/dist/icons.d.ts.map +1 -1
  23. package/dist/icons.js +6 -1
  24. package/dist/icons.js.map +1 -1
  25. package/dist/index.js +33 -15
  26. package/dist/index.js.map +1 -1
  27. package/dist/instructions.d.ts +1 -1
  28. package/dist/instructions.d.ts.map +1 -1
  29. package/dist/instructions.js +6 -6
  30. package/dist/instructions.js.map +1 -1
  31. package/dist/prompts/index.d.ts +19 -1
  32. package/dist/prompts/index.d.ts.map +1 -1
  33. package/dist/prompts/index.js +495 -25
  34. package/dist/prompts/index.js.map +1 -1
  35. package/dist/prompts/periods.d.ts +45 -0
  36. package/dist/prompts/periods.d.ts.map +1 -0
  37. package/dist/prompts/periods.js +154 -0
  38. package/dist/prompts/periods.js.map +1 -0
  39. package/dist/resources/index.d.ts +14 -0
  40. package/dist/resources/index.d.ts.map +1 -1
  41. package/dist/resources/index.js +285 -0
  42. package/dist/resources/index.js.map +1 -1
  43. package/dist/resources/templates.d.ts +1 -1
  44. package/dist/resources/templates.d.ts.map +1 -1
  45. package/dist/resources/templates.js +2 -0
  46. package/dist/resources/templates.js.map +1 -1
  47. package/dist/schema-dialect.js.map +1 -1
  48. package/dist/schemas/filter-aliases.d.ts +1 -1
  49. package/dist/schemas/filter-aliases.d.ts.map +1 -1
  50. package/dist/schemas/filter-aliases.js +101 -2
  51. package/dist/schemas/filter-aliases.js.map +1 -1
  52. package/dist/schemas/index.d.ts +987 -161
  53. package/dist/schemas/index.d.ts.map +1 -1
  54. package/dist/schemas/index.js +853 -323
  55. package/dist/schemas/index.js.map +1 -1
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +10 -3
  58. package/dist/server.js.map +1 -1
  59. package/dist/services/boond-client.d.ts +29 -213
  60. package/dist/services/boond-client.d.ts.map +1 -1
  61. package/dist/services/boond-client.js +29 -1168
  62. package/dist/services/boond-client.js.map +1 -1
  63. package/dist/services/dictionary.d.ts +27 -3
  64. package/dist/services/dictionary.d.ts.map +1 -1
  65. package/dist/services/dictionary.js +60 -23
  66. package/dist/services/dictionary.js.map +1 -1
  67. package/dist/services/document-text.d.ts +32 -0
  68. package/dist/services/document-text.d.ts.map +1 -0
  69. package/dist/services/document-text.js +120 -0
  70. package/dist/services/document-text.js.map +1 -0
  71. package/dist/services/format/detail.d.ts +16 -0
  72. package/dist/services/format/detail.d.ts.map +1 -0
  73. package/dist/services/format/detail.js +36 -0
  74. package/dist/services/format/detail.js.map +1 -0
  75. package/dist/services/format/html.d.ts +19 -0
  76. package/dist/services/format/html.d.ts.map +1 -0
  77. package/dist/services/format/html.js +87 -0
  78. package/dist/services/format/html.js.map +1 -0
  79. package/dist/services/format/list.d.ts +3 -0
  80. package/dist/services/format/list.d.ts.map +1 -0
  81. package/dist/services/format/list.js +44 -0
  82. package/dist/services/format/list.js.map +1 -0
  83. package/dist/services/format/summary.d.ts +16 -0
  84. package/dist/services/format/summary.d.ts.map +1 -0
  85. package/dist/services/format/summary.js +172 -0
  86. package/dist/services/format/summary.js.map +1 -0
  87. package/dist/services/format/tab.d.ts +9 -0
  88. package/dist/services/format/tab.d.ts.map +1 -0
  89. package/dist/services/format/tab.js +32 -0
  90. package/dist/services/format/tab.js.map +1 -0
  91. package/dist/services/http/auth.d.ts +44 -0
  92. package/dist/services/http/auth.d.ts.map +1 -0
  93. package/dist/services/http/auth.js +146 -0
  94. package/dist/services/http/auth.js.map +1 -0
  95. package/dist/services/http/download.d.ts +60 -0
  96. package/dist/services/http/download.d.ts.map +1 -0
  97. package/dist/services/http/download.js +160 -0
  98. package/dist/services/http/download.js.map +1 -0
  99. package/dist/services/http/errors.d.ts +45 -0
  100. package/dist/services/http/errors.d.ts.map +1 -0
  101. package/dist/services/http/errors.js +188 -0
  102. package/dist/services/http/errors.js.map +1 -0
  103. package/dist/services/http/rate-limit.d.ts +40 -0
  104. package/dist/services/http/rate-limit.d.ts.map +1 -0
  105. package/dist/services/http/rate-limit.js +92 -0
  106. package/dist/services/http/rate-limit.js.map +1 -0
  107. package/dist/services/http/retry.d.ts +46 -0
  108. package/dist/services/http/retry.d.ts.map +1 -0
  109. package/dist/services/http/retry.js +101 -0
  110. package/dist/services/http/retry.js.map +1 -0
  111. package/dist/services/http/transport.d.ts +73 -0
  112. package/dist/services/http/transport.d.ts.map +1 -0
  113. package/dist/services/http/transport.js +272 -0
  114. package/dist/services/http/transport.js.map +1 -0
  115. package/dist/services/logger.d.ts +53 -2
  116. package/dist/services/logger.d.ts.map +1 -1
  117. package/dist/services/logger.js +82 -21
  118. package/dist/services/logger.js.map +1 -1
  119. package/dist/services/oauth.d.ts +12 -0
  120. package/dist/services/oauth.d.ts.map +1 -1
  121. package/dist/services/oauth.js +21 -8
  122. package/dist/services/oauth.js.map +1 -1
  123. package/dist/services/rate-limiter.d.ts +9 -2
  124. package/dist/services/rate-limiter.d.ts.map +1 -1
  125. package/dist/services/rate-limiter.js +18 -6
  126. package/dist/services/rate-limiter.js.map +1 -1
  127. package/dist/services/request-context.d.ts +47 -0
  128. package/dist/services/request-context.d.ts.map +1 -0
  129. package/dist/services/request-context.js +112 -0
  130. package/dist/services/request-context.js.map +1 -0
  131. package/dist/services/search.d.ts +26 -0
  132. package/dist/services/search.d.ts.map +1 -0
  133. package/dist/services/search.js +98 -0
  134. package/dist/services/search.js.map +1 -0
  135. package/dist/services/update-checker.d.ts.map +1 -1
  136. package/dist/services/update-checker.js +15 -8
  137. package/dist/services/update-checker.js.map +1 -1
  138. package/dist/tools/absences.d.ts +9 -0
  139. package/dist/tools/absences.d.ts.map +1 -1
  140. package/dist/tools/absences.js +65 -20
  141. package/dist/tools/absences.js.map +1 -1
  142. package/dist/tools/actions.d.ts.map +1 -1
  143. package/dist/tools/actions.js +11 -27
  144. package/dist/tools/actions.js.map +1 -1
  145. package/dist/tools/advantages.d.ts +2 -0
  146. package/dist/tools/advantages.d.ts.map +1 -1
  147. package/dist/tools/advantages.js +66 -13
  148. package/dist/tools/advantages.js.map +1 -1
  149. package/dist/tools/alerts.d.ts +18 -0
  150. package/dist/tools/alerts.d.ts.map +1 -0
  151. package/dist/tools/alerts.js +82 -0
  152. package/dist/tools/alerts.js.map +1 -0
  153. package/dist/tools/application.d.ts +2 -1
  154. package/dist/tools/application.d.ts.map +1 -1
  155. package/dist/tools/application.js +9 -3
  156. package/dist/tools/application.js.map +1 -1
  157. package/dist/tools/contacts.d.ts.map +1 -1
  158. package/dist/tools/contacts.js +3 -7
  159. package/dist/tools/contacts.js.map +1 -1
  160. package/dist/tools/contracts.d.ts +22 -0
  161. package/dist/tools/contracts.d.ts.map +1 -1
  162. package/dist/tools/contracts.js +185 -52
  163. package/dist/tools/contracts.js.map +1 -1
  164. package/dist/tools/crud-factory.d.ts +33 -2
  165. package/dist/tools/crud-factory.d.ts.map +1 -1
  166. package/dist/tools/crud-factory.js +42 -15
  167. package/dist/tools/crud-factory.js.map +1 -1
  168. package/dist/tools/deliveries.d.ts +3 -0
  169. package/dist/tools/deliveries.d.ts.map +1 -1
  170. package/dist/tools/deliveries.js +50 -94
  171. package/dist/tools/deliveries.js.map +1 -1
  172. package/dist/tools/description-builders.d.ts +0 -1
  173. package/dist/tools/description-builders.d.ts.map +1 -1
  174. package/dist/tools/description-builders.js +46 -3
  175. package/dist/tools/description-builders.js.map +1 -1
  176. package/dist/tools/documents.d.ts.map +1 -1
  177. package/dist/tools/documents.js +73 -21
  178. package/dist/tools/documents.js.map +1 -1
  179. package/dist/tools/expenses.d.ts.map +1 -1
  180. package/dist/tools/expenses.js +2 -11
  181. package/dist/tools/expenses.js.map +1 -1
  182. package/dist/tools/find.d.ts +61 -0
  183. package/dist/tools/find.d.ts.map +1 -0
  184. package/dist/tools/find.js +221 -0
  185. package/dist/tools/find.js.map +1 -0
  186. package/dist/tools/flags.d.ts +5 -0
  187. package/dist/tools/flags.d.ts.map +1 -1
  188. package/dist/tools/flags.js +116 -1
  189. package/dist/tools/flags.js.map +1 -1
  190. package/dist/tools/forms.d.ts +5 -0
  191. package/dist/tools/forms.d.ts.map +1 -0
  192. package/dist/tools/forms.js +64 -0
  193. package/dist/tools/forms.js.map +1 -0
  194. package/dist/tools/groupments.d.ts +5 -0
  195. package/dist/tools/groupments.d.ts.map +1 -0
  196. package/dist/tools/groupments.js +68 -0
  197. package/dist/tools/groupments.js.map +1 -0
  198. package/dist/tools/inactivities.d.ts +5 -0
  199. package/dist/tools/inactivities.d.ts.map +1 -0
  200. package/dist/tools/inactivities.js +59 -0
  201. package/dist/tools/inactivities.js.map +1 -0
  202. package/dist/tools/index.d.ts +4 -0
  203. package/dist/tools/index.d.ts.map +1 -1
  204. package/dist/tools/index.js +4 -0
  205. package/dist/tools/index.js.map +1 -1
  206. package/dist/tools/invoices.d.ts.map +1 -1
  207. package/dist/tools/invoices.js +27 -2
  208. package/dist/tools/invoices.js.map +1 -1
  209. package/dist/tools/linked-entity-filters.d.ts +48 -0
  210. package/dist/tools/linked-entity-filters.d.ts.map +1 -0
  211. package/dist/tools/linked-entity-filters.js +64 -0
  212. package/dist/tools/linked-entity-filters.js.map +1 -0
  213. package/dist/tools/notifications.d.ts.map +1 -1
  214. package/dist/tools/notifications.js +0 -5
  215. package/dist/tools/notifications.js.map +1 -1
  216. package/dist/tools/orders.d.ts.map +1 -1
  217. package/dist/tools/orders.js +33 -4
  218. package/dist/tools/orders.js.map +1 -1
  219. package/dist/tools/parameter-disclosure.js.map +1 -1
  220. package/dist/tools/payments.d.ts +3 -0
  221. package/dist/tools/payments.d.ts.map +1 -1
  222. package/dist/tools/payments.js +43 -109
  223. package/dist/tools/payments.js.map +1 -1
  224. package/dist/tools/positionings.d.ts.map +1 -1
  225. package/dist/tools/positionings.js +9 -39
  226. package/dist/tools/positionings.js.map +1 -1
  227. package/dist/tools/projects.d.ts.map +1 -1
  228. package/dist/tools/projects.js +5 -12
  229. package/dist/tools/projects.js.map +1 -1
  230. package/dist/tools/provider-invoices.d.ts +2 -0
  231. package/dist/tools/provider-invoices.d.ts.map +1 -1
  232. package/dist/tools/provider-invoices.js +30 -111
  233. package/dist/tools/provider-invoices.js.map +1 -1
  234. package/dist/tools/purchases.d.ts +2 -0
  235. package/dist/tools/purchases.d.ts.map +1 -1
  236. package/dist/tools/purchases.js +50 -104
  237. package/dist/tools/purchases.js.map +1 -1
  238. package/dist/tools/registration-decorators.d.ts +28 -0
  239. package/dist/tools/registration-decorators.d.ts.map +1 -1
  240. package/dist/tools/registration-decorators.js +107 -0
  241. package/dist/tools/registration-decorators.js.map +1 -1
  242. package/dist/tools/resources.d.ts.map +1 -1
  243. package/dist/tools/resources.js +27 -5
  244. package/dist/tools/resources.js.map +1 -1
  245. package/dist/tools/rights.d.ts +101 -0
  246. package/dist/tools/rights.d.ts.map +1 -0
  247. package/dist/tools/rights.js +75 -0
  248. package/dist/tools/rights.js.map +1 -0
  249. package/dist/tools/tab-tools.js +3 -3
  250. package/dist/tools/tab-tools.js.map +1 -1
  251. package/dist/tools/timesheets.d.ts +29 -0
  252. package/dist/tools/timesheets.d.ts.map +1 -1
  253. package/dist/tools/timesheets.js +196 -126
  254. package/dist/tools/timesheets.js.map +1 -1
  255. package/dist/tools/todolists.d.ts +2 -0
  256. package/dist/tools/todolists.d.ts.map +1 -1
  257. package/dist/tools/todolists.js +64 -1
  258. package/dist/tools/todolists.js.map +1 -1
  259. package/dist/tools/validations.d.ts +9 -0
  260. package/dist/tools/validations.d.ts.map +1 -1
  261. package/dist/tools/validations.js +86 -9
  262. package/dist/tools/validations.js.map +1 -1
  263. package/dist/tools/workflows.js +1 -1
  264. package/dist/tools/workflows.js.map +1 -1
  265. package/dist/transports/http.d.ts +116 -0
  266. package/dist/transports/http.d.ts.map +1 -1
  267. package/dist/transports/http.js +422 -92
  268. package/dist/transports/http.js.map +1 -1
  269. package/dist/types.d.ts +3 -3
  270. package/dist/types.d.ts.map +1 -1
  271. package/manifest.json +7 -7
  272. package/package.json +14 -10
@@ -0,0 +1,160 @@
1
+ import { send } from "./transport.js";
2
+ /**
3
+ * Parse the filename out of a `Content-Disposition` header. Handles the
4
+ * common `filename="…"`/`filename=…` forms and the RFC 5987
5
+ * `filename*=UTF-8''…` form. Returns undefined when absent. Exported for
6
+ * unit testing.
7
+ */
8
+ export function parseContentDispositionFilename(header) {
9
+ if (!header)
10
+ return undefined;
11
+ const star = header.match(/filename\*\s*=\s*(?:UTF-8|utf-8)''([^;]+)/);
12
+ if (star) {
13
+ try {
14
+ return decodeURIComponent((star[1] ?? "").trim());
15
+ }
16
+ catch {
17
+ // fall through to the plain form
18
+ }
19
+ }
20
+ const plain = header.match(/filename\s*=\s*"([^"]+)"/) ?? header.match(/filename\s*=\s*([^;]+)/);
21
+ const name = plain?.[1]?.trim();
22
+ return name === undefined ? undefined : repairUtf8Filename(name);
23
+ }
24
+ /**
25
+ * BoondManager writes the plain `filename="…"` form with UTF-8 bytes (often
26
+ * NFD: `e` + combining acute), and `fetch` decodes header bytes as Latin-1 as
27
+ * the spec says — so "Frédéric" arrives as "FreÌdeÌric" (observed live on
28
+ * 2026-09-26, issue #311). Re-decode those bytes as UTF-8 when that yields a
29
+ * clean string, and normalise to NFC so the name compares like a normal one.
30
+ */
31
+ export function repairUtf8Filename(name) {
32
+ if (!/[\u0080-\u00ff]/.test(name))
33
+ return name;
34
+ const decoded = Buffer.from(name, "latin1").toString("utf8");
35
+ if (decoded.includes("\ufffd"))
36
+ return name.normalize("NFC");
37
+ return decoded.normalize("NFC");
38
+ }
39
+ /** Human-readable byte count for progress messages (same units as the tool output). */
40
+ function formatBytes(bytes) {
41
+ return bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} Mo` : `${Math.round(bytes / 1024)} Ko`;
42
+ }
43
+ /** Progress steps emitted while streaming a download (≈ every 10 %). */
44
+ const DOWNLOAD_PROGRESS_STEPS = 10;
45
+ /**
46
+ * Thrown by `apiDownload` when the payload exceeds `maxBytes` — before the
47
+ * body is read when the response announces its size, otherwise the moment the
48
+ * running total crosses the cap (the stream is cancelled, nothing more is
49
+ * buffered). `bytes` is the announced size in the first case and a lower
50
+ * bound (bytes received so far) in the second; `announced` tells which.
51
+ */
52
+ export class DownloadTooLargeError extends Error {
53
+ bytes;
54
+ maxBytes;
55
+ announced;
56
+ constructor(path, bytes, maxBytes, announced) {
57
+ super(`Document exceeds the ${formatBytes(maxBytes)} limit (${announced ? "announced size" : "more than"} ${formatBytes(bytes)}).\nEndpoint: GET ${path}`);
58
+ this.name = "DownloadTooLargeError";
59
+ this.bytes = bytes;
60
+ this.maxBytes = maxBytes;
61
+ this.announced = announced;
62
+ }
63
+ }
64
+ /**
65
+ * Read a download body chunk by chunk, enforcing `maxBytes` as the bytes
66
+ * arrive and reporting progress as it goes.
67
+ *
68
+ * Streaming is the single path (#235): the cap has to be applied *while*
69
+ * reading, or a 200 MB file is fully buffered before it is refused. Progress
70
+ * is only an observer on top of it — emitted when someone is listening and the
71
+ * response announced a `Content-Length` (without a total there is nothing
72
+ * meaningful to report). A response without a readable stream falls back to
73
+ * `arrayBuffer()` and is checked afterwards.
74
+ */
75
+ async function readDownloadBody(response, path, maxBytes, onProgress) {
76
+ const contentLength = Number(response.headers.get("content-length"));
77
+ const totalBytes = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : undefined;
78
+ const body = response.body;
79
+ if (!body) {
80
+ const buffered = Buffer.from(await response.arrayBuffer());
81
+ if (buffered.byteLength > maxBytes)
82
+ throw new DownloadTooLargeError(path, buffered.byteLength, maxBytes, true);
83
+ return buffered;
84
+ }
85
+ const reporting = onProgress?.enabled === true && totalBytes !== undefined;
86
+ const reader = body.getReader();
87
+ const step = totalBytes !== undefined ? Math.max(1, Math.floor(totalBytes / DOWNLOAD_PROGRESS_STEPS)) : Infinity;
88
+ const chunks = [];
89
+ let received = 0;
90
+ let reported = 0;
91
+ for (;;) {
92
+ const { done, value } = await reader.read();
93
+ if (done)
94
+ break;
95
+ if (!value)
96
+ continue;
97
+ received += value.byteLength;
98
+ if (received > maxBytes) {
99
+ // Stop pulling: what is already in `chunks` is dropped with this frame.
100
+ await reader.cancel().catch(() => undefined);
101
+ throw new DownloadTooLargeError(path, received, maxBytes, false);
102
+ }
103
+ chunks.push(value);
104
+ // Throttled to ~10 notifications: a 5 MiB file arrives in ~80 network
105
+ // chunks, and one notification each would be its own kind of flood.
106
+ if (reporting && received - reported >= step) {
107
+ reported = received;
108
+ onProgress(received, totalBytes, `Téléchargement — ${formatBytes(received)} / ${formatBytes(totalBytes)}`);
109
+ }
110
+ }
111
+ if (reporting && received > reported) {
112
+ onProgress(received, totalBytes, `Téléchargement terminé — ${formatBytes(received)}`);
113
+ }
114
+ return Buffer.concat(chunks);
115
+ }
116
+ /**
117
+ * Download a binary payload (documents, justificatifs…) from the BoondManager
118
+ * API. Same pipeline as `apiRequest` (`send()`: auth, path guard, rate limit,
119
+ * timeout, retry policy for a GET), but the body is streamed raw instead of
120
+ * being parsed as JSON:API.
121
+ *
122
+ * `onProgress` reports bytes received when the client asked for progress and
123
+ * the response carries a `Content-Length`; otherwise nothing is emitted.
124
+ *
125
+ * `options.maxBytes` bounds memory: a `Content-Length` above it is refused
126
+ * before a single body byte is read, and a body without one is cancelled the
127
+ * moment it crosses the cap (`DownloadTooLargeError` either way).
128
+ */
129
+ export async function apiDownload(path, onProgress, options = {}) {
130
+ const maxBytes = options.maxBytes ?? Infinity;
131
+ const response = await send(path, { method: "GET", headers: { Accept: "*/*" } });
132
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream";
133
+ const filename = parseContentDispositionFilename(response.headers.get("content-disposition"));
134
+ // BoondManager only answers 404 on an unknown document when the request asks
135
+ // for JSON. With the `Accept: */*` this function sends, it serves the
136
+ // application shell instead — HTTP 200, `text/html`, ~9 KB — which the caller
137
+ // would happily surface as the document's text content. A truncated id then
138
+ // looks like a corrupted file rather than a wrong id, so refuse the shell
139
+ // here. An HTML *document* is still downloadable: a real file download
140
+ // carries a `Content-Disposition` filename, the shell doesn't.
141
+ if (contentType === "text/html" && !filename) {
142
+ throw new Error([
143
+ "BoondManager returned an HTML page instead of a document (HTTP 200, text/html).",
144
+ `Endpoint: GET ${path}`,
145
+ "Hint: The document id is most likely wrong or truncated. Entity relations expose suffixed ids " +
146
+ "(e.g. `123_resume`, `123_file`) — pass the id verbatim, suffix included. BoondManager serves its " +
147
+ "application shell for an unknown /documents/<id> instead of a 404.",
148
+ ].join("\n"));
149
+ }
150
+ // Announced size over the cap: refuse without reading the body. The
151
+ // 30 s timeout used to be the only bound on a 200 MB document (#235).
152
+ const announced = Number(response.headers.get("content-length"));
153
+ if (Number.isFinite(announced) && announced > maxBytes) {
154
+ await response.body?.cancel().catch(() => undefined);
155
+ throw new DownloadTooLargeError(path, announced, maxBytes, true);
156
+ }
157
+ const data = await readDownloadBody(response, path, maxBytes, onProgress);
158
+ return { data, contentType, ...(filename !== undefined ? { filename } : {}) };
159
+ }
160
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../../../src/services/http/download.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pull the human-readable bits out of a BoondManager error body.
3
+ *
4
+ * Boond returns JSON:API errors of the form:
5
+ * { "errors": [ { "status": "422", "code": "422", "detail": "...", "title": "..." } ] }
6
+ *
7
+ * Surfacing `detail` (and `title` when present) gives the model a focused
8
+ * message like `422 - password mismatch` instead of the full ~500-char body
9
+ * dump that previously made it hard for the LLM to reason about the failure.
10
+ *
11
+ * Exported for unit testing.
12
+ */
13
+ export declare function parseBoondErrorBody(body: string): string | null;
14
+ /**
15
+ * What a caller can do about a BoondManager 401, which depends on who holds
16
+ * the credentials (issue #234). Under the HTTP OAuth transport the token
17
+ * belongs to the MCP client, and the only remedy is a new authorization —
18
+ * there is no CLI to re-run (the historical hint named one that is no longer
19
+ * shipped). With env credentials it is the operator's configuration.
20
+ */
21
+ export declare function hintForUnauthorized(): string;
22
+ /**
23
+ * A non-2xx answer from BoondManager, with the status kept as data so callers
24
+ * (the HTTP transport's token validation, #234) can branch on it instead of
25
+ * parsing the message. The message is unchanged from the plain `Error` this
26
+ * replaces — `formatApiError()` output.
27
+ */
28
+ export declare class BoondApiError extends Error {
29
+ readonly status: number;
30
+ readonly method: string;
31
+ readonly path: string;
32
+ constructor(message: string, status: number, method: string, path: string);
33
+ }
34
+ /** Build the Error message for a non-2xx HTTP response. Exported for testing. */
35
+ export declare function formatApiError(status: number, statusText: string, method: string, path: string, body: string): string;
36
+ /** The error every helper raises when an attempt never completed within `BOOND_HTTP_TIMEOUT_MS`. */
37
+ export declare function timeoutError(timeoutMs: number, method: string, path: string, cause: unknown): Error;
38
+ /**
39
+ * A 2xx whose body is not JSON. Before #239 this surfaced as a bare
40
+ * `SyntaxError: Unexpected token '<'` with no endpoint — indistinguishable
41
+ * from a bug in the server — although it almost always means the request hit
42
+ * the application shell or a WAF page rather than the API.
43
+ */
44
+ export declare function nonJsonResponseError(status: number, method: string, path: string, cause: unknown): Error;
45
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/services/http/errors.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiC/D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAS5C;AAED;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAO1E;AAyDD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA0BrH;AAED,oGAAoG;AACpG,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,CASnG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,CASxG"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Error shaping for BoondManager responses: JSON:API error envelope parsing,
3
+ * Cloudflare block detection, status-specific hints and the typed
4
+ * `BoondApiError`. Pure functions — nothing here touches the network.
5
+ */
6
+ import { oauthContext } from "../oauth.js";
7
+ /**
8
+ * Pull the human-readable bits out of a BoondManager error body.
9
+ *
10
+ * Boond returns JSON:API errors of the form:
11
+ * { "errors": [ { "status": "422", "code": "422", "detail": "...", "title": "..." } ] }
12
+ *
13
+ * Surfacing `detail` (and `title` when present) gives the model a focused
14
+ * message like `422 - password mismatch` instead of the full ~500-char body
15
+ * dump that previously made it hard for the LLM to reason about the failure.
16
+ *
17
+ * Exported for unit testing.
18
+ */
19
+ export function parseBoondErrorBody(body) {
20
+ if (!body)
21
+ return null;
22
+ try {
23
+ const parsed = JSON.parse(body);
24
+ const errors = Array.isArray(parsed.errors) ? parsed.errors : [];
25
+ const messages = errors
26
+ .map((e) => {
27
+ const parts = [];
28
+ if (e.title && e.title !== e.detail)
29
+ parts.push(e.title);
30
+ if (e.detail)
31
+ parts.push(e.detail);
32
+ else if (e.code)
33
+ parts.push(`code ${e.code}`);
34
+ // Boond's JSON:API errors put the offending query/body field in
35
+ // source.parameter (or source.pointer). Surfacing it turns the
36
+ // otherwise-opaque "1017 - Missing required attribute" into
37
+ // "1017 - Missing required attribute (parameter: startMonth)".
38
+ const ref = e.source?.parameter ?? e.source?.pointer;
39
+ const head = parts.join(": ").trim();
40
+ if (!head)
41
+ return ref ? `parameter: ${ref}` : "";
42
+ return ref ? `${head} (parameter: ${ref})` : head;
43
+ })
44
+ .filter((m) => m.length > 0);
45
+ if (messages.length === 0)
46
+ return null;
47
+ return messages.join(" | ");
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * What a caller can do about a BoondManager 401, which depends on who holds
55
+ * the credentials (issue #234). Under the HTTP OAuth transport the token
56
+ * belongs to the MCP client, and the only remedy is a new authorization —
57
+ * there is no CLI to re-run (the historical hint named one that is no longer
58
+ * shipped). With env credentials it is the operator's configuration.
59
+ */
60
+ export function hintForUnauthorized() {
61
+ if (oauthContext.getStore()) {
62
+ return ("The BoondManager access token was rejected (expired or revoked). Re-authorize the connector from the MCP client. " +
63
+ 'With MCP_HTTP_VALIDATE_TOKEN=true the server answers the next request with HTTP 401 + `error="invalid_token"`, ' +
64
+ "which makes spec-compliant clients restart the OAuth flow on their own; otherwise disconnect and reconnect the server in the client.");
65
+ }
66
+ return "Authentication failed. Verify BOOND_USER_TOKEN + BOOND_CLIENT_TOKEN + BOOND_CLIENT_KEY (or BOOND_API_TOKEN, or BOOND_USER + BOOND_PASSWORD).";
67
+ }
68
+ /**
69
+ * A non-2xx answer from BoondManager, with the status kept as data so callers
70
+ * (the HTTP transport's token validation, #234) can branch on it instead of
71
+ * parsing the message. The message is unchanged from the plain `Error` this
72
+ * replaces — `formatApiError()` output.
73
+ */
74
+ export class BoondApiError extends Error {
75
+ status;
76
+ method;
77
+ path;
78
+ constructor(message, status, method, path) {
79
+ super(message);
80
+ this.name = "BoondApiError";
81
+ this.status = status;
82
+ this.method = method;
83
+ this.path = path;
84
+ }
85
+ }
86
+ /** Status-specific hint to help the LLM (or human) recover from common failures. */
87
+ function hintForStatus(status) {
88
+ switch (status) {
89
+ case 400:
90
+ return "Check the request body or query parameters — likely a malformed field.";
91
+ case 401:
92
+ return hintForUnauthorized();
93
+ case 403:
94
+ return "Authenticated, but the user lacks permission for this endpoint or scope.";
95
+ case 404:
96
+ return "Endpoint or entity not found. Double-check the id and the API path.";
97
+ case 422:
98
+ return "Unprocessable: typically wrong credentials (the API returns 422 for password mismatch) or a query parameter the API rejects.";
99
+ case 429:
100
+ return "Rate-limited. Back off and retry after a few seconds.";
101
+ default:
102
+ if (status >= 500)
103
+ return "BoondManager-side error. Retrying after a short delay usually helps.";
104
+ return "Check your credentials and permissions for this endpoint.";
105
+ }
106
+ }
107
+ /**
108
+ * Detect whether the response body looks like a Cloudflare WAF challenge or
109
+ * block page rather than a BoondManager JSON:API response. When this is true,
110
+ * the upstream service is unreachable and the JSON:API hint above is
111
+ * misleading — the request never reached BoondManager.
112
+ */
113
+ function containsCloudflareChallengeHost(htmlSnippet) {
114
+ const urlMatches = htmlSnippet.match(/https?:\/\/[^\s"'<>]+/gi) ?? [];
115
+ for (const rawUrl of urlMatches) {
116
+ try {
117
+ const hostname = new URL(rawUrl).hostname.toLowerCase();
118
+ if (hostname === "challenges.cloudflare.com" || hostname.endsWith(".challenges.cloudflare.com")) {
119
+ return true;
120
+ }
121
+ }
122
+ catch {
123
+ // Ignore unparsable URL fragments in HTML.
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ function looksLikeCloudflareBlock(body) {
129
+ if (!body)
130
+ return false;
131
+ const head = body.slice(0, 1000).toLowerCase();
132
+ if (!head.includes("<!doctype html") && !head.includes("<html"))
133
+ return false;
134
+ return (head.includes("cloudflare") ||
135
+ head.includes("attention required") ||
136
+ head.includes("just a moment") ||
137
+ head.includes("cf-ray") ||
138
+ containsCloudflareChallengeHost(head));
139
+ }
140
+ /** Build the Error message for a non-2xx HTTP response. Exported for testing. */
141
+ export function formatApiError(status, statusText, method, path, body) {
142
+ const detail = parseBoondErrorBody(body);
143
+ const cloudflareBlocked = looksLikeCloudflareBlock(body);
144
+ const headline = cloudflareBlocked
145
+ ? `BoondManager API ${status} ${statusText} — request blocked by Cloudflare WAF before reaching the API`
146
+ : detail
147
+ ? `BoondManager API ${status} ${statusText}: ${detail}`
148
+ : `BoondManager API ${status} ${statusText}`;
149
+ const lines = [headline, `Endpoint: ${method} ${path}`];
150
+ // Only attach the raw body when we couldn't extract a structured detail
151
+ // and we don't already know it's a Cloudflare HTML page — in either case
152
+ // the raw HTML/error chunk just buries the useful message.
153
+ if (!detail && !cloudflareBlocked && body) {
154
+ const trimmed = body.length > 500 ? body.slice(0, 500) + "…" : body;
155
+ lines.push(`Body: ${trimmed}`);
156
+ }
157
+ if (cloudflareBlocked) {
158
+ lines.push("Hint: The BoondManager edge (Cloudflare) blocked this request. " +
159
+ "This often means the endpoint is restricted on this tenant, or you've made too many calls in a short window. " +
160
+ "Wait a few seconds and retry; if it persists, the endpoint is not enabled for this account.");
161
+ }
162
+ else {
163
+ lines.push(`Hint: ${hintForStatus(status)}`);
164
+ }
165
+ return lines.join("\n");
166
+ }
167
+ /** The error every helper raises when an attempt never completed within `BOOND_HTTP_TIMEOUT_MS`. */
168
+ export function timeoutError(timeoutMs, method, path, cause) {
169
+ return new Error([
170
+ `BoondManager API request timed out after ${timeoutMs}ms`,
171
+ `Endpoint: ${method} ${path}`,
172
+ "Hint: Increase BOOND_HTTP_TIMEOUT_MS or check connectivity to the BoondManager API.",
173
+ ].join("\n"), { cause });
174
+ }
175
+ /**
176
+ * A 2xx whose body is not JSON. Before #239 this surfaced as a bare
177
+ * `SyntaxError: Unexpected token '<'` with no endpoint — indistinguishable
178
+ * from a bug in the server — although it almost always means the request hit
179
+ * the application shell or a WAF page rather than the API.
180
+ */
181
+ export function nonJsonResponseError(status, method, path, cause) {
182
+ return new Error([
183
+ `BoondManager API answered HTTP ${status} with a body that is not JSON.`,
184
+ `Endpoint: ${method} ${path}`,
185
+ "Hint: The response is probably an HTML page (application shell, Cloudflare challenge). Check the path and the tenant's access to this endpoint.",
186
+ ].join("\n"), { cause });
187
+ }
188
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/services/http/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { TokenBucket } from "../rate-limiter.js";
2
+ export interface RateLimitConfig {
3
+ rps: number;
4
+ burst: number;
5
+ }
6
+ /**
7
+ * Read rate-limit env vars. `rps` of 0 (or non-numeric) disables rate
8
+ * limiting entirely. `burst` falls back to `rps * 2` when unset, mirroring
9
+ * the documented default behaviour. Exported for unit testing.
10
+ */
11
+ export declare function resolveRateLimitConfig(): RateLimitConfig | null;
12
+ /**
13
+ * Upper bound on live per-identity buckets. A bucket is a few numbers, so
14
+ * 500 idle users cost nothing measurable; the cap exists so an attacker
15
+ * cycling tokens cannot grow the map without bound. Eviction is LRU: an
16
+ * evicted identity simply starts again with a full bucket.
17
+ */
18
+ export declare const MAX_RATE_LIMIT_BUCKETS = 500;
19
+ /**
20
+ * The token bucket for the *current* caller (issue #232).
21
+ *
22
+ * One bucket per process was right for stdio (one user) and wrong for the
23
+ * HTTP OAuth transport, where every request may belong to a different user:
24
+ * a single client's burst throttled everyone else, and one buggy client
25
+ * could hold the whole deployment at the rate limit. Buckets are keyed by
26
+ * `currentAuthIdentity()` — `sha256(token)` in OAuth, the constant `env`
27
+ * identity on stdio / static auth, so those keep exactly one bucket.
28
+ *
29
+ * Note this is fairness *between* users of the same server, not a change to
30
+ * the ceiling BoondManager sees: `BOOND_HTTP_RATE_LIMIT_RPS` is per identity.
31
+ */
32
+ export declare function getRateLimiter(): TokenBucket | null;
33
+ /** Number of live per-identity buckets. Exposed for tests. */
34
+ export declare function rateLimiterBucketCountForTests(): number;
35
+ /**
36
+ * Reset the rate limiters so the next request re-reads env vars.
37
+ * Intended for tests that toggle `BOOND_HTTP_RATE_LIMIT_*` between cases.
38
+ */
39
+ export declare function resetRateLimiterForTests(): void;
40
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/services/http/rate-limit.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,GAAG,IAAI,CAa/D;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAO1C;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAqBnD;AAED,8DAA8D;AAC9D,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Client-side rate limiting towards BoondManager: one token bucket per caller
3
+ * identity (issue #232), LRU-bounded. The bucket itself is `TokenBucket` in
4
+ * `services/rate-limiter.ts`; this module owns the configuration and the map.
5
+ */
6
+ import { readString } from "../../config/env.js";
7
+ import { DEFAULT_HTTP_RATE_LIMIT_BURST, DEFAULT_HTTP_RATE_LIMIT_RPS } from "../../constants.js";
8
+ import { currentAuthIdentity } from "../oauth.js";
9
+ import { TokenBucket } from "../rate-limiter.js";
10
+ /**
11
+ * Read rate-limit env vars. `rps` of 0 (or non-numeric) disables rate
12
+ * limiting entirely. `burst` falls back to `rps * 2` when unset, mirroring
13
+ * the documented default behaviour. Exported for unit testing.
14
+ */
15
+ export function resolveRateLimitConfig() {
16
+ const rpsRaw = readString("BOOND_HTTP_RATE_LIMIT_RPS");
17
+ const rps = rpsRaw === undefined ? DEFAULT_HTTP_RATE_LIMIT_RPS : Number(rpsRaw);
18
+ if (!Number.isFinite(rps) || rps <= 0)
19
+ return null;
20
+ const burstRaw = readString("BOOND_HTTP_RATE_LIMIT_BURST");
21
+ let burst;
22
+ if (burstRaw === undefined) {
23
+ burst = rpsRaw === undefined ? DEFAULT_HTTP_RATE_LIMIT_BURST : Math.max(1, Math.ceil(rps));
24
+ }
25
+ else {
26
+ const parsed = Number(burstRaw);
27
+ burst = Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : Math.max(1, Math.ceil(rps));
28
+ }
29
+ return { rps, burst };
30
+ }
31
+ /**
32
+ * Upper bound on live per-identity buckets. A bucket is a few numbers, so
33
+ * 500 idle users cost nothing measurable; the cap exists so an attacker
34
+ * cycling tokens cannot grow the map without bound. Eviction is LRU: an
35
+ * evicted identity simply starts again with a full bucket.
36
+ */
37
+ export const MAX_RATE_LIMIT_BUCKETS = 500;
38
+ let rateLimitConfig = null;
39
+ let rateLimitConfigResolved = false;
40
+ /** Insertion-ordered map used as an LRU: a hit re-inserts, an insert past the cap evicts the oldest. */
41
+ const rateLimiters = new Map();
42
+ /**
43
+ * The token bucket for the *current* caller (issue #232).
44
+ *
45
+ * One bucket per process was right for stdio (one user) and wrong for the
46
+ * HTTP OAuth transport, where every request may belong to a different user:
47
+ * a single client's burst throttled everyone else, and one buggy client
48
+ * could hold the whole deployment at the rate limit. Buckets are keyed by
49
+ * `currentAuthIdentity()` — `sha256(token)` in OAuth, the constant `env`
50
+ * identity on stdio / static auth, so those keep exactly one bucket.
51
+ *
52
+ * Note this is fairness *between* users of the same server, not a change to
53
+ * the ceiling BoondManager sees: `BOOND_HTTP_RATE_LIMIT_RPS` is per identity.
54
+ */
55
+ export function getRateLimiter() {
56
+ if (!rateLimitConfigResolved) {
57
+ rateLimitConfig = resolveRateLimitConfig();
58
+ rateLimitConfigResolved = true;
59
+ }
60
+ if (!rateLimitConfig)
61
+ return null;
62
+ const key = currentAuthIdentity();
63
+ const existing = rateLimiters.get(key);
64
+ if (existing) {
65
+ rateLimiters.delete(key);
66
+ rateLimiters.set(key, existing);
67
+ return existing;
68
+ }
69
+ const bucket = new TokenBucket(rateLimitConfig.burst, rateLimitConfig.rps);
70
+ rateLimiters.set(key, bucket);
71
+ while (rateLimiters.size > MAX_RATE_LIMIT_BUCKETS) {
72
+ const oldest = rateLimiters.keys().next().value;
73
+ if (oldest === undefined)
74
+ break;
75
+ rateLimiters.delete(oldest);
76
+ }
77
+ return bucket;
78
+ }
79
+ /** Number of live per-identity buckets. Exposed for tests. */
80
+ export function rateLimiterBucketCountForTests() {
81
+ return rateLimiters.size;
82
+ }
83
+ /**
84
+ * Reset the rate limiters so the next request re-reads env vars.
85
+ * Intended for tests that toggle `BOOND_HTTP_RATE_LIMIT_*` between cases.
86
+ */
87
+ export function resetRateLimiterForTests() {
88
+ rateLimiters.clear();
89
+ rateLimitConfig = null;
90
+ rateLimitConfigResolved = false;
91
+ }
92
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../../src/services/http/rate-limit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ export interface RetryConfig {
2
+ maxRetries: number;
3
+ baseDelayMs: number;
4
+ maxDelayMs: number;
5
+ }
6
+ /** Resolve retry configuration from env, with safe fallbacks. Exported for tests. */
7
+ export declare function resolveRetryConfig(): RetryConfig;
8
+ /**
9
+ * Decide whether a failed attempt is worth retrying.
10
+ *
11
+ * Retry policy is intentionally conservative for non-idempotent verbs to avoid
12
+ * silently duplicating writes when the server's response was lost or delayed:
13
+ * - 429 (Too Many Requests) is always retried — the server explicitly
14
+ * rejected the request before processing it, so it is safe regardless of
15
+ * verb.
16
+ * - For GET only, 5xx responses, network failures, and timeouts are retried
17
+ * because GET is idempotent.
18
+ * - 4xx responses (other than 429) are never retried — the client must change
19
+ * the request before another attempt makes sense.
20
+ *
21
+ * Exported for unit testing.
22
+ */
23
+ export declare function isRetryable(method: string, status: number | undefined, isNetworkOrTimeout: boolean): boolean;
24
+ /**
25
+ * Parse a `Retry-After` header value into milliseconds.
26
+ *
27
+ * Accepts either a non-negative number of seconds or an HTTP-date. Returns
28
+ * null when the value is absent or unparseable. Negative computed delays are
29
+ * clamped to 0. Exported for unit testing.
30
+ */
31
+ export declare function parseRetryAfter(value: string | null, now?: number): number | null;
32
+ /**
33
+ * Compute the next backoff delay using full jitter:
34
+ * delay = random(0, min(maxMs, baseMs * 2^attempt))
35
+ *
36
+ * Full jitter (vs. exponential-only) reduces thundering-herd risk when many
37
+ * clients retry in lockstep. Exported for unit testing.
38
+ */
39
+ export declare function computeBackoffMs(attempt: number, baseMs: number, maxMs: number, random?: () => number): number;
40
+ /**
41
+ * Backoff sleep. With a `signal`, the wait ends the moment it fires — a
42
+ * cancelled caller does not sit out a 5 s `Retry-After` (issue #231); the
43
+ * rejection carries the signal's reason.
44
+ */
45
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
46
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/services/http/retry.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qFAAqF;AACrF,wBAAgB,kBAAkB,IAAI,WAAW,CAMhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,kBAAkB,EAAE,OAAO,GAAG,OAAO,CAM5G;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAIR;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrE"}