@37signals/basecamp 0.2.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 (356) hide show
  1. package/README.md +600 -0
  2. package/dist/auth-strategy.d.ts +36 -0
  3. package/dist/auth-strategy.d.ts.map +1 -0
  4. package/dist/auth-strategy.js +33 -0
  5. package/dist/auth-strategy.js.map +1 -0
  6. package/dist/client.d.ts +224 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +715 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/errors.d.ts +137 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +269 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/generated/metadata.json +2281 -0
  15. package/dist/generated/openapi-stripped.json +21268 -0
  16. package/dist/generated/path-mapping.d.ts +8 -0
  17. package/dist/generated/path-mapping.d.ts.map +1 -0
  18. package/dist/generated/path-mapping.js +192 -0
  19. package/dist/generated/path-mapping.js.map +1 -0
  20. package/dist/generated/path-mapping.ts +199 -0
  21. package/dist/generated/schema.d.ts +14883 -0
  22. package/dist/generated/services/attachments.d.ts +27 -0
  23. package/dist/generated/services/attachments.d.ts.map +1 -0
  24. package/dist/generated/services/attachments.js +49 -0
  25. package/dist/generated/services/attachments.js.map +1 -0
  26. package/dist/generated/services/attachments.ts +59 -0
  27. package/dist/generated/services/boosts.d.ts +114 -0
  28. package/dist/generated/services/boosts.d.ts.map +1 -0
  29. package/dist/generated/services/boosts.js +179 -0
  30. package/dist/generated/services/boosts.js.map +1 -0
  31. package/dist/generated/services/boosts.ts +250 -0
  32. package/dist/generated/services/campfires.d.ts +202 -0
  33. package/dist/generated/services/campfires.d.ts.map +1 -0
  34. package/dist/generated/services/campfires.js +309 -0
  35. package/dist/generated/services/campfires.js.map +1 -0
  36. package/dist/generated/services/campfires.ts +433 -0
  37. package/dist/generated/services/card-columns.d.ts +163 -0
  38. package/dist/generated/services/card-columns.d.ts.map +1 -0
  39. package/dist/generated/services/card-columns.js +264 -0
  40. package/dist/generated/services/card-columns.js.map +1 -0
  41. package/dist/generated/services/card-columns.ts +360 -0
  42. package/dist/generated/services/card-steps.d.ts +105 -0
  43. package/dist/generated/services/card-steps.d.ts.map +1 -0
  44. package/dist/generated/services/card-steps.js +148 -0
  45. package/dist/generated/services/card-steps.js.map +1 -0
  46. package/dist/generated/services/card-steps.ts +221 -0
  47. package/dist/generated/services/card-tables.d.ts +27 -0
  48. package/dist/generated/services/card-tables.d.ts.map +1 -0
  49. package/dist/generated/services/card-tables.js +40 -0
  50. package/dist/generated/services/card-tables.js.map +1 -0
  51. package/dist/generated/services/card-tables.ts +55 -0
  52. package/dist/generated/services/cards.d.ts +118 -0
  53. package/dist/generated/services/cards.d.ts.map +1 -0
  54. package/dist/generated/services/cards.js +166 -0
  55. package/dist/generated/services/cards.js.map +1 -0
  56. package/dist/generated/services/cards.ts +247 -0
  57. package/dist/generated/services/checkins.d.ts +278 -0
  58. package/dist/generated/services/checkins.d.ts.map +1 -0
  59. package/dist/generated/services/checkins.js +420 -0
  60. package/dist/generated/services/checkins.js.map +1 -0
  61. package/dist/generated/services/checkins.ts +600 -0
  62. package/dist/generated/services/client-approvals.d.ts +45 -0
  63. package/dist/generated/services/client-approvals.d.ts.map +1 -0
  64. package/dist/generated/services/client-approvals.js +58 -0
  65. package/dist/generated/services/client-approvals.js.map +1 -0
  66. package/dist/generated/services/client-approvals.ts +89 -0
  67. package/dist/generated/services/client-correspondences.d.ts +45 -0
  68. package/dist/generated/services/client-correspondences.d.ts.map +1 -0
  69. package/dist/generated/services/client-correspondences.js +58 -0
  70. package/dist/generated/services/client-correspondences.js.map +1 -0
  71. package/dist/generated/services/client-correspondences.ts +89 -0
  72. package/dist/generated/services/client-replies.d.ts +47 -0
  73. package/dist/generated/services/client-replies.d.ts.map +1 -0
  74. package/dist/generated/services/client-replies.js +65 -0
  75. package/dist/generated/services/client-replies.js.map +1 -0
  76. package/dist/generated/services/client-replies.ts +95 -0
  77. package/dist/generated/services/client-visibility.d.ts +35 -0
  78. package/dist/generated/services/client-visibility.d.ts.map +1 -0
  79. package/dist/generated/services/client-visibility.js +44 -0
  80. package/dist/generated/services/client-visibility.js.map +1 -0
  81. package/dist/generated/services/client-visibility.ts +68 -0
  82. package/dist/generated/services/comments.d.ts +86 -0
  83. package/dist/generated/services/comments.d.ts.map +1 -0
  84. package/dist/generated/services/comments.js +129 -0
  85. package/dist/generated/services/comments.js.map +1 -0
  86. package/dist/generated/services/comments.ts +185 -0
  87. package/dist/generated/services/documents.d.ts +92 -0
  88. package/dist/generated/services/documents.d.ts.map +1 -0
  89. package/dist/generated/services/documents.js +129 -0
  90. package/dist/generated/services/documents.js.map +1 -0
  91. package/dist/generated/services/documents.ts +191 -0
  92. package/dist/generated/services/events.d.ts +34 -0
  93. package/dist/generated/services/events.d.ts.map +1 -0
  94. package/dist/generated/services/events.js +39 -0
  95. package/dist/generated/services/events.js.map +1 -0
  96. package/dist/generated/services/events.ts +64 -0
  97. package/dist/generated/services/forwards.d.ts +112 -0
  98. package/dist/generated/services/forwards.d.ts.map +1 -0
  99. package/dist/generated/services/forwards.js +172 -0
  100. package/dist/generated/services/forwards.js.map +1 -0
  101. package/dist/generated/services/forwards.ts +241 -0
  102. package/dist/generated/services/index.d.ts +39 -0
  103. package/dist/generated/services/index.d.ts.map +1 -0
  104. package/dist/generated/services/index.js +39 -0
  105. package/dist/generated/services/index.js.map +1 -0
  106. package/dist/generated/services/index.ts +38 -0
  107. package/dist/generated/services/lineup.d.ts +67 -0
  108. package/dist/generated/services/lineup.d.ts.map +1 -0
  109. package/dist/generated/services/lineup.js +99 -0
  110. package/dist/generated/services/lineup.js.map +1 -0
  111. package/dist/generated/services/lineup.ts +143 -0
  112. package/dist/generated/services/message-boards.d.ts +27 -0
  113. package/dist/generated/services/message-boards.d.ts.map +1 -0
  114. package/dist/generated/services/message-boards.js +40 -0
  115. package/dist/generated/services/message-boards.js.map +1 -0
  116. package/dist/generated/services/message-boards.ts +55 -0
  117. package/dist/generated/services/message-types.d.ts +100 -0
  118. package/dist/generated/services/message-types.d.ts.map +1 -0
  119. package/dist/generated/services/message-types.js +144 -0
  120. package/dist/generated/services/message-types.js.map +1 -0
  121. package/dist/generated/services/message-types.ts +210 -0
  122. package/dist/generated/services/messages.d.ts +122 -0
  123. package/dist/generated/services/messages.d.ts.map +1 -0
  124. package/dist/generated/services/messages.js +180 -0
  125. package/dist/generated/services/messages.js.map +1 -0
  126. package/dist/generated/services/messages.ts +258 -0
  127. package/dist/generated/services/people.d.ts +122 -0
  128. package/dist/generated/services/people.d.ts.map +1 -0
  129. package/dist/generated/services/people.js +167 -0
  130. package/dist/generated/services/people.js.map +1 -0
  131. package/dist/generated/services/people.ts +252 -0
  132. package/dist/generated/services/projects.d.ts +109 -0
  133. package/dist/generated/services/projects.d.ts.map +1 -0
  134. package/dist/generated/services/projects.js +153 -0
  135. package/dist/generated/services/projects.js.map +1 -0
  136. package/dist/generated/services/projects.ts +224 -0
  137. package/dist/generated/services/recordings.d.ts +93 -0
  138. package/dist/generated/services/recordings.d.ts.map +1 -0
  139. package/dist/generated/services/recordings.js +138 -0
  140. package/dist/generated/services/recordings.js.map +1 -0
  141. package/dist/generated/services/recordings.ts +191 -0
  142. package/dist/generated/services/reports.d.ts +90 -0
  143. package/dist/generated/services/reports.d.ts.map +1 -0
  144. package/dist/generated/services/reports.js +124 -0
  145. package/dist/generated/services/reports.js.map +1 -0
  146. package/dist/generated/services/reports.ts +187 -0
  147. package/dist/generated/services/schedules.d.ts +162 -0
  148. package/dist/generated/services/schedules.d.ts.map +1 -0
  149. package/dist/generated/services/schedules.js +228 -0
  150. package/dist/generated/services/schedules.js.map +1 -0
  151. package/dist/generated/services/schedules.ts +335 -0
  152. package/dist/generated/services/search.d.ts +45 -0
  153. package/dist/generated/services/search.d.ts.map +1 -0
  154. package/dist/generated/services/search.js +56 -0
  155. package/dist/generated/services/search.js.map +1 -0
  156. package/dist/generated/services/search.ts +89 -0
  157. package/dist/generated/services/subscriptions.d.ts +73 -0
  158. package/dist/generated/services/subscriptions.d.ts.map +1 -0
  159. package/dist/generated/services/subscriptions.js +119 -0
  160. package/dist/generated/services/subscriptions.js.map +1 -0
  161. package/dist/generated/services/subscriptions.ts +160 -0
  162. package/dist/generated/services/templates.d.ts +140 -0
  163. package/dist/generated/services/templates.d.ts.map +1 -0
  164. package/dist/generated/services/templates.js +207 -0
  165. package/dist/generated/services/templates.js.map +1 -0
  166. package/dist/generated/services/templates.ts +294 -0
  167. package/dist/generated/services/timeline.d.ts +31 -0
  168. package/dist/generated/services/timeline.d.ts.map +1 -0
  169. package/dist/generated/services/timeline.js +39 -0
  170. package/dist/generated/services/timeline.js.map +1 -0
  171. package/dist/generated/services/timeline.ts +62 -0
  172. package/dist/generated/services/timesheets.d.ts +145 -0
  173. package/dist/generated/services/timesheets.d.ts.map +1 -0
  174. package/dist/generated/services/timesheets.js +186 -0
  175. package/dist/generated/services/timesheets.js.map +1 -0
  176. package/dist/generated/services/timesheets.ts +289 -0
  177. package/dist/generated/services/todolist-groups.d.ts +74 -0
  178. package/dist/generated/services/todolist-groups.d.ts.map +1 -0
  179. package/dist/generated/services/todolist-groups.js +100 -0
  180. package/dist/generated/services/todolist-groups.js.map +1 -0
  181. package/dist/generated/services/todolist-groups.ts +151 -0
  182. package/dist/generated/services/todolists.d.ts +95 -0
  183. package/dist/generated/services/todolists.d.ts.map +1 -0
  184. package/dist/generated/services/todolists.js +130 -0
  185. package/dist/generated/services/todolists.js.map +1 -0
  186. package/dist/generated/services/todolists.ts +192 -0
  187. package/dist/generated/services/todos.d.ts +175 -0
  188. package/dist/generated/services/todos.d.ts.map +1 -0
  189. package/dist/generated/services/todos.js +255 -0
  190. package/dist/generated/services/todos.js.map +1 -0
  191. package/dist/generated/services/todos.ts +369 -0
  192. package/dist/generated/services/todosets.d.ts +27 -0
  193. package/dist/generated/services/todosets.d.ts.map +1 -0
  194. package/dist/generated/services/todosets.js +40 -0
  195. package/dist/generated/services/todosets.js.map +1 -0
  196. package/dist/generated/services/todosets.ts +55 -0
  197. package/dist/generated/services/tools.d.ts +122 -0
  198. package/dist/generated/services/tools.d.ts.map +1 -0
  199. package/dist/generated/services/tools.js +197 -0
  200. package/dist/generated/services/tools.js.map +1 -0
  201. package/dist/generated/services/tools.ts +267 -0
  202. package/dist/generated/services/uploads.d.ts +109 -0
  203. package/dist/generated/services/uploads.d.ts.map +1 -0
  204. package/dist/generated/services/uploads.js +153 -0
  205. package/dist/generated/services/uploads.js.map +1 -0
  206. package/dist/generated/services/uploads.ts +227 -0
  207. package/dist/generated/services/vaults.d.ts +86 -0
  208. package/dist/generated/services/vaults.d.ts.map +1 -0
  209. package/dist/generated/services/vaults.js +126 -0
  210. package/dist/generated/services/vaults.js.map +1 -0
  211. package/dist/generated/services/vaults.ts +182 -0
  212. package/dist/generated/services/webhooks.d.ts +106 -0
  213. package/dist/generated/services/webhooks.d.ts.map +1 -0
  214. package/dist/generated/services/webhooks.js +157 -0
  215. package/dist/generated/services/webhooks.js.map +1 -0
  216. package/dist/generated/services/webhooks.ts +226 -0
  217. package/dist/hooks/otel.d.ts +136 -0
  218. package/dist/hooks/otel.d.ts.map +1 -0
  219. package/dist/hooks/otel.js +240 -0
  220. package/dist/hooks/otel.js.map +1 -0
  221. package/dist/hooks.d.ts +171 -0
  222. package/dist/hooks.d.ts.map +1 -0
  223. package/dist/hooks.js +196 -0
  224. package/dist/hooks.js.map +1 -0
  225. package/dist/index.d.ts +90 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +122 -0
  228. package/dist/index.js.map +1 -0
  229. package/dist/oauth/authorize.d.ts +48 -0
  230. package/dist/oauth/authorize.d.ts.map +1 -0
  231. package/dist/oauth/authorize.js +56 -0
  232. package/dist/oauth/authorize.js.map +1 -0
  233. package/dist/oauth/callback-server.d.ts +56 -0
  234. package/dist/oauth/callback-server.d.ts.map +1 -0
  235. package/dist/oauth/callback-server.js +150 -0
  236. package/dist/oauth/callback-server.js.map +1 -0
  237. package/dist/oauth/discovery.d.ts +54 -0
  238. package/dist/oauth/discovery.d.ts.map +1 -0
  239. package/dist/oauth/discovery.js +116 -0
  240. package/dist/oauth/discovery.js.map +1 -0
  241. package/dist/oauth/exchange.d.ts +96 -0
  242. package/dist/oauth/exchange.d.ts.map +1 -0
  243. package/dist/oauth/exchange.js +324 -0
  244. package/dist/oauth/exchange.js.map +1 -0
  245. package/dist/oauth/identity.d.ts +30 -0
  246. package/dist/oauth/identity.d.ts.map +1 -0
  247. package/dist/oauth/identity.js +82 -0
  248. package/dist/oauth/identity.js.map +1 -0
  249. package/dist/oauth/index.d.ts +50 -0
  250. package/dist/oauth/index.d.ts.map +1 -0
  251. package/dist/oauth/index.js +58 -0
  252. package/dist/oauth/index.js.map +1 -0
  253. package/dist/oauth/interactive-login.d.ts +62 -0
  254. package/dist/oauth/interactive-login.d.ts.map +1 -0
  255. package/dist/oauth/interactive-login.js +106 -0
  256. package/dist/oauth/interactive-login.js.map +1 -0
  257. package/dist/oauth/pkce.d.ts +68 -0
  258. package/dist/oauth/pkce.d.ts.map +1 -0
  259. package/dist/oauth/pkce.js +80 -0
  260. package/dist/oauth/pkce.js.map +1 -0
  261. package/dist/oauth/token-manager.d.ts +92 -0
  262. package/dist/oauth/token-manager.d.ts.map +1 -0
  263. package/dist/oauth/token-manager.js +126 -0
  264. package/dist/oauth/token-manager.js.map +1 -0
  265. package/dist/oauth/token-store.d.ts +45 -0
  266. package/dist/oauth/token-store.d.ts.map +1 -0
  267. package/dist/oauth/token-store.js +105 -0
  268. package/dist/oauth/token-store.js.map +1 -0
  269. package/dist/oauth/types.d.ts +98 -0
  270. package/dist/oauth/types.d.ts.map +1 -0
  271. package/dist/oauth/types.js +8 -0
  272. package/dist/oauth/types.js.map +1 -0
  273. package/dist/pagination-utils.d.ts +27 -0
  274. package/dist/pagination-utils.d.ts.map +1 -0
  275. package/dist/pagination-utils.js +54 -0
  276. package/dist/pagination-utils.js.map +1 -0
  277. package/dist/pagination.d.ts +56 -0
  278. package/dist/pagination.d.ts.map +1 -0
  279. package/dist/pagination.js +57 -0
  280. package/dist/pagination.js.map +1 -0
  281. package/dist/security.d.ts +68 -0
  282. package/dist/security.d.ts.map +1 -0
  283. package/dist/security.js +107 -0
  284. package/dist/security.js.map +1 -0
  285. package/dist/services/authorization.d.ts +124 -0
  286. package/dist/services/authorization.d.ts.map +1 -0
  287. package/dist/services/authorization.js +117 -0
  288. package/dist/services/authorization.js.map +1 -0
  289. package/dist/services/base.d.ts +102 -0
  290. package/dist/services/base.d.ts.map +1 -0
  291. package/dist/services/base.js +230 -0
  292. package/dist/services/base.js.map +1 -0
  293. package/dist/webhooks/adapters/node-http.d.ts +14 -0
  294. package/dist/webhooks/adapters/node-http.d.ts.map +1 -0
  295. package/dist/webhooks/adapters/node-http.js +60 -0
  296. package/dist/webhooks/adapters/node-http.js.map +1 -0
  297. package/dist/webhooks/events.d.ts +52 -0
  298. package/dist/webhooks/events.d.ts.map +1 -0
  299. package/dist/webhooks/events.js +52 -0
  300. package/dist/webhooks/events.js.map +1 -0
  301. package/dist/webhooks/handler.d.ts +62 -0
  302. package/dist/webhooks/handler.d.ts.map +1 -0
  303. package/dist/webhooks/handler.js +226 -0
  304. package/dist/webhooks/handler.js.map +1 -0
  305. package/dist/webhooks/index.d.ts +5 -0
  306. package/dist/webhooks/index.d.ts.map +1 -0
  307. package/dist/webhooks/index.js +5 -0
  308. package/dist/webhooks/index.js.map +1 -0
  309. package/dist/webhooks/verify.d.ts +11 -0
  310. package/dist/webhooks/verify.d.ts.map +1 -0
  311. package/dist/webhooks/verify.js +25 -0
  312. package/dist/webhooks/verify.js.map +1 -0
  313. package/package.json +70 -0
  314. package/src/generated/metadata.json +2281 -0
  315. package/src/generated/openapi-stripped.json +21268 -0
  316. package/src/generated/path-mapping.ts +199 -0
  317. package/src/generated/schema.d.ts +14883 -0
  318. package/src/generated/services/attachments.ts +59 -0
  319. package/src/generated/services/boosts.ts +250 -0
  320. package/src/generated/services/campfires.ts +433 -0
  321. package/src/generated/services/card-columns.ts +360 -0
  322. package/src/generated/services/card-steps.ts +221 -0
  323. package/src/generated/services/card-tables.ts +55 -0
  324. package/src/generated/services/cards.ts +247 -0
  325. package/src/generated/services/checkins.ts +600 -0
  326. package/src/generated/services/client-approvals.ts +89 -0
  327. package/src/generated/services/client-correspondences.ts +89 -0
  328. package/src/generated/services/client-replies.ts +95 -0
  329. package/src/generated/services/client-visibility.ts +68 -0
  330. package/src/generated/services/comments.ts +185 -0
  331. package/src/generated/services/documents.ts +191 -0
  332. package/src/generated/services/events.ts +64 -0
  333. package/src/generated/services/forwards.ts +241 -0
  334. package/src/generated/services/index.ts +38 -0
  335. package/src/generated/services/lineup.ts +143 -0
  336. package/src/generated/services/message-boards.ts +55 -0
  337. package/src/generated/services/message-types.ts +210 -0
  338. package/src/generated/services/messages.ts +258 -0
  339. package/src/generated/services/people.ts +252 -0
  340. package/src/generated/services/projects.ts +224 -0
  341. package/src/generated/services/recordings.ts +191 -0
  342. package/src/generated/services/reports.ts +187 -0
  343. package/src/generated/services/schedules.ts +335 -0
  344. package/src/generated/services/search.ts +89 -0
  345. package/src/generated/services/subscriptions.ts +160 -0
  346. package/src/generated/services/templates.ts +294 -0
  347. package/src/generated/services/timeline.ts +62 -0
  348. package/src/generated/services/timesheets.ts +289 -0
  349. package/src/generated/services/todolist-groups.ts +151 -0
  350. package/src/generated/services/todolists.ts +192 -0
  351. package/src/generated/services/todos.ts +369 -0
  352. package/src/generated/services/todosets.ts +55 -0
  353. package/src/generated/services/tools.ts +267 -0
  354. package/src/generated/services/uploads.ts +227 -0
  355. package/src/generated/services/vaults.ts +182 -0
  356. package/src/generated/services/webhooks.ts +226 -0
