@37signals/basecamp 0.12.0 → 0.14.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 (294) hide show
  1. package/README.md +114 -17
  2. package/dist/.build-complete +0 -0
  3. package/dist/client.d.ts +53 -11
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +132 -150
  6. package/dist/client.js.map +1 -1
  7. package/dist/download.d.ts +7 -0
  8. package/dist/download.d.ts.map +1 -1
  9. package/dist/download.js +82 -22
  10. package/dist/download.js.map +1 -1
  11. package/dist/errors.d.ts +19 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +138 -14
  14. package/dist/errors.js.map +1 -1
  15. package/dist/generated/metadata.d.ts.map +1 -1
  16. package/dist/generated/metadata.js +291 -141
  17. package/dist/generated/metadata.js.map +1 -1
  18. package/dist/generated/metadata.ts +291 -141
  19. package/dist/generated/openapi-stripped.json +5954 -3523
  20. package/dist/generated/path-mapping.d.ts.map +1 -1
  21. package/dist/generated/path-mapping.js +28 -16
  22. package/dist/generated/path-mapping.js.map +1 -1
  23. package/dist/generated/path-mapping.ts +28 -16
  24. package/dist/generated/schema.d.ts +4390 -2357
  25. package/dist/generated/services/bookmarks.d.ts +1 -1
  26. package/dist/generated/services/bookmarks.d.ts.map +1 -1
  27. package/dist/generated/services/bookmarks.ts +1 -1
  28. package/dist/generated/services/boosts.d.ts +10 -0
  29. package/dist/generated/services/boosts.d.ts.map +1 -1
  30. package/dist/generated/services/boosts.js +8 -0
  31. package/dist/generated/services/boosts.js.map +1 -1
  32. package/dist/generated/services/boosts.ts +12 -0
  33. package/dist/generated/services/campfires.d.ts +54 -40
  34. package/dist/generated/services/campfires.d.ts.map +1 -1
  35. package/dist/generated/services/campfires.js +82 -65
  36. package/dist/generated/services/campfires.js.map +1 -1
  37. package/dist/generated/services/campfires.ts +107 -85
  38. package/dist/generated/services/cards.d.ts +5 -0
  39. package/dist/generated/services/cards.d.ts.map +1 -1
  40. package/dist/generated/services/cards.js +4 -0
  41. package/dist/generated/services/cards.js.map +1 -1
  42. package/dist/generated/services/cards.ts +6 -0
  43. package/dist/generated/services/checkins.d.ts +14 -0
  44. package/dist/generated/services/checkins.d.ts.map +1 -1
  45. package/dist/generated/services/checkins.js +14 -1
  46. package/dist/generated/services/checkins.js.map +1 -1
  47. package/dist/generated/services/checkins.ts +20 -0
  48. package/dist/generated/services/client-approvals.d.ts +6 -3
  49. package/dist/generated/services/client-approvals.d.ts.map +1 -1
  50. package/dist/generated/services/client-approvals.js +8 -5
  51. package/dist/generated/services/client-approvals.js.map +1 -1
  52. package/dist/generated/services/client-approvals.ts +10 -5
  53. package/dist/generated/services/client-correspondences.d.ts +6 -3
  54. package/dist/generated/services/client-correspondences.d.ts.map +1 -1
  55. package/dist/generated/services/client-correspondences.js +8 -5
  56. package/dist/generated/services/client-correspondences.js.map +1 -1
  57. package/dist/generated/services/client-correspondences.ts +10 -5
  58. package/dist/generated/services/client-replies.d.ts +11 -4
  59. package/dist/generated/services/client-replies.d.ts.map +1 -1
  60. package/dist/generated/services/client-replies.js +16 -8
  61. package/dist/generated/services/client-replies.js.map +1 -1
  62. package/dist/generated/services/client-replies.ts +18 -8
  63. package/dist/generated/services/cloud-files.d.ts +92 -0
  64. package/dist/generated/services/cloud-files.d.ts.map +1 -0
  65. package/dist/generated/services/cloud-files.js +121 -0
  66. package/dist/generated/services/cloud-files.js.map +1 -0
  67. package/dist/generated/services/cloud-files.ts +188 -0
  68. package/dist/generated/services/comments.d.ts +5 -0
  69. package/dist/generated/services/comments.d.ts.map +1 -1
  70. package/dist/generated/services/comments.js +4 -0
  71. package/dist/generated/services/comments.js.map +1 -1
  72. package/dist/generated/services/comments.ts +6 -0
  73. package/dist/generated/services/documents.d.ts +12 -7
  74. package/dist/generated/services/documents.d.ts.map +1 -1
  75. package/dist/generated/services/documents.js +10 -6
  76. package/dist/generated/services/documents.js.map +1 -1
  77. package/dist/generated/services/documents.ts +14 -8
  78. package/dist/generated/services/drafts.d.ts +1 -1
  79. package/dist/generated/services/drafts.d.ts.map +1 -1
  80. package/dist/generated/services/drafts.ts +1 -1
  81. package/dist/generated/services/events.d.ts +5 -0
  82. package/dist/generated/services/events.d.ts.map +1 -1
  83. package/dist/generated/services/events.js +4 -0
  84. package/dist/generated/services/events.js.map +1 -1
  85. package/dist/generated/services/events.ts +6 -0
  86. package/dist/generated/services/everything.d.ts +14 -14
  87. package/dist/generated/services/everything.d.ts.map +1 -1
  88. package/dist/generated/services/everything.ts +14 -14
  89. package/dist/generated/services/folders.d.ts +96 -0
  90. package/dist/generated/services/folders.d.ts.map +1 -0
  91. package/dist/generated/services/folders.js +140 -0
  92. package/dist/generated/services/folders.js.map +1 -0
  93. package/dist/generated/services/folders.ts +201 -0
  94. package/dist/generated/services/forwards.d.ts +7 -20
  95. package/dist/generated/services/forwards.d.ts.map +1 -1
  96. package/dist/generated/services/forwards.js +6 -35
  97. package/dist/generated/services/forwards.js.map +1 -1
  98. package/dist/generated/services/forwards.ts +10 -48
  99. package/dist/generated/services/gauges.d.ts +7 -0
  100. package/dist/generated/services/gauges.d.ts.map +1 -1
  101. package/dist/generated/services/gauges.js +5 -1
  102. package/dist/generated/services/gauges.js.map +1 -1
  103. package/dist/generated/services/gauges.ts +9 -1
  104. package/dist/generated/services/google-documents.d.ts +98 -0
  105. package/dist/generated/services/google-documents.d.ts.map +1 -0
  106. package/dist/generated/services/google-documents.js +123 -0
  107. package/dist/generated/services/google-documents.js.map +1 -0
  108. package/dist/generated/services/google-documents.ts +196 -0
  109. package/dist/generated/services/index.d.ts +7 -4
  110. package/dist/generated/services/index.d.ts.map +1 -1
  111. package/dist/generated/services/index.js +7 -4
  112. package/dist/generated/services/index.js.map +1 -1
  113. package/dist/generated/services/index.ts +7 -4
  114. package/dist/generated/services/messages.d.ts +2 -0
  115. package/dist/generated/services/messages.d.ts.map +1 -1
  116. package/dist/generated/services/messages.js +1 -1
  117. package/dist/generated/services/messages.js.map +1 -1
  118. package/dist/generated/services/messages.ts +3 -1
  119. package/dist/generated/services/my-notes.d.ts +1 -1
  120. package/dist/generated/services/my-notes.js +1 -1
  121. package/dist/generated/services/my-notes.ts +1 -1
  122. package/dist/generated/services/my-notifications.d.ts +4 -2
  123. package/dist/generated/services/my-notifications.d.ts.map +1 -1
  124. package/dist/generated/services/my-notifications.js.map +1 -1
  125. package/dist/generated/services/my-notifications.ts +4 -2
  126. package/dist/generated/services/people.d.ts +11 -1
  127. package/dist/generated/services/people.d.ts.map +1 -1
  128. package/dist/generated/services/people.js +13 -2
  129. package/dist/generated/services/people.js.map +1 -1
  130. package/dist/generated/services/people.ts +15 -1
  131. package/dist/generated/services/projects.d.ts +26 -0
  132. package/dist/generated/services/projects.d.ts.map +1 -1
  133. package/dist/generated/services/projects.js +49 -1
  134. package/dist/generated/services/projects.js.map +1 -1
  135. package/dist/generated/services/projects.ts +61 -1
  136. package/dist/generated/services/recordings.d.ts +2 -12
  137. package/dist/generated/services/recordings.d.ts.map +1 -1
  138. package/dist/generated/services/recordings.js +1 -26
  139. package/dist/generated/services/recordings.js.map +1 -1
  140. package/dist/generated/services/recordings.ts +3 -31
  141. package/dist/generated/services/reports.d.ts +8 -12
  142. package/dist/generated/services/reports.d.ts.map +1 -1
  143. package/dist/generated/services/reports.js +11 -5
  144. package/dist/generated/services/reports.js.map +1 -1
  145. package/dist/generated/services/reports.ts +13 -14
  146. package/dist/generated/services/schedules.d.ts +94 -15
  147. package/dist/generated/services/schedules.d.ts.map +1 -1
  148. package/dist/generated/services/schedules.js +19 -8
  149. package/dist/generated/services/schedules.js.map +1 -1
  150. package/dist/generated/services/schedules.ts +107 -17
  151. package/dist/generated/services/search.d.ts +2 -0
  152. package/dist/generated/services/search.d.ts.map +1 -1
  153. package/dist/generated/services/search.js +1 -1
  154. package/dist/generated/services/search.js.map +1 -1
  155. package/dist/generated/services/search.ts +3 -1
  156. package/dist/generated/services/templates.d.ts +2 -0
  157. package/dist/generated/services/templates.d.ts.map +1 -1
  158. package/dist/generated/services/templates.js +1 -1
  159. package/dist/generated/services/templates.js.map +1 -1
  160. package/dist/generated/services/templates.ts +3 -1
  161. package/dist/generated/services/timeline.d.ts +2 -0
  162. package/dist/generated/services/timeline.d.ts.map +1 -1
  163. package/dist/generated/services/timeline.js +1 -0
  164. package/dist/generated/services/timeline.js.map +1 -1
  165. package/dist/generated/services/timeline.ts +3 -0
  166. package/dist/generated/services/timesheets.d.ts +16 -0
  167. package/dist/generated/services/timesheets.d.ts.map +1 -1
  168. package/dist/generated/services/timesheets.js +26 -2
  169. package/dist/generated/services/timesheets.js.map +1 -1
  170. package/dist/generated/services/timesheets.ts +35 -2
  171. package/dist/generated/services/todolist-groups.d.ts +11 -6
  172. package/dist/generated/services/todolist-groups.d.ts.map +1 -1
  173. package/dist/generated/services/todolist-groups.js +7 -3
  174. package/dist/generated/services/todolist-groups.js.map +1 -1
  175. package/dist/generated/services/todolist-groups.ts +13 -7
  176. package/dist/generated/services/todolists.d.ts +15 -13
  177. package/dist/generated/services/todolists.d.ts.map +1 -1
  178. package/dist/generated/services/todolists.js +11 -8
  179. package/dist/generated/services/todolists.js.map +1 -1
  180. package/dist/generated/services/todolists.ts +19 -14
  181. package/dist/generated/services/todos.d.ts +2 -12
  182. package/dist/generated/services/todos.d.ts.map +1 -1
  183. package/dist/generated/services/todos.js +1 -25
  184. package/dist/generated/services/todos.js.map +1 -1
  185. package/dist/generated/services/todos.ts +3 -30
  186. package/dist/generated/services/uploads.d.ts +41 -2
  187. package/dist/generated/services/uploads.d.ts.map +1 -1
  188. package/dist/generated/services/uploads.js +41 -1
  189. package/dist/generated/services/uploads.js.map +1 -1
  190. package/dist/generated/services/uploads.ts +71 -2
  191. package/dist/generated/services/vaults.d.ts +5 -0
  192. package/dist/generated/services/vaults.d.ts.map +1 -1
  193. package/dist/generated/services/vaults.js +4 -0
  194. package/dist/generated/services/vaults.js.map +1 -1
  195. package/dist/generated/services/vaults.ts +6 -0
  196. package/dist/index.d.ts +16 -10
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js +19 -8
  199. package/dist/index.js.map +1 -1
  200. package/dist/oauth/authorization-document.d.ts +201 -0
  201. package/dist/oauth/authorization-document.d.ts.map +1 -0
  202. package/dist/oauth/authorization-document.js +130 -0
  203. package/dist/oauth/authorization-document.js.map +1 -0
  204. package/dist/oauth/identity.d.ts +7 -1
  205. package/dist/oauth/identity.d.ts.map +1 -1
  206. package/dist/oauth/identity.js +8 -19
  207. package/dist/oauth/identity.js.map +1 -1
  208. package/dist/oauth/index.d.ts +1 -0
  209. package/dist/oauth/index.d.ts.map +1 -1
  210. package/dist/oauth/index.js +2 -0
  211. package/dist/oauth/index.js.map +1 -1
  212. package/dist/pagination-utils.d.ts +46 -0
  213. package/dist/pagination-utils.d.ts.map +1 -1
  214. package/dist/pagination-utils.js +85 -2
  215. package/dist/pagination-utils.js.map +1 -1
  216. package/dist/pagination.d.ts +11 -0
  217. package/dist/pagination.d.ts.map +1 -1
  218. package/dist/pagination.js.map +1 -1
  219. package/dist/retry.d.ts +110 -0
  220. package/dist/retry.d.ts.map +1 -0
  221. package/dist/retry.js +253 -0
  222. package/dist/retry.js.map +1 -0
  223. package/dist/services/authorization.d.ts +22 -52
  224. package/dist/services/authorization.d.ts.map +1 -1
  225. package/dist/services/authorization.js +14 -32
  226. package/dist/services/authorization.js.map +1 -1
  227. package/dist/services/base.d.ts.map +1 -1
  228. package/dist/services/base.js +59 -5
  229. package/dist/services/base.js.map +1 -1
  230. package/dist/services/cards-extensions.d.ts +26 -21
  231. package/dist/services/cards-extensions.d.ts.map +1 -1
  232. package/dist/services/cards-extensions.js +28 -31
  233. package/dist/services/cards-extensions.js.map +1 -1
  234. package/dist/services/documents-extensions.d.ts +122 -0
  235. package/dist/services/documents-extensions.d.ts.map +1 -0
  236. package/dist/services/documents-extensions.js +125 -0
  237. package/dist/services/documents-extensions.js.map +1 -0
  238. package/dist/services/merge-safe.d.ts +167 -0
  239. package/dist/services/merge-safe.d.ts.map +1 -0
  240. package/dist/services/merge-safe.js +217 -0
  241. package/dist/services/merge-safe.js.map +1 -0
  242. package/dist/services/schedules-extensions.d.ts +266 -0
  243. package/dist/services/schedules-extensions.d.ts.map +1 -0
  244. package/dist/services/schedules-extensions.js +268 -0
  245. package/dist/services/schedules-extensions.js.map +1 -0
  246. package/dist/services/todolists-extensions.d.ts +110 -0
  247. package/dist/services/todolists-extensions.d.ts.map +1 -0
  248. package/dist/services/todolists-extensions.js +262 -0
  249. package/dist/services/todolists-extensions.js.map +1 -0
  250. package/dist/services/todos-extensions.d.ts +12 -1
  251. package/dist/services/todos-extensions.d.ts.map +1 -1
  252. package/dist/services/todos-extensions.js +27 -8
  253. package/dist/services/todos-extensions.js.map +1 -1
  254. package/package.json +6 -5
  255. package/src/generated/metadata.ts +291 -141
  256. package/src/generated/openapi-stripped.json +5954 -3523
  257. package/src/generated/path-mapping.ts +28 -16
  258. package/src/generated/schema.d.ts +4390 -2357
  259. package/src/generated/services/bookmarks.ts +1 -1
  260. package/src/generated/services/boosts.ts +12 -0
  261. package/src/generated/services/campfires.ts +107 -85
  262. package/src/generated/services/cards.ts +6 -0
  263. package/src/generated/services/checkins.ts +20 -0
  264. package/src/generated/services/client-approvals.ts +10 -5
  265. package/src/generated/services/client-correspondences.ts +10 -5
  266. package/src/generated/services/client-replies.ts +18 -8
  267. package/src/generated/services/cloud-files.ts +188 -0
  268. package/src/generated/services/comments.ts +6 -0
  269. package/src/generated/services/documents.ts +14 -8
  270. package/src/generated/services/drafts.ts +1 -1
  271. package/src/generated/services/events.ts +6 -0
  272. package/src/generated/services/everything.ts +14 -14
  273. package/src/generated/services/folders.ts +201 -0
  274. package/src/generated/services/forwards.ts +10 -48
  275. package/src/generated/services/gauges.ts +9 -1
  276. package/src/generated/services/google-documents.ts +196 -0
  277. package/src/generated/services/index.ts +7 -4
  278. package/src/generated/services/messages.ts +3 -1
  279. package/src/generated/services/my-notes.ts +1 -1
  280. package/src/generated/services/my-notifications.ts +4 -2
  281. package/src/generated/services/people.ts +15 -1
  282. package/src/generated/services/projects.ts +61 -1
  283. package/src/generated/services/recordings.ts +3 -31
  284. package/src/generated/services/reports.ts +13 -14
  285. package/src/generated/services/schedules.ts +107 -17
  286. package/src/generated/services/search.ts +3 -1
  287. package/src/generated/services/templates.ts +3 -1
  288. package/src/generated/services/timeline.ts +3 -0
  289. package/src/generated/services/timesheets.ts +35 -2
  290. package/src/generated/services/todolist-groups.ts +13 -7
  291. package/src/generated/services/todolists.ts +19 -14
  292. package/src/generated/services/todos.ts +3 -30
  293. package/src/generated/services/uploads.ts +71 -2
  294. package/src/generated/services/vaults.ts +6 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The authorization document, as served by both issuers.
