forges 0.0.0 → 0.1.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 (152) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +72 -0
  3. package/dist/azure-devops/index.d.mts +2 -0
  4. package/dist/azure-devops/index.mjs +1 -0
  5. package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
  6. package/dist/bitbucket/index.d.mts +2 -0
  7. package/dist/bitbucket/index.mjs +1 -0
  8. package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
  9. package/dist/crypto-CQNEyYHD.mjs +156 -0
  10. package/dist/cursor-origin/index.d.mts +2 -0
  11. package/dist/cursor-origin/index.mjs +1 -0
  12. package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
  13. package/dist/define-B2_4ob2i.d.mts +307 -0
  14. package/dist/define-D3yEXtKK.mjs +2276 -0
  15. package/dist/env.d.mts +46 -0
  16. package/dist/env.mjs +283 -0
  17. package/dist/fake/index.d.mts +227 -0
  18. package/dist/fake/index.mjs +1001 -0
  19. package/dist/fetch-D4pSRvvA.d.mts +1723 -0
  20. package/dist/forgejo/index.d.mts +2 -0
  21. package/dist/forgejo/index.mjs +1 -0
  22. package/dist/forgejo-BCefbHUN.mjs +2005 -0
  23. package/dist/gitea/index.d.mts +2 -0
  24. package/dist/gitea/index.mjs +22 -0
  25. package/dist/gitee/index.d.mts +2 -0
  26. package/dist/gitee/index.mjs +1 -0
  27. package/dist/gitee-CA-f7Rs8.mjs +1272 -0
  28. package/dist/github/index.d.mts +2 -0
  29. package/dist/github/index.mjs +1 -0
  30. package/dist/github-B0umL8_f.mjs +3259 -0
  31. package/dist/gitlab/index.d.mts +2 -0
  32. package/dist/gitlab/index.mjs +1 -0
  33. package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
  34. package/dist/graphql-D7uGzZTf.mjs +366 -0
  35. package/dist/index-BQ6VS31s.d.mts +32 -0
  36. package/dist/index-C-jz9Mo3.d.mts +16 -0
  37. package/dist/index-C4fYzmK3.d.mts +102 -0
  38. package/dist/index-CQiIX4SH.d.mts +27 -0
  39. package/dist/index-CTj5gQzl.d.mts +35 -0
  40. package/dist/index-CliBWzOF.d.mts +23 -0
  41. package/dist/index-Cz_y-t3d.d.mts +36 -0
  42. package/dist/index-DEt25E1w.d.mts +77 -0
  43. package/dist/index-DIuS0EOy.d.mts +43 -0
  44. package/dist/index-uSpsMCrC.d.mts +26 -0
  45. package/dist/index.d.mts +41 -0
  46. package/dist/index.mjs +13 -0
  47. package/dist/kit.d.mts +183 -0
  48. package/dist/kit.mjs +4 -0
  49. package/dist/provider-CoZ7cCxp.d.mts +1439 -0
  50. package/dist/provider-Ed1YhkQN.mjs +169 -0
  51. package/dist/pushin/index.d.mts +2 -0
  52. package/dist/pushin/index.mjs +1 -0
  53. package/dist/pushin-CkxgJ2cU.mjs +343 -0
  54. package/dist/schema/Actor.json +96 -0
  55. package/dist/schema/AlertSupport.json +49 -0
  56. package/dist/schema/AuthKind.json +15 -0
  57. package/dist/schema/Branch.json +31 -0
  58. package/dist/schema/ChangedFile.json +58 -0
  59. package/dist/schema/Check.json +169 -0
  60. package/dist/schema/CheckRef.json +114 -0
  61. package/dist/schema/CheckState.json +14 -0
  62. package/dist/schema/ChecksSummary.json +34 -0
  63. package/dist/schema/CiJob.json +186 -0
  64. package/dist/schema/CiJobRef.json +132 -0
  65. package/dist/schema/CiRun.json +234 -0
  66. package/dist/schema/CiRunRef.json +103 -0
  67. package/dist/schema/CodeFragment.json +19 -0
  68. package/dist/schema/CodeMatch.json +128 -0
  69. package/dist/schema/Collaborator.json +169 -0
  70. package/dist/schema/Comment.json +336 -0
  71. package/dist/schema/CommentRef.json +158 -0
  72. package/dist/schema/Commit.json +307 -0
  73. package/dist/schema/CommitRef.json +104 -0
  74. package/dist/schema/CommitSignature.json +118 -0
  75. package/dist/schema/Comparison.json +355 -0
  76. package/dist/schema/Cursor.json +21 -0
  77. package/dist/schema/EventAction.json +33 -0
  78. package/dist/schema/EventDetail.json +649 -0
  79. package/dist/schema/EventKind.json +26 -0
  80. package/dist/schema/FileContent.json +73 -0
  81. package/dist/schema/FileStatus.json +15 -0
  82. package/dist/schema/ForgeCapabilities.json +737 -0
  83. package/dist/schema/ForgeEvent.json +789 -0
  84. package/dist/schema/ForgeInstance.json +7 -0
  85. package/dist/schema/ForgeKind.json +32 -0
  86. package/dist/schema/ForgeWarning.json +49 -0
  87. package/dist/schema/ForgeWarningCode.json +7 -0
  88. package/dist/schema/GetManyResult.json +699 -0
  89. package/dist/schema/Installation.json +131 -0
  90. package/dist/schema/InstallationToken.json +31 -0
  91. package/dist/schema/KnownForgeKind.json +19 -0
  92. package/dist/schema/Label.json +22 -0
  93. package/dist/schema/MergeMethod.json +14 -0
  94. package/dist/schema/Milestone.json +40 -0
  95. package/dist/schema/Notification.json +498 -0
  96. package/dist/schema/NotificationReason.json +28 -0
  97. package/dist/schema/NotificationRef.json +60 -0
  98. package/dist/schema/NotificationSubject.json +376 -0
  99. package/dist/schema/PerKind.json +49 -0
  100. package/dist/schema/PullBranches.json +125 -0
  101. package/dist/schema/PushCommit.json +24 -0
  102. package/dist/schema/Reaction.json +148 -0
  103. package/dist/schema/ReactionContent.json +17 -0
  104. package/dist/schema/ReactionSummary.json +70 -0
  105. package/dist/schema/Release.json +286 -0
  106. package/dist/schema/ReleaseAsset.json +169 -0
  107. package/dist/schema/ReleaseAssetRef.json +138 -0
  108. package/dist/schema/ReleaseRef.json +107 -0
  109. package/dist/schema/Repo.json +354 -0
  110. package/dist/schema/RepoFeatures.json +35 -0
  111. package/dist/schema/RepoPermissions.json +35 -0
  112. package/dist/schema/RepoRef.json +77 -0
  113. package/dist/schema/RepoRole.json +15 -0
  114. package/dist/schema/RepoVisibility.json +12 -0
  115. package/dist/schema/Review.json +371 -0
  116. package/dist/schema/ReviewComment.json +280 -0
  117. package/dist/schema/ReviewCommentInput.json +34 -0
  118. package/dist/schema/ReviewRef.json +158 -0
  119. package/dist/schema/ReviewState.json +15 -0
  120. package/dist/schema/Reviewer.json +133 -0
  121. package/dist/schema/SecurityAlert.json +219 -0
  122. package/dist/schema/SecurityAlertKind.json +14 -0
  123. package/dist/schema/SecurityAlertRef.json +122 -0
  124. package/dist/schema/SecuritySeverity.json +14 -0
  125. package/dist/schema/SourceKind.json +12 -0
  126. package/dist/schema/SubscriptionState.json +12 -0
  127. package/dist/schema/Support.json +23 -0
  128. package/dist/schema/Tag.json +25 -0
  129. package/dist/schema/TextLimits.json +18 -0
  130. package/dist/schema/Thread.json +653 -0
  131. package/dist/schema/ThreadKind.json +14 -0
  132. package/dist/schema/ThreadRef.json +131 -0
  133. package/dist/schema/ThreadStack.json +146 -0
  134. package/dist/schema/ThreadState.json +13 -0
  135. package/dist/schema/TreeEntry.json +46 -0
  136. package/dist/schema/TreeEntryType.json +13 -0
  137. package/dist/schema/UpsertCommentResult.json +355 -0
  138. package/dist/schema/User.json +123 -0
  139. package/dist/schema/Webhook.json +188 -0
  140. package/dist/schema/WebhookDeliveryRecord.json +173 -0
  141. package/dist/schema/WebhookDeliveryRef.json +135 -0
  142. package/dist/schema/WebhookRef.json +105 -0
  143. package/dist/schema/index.d.mts +4 -0
  144. package/dist/schema/index.mjs +3219 -0
  145. package/dist/tangled/index.d.mts +2 -0
  146. package/dist/tangled/index.mjs +1 -0
  147. package/dist/tangled-ChdPmiv-.mjs +1570 -0
  148. package/dist/testing/index.d.mts +123 -0
  149. package/dist/testing/index.mjs +242 -0
  150. package/dist/webhooks-rn9-8lxT.mjs +70 -0
  151. package/package.json +115 -3
  152. package/dist/.gitkeep +0 -0