package/dist/client.js ADDED
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Basecamp TypeScript SDK Client
3
+ *
4
+ * Creates a type-safe client for the Basecamp 3 API using openapi-fetch.
5
+ * Includes middleware for authentication, retry with exponential backoff,
6
+ * and ETag-based caching.
7
+ */
8
+ import createClient from "openapi-fetch";
9
+ import { createRequire } from "node:module";
10
+ import { PATH_TO_OPERATION } from "./generated/path-mapping.js";
11
+ import { BasecampError } from "./errors.js";
12
+ import { isLocalhost } from "./security.js";
13
+ import { parseNextLink, resolveURL, isSameOrigin } from "./pagination-utils.js";
14
+ import { bearerAuth } from "./auth-strategy.js";
15
+ // Use createRequire for JSON import (Node 18+ compatible)
16
+ const require = createRequire(import.meta.url);
17
+ const metadata = require("./generated/metadata.json");
18
+ // ============================================================================
19
+ // Services - Generated from OpenAPI spec (spec-driven, not hand-written)
20
+ // ============================================================================
21
+ import { ProjectsService } from "./generated/services/projects.js";
22
+ import { TodosService } from "./generated/services/todos.js";
23
+ import { TodolistsService } from "./generated/services/todolists.js";
24
+ import { TodosetsService } from "./generated/services/todosets.js";
25
+ import { PeopleService } from "./generated/services/people.js";
26
+ import { MessagesService } from "./generated/services/messages.js";
27
+ import { CommentsService } from "./generated/services/comments.js";
28
+ import { CampfiresService } from "./generated/services/campfires.js";
29
+ import { CardTablesService } from "./generated/services/card-tables.js";
30
+ import { CardsService } from "./generated/services/cards.js";
31
+ import { CardColumnsService } from "./generated/services/card-columns.js";
32
+ import { CardStepsService } from "./generated/services/card-steps.js";
33
+ import { MessageBoardsService } from "./generated/services/message-boards.js";
34
+ import { MessageTypesService } from "./generated/services/message-types.js";
35
+ import { ForwardsService } from "./generated/services/forwards.js";
36
+ import { CheckinsService } from "./generated/services/checkins.js";
37
+ import { ClientApprovalsService } from "./generated/services/client-approvals.js";
38
+ import { ClientCorrespondencesService } from "./generated/services/client-correspondences.js";
39
+ import { ClientRepliesService } from "./generated/services/client-replies.js";
40
+ import { WebhooksService } from "./generated/services/webhooks.js";
41
+ import { SubscriptionsService } from "./generated/services/subscriptions.js";
42
+ import { AttachmentsService } from "./generated/services/attachments.js";
43
+ import { VaultsService } from "./generated/services/vaults.js";
44
+ import { DocumentsService } from "./generated/services/documents.js";
45
+ import { UploadsService } from "./generated/services/uploads.js";
46
+ import { SchedulesService } from "./generated/services/schedules.js";
47
+ import { EventsService } from "./generated/services/events.js";
48
+ import { RecordingsService } from "./generated/services/recordings.js";
49
+ import { SearchService } from "./generated/services/search.js";
50
+ import { ReportsService } from "./generated/services/reports.js";
51
+ import { TemplatesService } from "./generated/services/templates.js";
52
+ import { LineupService } from "./generated/services/lineup.js";
53
+ import { TodolistGroupsService } from "./generated/services/todolist-groups.js";
54
+ import { ToolsService } from "./generated/services/tools.js";
55
+ import { TimesheetsService } from "./generated/services/timesheets.js";
56
+ import { TimelineService } from "./generated/services/timeline.js";
57
+ import { ClientVisibilityService } from "./generated/services/client-visibility.js";
58
+ import { BoostsService } from "./generated/services/boosts.js";
59
+ // ============================================================================
60
+ // Services - Hand-written (not spec-driven, e.g., OAuth flows)
61
+ // ============================================================================
62
+ import { AuthorizationService } from "./services/authorization.js";
63
+ const VERSION = "0.2.0";
64
+ const DEFAULT_USER_AGENT = `basecamp-sdk-ts/${VERSION}`;
65
+ /**
66
+ * Creates a type-safe Basecamp API client with built-in middleware for:
67
+ * - Authentication (Bearer token)
68
+ * - Retry with exponential backoff (respects Retry-After header)
69
+ * - ETag-based HTTP caching
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { createBasecampClient } from "@37signals/basecamp";
74
+ *
75
+ * const client = createBasecampClient({
76
+ * accountId: "12345",
77
+ * accessToken: process.env.BASECAMP_TOKEN!,
78
+ * });
79
+ *
80
+ * const { data, error } = await client.GET("/projects.json");
81
+ * ```
82
+ */
83
+ export function createBasecampClient(options) {
84
+ const { accountId, accessToken, auth, baseUrl = `https://3.basecampapi.com/${accountId}`, userAgent = DEFAULT_USER_AGENT, enableCache = false, enableRetry = true, hooks, } = options;
85
+ // Validate auth options: exactly one of auth or accessToken must be provided
86
+ if (auth && accessToken) {
87
+ throw new BasecampError("usage", "Provide either 'auth' or 'accessToken', not both");
88
+ }
89
+ if (!auth && !accessToken) {
90
+ throw new BasecampError("usage", "Either 'auth' or 'accessToken' is required");
91
+ }
92
+ const authStrategy = auth ?? bearerAuth(accessToken);
93
+ // Validate configuration (skip HTTPS check for localhost in dev/test)
94
+ if (baseUrl) {
95
+ try {
96
+ const parsed = new URL(baseUrl);
97
+ if (parsed.protocol !== "https:" && !isLocalhost(parsed.hostname)) {
98
+ throw new BasecampError("usage", `Base URL must use HTTPS: ${baseUrl}`);
99
+ }
100
+ }
101
+ catch (err) {
102
+ if (err instanceof BasecampError)
103
+ throw err;
104
+ throw new BasecampError("usage", `Invalid base URL: ${baseUrl}`);
105
+ }
106
+ }
107
+ const client = createClient({ baseUrl });
108
+ // Apply middleware in order: auth first, then hooks, then cache, then retry
109
+ client.use(createAuthMiddleware(authStrategy, userAgent));
110
+ if (hooks) {
111
+ client.use(createHooksMiddleware(hooks));
112
+ }
113
+ if (enableCache) {
114
+ client.use(createCacheMiddleware());
115
+ }
116
+ if (enableRetry) {
117
+ client.use(createRetryMiddleware(hooks));
118
+ }
119
+ // Create enhanced client with additional properties
120
+ const enhancedClient = client;
121
+ Object.defineProperty(enhancedClient, "raw", {
122
+ value: client,
123
+ writable: false,
124
+ enumerable: false,
125
+ });
126
+ Object.defineProperty(enhancedClient, "hooks", {
127
+ value: hooks,
128
+ writable: false,
129
+ enumerable: false,
130
+ });
131
+ // Create fetchPage closure for pagination — uses same auth & User-Agent as main client
132
+ const fetchPage = async (url) => {
133
+ const headers = new Headers({
134
+ "User-Agent": userAgent,
135
+ Accept: "application/json",
136
+ });
137
+ await authStrategy.authenticate(headers);
138
+ return fetch(url, { headers });
139
+ };
140
+ // Add lazy-initialized service accessors
141
+ // Services are created on first access and cached
142
+ // Uses nullish coalescing assignment for atomic check-and-set in single-threaded JS
143
+ const serviceCache = {};
144
+ const defineService = (name, factory) => {
145
+ Object.defineProperty(enhancedClient, name, {
146
+ get() {
147
+ // Nullish coalescing assignment is atomic in single-threaded JS.
148
+ // This prevents duplicate service creation during async interleaving.
149
+ return (serviceCache[name] ??= factory());
150
+ },
151
+ enumerable: true,
152
+ configurable: false,
153
+ });
154
+ };
155
+ defineService("projects", () => new ProjectsService(client, hooks, fetchPage));
156
+ defineService("todos", () => new TodosService(client, hooks, fetchPage));
157
+ defineService("todolists", () => new TodolistsService(client, hooks, fetchPage));
158
+ defineService("todosets", () => new TodosetsService(client, hooks, fetchPage));
159
+ defineService("people", () => new PeopleService(client, hooks, fetchPage));
160
+ defineService("authorization", () => new AuthorizationService(client, hooks, authStrategy, userAgent));
161
+ defineService("messages", () => new MessagesService(client, hooks, fetchPage));
162
+ defineService("comments", () => new CommentsService(client, hooks, fetchPage));
163
+ defineService("campfires", () => new CampfiresService(client, hooks, fetchPage));
164
+ defineService("cardTables", () => new CardTablesService(client, hooks, fetchPage));
165
+ defineService("cards", () => new CardsService(client, hooks, fetchPage));
166
+ defineService("cardColumns", () => new CardColumnsService(client, hooks, fetchPage));
167
+ defineService("cardSteps", () => new CardStepsService(client, hooks, fetchPage));
168
+ defineService("messageBoards", () => new MessageBoardsService(client, hooks, fetchPage));
169
+ defineService("messageTypes", () => new MessageTypesService(client, hooks, fetchPage));
170
+ defineService("forwards", () => new ForwardsService(client, hooks, fetchPage));
171
+ defineService("checkins", () => new CheckinsService(client, hooks, fetchPage));
172
+ defineService("clientApprovals", () => new ClientApprovalsService(client, hooks, fetchPage));
173
+ defineService("clientCorrespondences", () => new ClientCorrespondencesService(client, hooks, fetchPage));
174
+ defineService("clientReplies", () => new ClientRepliesService(client, hooks, fetchPage));
175
+ defineService("webhooks", () => new WebhooksService(client, hooks, fetchPage));
176
+ defineService("subscriptions", () => new SubscriptionsService(client, hooks, fetchPage));
177
+ defineService("attachments", () => new AttachmentsService(client, hooks, fetchPage));
178
+ defineService("vaults", () => new VaultsService(client, hooks, fetchPage));
179
+ defineService("documents", () => new DocumentsService(client, hooks, fetchPage));
180
+ defineService("uploads", () => new UploadsService(client, hooks, fetchPage));
181
+ defineService("schedules", () => new SchedulesService(client, hooks, fetchPage));
182
+ defineService("events", () => new EventsService(client, hooks, fetchPage));
183
+ defineService("recordings", () => new RecordingsService(client, hooks, fetchPage));
184
+ defineService("search", () => new SearchService(client, hooks, fetchPage));
185
+ defineService("reports", () => new ReportsService(client, hooks, fetchPage));
186
+ defineService("templates", () => new TemplatesService(client, hooks, fetchPage));
187
+ defineService("lineup", () => new LineupService(client, hooks, fetchPage));
188
+ defineService("todolistGroups", () => new TodolistGroupsService(client, hooks, fetchPage));
189
+ defineService("tools", () => new ToolsService(client, hooks, fetchPage));
190
+ defineService("timesheets", () => new TimesheetsService(client, hooks, fetchPage));
191
+ defineService("timeline", () => new TimelineService(client, hooks, fetchPage));
192
+ defineService("clientVisibility", () => new ClientVisibilityService(client, hooks, fetchPage));
193
+ defineService("boosts", () => new BoostsService(client, hooks, fetchPage));
194
+ return enhancedClient;
195
+ }
196
+ // =============================================================================
197
+ // Auth Middleware
198
+ // =============================================================================
199
+ function createAuthMiddleware(authStrategy, userAgent) {
200
+ return {
201
+ async onRequest({ request }) {
202
+ await authStrategy.authenticate(request.headers);
203
+ request.headers.set("User-Agent", userAgent);
204
+ // Only set Content-Type if not already set (preserves binary uploads, etc.)
205
+ if (!request.headers.has("Content-Type")) {
206
+ request.headers.set("Content-Type", "application/json");
207
+ }
208
+ request.headers.set("Accept", "application/json");
209
+ return request;
210
+ },
211
+ };
212
+ }
213
+ /** Counter for generating unique request IDs */
214
+ let requestIdCounter = 0;
215
+ function createHooksMiddleware(hooks) {
216
+ // Track request timing by unique request ID
217
+ const timings = new Map();
218
+ return {
219
+ async onRequest({ request }) {
220
+ // Generate unique request ID to handle concurrent identical requests
221
+ const requestId = `${++requestIdCounter}`;
222
+ request.headers.set("X-SDK-Request-Id", requestId);
223
+ const attemptHeader = request.headers.get("X-Retry-Attempt");
224
+ const attempt = attemptHeader ? parseInt(attemptHeader, 10) + 1 : 1;
225
+ timings.set(requestId, { startTime: performance.now(), attempt });
226
+ const info = {
227
+ method: request.method,
228
+ url: request.url,
229
+ attempt,
230
+ };
231
+ try {
232
+ hooks.onRequestStart?.(info);
233
+ }
234
+ catch {
235
+ // Hooks should not interrupt the request
236
+ }
237
+ return request;
238
+ },
239
+ async onResponse({ request, response }) {
240
+ const requestId = request.headers.get("X-SDK-Request-Id") ?? "";
241
+ const timing = timings.get(requestId);
242
+ const durationMs = timing ? Math.round(performance.now() - timing.startTime) : 0;
243
+ const attempt = timing?.attempt ?? 1;
244
+ timings.delete(requestId);
245
+ const info = {
246
+ method: request.method,
247
+ url: request.url,
248
+ attempt,
249
+ };
250
+ // Check for cache hit via header set by cache middleware
251
+ const fromCacheHeader = response.headers.get("X-From-Cache");
252
+ const fromCache = fromCacheHeader === "1" ||
253
+ response.status === 304;
254
+ const result = {
255
+ statusCode: response.status,
256
+ durationMs,
257
+ fromCache,
258
+ };
259
+ try {
260
+ hooks.onRequestEnd?.(info, result);
261
+ }
262
+ catch {
263
+ // Hooks should not interrupt the response
264
+ }
265
+ return response;
266
+ },
267
+ };
268
+ }
269
+ const MAX_CACHE_ENTRIES = 1000;
270
+ function createCacheMiddleware() {
271
+ // Use Map for insertion-order iteration (approximates LRU)
272
+ const cache = new Map();
273
+ // Store cache keys per-request without leaking them onto the wire.
274
+ const cacheKeyStore = new WeakMap();
275
+ // Derive a short token hash from the Authorization header for cache key isolation.
276
+ // Different auth contexts must not share cached responses.
277
+ // Re-computed per request so refreshed tokens produce new cache keys.
278
+ //
279
+ // Security: The map is bounded to MAX_TOKEN_HASH_ENTRIES to prevent unbounded growth.
280
+ // LRU-like eviction removes oldest entries when the limit is reached.
281
+ const MAX_TOKEN_HASH_ENTRIES = 100;
282
+ const hashTokenMap = new Map();
283
+ // Track pending hash computations to coalesce concurrent requests for the same token.
284
+ // This prevents duplicate crypto operations during async interleaving.
285
+ const pendingHashes = new Map();
286
+ const evictOldestHash = () => {
287
+ if (hashTokenMap.size >= MAX_TOKEN_HASH_ENTRIES) {
288
+ // Delete oldest entry (first key in insertion order)
289
+ const firstKey = hashTokenMap.keys().next().value;
290
+ if (firstKey)
291
+ hashTokenMap.delete(firstKey);
292
+ }
293
+ };
294
+ const getTokenHash = async (authHeader) => {
295
+ if (!authHeader)
296
+ return "";
297
+ // Check completed cache first
298
+ const cached = hashTokenMap.get(authHeader);
299
+ if (cached)
300
+ return cached;
301
+ // Check if computation already in progress (coalesce concurrent requests)
302
+ const pending = pendingHashes.get(authHeader);
303
+ if (pending)
304
+ return pending;
305
+ // Start new computation with promise coalescing
306
+ const promise = (async () => {
307
+ const data = new TextEncoder().encode(authHeader);
308
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
309
+ const hashArray = new Uint8Array(hashBuffer);
310
+ const hash = Array.from(hashArray.slice(0, 8))
311
+ .map((b) => b.toString(16).padStart(2, "0"))
312
+ .join("");
313
+ // Evict oldest before adding new entry
314
+ evictOldestHash();
315
+ hashTokenMap.set(authHeader, hash);
316
+ return hash;
317
+ })();
318
+ pendingHashes.set(authHeader, promise);
319
+ promise.finally(() => pendingHashes.delete(authHeader));
320
+ return promise;
321
+ };
322
+ const evictOldest = () => {
323
+ if (cache.size >= MAX_CACHE_ENTRIES) {
324
+ // Delete oldest entry (first key in insertion order)
325
+ const firstKey = cache.keys().next().value;
326
+ if (firstKey)
327
+ cache.delete(firstKey);
328
+ }
329
+ };
330
+ return {
331
+ async onRequest({ request }) {
332
+ if (request.method !== "GET")
333
+ return request;
334
+ const tokenHash = await getTokenHash(request.headers.get("Authorization"));
335
+ const cacheKey = getCacheKey(request.url, tokenHash);
336
+ const entry = cache.get(cacheKey);
337
+ if (entry?.etag) {
338
+ request.headers.set("If-None-Match", entry.etag);
339
+ }
340
+ // Store cache key internally — not on the wire
341
+ cacheKeyStore.set(request, cacheKey);
342
+ return request;
343
+ },
344
+ async onResponse({ request, response }) {
345
+ if (request.method !== "GET")
346
+ return response;
347
+ // Prefer stored key; fall back to recomputing from the Authorization header
348
+ // (handles cases where middleware clones the Request, breaking WeakMap identity).
349
+ const cacheKey = cacheKeyStore.get(request) ??
350
+ getCacheKey(request.url, await getTokenHash(request.headers.get("Authorization")));
351
+ // Handle 304 Not Modified - return cached body with cache indicator
352
+ if (response.status === 304) {
353
+ const entry = cache.get(cacheKey);
354
+ if (entry) {
355
+ const headers = new Headers(response.headers);
356
+ headers.set("X-From-Cache", "1");
357
+ return new Response(entry.body, {
358
+ status: 200,
359
+ headers,
360
+ });
361
+ }
362
+ }
363
+ // Cache successful responses with ETag
364
+ if (response.ok) {
365
+ const etag = response.headers.get("ETag");
366
+ if (etag) {
367
+ const body = await response.clone().text();
368
+ evictOldest();
369
+ cache.set(cacheKey, { etag, body });
370
+ }
371
+ }
372
+ return response;
373
+ },
374
+ };
375
+ }
376
+ function getCacheKey(url, tokenHash) {
377
+ return `${tokenHash}:${url}`;
378
+ }
379
+ /** Default retry config used when no operation-specific config is available */
380
+ const DEFAULT_RETRY_CONFIG = {
381
+ maxAttempts: 3,
382
+ baseDelayMs: 1000,
383
+ backoff: "exponential",
384
+ retryOn: [429, 503],
385
+ };
386
+ const MAX_JITTER_MS = 100;
387
+ // PATH_TO_OPERATION is imported from generated/path-mapping.js
388
+ /**
389
+ * Normalizes a URL path by replacing numeric IDs with placeholder tokens.
390
+ * For example: /12345/todos/456 → /{accountId}/todos/{todoId}
391
+ */
392
+ export function normalizeUrlPath(url) {
393
+ // Parse the URL and extract the pathname
394
+ const urlObj = new URL(url);
395
+ let path = urlObj.pathname;
396
+ // Remove .json suffix if present (we'll add it back for matching)
397
+ const hasJsonSuffix = path.endsWith(".json");
398
+ if (hasJsonSuffix) {
399
+ path = path.slice(0, -5);
400
+ }
401
+ // Split path into segments
402
+ const segments = path.split("/").filter(Boolean);
403
+ // Map of resource names to their ID placeholder tokens
404
+ // Note: Some paths have context-dependent placeholders, but we use consistent
405
+ // placeholders that match our PATH_TO_OPERATION entries
406
+ const idMapping = {
407
+ buckets: "{projectId}",
408
+ projects: "{projectId}",
409
+ templates: "{templateId}",
410
+ card_tables: "{cardTableId}",
411
+ cards: "{cardId}",
412
+ columns: "{columnId}",
413
+ lists: "{columnId}",
414
+ steps: "{stepId}",
415
+ categories: "{typeId}",
416
+ chats: "{campfireId}",
417
+ integrations: "{chatbotId}",
418
+ lines: "{lineId}",
419
+ approvals: "{approvalId}",
420
+ correspondences: "{correspondenceId}",
421
+ replies: "{replyId}",
422
+ recordings: "{recordingId}",
423
+ comments: "{commentId}",
424
+ tools: "{toolId}", // dock/tools/{toolId}
425
+ documents: "{documentId}",
426
+ inbox_forwards: "{forwardId}",
427
+ inboxes: "{inboxId}",
428
+ message_boards: "{boardId}",
429
+ messages: "{messageId}",
430
+ question_answers: "{answerId}",
431
+ questionnaires: "{questionnaireId}",
432
+ questions: "{questionId}",
433
+ by: "{personId}", // questions/{questionId}/answers/by/{personId}
434
+ schedule_entries: "{entryId}",
435
+ occurrences: "{date}", // schedule_entries/{entryId}/occurrences/{date}
436
+ schedules: "{scheduleId}",
437
+ todolists: "{todolistId}", // Also handles {id} and {groupId} via context
438
+ groups: "{groupId}", // todolists/{todolistId}/groups
439
+ todos: "{todoId}",
440
+ todosets: "{todosetId}",
441
+ uploads: "{uploadId}",
442
+ vaults: "{vaultId}",
443
+ webhooks: "{webhookId}",
444
+ timesheet_entries: "{entryId}",
445
+ people: "{personId}",
446
+ markers: "{markerId}", // lineup/markers/{markerId}
447
+ project_constructions: "{constructionId}",
448
+ assigned: "{personId}", // reports/todos/assigned/{personId}
449
+ progress: "{personId}", // reports/users/progress/{personId}
450
+ users: "{personId}", // Alternative for users/progress
451
+ };
452
+ // Context-dependent overrides: when the segment following the ID matches a key,
453
+ // override the placeholder from the default idMapping. This handles cases like
454
+ // /buckets/{id}/webhooks → {bucketId} vs /buckets/{id}/timeline → {projectId}.
455
+ const contextOverrides = {
456
+ buckets: { webhooks: "{bucketId}" },
457
+ };
458
+ // Build normalized path by replacing IDs and dates based on context
459
+ const normalized = [];
460
+ let prevSegment = null;
461
+ let isFirstSegment = true;
462
+ // Pattern for ISO-8601 date (YYYY-MM-DD)
463
+ const datePattern = /^\d{4}-\d{2}-\d{2}$/;
464
+ for (let i = 0; i < segments.length; i++) {
465
+ const segment = segments[i];
466
+ const nextSegment = i + 1 < segments.length ? segments[i + 1] : undefined;
467
+ // Check if this segment is a numeric ID
468
+ if (/^\d+$/.test(segment)) {
469
+ // First numeric segment is always the accountId
470
+ if (isFirstSegment) {
471
+ normalized.push("{accountId}");
472
+ }
473
+ else {
474
+ // Check context-dependent overrides first (look ahead to next segment)
475
+ const overrides = prevSegment ? contextOverrides[prevSegment] : undefined;
476
+ const override = overrides && nextSegment ? overrides[nextSegment] : undefined;
477
+ // Fall back to default idMapping
478
+ const placeholder = override ?? (prevSegment ? idMapping[prevSegment] : undefined);
479
+ normalized.push(placeholder ?? "{id}");
480
+ }
481
+ }
482
+ else if (datePattern.test(segment)) {
483
+ // ISO-8601 date - map based on preceding segment (e.g., occurrences → {date})
484
+ const placeholder = prevSegment ? idMapping[prevSegment] : undefined;
485
+ normalized.push(placeholder ?? "{date}");
486
+ }
487
+ else {
488
+ normalized.push(segment);
489
+ }
490
+ prevSegment = segment;
491
+ isFirstSegment = false;
492
+ }
493
+ // Reconstruct path
494
+ let normalizedPath = "/" + normalized.join("/");
495
+ if (hasJsonSuffix) {
496
+ normalizedPath += ".json";
497
+ }
498
+ return normalizedPath;
499
+ }
500
+ /**
501
+ * Gets the retry config for a specific request based on operation metadata.
502
+ */
503
+ function getRetryConfigForRequest(method, url) {
504
+ const normalizedPath = normalizeUrlPath(url);
505
+ const key = `${method.toUpperCase()}:${normalizedPath}`;
506
+ const operationName = PATH_TO_OPERATION[key];
507
+ if (operationName) {
508
+ const opMeta = metadata.operations[operationName];
509
+ if (opMeta?.retry) {
510
+ return opMeta.retry;
511
+ }
512
+ }
513
+ return DEFAULT_RETRY_CONFIG;
514
+ }
515
+ function createRetryMiddleware(hooks) {
516
+ // Store request body clones keyed by a request identifier
517
+ // This is needed because Request.body can only be read once
518
+ const bodyCache = new Map();
519
+ return {
520
+ async onRequest({ request }) {
521
+ // For methods that may have a body, clone it before the initial fetch
522
+ // so we can use it for retries. Request.body can only be consumed once.
523
+ const method = request.method.toUpperCase();
524
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
525
+ const requestId = `${method}:${request.url}:${Date.now()}`;
526
+ request.headers.set("X-Request-Id", requestId);
527
+ if (request.body) {
528
+ // Clone the body before it gets consumed
529
+ const cloned = request.clone();
530
+ bodyCache.set(requestId, await cloned.arrayBuffer());
531
+ }
532
+ else {
533
+ bodyCache.set(requestId, null);
534
+ }
535
+ }
536
+ return request;
537
+ },
538
+ async onResponse({ request, response }) {
539
+ // Get operation-specific retry config from metadata
540
+ const retryConfig = getRetryConfigForRequest(request.method, request.url);
541
+ const requestId = request.headers.get("X-Request-Id");
542
+ // Helper to clean up cached body
543
+ const cleanupBody = () => {
544
+ if (requestId)
545
+ bodyCache.delete(requestId);
546
+ };
547
+ // Check if status code should trigger retry
548
+ if (!retryConfig.retryOn.includes(response.status)) {
549
+ cleanupBody();
550
+ return response;
551
+ }
552
+ // Extract current retry attempt from custom header
553
+ const attemptHeader = request.headers.get("X-Retry-Attempt");
554
+ const attempt = attemptHeader ? parseInt(attemptHeader, 10) : 0;
555
+ // Check if we've exhausted retries (maxAttempts is total attempts, not retries)
556
+ // With maxAttempts=3: attempt 0 (initial), 1 (retry 1), 2 (retry 2) = 3 total
557
+ if (attempt >= retryConfig.maxAttempts - 1) {
558
+ cleanupBody();
559
+ return response;
560
+ }
561
+ // Calculate delay
562
+ let delay;
563
+ // For 429, respect Retry-After header
564
+ if (response.status === 429) {
565
+ const retryAfter = response.headers.get("Retry-After");
566
+ if (retryAfter) {
567
+ const seconds = parseInt(retryAfter, 10);
568
+ if (!isNaN(seconds)) {
569
+ delay = seconds * 1000;
570
+ }
571
+ else {
572
+ delay = calculateBackoffDelay(retryConfig, attempt);
573
+ }
574
+ }
575
+ else {
576
+ delay = calculateBackoffDelay(retryConfig, attempt);
577
+ }
578
+ }
579
+ else {
580
+ delay = calculateBackoffDelay(retryConfig, attempt);
581
+ }
582
+ // Notify hooks of retry
583
+ if (hooks?.onRetry) {
584
+ const info = {
585
+ method: request.method,
586
+ url: request.url,
587
+ attempt: attempt + 1,
588
+ };
589
+ const error = new Error(`HTTP ${response.status}: ${response.statusText || "Request failed"}`);
590
+ try {
591
+ hooks.onRetry(info, attempt + 1, error, delay);
592
+ }
593
+ catch {
594
+ // Hooks should not interrupt the retry
595
+ }
596
+ }
597
+ // Wait before retry
598
+ await sleep(delay);
599
+ // Get cached body for methods that may have one
600
+ let body = null;
601
+ if (requestId && bodyCache.has(requestId)) {
602
+ const cachedBody = bodyCache.get(requestId);
603
+ if (cachedBody) {
604
+ body = cachedBody;
605
+ }
606
+ }
607
+ // Create retry request with fresh body
608
+ const retryRequest = new Request(request.url, {
609
+ method: request.method,
610
+ headers: new Headers(request.headers),
611
+ body,
612
+ signal: request.signal,
613
+ });
614
+ retryRequest.headers.set("X-Retry-Attempt", String(attempt + 1));
615
+ // Retry using native fetch
616
+ return fetch(retryRequest);
617
+ },
618
+ };
619
+ }
620
+ function calculateBackoffDelay(config, attempt) {
621
+ const base = config.baseDelayMs;
622
+ let delay;
623
+ switch (config.backoff) {
624
+ case "exponential":
625
+ delay = base * Math.pow(2, attempt);
626
+ break;
627
+ case "linear":
628
+ delay = base * (attempt + 1);
629
+ break;
630
+ case "constant":
631
+ default:
632
+ delay = base;
633
+ }
634
+ // Add jitter (0-100ms)
635
+ const jitter = Math.random() * MAX_JITTER_MS;
636
+ return delay + jitter;
637
+ }
638
+ function sleep(ms) {
639
+ return new Promise((resolve) => setTimeout(resolve, ms));
640
+ }
641
+ // =============================================================================
642
+ // Pagination Helper
643
+ // =============================================================================
644
+ /**
645
+ * Fetches all pages of a paginated resource using Link header pagination.
646
+ * Automatically follows rel="next" links until no more pages exist.
647
+ *
648
+ * @example
649
+ * ```ts
650
+ * const response = await client.GET("/projects.json");
651
+ *
652
+ * const allProjects = await fetchAllPages(
653
+ * response.response,
654
+ * (r) => r.json()
655
+ * );
656
+ * ```
657
+ */
658
+ export async function fetchAllPages(initialResponse, parse, authHeader) {
659
+ const results = [];
660
+ let response = initialResponse;
661
+ while (true) {
662
+ const items = await parse(response.clone());
663
+ results.push(...items);
664
+ const rawNextUrl = parseNextLink(response.headers.get("Link"));
665
+ if (!rawNextUrl)
666
+ break;
667
+ // Resolve relative URLs against the current page URL (handles path-relative links)
668
+ const nextUrl = resolveURL(response.url, rawNextUrl);
669
+ // Validate same-origin to prevent SSRF / token leakage via poisoned Link headers
670
+ if (!isSameOrigin(nextUrl, initialResponse.url)) {
671
+ throw new Error(`Pagination Link header points to different origin: ${nextUrl}`);
672
+ }
673
+ const headers = { Accept: "application/json" };
674
+ if (authHeader) {
675
+ headers["Authorization"] = authHeader;
676
+ }
677
+ response = await fetch(nextUrl, { headers });
678
+ }
679
+ return results;
680
+ }
681
+ /**
682
+ * Async generator that yields pages of results one at a time.
683
+ * Useful for processing large datasets without loading everything into memory.
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * for await (const page of paginateAll(response.response, (r) => r.json())) {
688
+ * console.log(`Processing ${page.length} items`);
689
+ * }
690
+ * ```
691
+ */
692
+ export async function* paginateAll(initialResponse, parse, authHeader) {
693
+ let response = initialResponse;
694
+ while (true) {
695
+ const items = await parse(response.clone());
696
+ yield items;
697
+ const rawNextUrl = parseNextLink(response.headers.get("Link"));
698
+ if (!rawNextUrl)
699
+ break;
700
+ // Resolve relative URLs against the current page URL (handles path-relative links)
701
+ const nextUrl = resolveURL(response.url, rawNextUrl);
702
+ // Validate same-origin to prevent SSRF / token leakage via poisoned Link headers
703
+ if (!isSameOrigin(nextUrl, initialResponse.url)) {
704
+ throw new Error(`Pagination Link header points to different origin: ${nextUrl}`);
705
+ }
706
+ const headers = { Accept: "application/json" };
707
+ if (authHeader) {
708
+ headers["Authorization"] = authHeader;
709
+ }
710
+ response = await fetch(nextUrl, { headers });
711
+ }
712
+ }
713
+ // Re-export pagination utilities (defined in pagination-utils.ts to avoid circular deps)
714
+ export { parseNextLink, resolveURL, isSameOrigin };
715
+ //# sourceMappingURL=client.js.map