3
+ *
4
+ * Two different services fetch this document — {@link ../services/authorization.js | AuthorizationService}
5
+ * and {@link ./identity.js | discoverIdentity} — and they used to carry a copy of
6
+ * the mapping each. The copies had already drifted apart (`app_href` was required
7
+ * in one and optional in the other), which is exactly how a shape fix gets applied
8
+ * to one caller and missed on the other. The mapping lives here once.
9
+ *
10
+ * This module is a leaf on purpose: it imports nothing from the SDK, so either
11
+ * caller can take a *value* dependency on it without adding a runtime edge to the
12
+ * `base.ts` / `client.ts` cycle.
13
+ *
14
+ * ## Two issuers, two shapes
15
+ *
16
+ * Launchpad and a BC5 (bc3) issuer both serve `GET /authorization.json`, and the
17
+ * documents are not the same shape. bc3 renders its own from
18
+ * `app/views/api/authorizations/show.json.jbuilder`:
19
+ *
20
+ * | Field | Launchpad | BC5 |
21
+ * |---|---|---|
22
+ * | `identity.first_name` / `last_name` / `email_address` | present | absent |
23
+ * | `accounts[].product` / `app_href` | present | absent |
24
+ * | `accounts[].resource` | absent | present (RFC 8707 indicator) |
25
+ * | `scope` | absent | present, BC3-issued tokens only |
26
+ * | `expires_at` | ISO-8601 string | integer epoch **seconds** |
27
+ *
28
+ * Every field either issuer omits is typed optional here. The union is modelled
29
+ * rather than either shape alone, because a consumer reaches a BC5 issuer just by
30
+ * passing `endpoint:`, and BC5 issuers serving their own document is the intended
31
+ * end state.
32
+ *
33
+ * @see spec/api-gaps/bc5-authorization-document-shape.md
34
+ */
35
+ /**
36
+ * The authenticated user's identity.
37
+ *
38
+ * Only `id` is guaranteed. A BC5 issuer emits nothing but the identity id — it
39
+ * deliberately drops the PII that the API docs already say not to use for
40
+ * identifying users — so the name and email fields are absent there.
41
+ */
42
+ export interface Identity {
43
+ /** User's unique identifier. Emitted by both issuers. */
44
+ id: number;
45
+ /** User's first name. Launchpad only — `undefined` from a BC5 issuer. */
46
+ firstName?: string;
47
+ /** User's last name. Launchpad only — `undefined` from a BC5 issuer. */
48
+ lastName?: string;
49
+ /** User's email address. Launchpad only — `undefined` from a BC5 issuer. */
50
+ emailAddress?: string;
51
+ }
52
+ /**
53
+ * A Basecamp account the user has access to.
54
+ */
55
+ export interface AuthorizedAccount {
56
+ /** Account's unique identifier. */
57
+ id: number;
58
+ /** Account name. */
59
+ name: string;
60
+ /**
61
+ * Product type (e.g. `"bc3"` for Basecamp, `"hey"` for HEY).
62
+ *
63
+ * Launchpad only. A BC5 issuer serves one product by construction and omits
64
+ * this, which is why {@link filterAccountsByProduct} treats a non-empty
65
+ * document with no `product` anywhere as one the filter cannot apply to.
66
+ */
67
+ product?: string;
68
+ /** API URL for this account. Emitted by both issuers. */
69
+ href: string;
70
+ /** Web app URL for this account. Launchpad only. */
71
+ appHref?: string;
72
+ /**
73
+ * RFC 8707 resource indicator for this account (`urn:bc:account:<id>`).
74
+ *
75
+ * BC5 issuers only. Pass it as the `resource` parameter when requesting a
76
+ * token scoped to this account.
77
+ */
78
+ resource?: string;
79
+ /** Whether the account is hidden from the user's view. */
80
+ hidden?: boolean;
81
+ /** Whether the account subscription has expired. */
82
+ expired?: boolean;
83
+ /** Whether this is the user's featured/primary account. */
84
+ featured?: boolean;
85
+ }
86
+ /**
87
+ * Authorization information response.
88
+ */
89
+ export interface AuthorizationInfo {
90
+ /**
91
+ * Token expiration timestamp.
92
+ *
93
+ * Parsed from either issuer's spelling — see {@link parseExpiresAt}.
94
+ */
95
+ expiresAt: Date;
96
+ /** The authenticated user's identity. */
97
+ identity: Identity;
98
+ /** List of accounts the user can access. */
99
+ accounts: AuthorizedAccount[];
100
+ /**
101
+ * The token's granted scope.
102
+ *
103
+ * BC5 issuers only, and only for BC3-issued tokens — legacy Signal tokens
104
+ * predate scopes, so its absence is not an error.
105
+ */
106
+ scope?: string;
107
+ /**
108
+ * Whether a requested `product` filter was actually applied.
109
+ *
110
+ * Only meaningful when a product filter was requested. `false` means the
111
+ * document carried at least one account and no `product` on any of them — a
112
+ * BC5 document — so the filter was inapplicable and `accounts` is unfiltered
113
+ * rather than empty. An empty account list reports `true`: it is no evidence
114
+ * about the issuer either way. See {@link filterAccountsByProduct}.
115
+ */
116
+ productFilterApplied?: boolean;
117
+ }
118
+ /**
119
+ * The raw wire document, covering both issuers.
120
+ *
121
+ * Every field that *either* issuer omits is optional, so this type describes what
122
+ * can actually arrive rather than what one issuer happens to send. `identity` and
123
+ * `accounts` are the two that stay required: both issuers emit both, so a document
124
+ * missing either is malformed, and throwing on it beats inventing an empty account
125
+ * list that a caller cannot distinguish from a real one.
126
+ */
127
+ export interface RawAuthorizationDocument {
128
+ /** ISO-8601 string (Launchpad) or integer epoch seconds (BC5). */
129
+ expires_at?: string | number | null;
130
+ /** Both issuers always emit this, so it stays required — see the note below. */
131
+ identity: {
132
+ id: number;
133
+ first_name?: string;
134
+ last_name?: string;
135
+ email_address?: string;
136
+ };
137
+ /** Both issuers always emit this, so it stays required — see the note below. */
138
+ accounts: Array<{
139
+ id: number;
140
+ name: string;
141
+ product?: string;
142
+ href: string;
143
+ app_href?: string;
144
+ resource?: string;
145
+ hidden?: boolean;
146
+ expired?: boolean;
147
+ featured?: boolean;
148
+ }>;
149
+ scope?: string;
150
+ }
151
+ /**
152
+ * Parses `expires_at` from either issuer's spelling.
153
+ *
154
+ * Launchpad sends an ISO-8601 string; bc3 sends `@token.expires_at.to_i`, an
155
+ * integer epoch **seconds**. The distinction is the whole point of branching on
156
+ * `typeof`: `new Date(2085213356)` treats the number as *milliseconds* and yields
157
+ * a date in 1970 — a wrong answer rather than an exception, which then reads as
158
+ * an expired credential rather than a schema mismatch.
159
+ *
160
+ * bc3 renders `.to_i`, so a nil expiry arrives as `0`, never `null`. A `0`, a
161
+ * `null` and an absent field are all "no expiry known" and produce an Invalid
162
+ * Date, which is what a `Date`-typed field can say about the absence.
163
+ */
164
+ export declare function parseExpiresAt(value: string | number | null | undefined): Date;
165
+ /**
166
+ * Filters accounts by product, or reports that it could not.
167
+ *
168
+ * A BC5 document carries no `product` on any account, so filtering it by product
169
+ * matches nothing. Returning `[]` there is silently wrong — the accounts exist,
170
+ * and the caller's next step is to pick a `href` out of a list the SDK just
171
+ * emptied. Instead: when *no* account in the document carries a `product`, the
172
+ * filter is inapplicable, and all accounts are returned with `applied: false` so
173
+ * a caller that cares can tell the two situations apart.
174
+ *
175
+ * When at least one account carries a `product`, the filter is meaningful and is
176
+ * applied normally — an empty result then genuinely means "no account matched".
177
+ *
178
+ * An **empty** account list is the edge that needs saying out loud. "No account
179
+ * carries a `product`" is vacuously true of `[]`, which would report the filter
180
+ * inapplicable — but Launchpad returns `accounts: []` for an identity with no
181
+ * currently accessible accounts, and that document is perfectly filterable. The
182
+ * result is the same list either way, so nothing observable changes; what changes
183
+ * is the claim the flag makes, and `applied: false` would assert "this issuer
184
+ * cannot filter by product" on no evidence at all. An empty list carries no
185
+ * evidence about the issuer, so treat the filter as applied: filtering nothing by
186
+ * anything yields nothing, which is exactly what happened.
187
+ */
188
+ export declare function filterAccountsByProduct(accounts: AuthorizedAccount[], product: string): {
189
+ accounts: AuthorizedAccount[];
190
+ applied: boolean;
191
+ };
192
+ /**
193
+ * Maps a raw authorization document to {@link AuthorizationInfo}.
194
+ *
195
+ * Optionally applies a product filter; see {@link filterAccountsByProduct} for
196
+ * what happens when the document cannot be filtered by product.
197
+ */
198
+ export declare function parseAuthorizationDocument(raw: RawAuthorizationDocument, options?: {
199
+ filterProduct?: string;
200
+ }): AuthorizationInfo;
201
+ //# sourceMappingURL=authorization-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-document.d.ts","sourceRoot":"","sources":["../../src/oauth/authorization-document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,EAAE,IAAI,CAAC;IAChB,yCAAyC;IACzC,QAAQ,EAAE,QAAQ,CAAC;IACnB,4CAA4C;IAC5C,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,gFAAgF;IAChF,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,gFAAgF;IAChF,QAAQ,EAAE,KAAK,CAAC;QACd,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAS9E;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,OAAO,EAAE,MAAM,GACd;IAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAMrD;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,wBAAwB,EAC7B,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACvC,iBAAiB,CAqCnB"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The authorization document, as served by both issuers.
3
+ *
4
+ * Two different services fetch this document — {@link ../services/authorization.js | AuthorizationService}
5
+ * and {@link ./identity.js | discoverIdentity} — and they used to carry a copy of
6
+ * the mapping each. The copies had already drifted apart (`app_href` was required
7
+ * in one and optional in the other), which is exactly how a shape fix gets applied
8
+ * to one caller and missed on the other. The mapping lives here once.
9
+ *
10
+ * This module is a leaf on purpose: it imports nothing from the SDK, so either
11
+ * caller can take a *value* dependency on it without adding a runtime edge to the
12
+ * `base.ts` / `client.ts` cycle.
13
+ *
14
+ * ## Two issuers, two shapes
15
+ *
16
+ * Launchpad and a BC5 (bc3) issuer both serve `GET /authorization.json`, and the
17
+ * documents are not the same shape. bc3 renders its own from
18
+ * `app/views/api/authorizations/show.json.jbuilder`:
19
+ *
20
+ * | Field | Launchpad | BC5 |
21
+ * |---|---|---|
22
+ * | `identity.first_name` / `last_name` / `email_address` | present | absent |
23
+ * | `accounts[].product` / `app_href` | present | absent |
24
+ * | `accounts[].resource` | absent | present (RFC 8707 indicator) |
25
+ * | `scope` | absent | present, BC3-issued tokens only |
26
+ * | `expires_at` | ISO-8601 string | integer epoch **seconds** |
27
+ *
28
+ * Every field either issuer omits is typed optional here. The union is modelled
29
+ * rather than either shape alone, because a consumer reaches a BC5 issuer just by
30
+ * passing `endpoint:`, and BC5 issuers serving their own document is the intended
31
+ * end state.
32
+ *
33
+ * @see spec/api-gaps/bc5-authorization-document-shape.md
34
+ */
35
+ /**
36
+ * Parses `expires_at` from either issuer's spelling.
37
+ *
38
+ * Launchpad sends an ISO-8601 string; bc3 sends `@token.expires_at.to_i`, an
39
+ * integer epoch **seconds**. The distinction is the whole point of branching on
40
+ * `typeof`: `new Date(2085213356)` treats the number as *milliseconds* and yields
41
+ * a date in 1970 — a wrong answer rather than an exception, which then reads as
42
+ * an expired credential rather than a schema mismatch.
43
+ *
44
+ * bc3 renders `.to_i`, so a nil expiry arrives as `0`, never `null`. A `0`, a
45
+ * `null` and an absent field are all "no expiry known" and produce an Invalid
46
+ * Date, which is what a `Date`-typed field can say about the absence.
47
+ */
48
+ export function parseExpiresAt(value) {
49
+ if (typeof value === "number") {
50
+ // Epoch seconds, not milliseconds. `0` is bc3's rendering of a nil expiry.
51
+ return value === 0 ? new Date(NaN) : new Date(value * 1000);
52
+ }
53
+ if (typeof value === "string" && value !== "") {
54
+ return new Date(value);
55
+ }
56
+ return new Date(NaN);
57
+ }
58
+ /**
59
+ * Filters accounts by product, or reports that it could not.
60
+ *
61
+ * A BC5 document carries no `product` on any account, so filtering it by product
62
+ * matches nothing. Returning `[]` there is silently wrong — the accounts exist,
63
+ * and the caller's next step is to pick a `href` out of a list the SDK just
64
+ * emptied. Instead: when *no* account in the document carries a `product`, the
65
+ * filter is inapplicable, and all accounts are returned with `applied: false` so
66
+ * a caller that cares can tell the two situations apart.
67
+ *
68
+ * When at least one account carries a `product`, the filter is meaningful and is
69
+ * applied normally — an empty result then genuinely means "no account matched".
70
+ *
71
+ * An **empty** account list is the edge that needs saying out loud. "No account
72
+ * carries a `product`" is vacuously true of `[]`, which would report the filter
73
+ * inapplicable — but Launchpad returns `accounts: []` for an identity with no
74
+ * currently accessible accounts, and that document is perfectly filterable. The
75
+ * result is the same list either way, so nothing observable changes; what changes
76
+ * is the claim the flag makes, and `applied: false` would assert "this issuer
77
+ * cannot filter by product" on no evidence at all. An empty list carries no
78
+ * evidence about the issuer, so treat the filter as applied: filtering nothing by
79
+ * anything yields nothing, which is exactly what happened.
80
+ */
81
+ export function filterAccountsByProduct(accounts, product) {
82
+ const filterable = accounts.length === 0 || accounts.some((a) => a.product !== undefined);
83
+ if (!filterable) {
84
+ return { accounts, applied: false };
85
+ }
86
+ return { accounts: accounts.filter((a) => a.product === product), applied: true };
87
+ }
88
+ /**
89
+ * Maps a raw authorization document to {@link AuthorizationInfo}.
90
+ *
91
+ * Optionally applies a product filter; see {@link filterAccountsByProduct} for
92
+ * what happens when the document cannot be filtered by product.
93
+ */
94
+ export function parseAuthorizationDocument(raw, options = {}) {
95
+ let accounts = raw.accounts.map((a) => ({
96
+ id: a.id,
97
+ name: a.name,
98
+ product: a.product,
99
+ href: a.href,
100
+ appHref: a.app_href,
101
+ resource: a.resource,
102
+ hidden: a.hidden,
103
+ expired: a.expired,
104
+ featured: a.featured,
105
+ }));
106
+ let productFilterApplied;
107
+ if (options.filterProduct !== undefined && options.filterProduct !== "") {
108
+ const filtered = filterAccountsByProduct(accounts, options.filterProduct);
109
+ accounts = filtered.accounts;
110
+ productFilterApplied = filtered.applied;
111
+ }
112
+ const info = {
113
+ expiresAt: parseExpiresAt(raw.expires_at),
114
+ identity: {
115
+ id: raw.identity.id,
116
+ firstName: raw.identity.first_name,
117
+ lastName: raw.identity.last_name,
118
+ emailAddress: raw.identity.email_address,
119
+ },
120
+ accounts,
121
+ };
122
+ if (raw.scope !== undefined) {
123
+ info.scope = raw.scope;
124
+ }
125
+ if (productFilterApplied !== undefined) {
126
+ info.productFilterApplied = productFilterApplied;
127
+ }
128
+ return info;
129
+ }
130
+ //# sourceMappingURL=authorization-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-document.js","sourceRoot":"","sources":["../../src/oauth/authorization-document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA0HH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,KAAyC;IACtE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,2EAA2E;QAC3E,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAC9C,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,uBAAuB,CACrC,QAA6B,EAC7B,OAAe;IAEf,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;IAC1F,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CACxC,GAA6B,EAC7B,UAAsC,EAAE;IAExC,IAAI,QAAQ,GAAwB,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,QAAQ;QACnB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;KACrB,CAAC,CAAC,CAAC;IAEJ,IAAI,oBAAyC,CAAC;IAC9C,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,IAAI,OAAO,CAAC,aAAa,KAAK,EAAE,EAAE,CAAC;QACxE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,QAAQ,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;QAC1E,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAC7B,oBAAoB,GAAG,QAAQ,CAAC,OAAO,CAAC;IAC1C,CAAC;IAED,MAAM,IAAI,GAAsB;QAC9B,SAAS,EAAE,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC;QACzC,QAAQ,EAAE;YACR,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE;YACnB,SAAS,EAAE,GAAG,CAAC,QAAQ,CAAC,UAAU;YAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,SAAS;YAChC,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,aAAa;SACzC;QACD,QAAQ;KACT,CAAC;IACF,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IACzB,CAAC;IACD,IAAI,oBAAoB,KAAK,SAAS,EAAE,CAAC;QACvC,IAAI,CAAC,oBAAoB,GAAG,oBAAoB,CAAC;IACnD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -3,9 +3,15 @@
3
3
  *