@@ -0,0 +1,2276 @@
1
+ //#region src/errors.ts
2
+ /** Base class for every error that forges throws. */
3
+ var ForgeError = class extends Error {
4
+ name = "ForgeError";
5
+ /** The forge that the request went to. */
6
+ forge;
7
+ instance;
8
+ url;
9
+ method;
10
+ constructor(message, context = {}, options) {
11
+ super(message, options);
12
+ this.forge = context.forge;
13
+ this.instance = context.instance;
14
+ this.url = context.url;
15
+ this.method = context.method;
16
+ }
17
+ };
18
+ /** The forge responded with an error. */
19
+ var ForgeApiError = class extends ForgeError {
20
+ name = "ForgeApiError";
21
+ status;
22
+ /** Truncated response body, for diagnostics. */
23
+ body;
24
+ constructor(message, status, body, context, options) {
25
+ super(message, context, options);
26
+ this.status = status;
27
+ this.body = body;
28
+ }
29
+ };
30
+ /** A 404: the resource does not exist, or the credential cannot see it. Many forges respond the same way to both. */
31
+ var NotFoundError = class extends ForgeApiError {
32
+ name = "NotFoundError";
33
+ };
34
+ /**
35
+ * A 401, or a 403 with a request budget of zero (GitHub GraphQL), for a request
36
+ * sent without credentials. A rejected credential is {@link TokenRevokedError}.
37
+ */
38
+ var AuthenticationRequiredError = class extends ForgeApiError {
39
+ name = "AuthenticationRequiredError";
40
+ };
41
+ /** A rate limit was hit and a retry didn't help. */
42
+ var RateLimitedError = class extends ForgeApiError {
43
+ name = "RateLimitedError";
44
+ /** When the limit resets, when the forge says. */
45
+ resetAt;
46
+ /** A secondary (abuse) limit, which has no remaining-request counter. */
47
+ secondary;
48
+ constructor(message, status, body, options = {}, errorOptions) {
49
+ super(message, status, body, options, errorOptions);
50
+ this.resetAt = options.resetAt;
51
+ this.secondary = options.secondary ?? false;
52
+ }
53
+ };
54
+ /** The credential was revoked or expired. */
55
+ var TokenRevokedError = class extends ForgeApiError {
56
+ name = "TokenRevokedError";
57
+ };
58
+ /** The credential is valid but lacks the scope or permission for this call. */
59
+ var InsufficientScopeError = class extends ForgeApiError {
60
+ name = "InsufficientScopeError";
61
+ };
62
+ /**
63
+ * The forge refused the request for a reason other than a missing scope: an
64
+ * organisation policy, an SSO requirement, an abuse limit, a protected
65
+ * resource, or a release the forge has marked immutable. A missing scope is
66
+ * {@link InsufficientScopeError}.
67
+ */
68
+ var ForbiddenError = class extends ForgeApiError {
69
+ name = "ForbiddenError";
70
+ /** Why the forge refused the request. */
71
+ reason;
72
+ /** The forge's own wording, for diagnostics and for reasons that have no `ForbiddenReason`. */
73
+ reasonRaw;
74
+ constructor(message, status, body, reason, context, options) {
75
+ super(message, status, body, context, options);
76
+ this.reason = reason;
77
+ this.reasonRaw = context?.reasonRaw;
78
+ }
79
+ };
80
+ /** Body patterns that identify a 403 reason, most specific first. */
81
+ const FORBIDDEN_PATTERNS = [
82
+ ["org_restriction", /third[- ]party application|OAuth App access restrictions|organization has enabled OAuth|not authorized by the organization|blocked by the organization/i],
83
+ ["sso_required", /SAML|single sign[- ]on|\bSSO\b|must be granted .* organization/i],
84
+ ["rate_limit_abuse", /abuse detection|secondary rate limit/i],
85
+ ["resource_protected", /archived|read[- ]only|protected branch|repository has been disabled|is disabled/i]
86
+ ];
87
+ /**
88
+ * Classifies a 403 body. Returns `undefined` for a body that names no policy
89
+ * the caller could act on, which stays {@link InsufficientScopeError}.
90
+ */
91
+ function forbiddenReason(body) {
92
+ for (const [reason, pattern] of FORBIDDEN_PATTERNS) {
93
+ const match = body.match(pattern);
94
+ if (match) return {
95
+ reason,
96
+ reasonRaw: match[0]
97
+ };
98
+ }
99
+ }
100
+ /** The pull request cannot be merged yet, typically because of failing or pending checks or branch protection. */
101
+ var MergeBlockedError = class extends ForgeApiError {
102
+ name = "MergeBlockedError";
103
+ };
104
+ /** The merge conflicts with the base branch, or the head moved since `sha` was read. */
105
+ var MergeConflictError = class extends ForgeApiError {
106
+ name = "MergeConflictError";
107
+ };
108
+ /** A request or a pending merge exceeded `timeout`. */
109
+ var ForgeTimeoutError = class extends ForgeError {
110
+ name = "ForgeTimeoutError";
111
+ /** The timeout that was exceeded, in milliseconds. */
112
+ timeout;
113
+ constructor(message, timeout, context, options) {
114
+ super(message, context, options);
115
+ this.timeout = timeout;
116
+ }
117
+ };
118
+ /** A subscription connection closed or failed. Resume from `cursor`. */
119
+ var SubscriptionClosedError = class extends ForgeError {
120
+ name = "SubscriptionClosedError";
121
+ /** The cursor of the last event received. Pass it to `subscribe()` to resume. */
122
+ cursor;
123
+ constructor(message, cursor, context, options) {
124
+ super(message, context, options);
125
+ this.cursor = cursor;
126
+ }
127
+ };
128
+ /** The request never got a response: DNS, TLS, connection refused or a proxy refusal. */
129
+ var ForgeNetworkError = class extends ForgeError {
130
+ name = "ForgeNetworkError";
131
+ };
132
+ /** A delivery's signature didn't match. */
133
+ var WebhookVerificationError = class extends ForgeError {
134
+ name = "WebhookVerificationError";
135
+ };
136
+ /** The forge doesn't support the operation. */
137
+ var UnsupportedOperationError = class extends ForgeError {
138
+ name = "UnsupportedOperationError";
139
+ };
140
+ /** A write was called on a provider constructed with `readOnly: true`. */
141
+ var ReadOnlyError = class extends UnsupportedOperationError {
142
+ name = "ReadOnlyError";
143
+ };
144
+ /** `contents.file` was asked for text and the file's bytes are not valid UTF-8. */
145
+ var ContentNotTextError = class extends ForgeError {
146
+ name = "ContentNotTextError";
147
+ };
148
+ /** A ref names a forge instance no registered provider serves. */
149
+ var UnknownForgeError = class extends ForgeError {
150
+ name = "UnknownForgeError";
151
+ };
152
+ /** Maps a generic merge-endpoint failure onto {@link MergeBlockedError} or {@link MergeConflictError}. */
153
+ function toMergeError(error) {
154
+ if (!(error instanceof ForgeApiError) || error.constructor !== ForgeApiError) return error;
155
+ const context = {
156
+ forge: error.forge,
157
+ instance: error.instance,
158
+ url: error.url,
159
+ method: error.method
160
+ };
161
+ if (error.status === 405) return new MergeBlockedError("Pull request is not mergeable yet", error.status, error.body, context, { cause: error });
162
+ if (error.status === 406 || error.status === 409) return new MergeConflictError("Pull request conflicts with its base or the head has moved", error.status, error.body, context, { cause: error });
163
+ return error;
164
+ }
165
+ /** The thread ref has no number, or is a kind the provider cannot address. */
166
+ var UnresolvedThreadError = class extends ForgeError {
167
+ name = "UnresolvedThreadError";
168
+ };
169
+ /** No merge method was given, and the repository allows several or reports none. */
170
+ var MergeMethodRequiredError = class extends ForgeError {
171
+ name = "MergeMethodRequiredError";
172
+ /** The merge methods that the repository allows. */
173
+ allowed;
174
+ constructor(allowed, context, options) {
175
+ super(allowed.length ? `Repository allows ${allowed.join(", ")}; pass \`method\` to choose one` : "Repository reports no merge method this provider can use; pass `method` explicitly", context, options);
176
+ this.allowed = allowed;
177
+ }
178
+ };
179
+ /** Picks the only allowed merge method, or throws {@link MergeMethodRequiredError}. */
180
+ function soleMergeMethod(allowed, context) {
181
+ const methods = Object.keys(allowed).filter((method) => allowed[method]);
182
+ if (methods.length !== 1) throw new MergeMethodRequiredError(methods, context);
183
+ return methods[0];
184
+ }
185
+ //#endregion
186
+ //#region src/contents.ts
187
+ /** Decodes base64, tolerating the newlines GitHub and Gitea wrap it in. */
188
+ function fromBase64(value) {
189
+ const binary = atob(value.replaceAll(/\s/g, ""));
190
+ const bytes = new Uint8Array(binary.length);
191
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
192
+ return bytes;
193
+ }
194
+ /**
195
+ * Shapes bytes as the caller asked: UTF-8 text for `as: 'text'`, bytes
196
+ * otherwise. Rejects with {@link ContentNotTextError} when text was asked for
197
+ * and the bytes are not valid UTF-8.
198
+ */
199
+ function toFileContent(bytes, file, options, context) {
200
+ if (options?.as !== "text") return {
201
+ ...file,
202
+ content: bytes,
203
+ encoding: "binary"
204
+ };
205
+ try {
206
+ return {
207
+ ...file,
208
+ content: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
209
+ encoding: "utf-8"
210
+ };
211
+ } catch (cause) {
212
+ throw new ContentNotTextError(`${file.path} is not valid UTF-8 text`, context, { cause });
213
+ }
214
+ }
215
+ /** True for a full SHA-1 or SHA-256 commit id, which needs no lookup to resolve. */
216
+ function isSha(ref) {
217
+ return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(ref);
218
+ }
219
+ /**
220
+ * Every capability with the verbs it covers and the flags that gate it, in the order the capability matrix lists them.
221
+ */
222
+ const CAPABILITY_TABLE = [
223
+ {
224
+ capability: "experimental",
225
+ derived: "experimental"
226
+ },
227
+ {
228
+ capability: "sources.poll",
229
+ derived: "poll"
230
+ },
231
+ {
232
+ capability: "sources.webhook",
233
+ derived: "webhook",
234
+ verbs: ["webhooks.verify", "webhooks.ingest"]
235
+ },
236
+ { capability: "sources.subscribe" },
237
+ { capability: "repos.get" },
238
+ { capability: "users.get" },
239
+ {
240
+ capability: "users.me",
241
+ account: true
242
+ },
243
+ {
244
+ capability: "repos.list",
245
+ account: true,
246
+ listing: true
247
+ },
248
+ {
249
+ capability: "repos.labels",
250
+ listing: true
251
+ },
252
+ {
253
+ capability: "repos.createLabel",
254
+ write: true
255
+ },
256
+ {
257
+ capability: "repos.milestones",
258
+ listing: true
259
+ },
260
+ {
261
+ capability: "repos.collaborators",
262
+ listing: true
263
+ },
264
+ { capability: "repos.permissionFor" },
265
+ {
266
+ capability: "repos.addCollaborator",
267
+ write: true
268
+ },
269
+ {
270
+ capability: "repos.assignableUsers",
271
+ listing: true
272
+ },
273
+ {
274
+ capability: "repos.reviewerCandidates",
275
+ listing: true
276
+ },
277
+ {
278
+ capability: "threads.get",
279
+ perKind: true
280
+ },
281
+ {
282
+ capability: "threads.list",
283
+ listing: true,
284
+ perKind: true
285
+ },
286
+ { capability: "threads.getMany" },
287
+ {
288
+ capability: "threads.events",
289
+ spec: "threads.eventsPage",
290
+ derived: "threadEvents"
291
+ },
292
+ {
293
+ capability: "comments.list",
294
+ spec: "threads.commentsPage",
295
+ perKind: true
296
+ },
297
+ {
298
+ capability: "comments.edit",
299
+ write: true,
300
+ spec: "threads.editComment",
301
+ perKind: true
302
+ },
303
+ {
304
+ capability: "comments.delete",
305
+ write: true,
306
+ spec: "threads.deleteComment",
307
+ perKind: true
308
+ },
309
+ {
310
+ capability: "reactions.list",
311
+ spec: "threads.reactionsPage",
312
+ perKind: true
313
+ },
314
+ {
315
+ capability: "notifications.list",
316
+ account: true,
317
+ listing: true
318
+ },
319
+ {
320
+ capability: "notifications.markRead",
321
+ account: true,
322
+ write: true
323
+ },
324
+ {
325
+ capability: "notifications.markDone",
326
+ account: true,
327
+ write: true
328
+ },
329
+ {
330
+ capability: "notifications.unsubscribe",
331
+ account: true,
332
+ write: true
333
+ },
334
+ {
335
+ capability: "notifications.markAllRead",
336
+ account: true,
337
+ write: true
338
+ },
339
+ {
340
+ capability: "notifications.markAllDone",
341
+ account: true,
342
+ write: true
343
+ },
344
+ {
345
+ capability: "notifications.unreadCount",
346
+ account: true
347
+ },
348
+ {
349
+ capability: "writes.comment",
350
+ write: true,
351
+ perKind: true
352
+ },
353
+ {
354
+ capability: "writes.upsertComment",
355
+ write: true,
356
+ derived: "upsertComment",
357
+ verbs: ["threads.upsertComment"]
358
+ },
359
+ {
360
+ capability: "writes.close",
361
+ write: true,
362
+ perKind: true
363
+ },
364
+ {
365
+ capability: "writes.reopen",
366
+ write: true,
367
+ perKind: true
368
+ },
369
+ {
370
+ capability: "writes.create",
371
+ write: true,
372
+ perKind: true
373
+ },
374
+ {
375
+ capability: "writes.update",
376
+ write: true,
377
+ perKind: true
378
+ },
379
+ {
380
+ capability: "writes.setLabels",
381
+ write: true,
382
+ perKind: true
383
+ },
384
+ {
385
+ capability: "writes.addLabels",
386
+ write: true,
387
+ perKind: true
388
+ },
389
+ {
390
+ capability: "writes.removeLabels",
391
+ write: true,
392
+ perKind: true
393
+ },
394
+ {
395
+ capability: "writes.setMilestone",
396
+ write: true,
397
+ perKind: true
398
+ },
399
+ {
400
+ capability: "writes.react",
401
+ write: true,
402
+ spec: "threads.reactions",
403
+ perKind: true,
404
+ verbs: ["threads.react", "threads.unreact"]
405
+ },
406
+ {
407
+ capability: "writes.setAssignees",
408
+ write: true,
409
+ perKind: true
410
+ },
411
+ {
412
+ capability: "writes.requestReview",
413
+ write: true,
414
+ perKind: true
415
+ },
416
+ {
417
+ capability: "writes.merge",
418
+ write: true
419
+ },
420
+ {
421
+ capability: "writes.approveAndMerge",
422
+ write: true,
423
+ derived: "approveAndMerge",
424
+ verbs: ["threads.approveAndMerge"]
425
+ },
426
+ {
427
+ capability: "writes.transfer",
428
+ write: true
429
+ },
430
+ {
431
+ capability: "writes.markDuplicate",
432
+ write: true
433
+ },
434
+ {
435
+ capability: "subscriptions.get",
436
+ account: true,
437
+ spec: "threads.subscriptions",
438
+ perKind: true,
439
+ verbs: ["threads.subscription"]
440
+ },
441
+ {
442
+ capability: "subscriptions.set",
443
+ write: true,
444
+ derived: "subscriptionSet",
445
+ verbs: ["threads.subscribe", "threads.unsubscribe"]
446
+ },
447
+ {
448
+ capability: "installations",
449
+ account: true,
450
+ verbs: [
451
+ "installations.list",
452
+ "installations.listPage",
453
+ "installations.get",
454
+ "installations.token",
455
+ "installations.repos",
456
+ "installations.reposPage",
457
+ "installations.provider",
458
+ "installations.providers"
459
+ ]
460
+ },
461
+ {
462
+ capability: "checks.thread",
463
+ spec: "threads.checks",
464
+ perKind: true
465
+ },
466
+ { capability: "checks.list" },
467
+ {
468
+ capability: "checks.report",
469
+ write: true
470
+ },
471
+ {
472
+ capability: "checks.rerun",
473
+ write: true
474
+ },
475
+ {
476
+ capability: "ci.runs",
477
+ listing: true
478
+ },
479
+ { capability: "ci.run" },
480
+ {
481
+ capability: "ci.jobs",
482
+ listing: true
483
+ },
484
+ { capability: "ci.log" },
485
+ { capability: "contents.file" },
486
+ {
487
+ capability: "contents.tree",
488
+ listing: true
489
+ },
490
+ {
491
+ capability: "contents.branches",
492
+ listing: true
493
+ },
494
+ {
495
+ capability: "contents.tags",
496
+ listing: true
497
+ },
498
+ { capability: "contents.resolveRef" },
499
+ {
500
+ capability: "contents.commits",
501
+ listing: true
502
+ },
503
+ { capability: "contents.commit" },
504
+ { capability: "contents.compare" },
505
+ {
506
+ capability: "contents.threadFiles",
507
+ spec: "threads.filesPage"
508
+ },
509
+ {
510
+ capability: "contents.threadCommits",
511
+ spec: "threads.commitsPage"
512
+ },
513
+ {
514
+ capability: "reviews.list",
515
+ spec: "threads.reviewsPage"
516
+ },
517
+ {
518
+ capability: "reviews.create",
519
+ write: true,
520
+ spec: "threads.createReview"
521
+ },
522
+ {
523
+ capability: "reviews.submit",
524
+ write: true,
525
+ spec: "threads.submitReview"
526
+ },
527
+ {
528
+ capability: "reviews.approve",
529
+ write: true,
530
+ spec: "threads.approve",
531
+ derived: "approve"
532
+ },
533
+ {
534
+ capability: "reviews.resolveThread",
535
+ write: true,
536
+ spec: "threads.reviewThreads",
537
+ verbs: ["threads.resolveReviewThread", "threads.unresolveReviewThread"]
538
+ },
539
+ {
540
+ capability: "releases.list",
541
+ listing: true
542
+ },
543
+ { capability: "releases.get" },
544
+ { capability: "releases.latest" },
545
+ { capability: "releases.getByTag" },
546
+ { capability: "releases.downloadAsset" },
547
+ {
548
+ capability: "releases.create",
549
+ write: true
550
+ },
551
+ {
552
+ capability: "releases.update",
553
+ write: true
554
+ },
555
+ {
556
+ capability: "webhooks.list",
557
+ account: true,
558
+ listing: true
559
+ },
560
+ {
561
+ capability: "webhooks.create",
562
+ write: true
563
+ },
564
+ {
565
+ capability: "webhooks.update",
566
+ write: true
567
+ },
568
+ {
569
+ capability: "webhooks.delete",
570
+ write: true
571
+ },
572
+ {
573
+ capability: "webhooks.rotateSecret",
574
+ write: true
575
+ },
576
+ {
577
+ capability: "webhooks.deliveries",
578
+ account: true,
579
+ listing: true
580
+ },
581
+ {
582
+ capability: "webhooks.redeliver",
583
+ write: true
584
+ },
585
+ {
586
+ capability: "search.threads",
587
+ listing: true
588
+ },
589
+ {
590
+ capability: "search.repos",
591
+ listing: true
592
+ },
593
+ {
594
+ capability: "search.commits",
595
+ listing: true
596
+ },
597
+ {
598
+ capability: "search.code",
599
+ listing: true
600
+ },
601
+ {
602
+ capability: "securityAlerts",
603
+ derived: "alertKinds",
604
+ verbs: ["securityAlerts.list", "securityAlerts.listPage"]
605
+ },
606
+ {
607
+ capability: "eventKinds",
608
+ derived: "eventKinds"
609
+ },
610
+ {
611
+ capability: "authKinds",
612
+ derived: "authKinds"
613
+ },
614
+ {
615
+ capability: "limits",
616
+ derived: "limits"
617
+ }
618
+ ].map(({ listing, ...row }) => {
619
+ const spec = row.spec ?? (row.derived ? void 0 : defaultSpec(row.capability, listing));
620
+ const verbs = row.verbs ?? (spec?.endsWith("Page") ? [spec.slice(0, -4), spec] : spec ? [spec] : void 0);
621
+ return {
622
+ ...row,
623
+ ...spec ? { spec } : {},
624
+ ...verbs ? { verbs } : {}
625
+ };
626
+ });
627
+ function defaultSpec(capability, listing) {
628
+ const path = capability.startsWith("writes.") ? `threads.${capability.slice(7)}` : capability;
629
+ return listing ? `${path}Page` : path;
630
+ }
631
+ //#endregion
632
+ //#region src/capabilities.ts
633
+ const ALERT_KINDS = [
634
+ "dependency",
635
+ "code_scanning",
636
+ "secret",
637
+ "advisory"
638
+ ];
639
+ const KINDS = [
640
+ "issue",
641
+ "pull_request",
642
+ "discussion",
643
+ "commit"
644
+ ];
645
+ function resolve(support, env) {
646
+ return typeof support === "function" ? support(env) : support ?? false;
647
+ }
648
+ function resolveKinds(kinds, env) {
649
+ return Object.fromEntries(KINDS.map((kind) => [kind, resolve(kinds?.[kind], env)]));
650
+ }
651
+ /** A capability composed of `parts` is `'unverified'` when any part is. */
652
+ function composed(parts, level) {
653
+ return parts.includes(false) ? false : parts.includes("unverified") ? "unverified" : level;
654
+ }
655
+ /** A thread's events are read for each kind the provider reads threads of, at the weaker of the two levels. */
656
+ function threadEventKinds(spec, env) {
657
+ const events = resolve(spec.threads.eventsPage.support, env);
658
+ const threads = resolveKinds(spec.threads.get.kinds, env);
659
+ return Object.fromEntries(KINDS.map((kind) => [kind, composed([events, threads[kind]], events === true ? threads[kind] : events)]));
660
+ }
661
+ /** `upsertComment` is composed of listing, creating and editing a comment, so it needs all three for the kind. */
662
+ function upsertKinds(spec, env) {
663
+ const parts = [
664
+ spec.threads.commentsPage,
665
+ spec.threads.comment,
666
+ spec.threads.editComment
667
+ ];
668
+ return Object.fromEntries(KINDS.map((kind) => [kind, composed(parts.map((part) => resolve(part?.kinds[kind], env)), "emulated")]));
669
+ }
670
+ /** Support for the composed verb, requiring both approval and merging. */
671
+ function approveAndMergeSupport(spec, env) {
672
+ const merge = resolve(spec.threads.merge?.support, env);
673
+ return composed([merge, resolve((spec.threads.approve ?? spec.threads.createReview)?.support, env)], resolve(spec.threads.approveAndMerge?.support ?? merge, env));
674
+ }
675
+ function read(source, path) {
676
+ return path.split(".").reduce((value, key) => value?.[key], source);
677
+ }
678
+ function write(target, path, value) {
679
+ const keys = path.split(".");
680
+ const leaf = keys.pop();
681
+ const parent = keys.reduce((node, key) => node[key] ??= {}, target);
682
+ parent[leaf] = value;
683
+ }
684
+ /** What `entry` contributes to the capability object; `undefined` leaves the field to core. */
685
+ function valueFor(entry, spec, env, flags) {
686
+ switch (entry.derived) {
687
+ case "experimental": return;
688
+ case "poll": return resolve(spec.notifications?.listPage.support, env);
689
+ case "webhook": return flags.webhook;
690
+ case "threadEvents": return threadEventKinds(spec, env);
691
+ case "upsertComment": return upsertKinds(spec, env);
692
+ case "subscriptionSet": return resolveKinds(flags.readOnly ? void 0 : spec.threads.subscriptions?.kinds, env);
693
+ case "approve": return resolve((spec.threads.approve ?? spec.threads.createReview)?.support, env);
694
+ case "approveAndMerge": return approveAndMergeSupport(spec, env);
695
+ case "alertKinds": return Object.fromEntries(ALERT_KINDS.map((kind) => [kind, resolve(spec.securityAlerts?.kinds[kind], env)]));
696
+ case "eventKinds": return spec.traits.eventKinds;
697
+ case "authKinds": return spec.traits.authKinds;
698
+ case "limits": return spec.traits.limits;
699
+ }
700
+ const declared = read(spec, entry.spec);
701
+ return entry.perKind ? resolveKinds(declared?.kinds, env) : resolve(declared?.support, env);
702
+ }
703
+ /** The capability object a spec declares, derived from {@link CAPABILITY_TABLE} with every support resolved against `env`. */
704
+ function capabilitiesOf(spec, env, flags) {
705
+ const capabilities = {
706
+ version: env.version,
707
+ ...flags.experimental ? { experimental: true } : {}
708
+ };
709
+ for (const entry of CAPABILITY_TABLE) {
710
+ const value = valueFor(entry, spec, env, flags);
711
+ if (value !== void 0) write(capabilities, entry.capability, value);
712
+ }
713
+ return capabilities;
714
+ }
715
+ /** Verbs `readOnly` blocks, by their {@link ForgeVerb} name. */
716
+ const WRITE_VERBS = new Set(CAPABILITY_TABLE.flatMap((entry) => entry.write ? entry.verbs ?? [] : []));
717
+ function update(target, path, change) {
718
+ const [key, ...rest] = path.split(".");
719
+ const node = target;
720
+ if (!key) return change(target);
721
+ if (node?.[key] === void 0) return target;
722
+ return {
723
+ ...node,
724
+ [key]: update(node[key], rest.join("."), change)
725
+ };
726
+ }
727
+ const unsupported = (declared) => declared && {
728
+ ...declared,
729
+ support: false
730
+ };
731
+ const unsupportedKinds = (declared) => declared && {
732
+ ...declared,
733
+ kinds: {}
734
+ };
735
+ /**
736
+ * Marks writes unsupported, with `anonymous` everything that needs an account,
737
+ * and with `webhooksOnly` every declared verb, leaving only webhook ingestion.
738
+ */
739
+ function restrict(spec, anonymous, webhooksOnly = false) {
740
+ let restricted = spec;
741
+ for (const entry of CAPABILITY_TABLE) if (entry.spec && (entry.write || anonymous && (entry.account || webhooksOnly))) restricted = update(restricted, entry.spec, entry.perKind ? unsupportedKinds : unsupported);
742
+ const result = restricted;
743
+ return anonymous && webhooksOnly ? {
744
+ ...result,
745
+ securityAlerts: void 0
746
+ } : result;
747
+ }
748
+ //#endregion
749
+ //#region src/events.ts
750
+ const ACTIONS = {
751
+ open: "opened",
752
+ opened: "opened",
753
+ close: "closed",
754
+ closed: "closed",
755
+ reopen: "reopened",
756
+ reopened: "reopened",
757
+ merge: "merged",
758
+ merged: "merged",
759
+ edit: "edited",
760
+ edited: "edited",
761
+ update: "edited",
762
+ updated: "edited",
763
+ create: "created",
764
+ created: "created",
765
+ delete: "deleted",
766
+ deleted: "deleted",
767
+ submitted: "submitted",
768
+ labeled: "labelled",
769
+ labelled: "labelled",
770
+ label_added: "labelled",
771
+ unlabeled: "unlabelled",
772
+ unlabelled: "unlabelled",
773
+ label_removed: "unlabelled",
774
+ label_updated: "edited",
775
+ label_cleared: "unlabelled",
776
+ assigned: "assigned",
777
+ unassigned: "unassigned",
778
+ review_requested: "review_requested",
779
+ synchronize: "synchronised",
780
+ synchronized: "synchronised",
781
+ synchronised: "synchronised",
782
+ ready_for_review: "ready_for_review",
783
+ converted_to_draft: "converted_to_draft",
784
+ convert_to_draft: "converted_to_draft",
785
+ publish: "published",
786
+ published: "published",
787
+ renamed: "renamed",
788
+ transferred: "transferred",
789
+ archived: "archived",
790
+ unarchived: "unarchived",
791
+ added: "added",
792
+ removed: "removed"
793
+ };
794
+ /** What a comment or review event means when the forge names no verb. */
795
+ const KIND_DEFAULTS = {
796
+ comment: "created",
797
+ review_comment: "created",
798
+ review: "submitted"
799
+ };
800
+ /** Normalises a forge-native verb (`closed`, `close`, `synchronize`, ...) into an {@link EventAction}. */
801
+ function eventAction(raw) {
802
+ return raw ? ACTIONS[raw.toLowerCase()] : void 0;
803
+ }
804
+ /**
805
+ * Fills `action` from `actionRaw`, or from the last `.`/`:` segment of
806
+ * `kindRaw`, falling back to the kind's default verb, then `other`.
807
+ */
808
+ function completeEvent(event) {
809
+ if (event.action) return event;
810
+ const raw = event.actionRaw ?? event.kindRaw?.split(/[.:]/).pop();
811
+ return {
812
+ ...event,
813
+ action: eventAction(raw) ?? KIND_DEFAULTS[event.kind] ?? "other"
814
+ };
815
+ }
816
+ const REVIEW_STATES = {
817
+ approved: "approved",
818
+ approve: "approved",
819
+ changes_requested: "changes_requested",
820
+ request_changes: "changes_requested",
821
+ commented: "commented",
822
+ comment: "commented",
823
+ pending: "pending",
824
+ dismissed: "dismissed"
825
+ };
826
+ /** Normalises a forge-native review verdict (`APPROVED`, `changes_requested`, ...) into a {@link ReviewState}. */
827
+ function reviewState(raw) {
828
+ return raw && REVIEW_STATES[raw.toLowerCase()] || "unknown";
829
+ }
830
+ //#endregion
831
+ //#region src/fetch.ts
832
+ const BODY_EXCERPT_LENGTH = 512;
833
+ const REDIRECT_STATUSES = /* @__PURE__ */ new Set([
834
+ 301,
835
+ 302,
836
+ 303,
837
+ 307,
838
+ 308
839
+ ]);
840
+ /** A forge's plain-text rate limit refusal, such as Gitee's `403 Forbidden (Rate Limit Exceeded)`. */
841
+ const PLAIN_RATE_LIMIT_RE = /^[^{<]{0,80}\brate limit exceeded\b/i;
842
+ const DOT_SEGMENT_RE = /^(?:\.|%2e){1,2}$/i;
843
+ /** Parses an RFC 5988 `Link` header into a map of rel to URL. */
844
+ function parseLinkHeader(header) {
845
+ const links = {};
846
+ if (!header) return links;
847
+ for (const part of header.split(",")) {
848
+ const match = /<([^>]+)>\s*;\s*(.+)/.exec(part.trim());
849
+ if (!match?.[1] || !match[2]) continue;
850
+ const rel = /rel\s*=\s*"?([^";]+)"?/.exec(match[2])?.[1];
851
+ if (rel) links[rel.trim()] = match[1];
852
+ }
853
+ return links;
854
+ }
855
+ function cursorFrom(response) {
856
+ const next = parseLinkHeader(response.headers.get("link")).next;
857
+ const etag = response.headers.get("etag") ?? void 0;
858
+ if (!next && !etag) return;
859
+ return {
860
+ nextUrl: next,
861
+ etag
862
+ };
863
+ }
864
+ function retryAfterMs(response) {
865
+ const header = response.headers.get("retry-after");
866
+ if (!header) return;
867
+ const seconds = Number(header);
868
+ if (Number.isFinite(seconds)) return seconds * 1e3;
869
+ const date = Date.parse(header);
870
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
871
+ }
872
+ /**
873
+ * `x-ratelimit-reset` is a Unix time in seconds. The IETF `ratelimit-reset`
874
+ * is seconds from now, though some forges send a Unix time there too.
875
+ */
876
+ function resetDate(response) {
877
+ const epoch = headerNumber(response, "x-ratelimit-reset");
878
+ if (epoch !== void 0) return /* @__PURE__ */ new Date(epoch * 1e3);
879
+ const reset = headerNumber(response, "ratelimit-reset");
880
+ if (reset === void 0) return;
881
+ return new Date(reset > 1e9 ? reset * 1e3 : Date.now() + reset * 1e3);
882
+ }
883
+ function rateLimitReset(response) {
884
+ const reset = resetDate(response);
885
+ if (reset) return reset;
886
+ const after = retryAfterMs(response);
887
+ return after === void 0 ? void 0 : new Date(Date.now() + after);
888
+ }
889
+ function headerNumber(response, ...names) {
890
+ for (const name of names) {
891
+ const value = response.headers.get(name);
892
+ if (value !== null && value !== "" && Number.isFinite(Number(value))) return Number(value);
893
+ }
894
+ }
895
+ /** The request budget a response reports, from `x-ratelimit-*` or `ratelimit-*` headers. */
896
+ function rateLimitOf(response) {
897
+ const limit = headerNumber(response, "x-ratelimit-limit", "ratelimit-limit");
898
+ const remaining = headerNumber(response, "x-ratelimit-remaining", "ratelimit-remaining");
899
+ const reset = resetDate(response);
900
+ if (limit === void 0 && remaining === void 0 && reset === void 0) return;
901
+ return {
902
+ ...limit === void 0 ? {} : { limit },
903
+ ...remaining === void 0 ? {} : { remaining },
904
+ ...reset === void 0 ? {} : { resetAt: reset }
905
+ };
906
+ }
907
+ function isRateLimited(response) {
908
+ if (response.status === 429) return true;
909
+ if (response.status !== 403 || headerNumber(response, "x-ratelimit-limit", "ratelimit-limit") === 0) return false;
910
+ return response.headers.get("x-ratelimit-remaining") === "0" || response.headers.get("ratelimit-remaining") === "0" || response.headers.has("retry-after");
911
+ }
912
+ /**
913
+ * One signal that aborts when any of `signals` does. `AbortSignal.any` would
914
+ * do this, but StarlingMonkey (the wasm runtime) does not implement it.
915
+ */
916
+ function anySignal(signals) {
917
+ const aborted = signals.find((signal) => signal.aborted);
918
+ if (aborted) return aborted;
919
+ if (signals.length === 1) return signals[0];
920
+ const controller = new AbortController();
921
+ const listeners = [];
922
+ for (const signal of signals) {
923
+ const listener = () => {
924
+ controller.abort(signal.reason);
925
+ for (const [other, removed] of listeners) other.removeEventListener("abort", removed);
926
+ };
927
+ listeners.push([signal, listener]);
928
+ signal.addEventListener("abort", listener, { once: true });
929
+ }
930
+ return controller.signal;
931
+ }
932
+ function assertUrl(url) {
933
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) throw new TypeError("Invalid request URL");
934
+ return url;
935
+ }
936
+ function assertInput(input) {
937
+ for (const char of input) {
938
+ const code = char.charCodeAt(0);
939
+ if (code <= 32 || code === 127) throw new TypeError("Invalid request URL");
940
+ }
941
+ }
942
+ function assertRelativePath(path) {
943
+ const pathname = path.split("?", 1)[0];
944
+ if (path.includes("#") || pathname.includes("\\") || pathname.split("/").some((segment) => DOT_SEGMENT_RE.test(segment))) throw new TypeError(`Unsafe request path ${JSON.stringify(path)}; check the refs passed to the provider`);
945
+ return path;
946
+ }
947
+ function sleep(ms, signal) {
948
+ return new Promise((resolve, reject) => {
949
+ if (signal?.aborted) {
950
+ reject(signal.reason);
951
+ return;
952
+ }
953
+ const timer = setTimeout(() => {
954
+ signal?.removeEventListener("abort", onAbort);
955
+ resolve();
956
+ }, ms);
957
+ function onAbort() {
958
+ clearTimeout(timer);
959
+ reject(signal.reason);
960
+ }
961
+ signal?.addEventListener("abort", onAbort, { once: true });
962
+ });
963
+ }
964
+ /** Settles as `promise` does, or rejects with the signal's reason once it aborts, leaving `promise` running. */
965
+ function abortable(promise, signal) {
966
+ if (!signal) return promise;
967
+ if (signal.aborted) return Promise.reject(signal.reason);
968
+ return new Promise((resolve, reject) => {
969
+ const onAbort = () => reject(signal.reason);
970
+ signal.addEventListener("abort", onAbort, { once: true });
971
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
972
+ });
973
+ }
974
+ /**
975
+ * Creates a fetcher for one forge API.
976
+ * @param options The base URL and the credentials, timeout and error context of every request.
977
+ */
978
+ function createFetcher(options) {
979
+ const timeout = options.timeout ?? 3e4;
980
+ const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
981
+ assertInput(options.baseUrl);
982
+ const base = assertUrl(new URL(options.baseUrl));
983
+ if (base.search || base.hash) throw new TypeError("Invalid API base URL");
984
+ base.pathname = `${base.pathname.replace(/\/$/, "")}/`;
985
+ const baseOrigin = base.origin;
986
+ function resolve(path, query) {
987
+ assertInput(path);
988
+ let url;
989
+ try {
990
+ url = new URL(path);
991
+ } catch {
992
+ if (path.startsWith("//") || path.split("/", 1)[0].includes(":")) throw new TypeError("Invalid request URL");
993
+ }
994
+ url = assertUrl(url ?? new URL(assertRelativePath(path).replace(/^\//, ""), base));
995
+ for (const [key, value] of Object.entries(query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
996
+ return url.toString();
997
+ }
998
+ function withDefaultQuery(url) {
999
+ if (!options.query) return url;
1000
+ const target = new URL(url);
1001
+ for (const [key, value] of Object.entries(options.query)) if (!target.searchParams.has(key)) target.searchParams.set(key, value);
1002
+ return target.toString();
1003
+ }
1004
+ async function send(url, options_, attempt, authenticated = true, download = false, redirects = 0, streaming = false, follow = false) {
1005
+ const target = assertUrl(new URL(url));
1006
+ const trusted = authenticated && target.origin === baseOrigin;
1007
+ const controller = streaming ? new AbortController() : void 0;
1008
+ const timer = controller ? setTimeout(() => controller.abort(new DOMException("Request timed out", "TimeoutError")), timeout) : void 0;
1009
+ const signals = [controller?.signal ?? AbortSignal.timeout(timeout)];
1010
+ if (options_.signal) signals.push(options_.signal);
1011
+ const headers = new Headers(trusted ? options.headers : void 0);
1012
+ for (const [key, value] of Object.entries(trusted ? await (options.authHeaders?.() ?? {}) : {})) headers.set(key, value);
1013
+ for (const [key, value] of new Headers(options_.headers)) headers.set(key, value);
1014
+ if (options_.etag) headers.set("if-none-match", options_.etag);
1015
+ const { json: payload, mapError: _mapError, ...init } = options_;
1016
+ if (payload !== void 0) headers.set("content-type", "application/json");
1017
+ let response;
1018
+ try {
1019
+ response = await doFetch(trusted ? withDefaultQuery(url) : url, {
1020
+ ...init,
1021
+ redirect: follow ? "follow" : "manual",
1022
+ credentials: trusted ? init.credentials : "omit",
1023
+ headers,
1024
+ body: payload === void 0 ? init.body : JSON.stringify(payload),
1025
+ signal: anySignal(signals)
1026
+ });
1027
+ } catch (cause) {
1028
+ if (options_.signal?.aborted) throw options_.signal.reason ?? cause;
1029
+ if (cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError")) throw new ForgeTimeoutError(`Request to ${url} timed out after ${timeout}ms`, timeout, {
1030
+ ...options.context,
1031
+ url,
1032
+ method: options_.method ?? "GET"
1033
+ }, { cause });
1034
+ throw new ForgeNetworkError(`Request to ${url} failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
1035
+ ...options.context,
1036
+ url,
1037
+ method: options_.method ?? "GET"
1038
+ }, { cause });
1039
+ } finally {
1040
+ clearTimeout(timer);
1041
+ }
1042
+ if (response.type === "opaqueredirect" && options_.redirect !== "manual") {
1043
+ const method = (options_.method ?? "GET").toUpperCase();
1044
+ if (options_.redirect === "error" || method !== "GET" && method !== "HEAD") throw new TypeError("Hidden redirect refused");
1045
+ return send(url, options_, attempt, authenticated, download, redirects, streaming, true);
1046
+ }
1047
+ if (follow && response.redirected && response.url) {
1048
+ const final = assertUrl(new URL(response.url));
1049
+ if (final.origin !== target.origin && !download || target.protocol === "https:" && final.protocol !== "https:") {
1050
+ await response.body?.cancel();
1051
+ throw new TypeError("Unsafe request redirect");
1052
+ }
1053
+ }
1054
+ const location = REDIRECT_STATUSES.has(response.status) ? response.headers.get("location") : null;
1055
+ if (location && options_.redirect !== "manual") {
1056
+ await response.body?.cancel();
1057
+ if (options_.redirect === "error" || redirects >= 20) throw new TypeError("Request redirect refused");
1058
+ assertInput(location);
1059
+ const next = assertUrl(new URL(location, target));
1060
+ const sameOrigin = next.origin === target.origin;
1061
+ if (!sameOrigin && !download || target.protocol === "https:" && next.protocol !== "https:") throw new TypeError("Unsafe request redirect");
1062
+ const method = (options_.method ?? "GET").toUpperCase();
1063
+ if (!sameOrigin && method !== "GET" && method !== "HEAD") throw new TypeError("Unsafe request redirect");
1064
+ let nextOptions = options_;
1065
+ if (!sameOrigin) nextOptions = {
1066
+ method,
1067
+ signal: options_.signal,
1068
+ credentials: "omit"
1069
+ };
1070
+ else if (response.status === 303 && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST") {
1071
+ const nextHeaders = new Headers(options_.headers);
1072
+ for (const name of [
1073
+ "content-type",
1074
+ "content-length",
1075
+ "content-encoding",
1076
+ "content-language",
1077
+ "content-location"
1078
+ ]) nextHeaders.delete(name);
1079
+ nextOptions = {
1080
+ ...options_,
1081
+ method: "GET",
1082
+ body: void 0,
1083
+ json: void 0,
1084
+ headers: nextHeaders
1085
+ };
1086
+ }
1087
+ return send(next.toString(), nextOptions, attempt, authenticated && sameOrigin, download, redirects + 1, streaming);
1088
+ }
1089
+ if (response.ok || response.status === 304 || options_.redirect === "manual" && (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400)) return response;
1090
+ const body = (await response.text()).slice(0, BODY_EXCERPT_LENGTH);
1091
+ const context = {
1092
+ ...options.context,
1093
+ url,
1094
+ method: options_.method ?? "GET"
1095
+ };
1096
+ const credentialed = trusted && options.authenticated || headers.has("authorization");
1097
+ if (response.status === 401) throw credentialed ? new TokenRevokedError("Credentials were rejected by the forge", 401, body, context) : new AuthenticationRequiredError("Credentials are required for this request", 401, body, context);
1098
+ if (response.status === 403 && !credentialed && headerNumber(response, "x-ratelimit-limit", "ratelimit-limit") === 0) throw new AuthenticationRequiredError("Credentials are required for this request", 403, body, context);
1099
+ if (response.status === 404) throw new NotFoundError(`Nothing found at ${options_.method ?? "GET"} ${url}, or the credential cannot see it`, 404, body, context);
1100
+ if (isRateLimited(response) || response.status === 403 && !rateLimitOf(response) && PLAIN_RATE_LIMIT_RE.test(body.trim())) {
1101
+ const secondary = response.status === 403 || response.headers.has("retry-after");
1102
+ const wait = retryAfterMs(response);
1103
+ if (secondary && attempt === 0 && wait !== void 0 && wait <= 6e4) {
1104
+ options.onRetry?.({
1105
+ url,
1106
+ method: options_.method ?? "GET",
1107
+ wait
1108
+ });
1109
+ await sleep(wait, options_.signal);
1110
+ return send(url, options_, attempt + 1, authenticated, download, redirects, streaming, follow);
1111
+ }
1112
+ throw new RateLimitedError("Rate limited by the forge", response.status, body, {
1113
+ ...context,
1114
+ resetAt: rateLimitReset(response),
1115
+ secondary
1116
+ });
1117
+ }
1118
+ if (response.status === 403) {
1119
+ const accepted = response.headers.get("x-accepted-oauth-scopes");
1120
+ const held = response.headers.get("x-oauth-scopes");
1121
+ const forbidden = accepted !== null && accepted !== "" && !accepted.split(",").some((scope) => (held ?? "").split(",").map((value) => value.trim()).includes(scope.trim())) ? void 0 : forbiddenReason(body);
1122
+ if (!forbidden) throw new InsufficientScopeError("Credentials lack the scope or permission for this request", 403, body, context);
1123
+ throw new ForbiddenError(`Forge refused the request (${forbidden.reason})`, 403, body, forbidden.reason, {
1124
+ ...context,
1125
+ reasonRaw: forbidden.reasonRaw
1126
+ });
1127
+ }
1128
+ throw new ForgeApiError(`Forge responded with ${response.status} for ${options_.method ?? "GET"} ${url}`, response.status, body, context);
1129
+ }
1130
+ async function raw(path, options_ = {}) {
1131
+ try {
1132
+ return await send(resolve(path, options_.query), options_, 0);
1133
+ } catch (error) {
1134
+ throw options_.mapError ? options_.mapError(error) : error;
1135
+ }
1136
+ }
1137
+ async function json(path, options_ = {}) {
1138
+ const response = await raw(path, options_);
1139
+ if (response.status === 304 || response.status === 204) return {
1140
+ data: void 0,
1141
+ response,
1142
+ notModified: response.status === 304,
1143
+ cursor: cursorFrom(response)
1144
+ };
1145
+ const text = await response.text();
1146
+ return {
1147
+ data: text ? parseJson(text) : void 0,
1148
+ response,
1149
+ notModified: false,
1150
+ cursor: cursorFrom(response)
1151
+ };
1152
+ }
1153
+ async function page(path, options_ = {}) {
1154
+ const { select, cursor, ...requestOptions } = options_;
1155
+ const initial = resolve(path, requestOptions.query);
1156
+ const url = cursor?.nextUrl ? resolve(cursor.nextUrl) : initial;
1157
+ if (new URL(url).origin !== new URL(initial).origin) throw new TypeError("Pagination URL must remain on the requested origin");
1158
+ const result = await json(url, {
1159
+ ...requestOptions,
1160
+ etag: cursor?.nextUrl ? void 0 : requestOptions.etag,
1161
+ query: void 0
1162
+ });
1163
+ const next = result.notModified ? void 0 : select ? select(result.data, result.cursor?.nextUrl) : void 0;
1164
+ if (!next) return {
1165
+ ...result,
1166
+ data: result.data ?? [],
1167
+ cursor: result.cursor?.nextUrl ? result.cursor : void 0
1168
+ };
1169
+ return {
1170
+ ...result,
1171
+ data: next.items,
1172
+ cursor: next.next ? {
1173
+ ...result.cursor,
1174
+ nextUrl: next.next
1175
+ } : void 0
1176
+ };
1177
+ }
1178
+ async function* items(path, options_ = {}) {
1179
+ let cursor;
1180
+ do {
1181
+ const result = await page(path, {
1182
+ ...options_,
1183
+ cursor
1184
+ });
1185
+ yield* result.data ?? [];
1186
+ cursor = result.notModified ? void 0 : result.cursor;
1187
+ } while (cursor?.nextUrl);
1188
+ }
1189
+ async function stream(path, options_ = {}) {
1190
+ try {
1191
+ const response = await send(resolve(path, options_.query), options_, 0, true, true, 0, true);
1192
+ return {
1193
+ status: response.status,
1194
+ headers: response.headers,
1195
+ body: response.body ?? emptyStream()
1196
+ };
1197
+ } catch (error) {
1198
+ throw options_.mapError ? options_.mapError(error) : error;
1199
+ }
1200
+ }
1201
+ return {
1202
+ raw,
1203
+ stream,
1204
+ json,
1205
+ page,
1206
+ items,
1207
+ resolve
1208
+ };
1209
+ }
1210
+ const LONG_NUMBER_RE = /\d{16}/;
1211
+ const INTEGER_RE = /^-?\d+$/;
1212
+ /**
1213
+ * Parses JSON, keeping integers beyond `Number.MAX_SAFE_INTEGER` (such as
1214
+ * GitHub webhook delivery ids) as their exact digits, where the runtime gives
1215
+ * the reviver the source text.
1216
+ */
1217
+ function parseJson(text) {
1218
+ if (!LONG_NUMBER_RE.test(text)) return JSON.parse(text);
1219
+ return JSON.parse(text, (_key, value, context) => typeof value === "number" && !Number.isSafeInteger(value) && context?.source && INTEGER_RE.test(context.source) ? context.source : value);
1220
+ }
1221
+ function emptyStream() {
1222
+ return new Blob([]).stream();
1223
+ }
1224
+ const SAFE_METHODS = /* @__PURE__ */ new Set([
1225
+ "GET",
1226
+ "HEAD",
1227
+ "OPTIONS"
1228
+ ]);
1229
+ /**
1230
+ * Builds a provider's `request()` escape hatch on top of its fetcher.
1231
+ * @param fetcher The fetcher to send requests with.
1232
+ * @param createOptions Where to reject writes, for a read-only provider.
1233
+ */
1234
+ function createRequest(fetcher, createOptions = {}) {
1235
+ const { readOnly } = createOptions;
1236
+ return (async (method, path, options = {}) => {
1237
+ const upperMethod = method.toUpperCase();
1238
+ if (readOnly && (options.mutates ?? !SAFE_METHODS.has(upperMethod))) throw readOnly(upperMethod, path);
1239
+ const json = options.body !== void 0 && isJsonBody(options.body);
1240
+ const init = {
1241
+ method: upperMethod,
1242
+ query: options.query,
1243
+ signal: options.signal,
1244
+ headers: {
1245
+ ...json ? { "content-type": "application/json" } : {},
1246
+ ...options.headers
1247
+ },
1248
+ body: options.body === void 0 ? void 0 : json ? JSON.stringify(options.body) : options.body
1249
+ };
1250
+ if ("raw" in options && options.raw) return fetcher.stream(path, init);
1251
+ const result = await fetcher.json(path, init);
1252
+ const rateLimit = rateLimitOf(result.response);
1253
+ return {
1254
+ status: result.response.status,
1255
+ data: result.data,
1256
+ headers: result.response.headers,
1257
+ ...rateLimit ? { rateLimit } : {}
1258
+ };
1259
+ });
1260
+ }
1261
+ function isJsonBody(value) {
1262
+ return typeof value !== "string" && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value) && !(typeof Blob !== "undefined" && value instanceof Blob) && !(typeof FormData !== "undefined" && value instanceof FormData) && !(value instanceof URLSearchParams) && !(typeof ReadableStream !== "undefined" && value instanceof ReadableStream);
1263
+ }
1264
+ //#endregion
1265
+ //#region src/model.ts
1266
+ /** Every name {@link ReactionContent} covers. */
1267
+ const REACTION_CONTENTS = [
1268
+ "+1",
1269
+ "-1",
1270
+ "laugh",
1271
+ "confused",
1272
+ "heart",
1273
+ "hooray",
1274
+ "rocket",
1275
+ "eyes"
1276
+ ];
1277
+ /** The normalised name for a forge-native reaction name, or `'other'` where there is none. */
1278
+ function reactionContent(raw) {
1279
+ return REACTION_CONTENTS.includes(raw) ? raw : "other";
1280
+ }
1281
+ /**
1282
+ * The hidden marker `threads.upsertComment()` writes into a comment body to
1283
+ * find it again. It renders as nothing on every forge that renders Markdown
1284
+ * as HTML.
1285
+ */
1286
+ function commentMarker(key) {
1287
+ const end = ["-->", "--!>"].find((token) => key.includes(token));
1288
+ if (end) throw new TypeError(`Comment key ${JSON.stringify(key)} contains \`${end}\`, which would end the hidden marker early`);
1289
+ return `<!-- forges:key=${key} -->`;
1290
+ }
1291
+ /** Whether `body` carries the marker for `key`. */
1292
+ function hasCommentMarker(body, key) {
1293
+ return body.includes(commentMarker(key));
1294
+ }
1295
+ const CASE_INSENSITIVE = /* @__PURE__ */ new Set([
1296
+ "github",
1297
+ "gitlab",
1298
+ "forgejo",
1299
+ "gitea",
1300
+ "bitbucket",
1301
+ "gitee",
1302
+ "azure-devops"
1303
+ ]);
1304
+ /** Whether `forge` treats owner and repository names case-insensitively. Unknown forges are assumed not to. */
1305
+ function namesAreCaseInsensitive(forge) {
1306
+ return CASE_INSENSITIVE.has(forge);
1307
+ }
1308
+ /** `repo` with `owner` and `name` lowercased where the forge ignores case, so equal repositories compare equal. */
1309
+ function normaliseRepoName(repo) {
1310
+ return namesAreCaseInsensitive(repo.forge) ? {
1311
+ ...repo,
1312
+ owner: repo.owner.toLowerCase(),
1313
+ name: repo.name.toLowerCase()
1314
+ } : repo;
1315
+ }
1316
+ /** Whether `ref` addresses an organisation, group or workspace rather than a repository. */
1317
+ function isNamespaceRef(ref) {
1318
+ return ref.kind === "namespace" || !ref.name;
1319
+ }
1320
+ /**
1321
+ * Stable identity key for a repository: by `externalId` when known, so renames
1322
+ * and transfers keep the key, otherwise by path, lowercased on forges that
1323
+ * ignore case. A hand-built ref without `externalId` therefore keys
1324
+ * differently from the same repo as a provider returns it.
1325
+ */
1326
+ function repoKey(repo) {
1327
+ if (repo.externalId) return `${repo.forge}:${repo.instance}/@${repo.kind === "namespace" ? "namespace:" : ""}${repo.externalId}`;
1328
+ const { owner, name } = normaliseRepoName(repo);
1329
+ return repo.kind === "namespace" ? `${repo.forge}:${repo.instance}/${owner}/` : `${repo.forge}:${repo.instance}/${owner}/${name}`;
1330
+ }
1331
+ /**
1332
+ * Reads a {@link repoKey} back. Keys built from an `externalId` carry no path,
1333
+ * so `owner` and `name` are empty on those.
1334
+ */
1335
+ function parseRepoKey(key) {
1336
+ const colon = key.indexOf(":");
1337
+ const slash = key.indexOf("/", colon + 1);
1338
+ if (colon <= 0 || slash <= colon + 1) return;
1339
+ const origin = {
1340
+ forge: key.slice(0, colon),
1341
+ instance: key.slice(colon + 1, slash)
1342
+ };
1343
+ const path = key.slice(slash + 1);
1344
+ if (path.startsWith("@")) {
1345
+ const namespace = path.startsWith("@namespace:");
1346
+ const externalId = path.slice(namespace ? 11 : 1);
1347
+ return externalId ? {
1348
+ ...origin,
1349
+ owner: "",
1350
+ name: "",
1351
+ externalId,
1352
+ ...namespace ? { kind: "namespace" } : {}
1353
+ } : void 0;
1354
+ }
1355
+ if (path.endsWith("/")) return path.length > 1 ? {
1356
+ ...origin,
1357
+ owner: path.slice(0, -1),
1358
+ name: "",
1359
+ kind: "namespace"
1360
+ } : void 0;
1361
+ const last = path.lastIndexOf("/");
1362
+ return last > 0 && last < path.length - 1 ? {
1363
+ ...origin,
1364
+ owner: path.slice(0, last),
1365
+ name: path.slice(last + 1)
1366
+ } : void 0;
1367
+ }
1368
+ /** The `owner/name` people read and type; the owner alone for a namespace. */
1369
+ function repoSlug(repo) {
1370
+ return repo.kind === "namespace" ? repo.owner : `${repo.owner}/${repo.name}`;
1371
+ }
1372
+ /** Whether a thread ref has a number, so that it is a `ResolvedThreadRef`. */
1373
+ function isResolvedThread(thread) {
1374
+ return typeof thread.number === "string" && thread.number !== "";
1375
+ }
1376
+ /**
1377
+ * Stable identity key for a thread. `'other'` threads are keyed by their
1378
+ * forge-native type so unrelated subjects with the same id never collide.
1379
+ */
1380
+ function threadKey(thread) {
1381
+ const kind = thread.kind === "other" ? `other:${thread.typeRaw ?? "unknown"}` : thread.kind;
1382
+ return `${repoKey(thread.repo)}#${kind}/${thread.number}`;
1383
+ }
1384
+ /** A string that identifies a notification, for use as a key in a map or a store. */
1385
+ function notificationKey(ref) {
1386
+ return `${ref.forge}:${ref.instance}:notification/${ref.id}`;
1387
+ }
1388
+ const THREAD_KINDS = /* @__PURE__ */ new Set([
1389
+ "issue",
1390
+ "pull_request",
1391
+ "discussion",
1392
+ "commit"
1393
+ ]);
1394
+ /** Reads a {@link threadKey} back. */
1395
+ function parseThreadKey(key) {
1396
+ const hash = key.indexOf("#");
1397
+ const repo = hash > 0 ? parseRepoKey(key.slice(0, hash)) : void 0;
1398
+ const rest = key.slice(hash + 1);
1399
+ const slash = rest.indexOf("/");
1400
+ if (!repo || slash <= 0 || slash === rest.length - 1) return;
1401
+ const kindText = rest.slice(0, slash);
1402
+ const number = rest.slice(slash + 1);
1403
+ const origin = {
1404
+ forge: repo.forge,
1405
+ instance: repo.instance
1406
+ };
1407
+ if (kindText.startsWith("other:")) return {
1408
+ ...origin,
1409
+ repo,
1410
+ kind: "other",
1411
+ typeRaw: kindText.slice(6),
1412
+ number
1413
+ };
1414
+ return THREAD_KINDS.has(kindText) ? {
1415
+ ...origin,
1416
+ repo,
1417
+ kind: kindText,
1418
+ number
1419
+ } : void 0;
1420
+ }
1421
+ /** Reads a {@link notificationKey} back. */
1422
+ function parseNotificationKey(key) {
1423
+ const marker = key.indexOf(":notification/");
1424
+ const colon = key.indexOf(":");
1425
+ if (colon <= 0 || marker <= colon) return;
1426
+ const id = key.slice(marker + 14);
1427
+ return id ? {
1428
+ forge: key.slice(0, colon),
1429
+ instance: key.slice(colon + 1, marker),
1430
+ id
1431
+ } : void 0;
1432
+ }
1433
+ /** `acme/widgets#42`, or `acme/widgets@abc1234` for a commit; uses the display number where the forge has one. */
1434
+ function threadSlug(ref) {
1435
+ const number = ref.displayNumber ?? ref.number ?? "?";
1436
+ return ref.kind === "commit" ? `${repoSlug(ref.repo)}@${number.slice(0, 7)}` : `${repoSlug(ref.repo)}#${number}`;
1437
+ }
1438
+ /** The thread a notification concerns, or `undefined` for repo, actor and other subjects. */
1439
+ function notificationThread(notification) {
1440
+ return notification.subject.type === "thread" ? notification.subject.thread : void 0;
1441
+ }
1442
+ /** Every `Date` field in the model, by name. */
1443
+ const DATE_FIELDS = /* @__PURE__ */ new Set([
1444
+ "closedAt",
1445
+ "completedAt",
1446
+ "createdAt",
1447
+ "date",
1448
+ "deliveredAt",
1449
+ "dismissedAt",
1450
+ "dueOn",
1451
+ "expiresAt",
1452
+ "lastActivityAt",
1453
+ "lastReadAt",
1454
+ "occurredAt",
1455
+ "publishedAt",
1456
+ "pushedAt",
1457
+ "resetAt",
1458
+ "startedAt",
1459
+ "submittedAt",
1460
+ "updatedAt"
1461
+ ]);
1462
+ const UNNORMALISED_FIELDS = /* @__PURE__ */ new Set(["payload", "raw"]);
1463
+ /**
1464
+ * Turns the ISO strings `JSON.stringify` left in a model value back into
1465
+ * `Date`s, at any depth, for every field in {@link DATE_FIELDS}, skipping
1466
+ * `raw` and `payload`. Returns a copy.
1467
+ */
1468
+ function reviveDates(value) {
1469
+ if (Array.isArray(value)) return value.map((item) => reviveDates(item));
1470
+ if (!value || typeof value !== "object" || value instanceof Date) return value;
1471
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, reviveField(key, item)]));
1472
+ }
1473
+ function reviveField(key, item) {
1474
+ if (UNNORMALISED_FIELDS.has(key)) return item;
1475
+ if (DATE_FIELDS.has(key) && typeof item === "string" && !Number.isNaN(Date.parse(item))) return new Date(item);
1476
+ return reviveDates(item);
1477
+ }
1478
+ //#endregion
1479
+ //#region src/supports.ts
1480
+ /** Where each verb's support lives in {@link ForgeCapabilities}, from {@link CAPABILITY_TABLE}. */
1481
+ const VERBS = new Map(CAPABILITY_TABLE.flatMap((entry) => (entry.verbs ?? []).map((verb) => [verb, entry.capability])));
1482
+ /**
1483
+ * Whether `verb` is supported according to `capabilities`, for `kind` where
1484
+ * support differs by thread kind (or by alert kind for `securityAlerts`).
1485
+ * Without `kind`, a per-kind verb counts as supported when any kind is.
1486
+ * @param capabilities The capabilities of a provider, such as `provider.capabilities`.
1487
+ * @param verb The operation, named by its path on the provider, such as `threads.comment`.
1488
+ * @param kind The thread or alert kind, for an operation whose support differs by kind.
1489
+ * @example
1490
+ * ```ts
1491
+ * if (supports(provider.capabilities, 'threads.merge')) {
1492
+ * await provider.threads.merge(ref)
1493
+ * }
1494
+ * ```
1495
+ */
1496
+ function supports(capabilities, verb, kind) {
1497
+ return supportOf(capabilities, verb, kind) !== false;
1498
+ }
1499
+ /** Levels from strongest to weakest, as the capability matrix ranks them. */
1500
+ const LEVELS = [
1501
+ true,
1502
+ "experimental",
1503
+ "emulated",
1504
+ "unverified"
1505
+ ];
1506
+ /**
1507
+ * How well `verb` is supported according to `capabilities`: `true`,
1508
+ * `'experimental'`, `'emulated'`, `'unverified'` or `false`, for `kind` where support differs
1509
+ * by thread kind (or by alert kind for `securityAlerts`). Without `kind`, a
1510
+ * per-kind verb reports its strongest level across kinds.
1511
+ * @param capabilities The capabilities of a provider, such as `provider.capabilities`.
1512
+ * @param verb The operation, named by its path on the provider, such as `threads.comment`.
1513
+ * @param kind The thread or alert kind, for an operation whose support differs by kind.
1514
+ */
1515
+ function supportOf(capabilities, verb, kind) {
1516
+ const support = capabilityOf(capabilities, verb);
1517
+ if (typeof support !== "object") return support;
1518
+ if (kind) return support[kind] ?? false;
1519
+ const levels = Object.values(support);
1520
+ return LEVELS.find((level) => levels.includes(level)) ?? false;
1521
+ }
1522
+ /** The capability entry behind `verb`: one support level, or one per kind. */
1523
+ function capabilityOf(capabilities, verb) {
1524
+ const capability = VERBS.get(verb);
1525
+ if (!capability) throw new TypeError(`Unknown verb ${JSON.stringify(verb)}; verbs are named by their path on the provider, such as 'threads.comment'`);
1526
+ return capability.split(".").reduce((value, key) => value[key], capabilities);
1527
+ }
1528
+ //#endregion
1529
+ //#region src/utils.ts
1530
+ /**
1531
+ * Wraps a generator so warnings it reports are collected on the returned
1532
+ * iterable rather than interrupting iteration.
1533
+ */
1534
+ function forgeIterable(run) {
1535
+ const warnings = [];
1536
+ return {
1537
+ warnings,
1538
+ [Symbol.asyncIterator]: () => run((warning) => warnings.push(warning))[Symbol.asyncIterator]()
1539
+ };
1540
+ }
1541
+ /** Drives a `page()` function until it reports no further cursor, collecting page warnings. */
1542
+ function iteratePages(page, options = {}) {
1543
+ return forgeIterable(async function* (warn) {
1544
+ let cursor = options.cursor;
1545
+ while (true) {
1546
+ const result = await page({
1547
+ ...options,
1548
+ cursor
1549
+ });
1550
+ result.warnings?.forEach(warn);
1551
+ yield* result.items;
1552
+ if (!result.cursor?.nextUrl && !result.cursor?.token || result.notModified) return;
1553
+ cursor = result.cursor;
1554
+ }
1555
+ });
1556
+ }
1557
+ /** Turns a caught error into a serialisable warning. */
1558
+ function toWarning(code, error, subject) {
1559
+ const err = error instanceof Error ? error : new Error(String(error));
1560
+ return {
1561
+ code,
1562
+ message: err.message,
1563
+ subject,
1564
+ cause: {
1565
+ name: err.name,
1566
+ message: err.message,
1567
+ status: err instanceof ForgeApiError ? err.status : void 0
1568
+ }
1569
+ };
1570
+ }
1571
+ /** Whether a failed side read may become a warning; rate limits, revoked tokens and errors from outside forges still stop the read. */
1572
+ function degradesToWarning(error) {
1573
+ return error instanceof ForgeError && !(error instanceof RateLimitedError) && !(error instanceof TokenRevokedError);
1574
+ }
1575
+ /** Maps with at most `limit` calls in flight, preserving order. */
1576
+ async function mapConcurrent(items, limit, fn) {
1577
+ const results = Array.from({ length: items.length });
1578
+ let next = 0;
1579
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
1580
+ while (next < items.length) {
1581
+ const index = next++;
1582
+ results[index] = await fn(items[index]);
1583
+ }
1584
+ }));
1585
+ return results;
1586
+ }
1587
+ /** `threads.getMany` for forges without a batch endpoint. */
1588
+ function getManyConcurrently(refs, get, options = {}) {
1589
+ return mapConcurrent(refs, 4, (ref) => get(ref, options).then((thread) => ({
1590
+ ok: true,
1591
+ ref,
1592
+ thread
1593
+ }), (error) => {
1594
+ if (options.signal?.aborted) throw options.signal.reason;
1595
+ return {
1596
+ ok: false,
1597
+ ref,
1598
+ warning: toWarning(error instanceof UnresolvedThreadError ? "thread_unresolved" : "thread_unreadable", error, ref.number)
1599
+ };
1600
+ }));
1601
+ }
1602
+ /** Compares dotted numeric versions, ignoring any suffix after the numbers. */
1603
+ function versionAtLeast(version, minimum) {
1604
+ if (!version) return false;
1605
+ const parts = (value) => (/^\D*(\d+(?:\.\d+)*)/.exec(value)?.[1] ?? "0").split(".").map(Number);
1606
+ const [a, b] = [parts(version), parts(minimum)];
1607
+ for (let index = 0; index < Math.max(a.length, b.length); index++) {
1608
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
1609
+ if (difference !== 0) return difference > 0;
1610
+ }
1611
+ return true;
1612
+ }
1613
+ /** Reads a timestamp from a forge response, or `undefined` when it is empty or not a date. */
1614
+ function toDate(value) {
1615
+ if (value === null || value === void 0 || value === "") return;
1616
+ const date = new Date(value);
1617
+ return Number.isNaN(date.getTime()) ? void 0 : date;
1618
+ }
1619
+ /** The host of `baseUrl`, including the port when there is one. */
1620
+ function hostOf(baseUrl) {
1621
+ return new URL(baseUrl).host;
1622
+ }
1623
+ /** Narrows a ref to one a provider can address, or throws {@link UnresolvedThreadError}. */
1624
+ function requireThread(ref, context) {
1625
+ if (ref.kind === "other" || !isResolvedThread(ref)) throw new UnresolvedThreadError(ref.kind === "other" ? `${ref.typeRaw ?? "This"} subject has no thread API` : "Thread ref has no number; resolve it before calling the forge", context ?? {
1626
+ forge: ref.forge,
1627
+ instance: ref.instance
1628
+ });
1629
+ return ref;
1630
+ }
1631
+ /**
1632
+ * Narrows a ref to an issue or pull request, or throws
1633
+ * {@link UnsupportedOperationError} naming the verb that needs one.
1634
+ */
1635
+ function requireIssueOrPull(ref, context, verb) {
1636
+ const thread = requireThread(ref, context);
1637
+ if (thread.kind !== "issue" && thread.kind !== "pull_request") throw new UnsupportedOperationError(`${context.forge ?? "This forge"} cannot ${verb} a ${thread.kind}`, context);
1638
+ return thread;
1639
+ }
1640
+ /** The token in `auth`, read again on every call when it is a function. */
1641
+ async function resolveToken(auth) {
1642
+ return typeof auth.token === "function" ? await auth.token() : auth.token;
1643
+ }
1644
+ /**
1645
+ * The forge-native id `threads.setMilestone` sends: the milestone's `id`, a
1646
+ * string of digits as it is, or the id of the milestone with that title.
1647
+ */
1648
+ async function milestoneId(milestone, page, context, options = {}) {
1649
+ const value = typeof milestone === "string" ? milestone : milestone.id;
1650
+ if (/^\d+$/.test(value)) return Number(value);
1651
+ for await (const candidate of iteratePages(page, {
1652
+ state: "all",
1653
+ signal: options.signal
1654
+ })) if (candidate.title === value) return Number(candidate.id);
1655
+ throw new UnsupportedOperationError(`No milestone titled ${value}`, context);
1656
+ }
1657
+ /** Joins the merge commit title and message for a forge that takes them as one string. */
1658
+ function mergeCommitMessage({ title, message }) {
1659
+ return title && message ? `${title}\n\n${message}` : title || message;
1660
+ }
1661
+ /** A login from either a login string or an actor. */
1662
+ function actorLogin(actor) {
1663
+ return typeof actor === "string" ? actor : actor.login;
1664
+ }
1665
+ /** Maps one fetched page into a {@link Page}. The cursor is kept only when there is a next page. */
1666
+ function toPage(result, map, warnings) {
1667
+ return {
1668
+ items: (result.data ?? []).flatMap((raw) => {
1669
+ const item = map(raw);
1670
+ return item === void 0 ? [] : [item];
1671
+ }),
1672
+ cursor: result.cursor?.nextUrl || result.cursor?.token ? result.cursor : void 0,
1673
+ notModified: result.notModified || void 0,
1674
+ ...warnings?.length ? { warnings } : {},
1675
+ ...rateLimitPart(result.response)
1676
+ };
1677
+ }
1678
+ /** Builds a {@link Listing} for a forge that names its page-size parameter `pageParam`. */
1679
+ function createListing(fetcher, pageParam, defaultPerPage) {
1680
+ return async (path, options, map, { warnings, query, ...extras } = {}) => toPage(await fetcher.page(path, {
1681
+ ...extras,
1682
+ query: {
1683
+ ...query,
1684
+ [pageParam]: options.perPage ?? defaultPerPage
1685
+ },
1686
+ cursor: options.cursor,
1687
+ signal: options.signal
1688
+ }), map, warnings);
1689
+ }
1690
+ function rateLimitPart(response) {
1691
+ const rateLimit = response && rateLimitOf(response);
1692
+ return rateLimit ? { rateLimit } : {};
1693
+ }
1694
+ /**
1695
+ * Chains listings that a forge serves from separate endpoints. The cursor
1696
+ * `token` records which phase is next, ahead of the phase's own token.
1697
+ */
1698
+ async function phased(phases, cursor) {
1699
+ const match = /^(\d+):(.*)$/s.exec(cursor?.token ?? "");
1700
+ const index = match ? Number(match[1]) : 0;
1701
+ const inner = cursor && (cursor.nextUrl || match?.[2]) ? {
1702
+ ...cursor,
1703
+ token: match?.[2] || void 0
1704
+ } : void 0;
1705
+ const page = await phases[index](inner);
1706
+ if (page.cursor) return {
1707
+ ...page,
1708
+ cursor: {
1709
+ ...page.cursor,
1710
+ token: `${index}:${page.cursor.token ?? ""}`
1711
+ }
1712
+ };
1713
+ return index + 1 < phases.length ? {
1714
+ ...page,
1715
+ cursor: { token: `${index + 1}:` }
1716
+ } : page;
1717
+ }
1718
+ /** Whether `labels` include every name in `wanted`; true when nothing is wanted. */
1719
+ function hasEveryLabel(labels, wanted) {
1720
+ return !wanted?.length || wanted.every((name) => labels?.some((label) => label.name === name));
1721
+ }
1722
+ /** Any failure fails, else any pending is pending, else any unknown (or none at all) is unknown, else success. */
1723
+ function summariseChecks(states, url) {
1724
+ const failed = states.filter((state) => state === "failure").length;
1725
+ return {
1726
+ state: failed ? "failure" : states.includes("pending") ? "pending" : !states.length || states.includes("unknown") ? "unknown" : "success",
1727
+ total: states.length,
1728
+ failed,
1729
+ ...url ? { url } : {}
1730
+ };
1731
+ }
1732
+ const FAILED_CONCLUSIONS = /* @__PURE__ */ new Set([
1733
+ "failure",
1734
+ "timed_out",
1735
+ "cancelled",
1736
+ "action_required",
1737
+ "startup_failure"
1738
+ ]);
1739
+ /**
1740
+ * State of a check run reported as a status plus a conclusion once completed.
1741
+ * Cancelled, timed-out, action-required and startup failures count as
1742
+ * failures; skipped and neutral runs as neutral.
1743
+ */
1744
+ function checkRunState(status, conclusion) {
1745
+ if (status !== "completed") return "pending";
1746
+ if (conclusion === "success") return "success";
1747
+ if (conclusion && FAILED_CONCLUSIONS.has(conclusion)) return "failure";
1748
+ return conclusion === "neutral" || conclusion === "skipped" ? "neutral" : "unknown";
1749
+ }
1750
+ /** Maps a forge's own file-change word onto {@link FileStatus}; unknown words become `'changed'`. */
1751
+ function toFileStatus(raw) {
1752
+ switch (raw?.toLowerCase()) {
1753
+ case "added":
1754
+ case "new":
1755
+ case "add": return "added";
1756
+ case "removed":
1757
+ case "deleted":
1758
+ case "remove": return "removed";
1759
+ case "renamed":
1760
+ case "rename": return "renamed";
1761
+ case "copied":
1762
+ case "copy": return "copied";
1763
+ case "modified":
1764
+ case "edit":
1765
+ case "change": return "modified";
1766
+ default: return "changed";
1767
+ }
1768
+ }
1769
+ /**
1770
+ * Caches the result of `load` after its first call. A rejected load is
1771
+ * forgotten so the next call retries; with `ttl` (milliseconds) a resolved
1772
+ * value is reloaded once it is older than that.
1773
+ */
1774
+ function memo(load, ttl) {
1775
+ let cached;
1776
+ return () => {
1777
+ if (!cached || ttl !== void 0 && Date.now() - cached.at > ttl) {
1778
+ const promise = load();
1779
+ cached = {
1780
+ promise,
1781
+ at: Date.now()
1782
+ };
1783
+ promise.catch(() => {
1784
+ if (cached?.promise === promise) cached = void 0;
1785
+ });
1786
+ }
1787
+ return cached.promise;
1788
+ };
1789
+ }
1790
+ /** {@link memo} per key. */
1791
+ function memoBy(load) {
1792
+ const cache = /* @__PURE__ */ new Map();
1793
+ return (key) => {
1794
+ let entry = cache.get(key);
1795
+ if (!entry) {
1796
+ entry = memo(() => load(key));
1797
+ cache.set(key, entry);
1798
+ }
1799
+ return entry();
1800
+ };
1801
+ }
1802
+ /**
1803
+ * A review standing in for an approval, vote or participant state on a forge
1804
+ * that has no review objects. `comments` is `false` because there is nothing
1805
+ * to hang inline comments on.
1806
+ */
1807
+ function syntheticReview(thread, id, state, extras) {
1808
+ const { author, stateRaw, submittedAt, raw } = extras;
1809
+ return {
1810
+ ref: {
1811
+ forge: thread.forge,
1812
+ instance: thread.instance,
1813
+ thread,
1814
+ id
1815
+ },
1816
+ author,
1817
+ state,
1818
+ stateRaw,
1819
+ submittedAt,
1820
+ comments: false,
1821
+ raw
1822
+ };
1823
+ }
1824
+ /** A label colour as six hex digits, with the `#` prefix the forge expects. `ededed` when none is given. */
1825
+ function hexColour(colour, prefix = "") {
1826
+ return `${prefix}${(colour ?? "ededed").replace(/^#/, "")}`;
1827
+ }
1828
+ //#endregion
1829
+ //#region src/web.ts
1830
+ function sameRepo(a, b) {
1831
+ if (!b || a.forge !== b.forge || a.instance !== b.instance) return false;
1832
+ const [left, right] = [normaliseRepoName(a), normaliseRepoName(b)];
1833
+ return left.owner === right.owner && left.name === right.name;
1834
+ }
1835
+ function encodePath(path) {
1836
+ return path.split("/").map(encodeURIComponent).join("/");
1837
+ }
1838
+ function webUrlFor(web, target) {
1839
+ let path;
1840
+ if ("repo" in target) path = web.repo(target.repo);
1841
+ else if ("thread" in target) path = isResolvedThread(target.thread) ? web.thread(target.thread) : void 0;
1842
+ else if ("comment" in target) {
1843
+ const { comment } = target;
1844
+ path = isResolvedThread(comment.thread) ? web.comment?.(comment) : void 0;
1845
+ } else if ("release" in target) path = web.release?.(target.release);
1846
+ else if ("file" in target) path = web.file?.(target.file.repo, target.file.path, target.file.at, target.file.line);
1847
+ else path = web.compare?.(target.compare.repo, target.compare.base, target.compare.head);
1848
+ return path === void 0 ? void 0 : `${web.origin}${path}`;
1849
+ }
1850
+ /** `ssh://user@host:port/path` or `git://host:port/path` with the host in group 1, or `user@host:path` with it in group 2; the path is group 3. */
1851
+ const CLONE_URL_RE = /^(?:(?:ssh|git):\/\/(?:[^@/]+@)?([^:/]+)(?::\d+)?\/|(?:[^@/\s]+@)?([^:/\s]+):(?!\/))(.+)$/i;
1852
+ /** The `git+` that npm's `repository` field writes before a transport scheme. */
1853
+ const GIT_PLUS_RE = /^git\+(?=[a-z]+:\/\/)/i;
1854
+ /** The npm `repository` shorthand, https://docs.npmjs.com/cli/configuring-npm/package-json#repository */
1855
+ const SHORTHAND_RE = /^(github|gitlab|bitbucket):(?!\/)/i;
1856
+ const SHORTHAND_HOSTS = {
1857
+ github: "github.com",
1858
+ gitlab: "gitlab.com",
1859
+ bitbucket: "bitbucket.org"
1860
+ };
1861
+ function parseWebUrl(web, input, origin) {
1862
+ const base = new URL(web.origin);
1863
+ const prefix = base.pathname.replace(/\/$/, "");
1864
+ const source = String(input).replace(GIT_PLUS_RE, "").replace(SHORTHAND_RE, (_, forge) => `https://${SHORTHAND_HOSTS[forge.toLowerCase()]}/`);
1865
+ const clone = CLONE_URL_RE.exec(source);
1866
+ let url;
1867
+ try {
1868
+ url = new URL(clone && (clone[1] ?? clone[2]).toLowerCase() === base.hostname ? `${base.origin}${prefix}/${clone[3]}` : source);
1869
+ } catch {
1870
+ return;
1871
+ }
1872
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.host !== base.host || url.username || url.password) return;
1873
+ if (prefix && !url.pathname.startsWith(`${prefix}/`)) return;
1874
+ let segments;
1875
+ try {
1876
+ segments = url.pathname.slice(prefix.length).split("/").filter(Boolean).map(decodeURIComponent);
1877
+ } catch {
1878
+ return;
1879
+ }
1880
+ return web.parse(segments, url, origin);
1881
+ }
1882
+ function referenceFor(web, ref, options = {}) {
1883
+ if (!web || !isResolvedThread(ref)) return;
1884
+ return web.reference?.(ref, sameRepo(ref.repo, options.from), options.expand ?? false) ?? webUrlFor(web, { thread: ref });
1885
+ }
1886
+ /** Web links for forges laid out like GitHub: `/{owner}/{name}/issues/{n}` and friends. */
1887
+ function githubShapedWeb(origin, shape) {
1888
+ const repoPath = (repo) => `/${encodePath(repo.owner)}/${encodeURIComponent(repo.name)}`;
1889
+ const kindPath = {
1890
+ issue: "issues",
1891
+ pull_request: shape.pull,
1892
+ commit: "commit",
1893
+ ...shape.discussions ? { discussion: "discussions" } : {}
1894
+ };
1895
+ const kindOf = new Map(Object.entries(kindPath).map(([kind, path]) => [path, kind]));
1896
+ const reserved = new Set(shape.reserved?.map((segment) => segment.toLowerCase()));
1897
+ return {
1898
+ origin,
1899
+ repo: repoPath,
1900
+ thread: (ref) => kindPath[ref.kind] && `${repoPath(ref.repo)}/${kindPath[ref.kind]}/${encodeURIComponent(ref.number)}`,
1901
+ comment: (ref) => ref.thread.kind === "commit" || !kindPath[ref.thread.kind] ? void 0 : `${repoPath(ref.thread.repo)}/${kindPath[ref.thread.kind]}/${encodeURIComponent(ref.thread.number)}#${shape.commentFragment}${ref.id}`,
1902
+ release: (ref) => ref.tag ? `${repoPath(ref.repo)}/releases/tag/${encodeURIComponent(ref.tag)}` : void 0,
1903
+ file: (repo, path, at, line) => `${repoPath(repo)}${shape.file(at)}/${encodePath(path)}${line ? `#${shape.lineFragment(line)}` : ""}`,
1904
+ compare: (repo, base, head) => `${repoPath(repo)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`,
1905
+ parse: (segments, url, from) => {
1906
+ const [owner, name, section, id] = segments;
1907
+ if (!owner || !name || reserved.has(owner.toLowerCase())) return;
1908
+ const repo = {
1909
+ ...from,
1910
+ owner,
1911
+ name: name.replace(/\.git$/, "")
1912
+ };
1913
+ const kind = section ? kindOf.get(section) : void 0;
1914
+ if (section === "releases" && id === "tag" && segments[4]) return {
1915
+ repo,
1916
+ release: {
1917
+ ...from,
1918
+ repo,
1919
+ id: segments[4],
1920
+ tag: segments[4]
1921
+ }
1922
+ };
1923
+ if (!kind || !id) return { repo };
1924
+ const thread = {
1925
+ ...from,
1926
+ repo,
1927
+ kind,
1928
+ number: id
1929
+ };
1930
+ const comment = url.hash.startsWith(`#${shape.commentFragment}`) ? url.hash.slice(shape.commentFragment.length + 1) : void 0;
1931
+ return {
1932
+ repo,
1933
+ thread,
1934
+ ...comment ? { comment: {
1935
+ ...from,
1936
+ thread,
1937
+ id: comment
1938
+ } } : {}
1939
+ };
1940
+ },
1941
+ reference: (ref, same) => {
1942
+ if (ref.kind === "commit") return same ? ref.number.slice(0, 7) : `${ref.repo.owner}/${ref.repo.name}@${ref.number.slice(0, 7)}`;
1943
+ const prefix = ref.kind === "pull_request" && shape.pullPrefix ? shape.pullPrefix : "#";
1944
+ return `${same ? "" : `${ref.repo.owner}/${ref.repo.name}`}${prefix}${ref.number}`;
1945
+ }
1946
+ };
1947
+ }
1948
+ /**
1949
+ * Reads `url` with whichever of `providers` serves it.
1950
+ * @param url The web URL to read.
1951
+ * @param providers The providers to ask. The first that serves the URL answers.
1952
+ */
1953
+ function parseForgeUrl(url, providers) {
1954
+ for (const provider of providers) {
1955
+ const parsed = provider.parseUrl(url);
1956
+ if (parsed) return parsed;
1957
+ }
1958
+ }
1959
+ //#endregion
1960
+ //#region src/define.ts
1961
+ /**
1962
+ * Declares a verb whose support does not depend on the thread kind.
1963
+ * @param support How well the verb is supported.
1964
+ * @param run The function that implements the verb.
1965
+ */
1966
+ function verb(support, run) {
1967
+ return {
1968
+ support,
1969
+ run
1970
+ };
1971
+ }
1972
+ /**
1973
+ * Declares a verb whose support depends on the thread kind.
1974
+ * @param kinds How well each thread kind is supported. Kinds that are left out are unsupported.
1975
+ * @param run The function that implements the verb.
1976
+ * @example
1977
+ * ```ts
1978
+ * const threads = {
1979
+ * get: perKind({ issue: true, pull_request: true }, getThread),
1980
+ * }
1981
+ * ```
1982
+ */
1983
+ function perKind(kinds, run) {
1984
+ return {
1985
+ kinds,
1986
+ run
1987
+ };
1988
+ }
1989
+ /** Iterables whose page takes only options, with no target before them. */
1990
+ const UNTARGETED = /* @__PURE__ */ new Set([
1991
+ "repos.list",
1992
+ "notifications.list",
1993
+ "installations.list",
1994
+ "search.threads",
1995
+ "search.repos",
1996
+ "search.commits",
1997
+ "search.code"
1998
+ ]);
1999
+ /** The thread kind a per-kind verb is called for, read from its arguments. */
2000
+ function kindOf(name, args) {
2001
+ const first = args[0];
2002
+ if (name === "create" || name === "listPage") return args[1]?.kind;
2003
+ return first?.thread?.kind ?? first?.kind;
2004
+ }
2005
+ async function* completeItems(items) {
2006
+ for await (const item of items) yield {
2007
+ ...item,
2008
+ event: completeEvent(item.event)
2009
+ };
2010
+ }
2011
+ function readPath(source, path) {
2012
+ return path.split(".").reduce((value, key) => value?.[key], source);
2013
+ }
2014
+ function instanceHost(baseUrl, kind) {
2015
+ try {
2016
+ return hostOf(baseUrl);
2017
+ } catch (cause) {
2018
+ throw new TypeError(`Invalid baseUrl for ${kind}: ${JSON.stringify(baseUrl)}`, { cause });
2019
+ }
2020
+ }
2021
+ /** Most reads spent looking for a first item when a provider's filtering empties a page. */
2022
+ const MAX_EMPTY_READS = 10;
2023
+ /**
2024
+ * Providers that cannot filter on the forge's side drop items from each page
2025
+ * they read, so a page can come back empty with a `cursor`. Reads on until the
2026
+ * page has an item, the listing ends or `MAX_EMPTY_READS` reads are spent.
2027
+ */
2028
+ async function nonEmptyPage(read, start) {
2029
+ let page = await read(start);
2030
+ const warnings = page.warnings;
2031
+ for (let reads = 1; !page.items.length && page.cursor && !page.notModified && reads < MAX_EMPTY_READS; reads++) page = await read(page.cursor);
2032
+ if (!warnings?.length || page.warnings === warnings) return page;
2033
+ const later = (page.warnings ?? []).filter((warning) => !warnings.some((seen) => seen.code === warning.code && seen.message === warning.message));
2034
+ return {
2035
+ ...page,
2036
+ warnings: [...warnings, ...later]
2037
+ };
2038
+ }
2039
+ function createProvider(definition, givenOptions, givenState) {
2040
+ const givenAuth = givenOptions.auth;
2041
+ const anonymous = Boolean(definition.anonymous) && (!givenAuth || givenAuth.type === "anonymous");
2042
+ const options = anonymous && !givenAuth ? {
2043
+ ...givenOptions,
2044
+ auth: { type: "anonymous" }
2045
+ } : givenOptions;
2046
+ const root = (options.baseUrl ?? definition.baseUrl).replace(/\/$/, "");
2047
+ const baseUrl = definition.apiPath && !root.endsWith(definition.apiPath) ? `${root}${definition.apiPath}` : root;
2048
+ const host = instanceHost(baseUrl, definition.forge);
2049
+ const instance = options.instance ?? definition.instance?.(host) ?? host;
2050
+ const origin = {
2051
+ forge: definition.forge,
2052
+ instance
2053
+ };
2054
+ const browser = "document" in globalThis || "WorkerGlobalScope" in globalThis;
2055
+ const userAgent = options.userAgent ?? (browser ? void 0 : "forges");
2056
+ const headers = {
2057
+ ...definition.headers,
2058
+ ...userAgent ? { "user-agent": userAgent } : {}
2059
+ };
2060
+ const prepared = {
2061
+ options,
2062
+ forge: definition.forge,
2063
+ instance,
2064
+ baseUrl,
2065
+ headers,
2066
+ origin,
2067
+ state: givenState
2068
+ };
2069
+ const state = definition.prepare ? definition.prepare(prepared) : givenState;
2070
+ const base = {
2071
+ ...prepared,
2072
+ state
2073
+ };
2074
+ const makeFetcher = (overrides) => createFetcher({
2075
+ fetch: options.fetch,
2076
+ timeout: options.timeout,
2077
+ onRetry: options.onRetry,
2078
+ headers,
2079
+ query: definition.query,
2080
+ context: origin,
2081
+ ...overrides
2082
+ });
2083
+ const fetcher = makeFetcher({
2084
+ baseUrl,
2085
+ authHeaders: anonymous ? void 0 : definition.authHeaders?.(base)
2086
+ });
2087
+ const ctx = {
2088
+ ...base,
2089
+ fetcher,
2090
+ createFetcher: makeFetcher,
2091
+ derive: (derivedOptions, derivedState) => createProvider(definition, derivedOptions, derivedState ?? state)
2092
+ };
2093
+ const declaredSpec = definition.setup(ctx);
2094
+ const handlers = definition.webhooks?.(ctx);
2095
+ const spec = anonymous || options.readOnly ? restrict(declaredSpec, anonymous, definition.anonymous === "webhooks") : declaredSpec;
2096
+ const authKind = anonymous ? "anonymous" : givenAuth?.type ?? spec.traits.authKinds[0] ?? "anonymous";
2097
+ const env = { version: options.instanceVersion };
2098
+ const probeVersion = spec.probeVersion && memo(spec.probeVersion);
2099
+ const flags = {
2100
+ experimental: Boolean(definition.experimental),
2101
+ readOnly: Boolean(options.readOnly),
2102
+ webhook: Boolean(handlers)
2103
+ };
2104
+ let capabilities = capabilitiesOf(spec, env, flags);
2105
+ const unsupported = (verb) => options.readOnly && WRITE_VERBS.has(verb) ? new ReadOnlyError(`${verb} is a write and this ${definition.forge} provider is read-only`, origin) : new UnsupportedOperationError(`${definition.forge} does not support ${verb}`, origin);
2106
+ /** An iterable that rejects on first read, for listings the forge does not have. */
2107
+ const emptyIterable = (verb) => forgeIterable(async function* () {
2108
+ throw unsupported(verb);
2109
+ });
2110
+ /** Support is resolved at call time, so a version gate is honoured once the version is known. */
2111
+ function gate(verb, support, run) {
2112
+ return (...args) => run && resolve(support, env) ? run(...args) : Promise.reject(unsupported(verb));
2113
+ }
2114
+ /** A per-kind verb: rejects for kinds it does not support, or entirely when undeclared. */
2115
+ function kindGate(verb, kinds, run) {
2116
+ const name = verb.slice(verb.indexOf(".") + 1);
2117
+ return (...args) => {
2118
+ if (!run || options.readOnly && WRITE_VERBS.has(verb)) return Promise.reject(unsupported(verb));
2119
+ const kind = kindOf(name, args);
2120
+ if (kind && KINDS.includes(kind) && !resolve(kinds?.[kind], env)) return Promise.reject(new UnsupportedOperationError(`${definition.forge} does not support ${verb} for a ${kind.replace("_", " ")}`, origin));
2121
+ return run(...args);
2122
+ };
2123
+ }
2124
+ const api = {};
2125
+ const set = (verb, value) => {
2126
+ const [group, name] = verb.split(".");
2127
+ (api[group] ??= {})[name] = value;
2128
+ };
2129
+ const call = (verb) => readPath(api, verb);
2130
+ const subscriptions = spec.threads.subscriptions;
2131
+ const createReview = spec.threads.createReview;
2132
+ const approve = spec.threads.approve ?? (createReview && {
2133
+ support: createReview.support,
2134
+ run: async (ref, body, options) => {
2135
+ await createReview.run(ref, {
2136
+ event: "approve",
2137
+ body
2138
+ }, options);
2139
+ }
2140
+ });
2141
+ const declaredSubscribe = spec.sources?.subscribe;
2142
+ const alerts = spec.securityAlerts;
2143
+ const declaredInstallations = spec.installations;
2144
+ const installationsSupported = () => Boolean(declaredInstallations && resolve(declaredInstallations.support, env));
2145
+ const listThreads = kindGate("threads.listPage", spec.threads.listPage.kinds, spec.threads.listPage.run);
2146
+ const listEvents = kindGate("threads.eventsPage", Object.fromEntries(KINDS.map((kind) => [kind, (kindEnv) => threadEventKinds(spec, kindEnv)[kind]])), spec.threads.eventsPage.run);
2147
+ const native = spec.search?.queryRaw;
2148
+ const search = (name) => {
2149
+ const run = gate(`search.${name}`, spec.search?.[name]?.support, spec.search?.[name]?.run);
2150
+ return async ({ queryRaw, ...query } = {}) => {
2151
+ const page = await run(native ? {
2152
+ queryRaw,
2153
+ ...query
2154
+ } : query);
2155
+ return !queryRaw || native || query.cursor ? page : {
2156
+ ...page,
2157
+ warnings: [{
2158
+ code: "filter_unsupported",
2159
+ message: "This search has no query syntax; queryRaw was ignored"
2160
+ }, ...page.warnings ?? []]
2161
+ };
2162
+ };
2163
+ };
2164
+ const special = {
2165
+ "search.threadsPage": search("threadsPage"),
2166
+ "search.reposPage": search("reposPage"),
2167
+ "search.commitsPage": search("commitsPage"),
2168
+ "search.codePage": search("codePage"),
2169
+ "threads.listPage": async (repo, query = {}) => {
2170
+ if (query.state === "merged" && query.kind && query.kind !== "pull_request") return { items: [] };
2171
+ const run = (cursor) => listThreads(repo, {
2172
+ ...query,
2173
+ ...query.state === "merged" ? { kind: "pull_request" } : {},
2174
+ cursor
2175
+ });
2176
+ return nonEmptyPage(run, query.cursor);
2177
+ },
2178
+ "threads.eventsPage": async (ref, listOptions) => {
2179
+ const page = await listEvents(ref, listOptions);
2180
+ return {
2181
+ ...page,
2182
+ items: page.items.map(completeEvent)
2183
+ };
2184
+ },
2185
+ "threads.upsertComment": kindGate("threads.upsertComment", Object.fromEntries(KINDS.map((kind) => [kind, (kindEnv) => upsertKinds(spec, kindEnv)[kind]])), upsertComment),
2186
+ "threads.subscribe": kindGate("threads.subscribe", subscriptions?.kinds, subscriptions?.run.subscribe),
2187
+ "threads.unsubscribe": kindGate("threads.unsubscribe", subscriptions?.kinds, subscriptions?.run.unsubscribe),
2188
+ "threads.approve": gate("threads.approve", approve?.support, approve?.run),
2189
+ "threads.approveAndMerge": gate("threads.approveAndMerge", (mergeEnv) => approveAndMergeSupport(spec, mergeEnv), async (ref, { body, ...mergeOptions } = {}) => {
2190
+ await spec.threads.merge.run(ref, mergeOptions, { beforeMerge: () => call("threads.approve")(ref, body, { signal: mergeOptions.signal }) });
2191
+ }),
2192
+ "contents.resolveRef": (repo, ref, options) => isSha(ref) ? Promise.resolve(ref.toLowerCase()) : gate("contents.resolveRef", spec.contents?.resolveRef?.support, spec.contents?.resolveRef?.run)(repo, ref, options),
2193
+ "webhooks.verify": handlers ? handlers.verify : () => Promise.reject(unsupported("webhooks.verify")),
2194
+ "webhooks.ingest": async (delivery) => {
2195
+ if (!handlers) throw unsupported("webhooks.ingest");
2196
+ if (!await handlers.verify(delivery)) throw new WebhookVerificationError("Webhook signature verification failed", origin);
2197
+ return (await handlers.translate(delivery)).map(completeEvent);
2198
+ },
2199
+ "sources.subscribe": (subscribeOptions) => declaredSubscribe && resolve(declaredSubscribe.support, env) ? completeItems(declaredSubscribe.run(subscribeOptions)) : emptyIterable("sources.subscribe"),
2200
+ "securityAlerts.listPage": (repo, listOptions = {}) => {
2201
+ if (!(alerts && (listOptions.kind ? resolve(alerts.kinds[listOptions.kind], env) : ALERT_KINDS.some((kind) => resolve(alerts.kinds[kind], env))))) return Promise.reject(listOptions.kind ? new UnsupportedOperationError(`${definition.forge} cannot list ${listOptions.kind.replace("_", " ")} alerts`, origin) : unsupported("securityAlerts.list"));
2202
+ return alerts.listPage(repo, listOptions);
2203
+ },
2204
+ "installations.provider": (installation) => {
2205
+ if (!installationsSupported()) throw unsupported("installations.provider");
2206
+ return declaredInstallations.run.provider(installation);
2207
+ },
2208
+ "installations.providers": () => installationsSupported() ? declaredInstallations.run.providers() : emptyIterable("installations.providers")
2209
+ };
2210
+ for (const entry of CAPABILITY_TABLE) {
2211
+ const declared = entry.spec ? readPath(spec, entry.spec) : void 0;
2212
+ for (const verb of entry.verbs ?? []) if (verb in special) set(verb, special[verb]);
2213
+ else if (entry.verbs.includes(`${verb}Page`)) {
2214
+ const untargeted = UNTARGETED.has(verb);
2215
+ set(verb, (...args) => {
2216
+ return iteratePages((page) => call(`${verb}Page`)(...untargeted ? [] : [args[0]], page), (untargeted ? args[0] : args[1]) ?? {});
2217
+ });
2218
+ } else {
2219
+ const run = typeof declared?.run === "function" ? declared.run : declared?.run?.[verb.slice(verb.indexOf(".") + 1)];
2220
+ set(verb, entry.perKind ? kindGate(verb, declared?.kinds, run) : gate(verb, declared?.support, run));
2221
+ }
2222
+ }
2223
+ api.webhooks.events = handlers?.events ?? [];
2224
+ const threads = api.threads;
2225
+ async function upsertComment(ref, input, options) {
2226
+ const body = hasCommentMarker(input.body, input.key) ? input.body : `${input.body}\n\n${commentMarker(input.key)}`;
2227
+ let existing;
2228
+ for await (const candidate of threads.comments(ref, options)) if (hasCommentMarker(candidate.body, input.key)) existing = candidate;
2229
+ return existing ? {
2230
+ comment: await threads.editComment(existing.ref, body, options),
2231
+ created: false
2232
+ } : {
2233
+ comment: await threads.comment(ref, body, options),
2234
+ created: true
2235
+ };
2236
+ }
2237
+ return {
2238
+ ...api,
2239
+ forge: definition.forge,
2240
+ instance,
2241
+ baseUrl,
2242
+ authKind,
2243
+ request: createRequest(spec.request ?? fetcher, options.readOnly ? { readOnly: (method, path) => new ReadOnlyError(`${method} ${path} is a write and this ${definition.forge} provider is read-only; pass \`mutates: false\` if it only reads`, origin) } : {}),
2244
+ get capabilities() {
2245
+ return capabilities;
2246
+ },
2247
+ async refreshCapabilities() {
2248
+ if (!options.instanceVersion && probeVersion) {
2249
+ env.version = await probeVersion();
2250
+ capabilities = capabilitiesOf(spec, env, flags);
2251
+ }
2252
+ return capabilities;
2253
+ },
2254
+ normaliseMarkdown: spec.normaliseMarkdown ?? ((body) => body),
2255
+ scopesFor: (verb) => spec.scopes?.(verb) ?? {},
2256
+ can: (verb, kind) => supports(capabilities, verb, kind),
2257
+ support: (verb, kind) => supportOf(capabilities, verb, kind),
2258
+ urlFor: (target) => spec.web ? webUrlFor(spec.web, target) : void 0,
2259
+ parseUrl: (url) => spec.web ? parseWebUrl(spec.web, url, origin) : void 0,
2260
+ referenceTo: (ref, referenceOptions) => referenceFor(spec.web, ref, referenceOptions)
2261
+ };
2262
+ }
2263
+ /**
2264
+ * Defines a forge provider. Returns the factory end users call, for example
2265
+ * `github({ auth })`. Nothing touches the network until `create()`.
2266
+ * @param definition The defaults, authentication and setup of the forge.
2267
+ */
2268
+ function defineForgeProvider(definition) {
2269
+ return ((options = {}) => ({
2270
+ forge: definition.forge,
2271
+ ...definition.experimental ? { experimental: true } : {},
2272
+ create: () => createProvider(definition, options)
2273
+ }));
2274
+ }
2275
+ //#endregion
2276
+ export { threadKey as $, toPage as A, UnresolvedThreadError as At, isNamespaceRef as B, requireIssueOrPull as C, MergeMethodRequiredError as Ct, syntheticReview as D, SubscriptionClosedError as Dt, summariseChecks as E, ReadOnlyError as Et, supports as F, notificationThread as G, namesAreCaseInsensitive as H, DATE_FIELDS as I, parseThreadKey as J, parseNotificationKey as K, REACTION_CONTENTS as L, versionAtLeast as M, WebhookVerificationError as Mt, capabilityOf as N, soleMergeMethod as Nt, toDate as O, TokenRevokedError as Ot, supportOf as P, toMergeError as Pt, reviveDates as Q, commentMarker as R, phased as S, MergeConflictError as St, resolveToken as T, RateLimitedError as Tt, normaliseRepoName as U, isResolvedThread as V, notificationKey as W, repoKey as X, reactionContent as Y, repoSlug as Z, mapConcurrent as _, ForgeError as _t, parseForgeUrl as a, parseLinkHeader as at, mergeCommitMessage as b, InsufficientScopeError as bt, checkRunState as c, eventAction as ct, forgeIterable as d, fromBase64 as dt, threadSlug as et, getManyConcurrently as f, toFileContent as ft, iteratePages as g, ForgeApiError as gt, hostOf as h, ForbiddenError as ht, githubShapedWeb as i, createRequest as it, toWarning as j, UnsupportedOperationError as jt, toFileStatus as k, UnknownForgeError as kt, createListing as l, reviewState as lt, hexColour as m, ContentNotTextError as mt, perKind as n, anySignal as nt, webUrlFor as o, sleep as ot, hasEveryLabel as p, AuthenticationRequiredError as pt, parseRepoKey as q, verb as r, createFetcher as rt, actorLogin as s, completeEvent as st, defineForgeProvider as t, abortable as tt, degradesToWarning as u, CAPABILITY_TABLE as ut, memo as v, ForgeNetworkError as vt, requireThread as w, NotFoundError as wt, milestoneId as x, MergeBlockedError as xt, memoBy as y, ForgeTimeoutError as yt, hasCommentMarker as z };