4
4
  * Fetches authorization information (identity and accounts) from
5
5
  * the Launchpad endpoint without requiring a full client instance.
6
+ *
7
+ * The endpoint is fixed to Launchpad here — unlike
8
+ * {@link ../services/authorization.js | AuthorizationService.getInfo}, which
9
+ * accepts an `endpoint:` override and so can be pointed at a BC5 issuer. Both
10
+ * share one mapping regardless; see `./authorization-document.js` for why the two
11
+ * documents differ and how the shared parser reconciles them.
6
12
  */
7
13
  import type { TokenProvider } from "../client.js";
8
- import type { AuthorizationInfo } from "../services/authorization.js";
14
+ import { type AuthorizationInfo } from "./authorization-document.js";
9
15
  /**
10
16
  * Fetches authorization information using an access token.
11
17
  *
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/oauth/identity.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AA2BtE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsD7F"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/oauth/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,6BAA6B,CAAC;AAIrC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoC7F"}
@@ -3,8 +3,15 @@
3
3
  *
4
4
  * Fetches authorization information (identity and accounts) from
5
5
  * the Launchpad endpoint without requiring a full client instance.
6
+ *
7
+ * The endpoint is fixed to Launchpad here — unlike
8
+ * {@link ../services/authorization.js | AuthorizationService.getInfo}, which
9
+ * accepts an `endpoint:` override and so can be pointed at a BC5 issuer. Both
10
+ * share one mapping regardless; see `./authorization-document.js` for why the two
11
+ * documents differ and how the shared parser reconciles them.
6
12
  */
7
13
  import { BasecampError } from "../errors.js";
14
+ import { parseAuthorizationDocument, } from "./authorization-document.js";
8
15
  const AUTHORIZATION_ENDPOINT = "https://launchpad.37signals.com/authorization.json";
9
16
  /**
10
17
  * Fetches authorization information using an access token.
@@ -59,24 +66,6 @@ export async function discoverIdentity(accessToken) {
59
66
  catch {
60
67
  throw new BasecampError("api_error", "Identity discovery returned invalid JSON");
61
68
  }
62
- return {
63
- expiresAt: new Date(raw.expires_at),
64
- identity: {
65
- id: raw.identity.id,
66
- firstName: raw.identity.first_name,
67
- lastName: raw.identity.last_name,
68
- emailAddress: raw.identity.email_address,
69
- },
70
- accounts: raw.accounts.map((a) => ({
71
- id: a.id,
72
- name: a.name,
73
- product: a.product,
74
- href: a.href,
75
- appHref: a.app_href ?? "",
76
- hidden: a.hidden,
77
- expired: a.expired,
78
- featured: a.featured,
79
- })),
80
- };
69
+ return parseAuthorizationDocument(raw);
81
70
  }
82
71
  //# sourceMappingURL=identity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/oauth/identity.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA2B7C,MAAM,sBAAsB,GAAG,oDAAoD,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,WAA0B;IAC/D,MAAM,KAAK,GAAG,OAAO,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IAEpF,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,sBAAsB,EAAE;YAC7C,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,MAAM,EAAE,kBAAkB;aAC3B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,aAAa,CAAC,SAAS,EAAE,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvH,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,aAAa,CAAC,eAAe,EAAE,iCAAiC,EAAE;gBAC1E,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,2CAA2C;aAClD,CAAC,CAAC;QACL,CAAC;QACD,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,mCAAmC,QAAQ,CAAC,MAAM,EAAE,EAAE;YACzF,UAAU,EAAE,QAAQ,CAAC,MAAM;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,IAAI,GAA6B,CAAC;IAClC,IAAI,CAAC;QACH,GAAG,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,0CAA0C,CAAC,CAAC;IACnF,CAAC;IAED,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QACnC,QAAQ,EAAE;YACR,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE;YACnB,SAAS,EAAE,GAAG,CAAC,QAAQ,CAAC,UAAU;YAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,SAAS;YAChC,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,aAAa;SACzC;QACD,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACjC,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,QAAQ,IAAI,EAAE;YACzB,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;SACrB,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/oauth/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EACL,0BAA0B,GAG3B,MAAM,6BAA6B,CAAC;AAErC,MAAM,sBAAsB,GAAG,oDAAoD,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,WAA0B;IAC/D,MAAM,KAAK,GAAG,OAAO,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IAEpF,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,sBAAsB,EAAE;YAC7C,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,MAAM,EAAE,kBAAkB;aAC3B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,aAAa,CAAC,SAAS,EAAE,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvH,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,aAAa,CAAC,eAAe,EAAE,iCAAiC,EAAE;gBAC1E,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,2CAA2C;aAClD,CAAC,CAAC;QACL,CAAC;QACD,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,mCAAmC,QAAQ,CAAC,MAAM,EAAE,EAAE;YACzF,UAAU,EAAE,QAAQ,CAAC,MAAM;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,IAAI,GAA6B,CAAC;IAClC,IAAI,CAAC;QACH,GAAG,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,0CAA0C,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,0BAA0B,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC"}
@@ -50,4 +50,5 @@ export { TokenManager, type TokenManagerOptions, } from "./token-manager.js";
50
50
  export { startCallbackServer, type CallbackResult, type CallbackServerOptions, } from "./callback-server.js";
51
51
  export { performInteractiveLogin, type InteractiveLoginOptions, } from "./interactive-login.js";
52
52
  export { discoverIdentity, } from "./identity.js";
53
+ export { parseAuthorizationDocument, parseExpiresAt, filterAccountsByProduct, type RawAuthorizationDocument, } from "./authorization-document.js";
53
54
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,YAAY,EACV,WAAW,EACX,UAAU,EACV,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,6BAA6B,EAC7B,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,EACtB,KAAK,gCAAgC,EACrC,KAAK,qBAAqB,EAC1B,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,QAAQ,EACR,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,2BAA2B,GACjC,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,KAAK,IAAI,GACV,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,cAAc,EACd,KAAK,UAAU,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,gBAAgB,GACjB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,YAAY,EACV,WAAW,EACX,UAAU,EACV,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,6BAA6B,EAC7B,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,EACtB,KAAK,gCAAgC,EACrC,KAAK,qBAAqB,EAC1B,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,QAAQ,EACR,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,2BAA2B,GACjC,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,KAAK,IAAI,GACV,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,cAAc,EACd,KAAK,UAAU,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,0BAA0B,EAC1B,cAAc,EACd,uBAAuB,EACvB,KAAK,wBAAwB,GAC9B,MAAM,6BAA6B,CAAC"}
@@ -59,4 +59,6 @@ export { startCallbackServer, } from "./callback-server.js";
59
59
  export { performInteractiveLogin, } from "./interactive-login.js";
60
60
  // Identity
61
61
  export { discoverIdentity, } from "./identity.js";
62
+ // The authorization document, shared by discoverIdentity and AuthorizationService
63
+ export { parseAuthorizationDocument, parseExpiresAt, filterAccountsByProduct, } from "./authorization-document.js";
62
64
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAiBH,yBAAyB;AACzB,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,GAIvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,GAEhB,MAAM,oBAAoB,CAAC;AAE5B,YAAY;AACZ,OAAO,EACL,QAAQ,EACR,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GAGnB,MAAM,gBAAgB,CAAC;AAExB,iBAAiB;AACjB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,GAEf,MAAM,eAAe,CAAC;AAEvB,iBAAiB;AACjB,OAAO,EACL,YAAY,EACZ,aAAa,GAEd,MAAM,WAAW,CAAC;AAEnB,oBAAoB;AACpB,OAAO,EACL,qBAAqB,GAEtB,MAAM,gBAAgB,CAAC;AAExB,cAAc;AACd,OAAO,EACL,cAAc,GAEf,MAAM,kBAAkB,CAAC;AAE1B,gBAAgB;AAChB,OAAO,EACL,YAAY,GAEb,MAAM,oBAAoB,CAAC;AAE5B,kBAAkB;AAClB,OAAO,EACL,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAE9B,oBAAoB;AACpB,OAAO,EACL,uBAAuB,GAExB,MAAM,wBAAwB,CAAC;AAEhC,WAAW;AACX,OAAO,EACL,gBAAgB,GACjB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAiBH,yBAAyB;AACzB,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,GAIvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,GAEhB,MAAM,oBAAoB,CAAC;AAE5B,YAAY;AACZ,OAAO,EACL,QAAQ,EACR,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GAGnB,MAAM,gBAAgB,CAAC;AAExB,iBAAiB;AACjB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,GAEf,MAAM,eAAe,CAAC;AAEvB,iBAAiB;AACjB,OAAO,EACL,YAAY,EACZ,aAAa,GAEd,MAAM,WAAW,CAAC;AAEnB,oBAAoB;AACpB,OAAO,EACL,qBAAqB,GAEtB,MAAM,gBAAgB,CAAC;AAExB,cAAc;AACd,OAAO,EACL,cAAc,GAEf,MAAM,kBAAkB,CAAC;AAE1B,gBAAgB;AAChB,OAAO,EACL,YAAY,GAEb,MAAM,oBAAoB,CAAC;AAE5B,kBAAkB;AAClB,OAAO,EACL,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAE9B,oBAAoB;AACpB,OAAO,EACL,uBAAuB,GAExB,MAAM,wBAAwB,CAAC;AAEhC,WAAW;AACX,OAAO,EACL,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAEvB,kFAAkF;AAClF,OAAO,EACL,0BAA0B,EAC1B,cAAc,EACd,uBAAuB,GAExB,MAAM,6BAA6B,CAAC"}
@@ -5,6 +5,52 @@
5
5
  * client.ts (which imports generated services) and base.ts (which
6
6
  * generated services extend).
7
7
  */
8
+ /**
9
+ * Rejects any `maxPages` that would defeat or invert the cap.
10
+ *
11
+ * Lives here, beside `DEFAULT_MAX_PAGES` and for the same reason: `client.ts`,
12
+ * `services/base.ts` and this module's own helpers all need it, and this is the
13
+ * only one of them they can all import without a cycle. Three entry points set
14
+ * a cap — `createBasecampClient`, the standalone `fetchAllPages`/`paginateAll`,
15
+ * and `BaseService`'s constructor, which is exported and directly constructible
16
+ * — and a guard on two of the three is not a guard.
17
+ *
18
+ * `Number.isSafeInteger(n) && n > 0` is one predicate covering every value that
19
+ * breaks the bound, each of which passed silently while `maxPages` was
20
+ * unvalidated:
21
+ *
22
+ * - `Infinity` — `page === maxPages` is never true, so the loop is unbounded
23
+ * and the cap does nothing at all. Precisely the runaway it exists to stop.
24
+ * - Anything ABOVE `2 ** 53`, such as `Number.MAX_VALUE` — the same runaway
25
+ * through a different door, and the reason this is `isSafeInteger` rather
26
+ * than `isInteger`. `Number.isInteger(Number.MAX_VALUE)` is `true`, so the
27
+ * obvious predicate admits it. Past `2 ** 53` the counter stalls: `page++`
28
+ * on `2 ** 53` yields `2 ** 53` again, because the next integer is not
29
+ * representable, so a bound of `2 ** 53 + 2` is never reached. `2 ** 53`
30
+ * ITSELF is fine — the counter arrives there from `2 ** 53 - 1` and breaks
31
+ * — so rejecting it is deliberately conservative rather than necessary. A
32
+ * cap must be a number the counter can actually arrive at, and
33
+ * `MAX_SAFE_INTEGER` is the honest edge of that guarantee.
34
+ * - `2.5` — consumes 2 pages, then fetches a 3rd and discards it: a request to
35
+ * a URL taken from an attacker-influenceable header, whose response is never
36
+ * parsed or returned.
37
+ * - `0`, negative, `NaN` — consume ZERO pages, throwing away a response the
38
+ * caller already fetched and passed in.
39
+ *
40
+ * SPEC.md §2 step 5: "Validate `max_pages > 0`. → `⊥ BasecampError(code:
41
+ * "usage")` otherwise."
42
+ */
43
+ export declare function assertValidMaxPages(maxPages: number): void;
44
+ /**
45
+ * Default maximum pages to follow as a safety cap against infinite loops.
46
+ *
47
+ * Lives here rather than in base.ts because both base.ts and client.ts need it
48
+ * and this module is the one they can both import without a cycle. A malformed
49
+ * or hostile Link header can name the page it was served from, so every loop
50
+ * that follows rel="next" needs a bound that does not depend on the server
51
+ * ever stopping.
52
+ */
53
+ export declare const DEFAULT_MAX_PAGES = 10000;
8
54
  /**
9
55
  * Parses the next URL from a Link header.
10
56
  * Looks for rel="next" in the header value.
@@ -1 +1 @@
1
- {"version":3,"file":"pagination-utils.d.ts","sourceRoot":"","sources":["../src/pagination-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAYtE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAQ1D"}
1
+ {"version":3,"file":"pagination-utils.d.ts","sourceRoot":"","sources":["../src/pagination-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA6BH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ1D;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAgBtE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAQ1D"}
@@ -5,6 +5,84 @@
5
5
  * client.ts (which imports generated services) and base.ts (which
6
6
  * generated services extend).
7
7
  */
8
+ /**
9
+ * Extracts the contents of the first non-empty `<...>` pair.
10
+ *
11
+ * This is the leftmost-match semantics of `/<([^>]+)>/` in linear time. The
12
+ * regex form is quadratic on a header carrying many `<` with no reachable `>`,
13
+ * because every `<` is retried as a start position and each scans to the end.
14
+ * Searching for `>` from *after* the `<` visits each character once instead.
15
+ *
16
+ * An empty `<>` is skipped rather than returned, because `[^>]+` requires at
17
+ * least one character — the regex would move on to the next `<`, and so does
18
+ * this. Skipping costs nothing: the scan that found `<>` was O(1), so the loop
19
+ * only ever repeats after work it did not do.
20
+ */
21
+ function extractAngleBracketed(part) {
22
+ let cursor = 0;
23
+ for (;;) {
24
+ const start = part.indexOf("<", cursor);
25
+ if (start < 0)
26
+ return null;
27
+ const end = part.indexOf(">", start + 1);
28
+ if (end < 0)
29
+ return null;
30
+ if (end > start + 1)
31
+ return part.slice(start + 1, end);
32
+ cursor = start + 1;
33
+ }
34
+ }
35
+ import { BasecampError } from "./errors.js";
36
+ /**
37
+ * Rejects any `maxPages` that would defeat or invert the cap.
38
+ *
39
+ * Lives here, beside `DEFAULT_MAX_PAGES` and for the same reason: `client.ts`,
40
+ * `services/base.ts` and this module's own helpers all need it, and this is the
41
+ * only one of them they can all import without a cycle. Three entry points set
42
+ * a cap — `createBasecampClient`, the standalone `fetchAllPages`/`paginateAll`,
43
+ * and `BaseService`'s constructor, which is exported and directly constructible
44
+ * — and a guard on two of the three is not a guard.
45
+ *
46
+ * `Number.isSafeInteger(n) && n > 0` is one predicate covering every value that
47
+ * breaks the bound, each of which passed silently while `maxPages` was
48
+ * unvalidated:
49
+ *
50
+ * - `Infinity` — `page === maxPages` is never true, so the loop is unbounded
51
+ * and the cap does nothing at all. Precisely the runaway it exists to stop.
52
+ * - Anything ABOVE `2 ** 53`, such as `Number.MAX_VALUE` — the same runaway
53
+ * through a different door, and the reason this is `isSafeInteger` rather
54
+ * than `isInteger`. `Number.isInteger(Number.MAX_VALUE)` is `true`, so the
55
+ * obvious predicate admits it. Past `2 ** 53` the counter stalls: `page++`
56
+ * on `2 ** 53` yields `2 ** 53` again, because the next integer is not
57
+ * representable, so a bound of `2 ** 53 + 2` is never reached. `2 ** 53`
58
+ * ITSELF is fine — the counter arrives there from `2 ** 53 - 1` and breaks
59
+ * — so rejecting it is deliberately conservative rather than necessary. A
60
+ * cap must be a number the counter can actually arrive at, and
61
+ * `MAX_SAFE_INTEGER` is the honest edge of that guarantee.
62
+ * - `2.5` — consumes 2 pages, then fetches a 3rd and discards it: a request to
63
+ * a URL taken from an attacker-influenceable header, whose response is never
64
+ * parsed or returned.
65
+ * - `0`, negative, `NaN` — consume ZERO pages, throwing away a response the
66
+ * caller already fetched and passed in.
67
+ *
68
+ * SPEC.md §2 step 5: "Validate `max_pages > 0`. → `⊥ BasecampError(code:
69
+ * "usage")` otherwise."
70
+ */
71
+ export function assertValidMaxPages(maxPages) {
72
+ if (!Number.isSafeInteger(maxPages) || maxPages <= 0) {
73
+ throw new BasecampError("usage", `maxPages must be a positive integer no larger than ${Number.MAX_SAFE_INTEGER}, got ${String(maxPages)}`, { hint: "Pass a whole number greater than 0, or omit maxPages to use the default cap." });
74
+ }
75
+ }
76
+ /**
77
+ * Default maximum pages to follow as a safety cap against infinite loops.
78
+ *
79
+ * Lives here rather than in base.ts because both base.ts and client.ts need it
80
+ * and this module is the one they can both import without a cycle. A malformed
81
+ * or hostile Link header can name the page it was served from, so every loop
82
+ * that follows rel="next" needs a bound that does not depend on the server
83
+ * ever stopping.
84
+ */
85
+ export const DEFAULT_MAX_PAGES = 10_000;
8
86
  /**
9
87
  * Parses the next URL from a Link header.
10
88
  * Looks for rel="next" in the header value.
@@ -18,8 +96,13 @@ export function parseNextLink(linkHeader) {
18
96
  for (const part of linkHeader.split(",")) {
19
97
  const trimmed = part.trim();
20
98
  if (trimmed.includes('rel="next"')) {
21
- const match = trimmed.match(/<([^>]+)>/);
22
- return match?.[1] ?? null;
99
+ // Keep scanning if this part is malformed. The previous code returned
100
+ // unconditionally here, so one unparseable `rel="next"` segment
101
+ // short-circuited the whole header to null; the other five SDKs fall
102
+ // through and keep looking.
103
+ const url = extractAngleBracketed(trimmed);
104
+ if (url !== null)
105
+ return url;
23
106
  }
24
107
  }
25
108
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"pagination-utils.js","sourceRoot":"","sources":["../src/pagination-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,UAAyB;IACrD,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YACzC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAc;IACrD,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,CAAS;IAC/C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"pagination-utils.js","sourceRoot":"","sources":["../src/pagination-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AACH,SAAS,qBAAqB,CAAC,IAAY;IACzC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,SAAS,CAAC;QACR,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACxC,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACzC,IAAI,GAAG,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACzB,IAAI,GAAG,GAAG,KAAK,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QACvD,MAAM,GAAG,KAAK,GAAG,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,aAAa,CACrB,OAAO,EACP,sDAAsD,MAAM,CAAC,gBAAgB,SAAS,MAAM,CAAC,QAAQ,CAAC,EAAE,EACxG,EAAE,IAAI,EAAE,8EAA8E,EAAE,CACzF,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,UAAyB;IACrD,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACnC,sEAAsE;YACtE,gEAAgE;YAChE,qEAAqE;YACrE,4BAA4B;YAC5B,MAAM,GAAG,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;YAC3C,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,GAAG,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAc;IACrD,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,CAAS;IAC/C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -26,6 +26,17 @@ export interface PaginationOptions {
26
26
  * When undefined or 0, all pages are fetched.
27
27
  */
28
28
  maxItems?: number;
29
+ /**
30
+ * Selects a single page. A positive `page` returns exactly that page in
31
+ * exactly one request: the `Link: rel="next"` header is not followed, and
32
+ * `.meta.truncated` reports whether a further page existed. When undefined,
33
+ * 0, or negative, auto-pagination walks the whole collection.
34
+ *
35
+ * Only meaningful for operations whose endpoint honors `?page=`; the handful
36
+ * of list endpoints that return their whole collection at once never emit a
37
+ * next link, so pinning a page there changes nothing. See SPEC section 8.
38
+ */
39
+ page?: number;
29
40
  }
30
41
  /**
31
42
  * An array of results with pagination metadata.
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAU,CAAC,CAAC,CAAE,SAAQ,KAAK,CAAC,CAAC,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAExB;;;;;OAKG;IACH,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,gBAAgB,CAE9C;gBAEW,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ;CAavC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAK1D"}
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAU,CAAC,CAAC,CAAE,SAAQ,KAAK,CAAC,CAAC,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAExB;;;;;OAKG;IACH,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,gBAAgB,CAE9C;gBAEW,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ;CAavC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAK1D"}