gitlab-mcp 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config/env.d.ts +56 -0
  3. package/dist/config/env.js +163 -0
  4. package/dist/config/env.js.map +1 -0
  5. package/dist/http-app.d.ts +45 -0
  6. package/dist/http-app.js +550 -0
  7. package/dist/http-app.js.map +1 -0
  8. package/dist/http.d.ts +2 -0
  9. package/dist/http.js +65 -0
  10. package/dist/http.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +65 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/lib/auth-context.d.ts +9 -0
  15. package/dist/lib/auth-context.js +9 -0
  16. package/dist/lib/auth-context.js.map +1 -0
  17. package/dist/lib/gitlab-client.d.ts +331 -0
  18. package/dist/lib/gitlab-client.js +1025 -0
  19. package/dist/lib/gitlab-client.js.map +1 -0
  20. package/dist/lib/logger.d.ts +2 -0
  21. package/dist/lib/logger.js +13 -0
  22. package/dist/lib/logger.js.map +1 -0
  23. package/dist/lib/network.d.ts +3 -0
  24. package/dist/lib/network.js +38 -0
  25. package/dist/lib/network.js.map +1 -0
  26. package/dist/lib/oauth.d.ts +29 -0
  27. package/dist/lib/oauth.js +220 -0
  28. package/dist/lib/oauth.js.map +1 -0
  29. package/dist/lib/output.d.ts +14 -0
  30. package/dist/lib/output.js +38 -0
  31. package/dist/lib/output.js.map +1 -0
  32. package/dist/lib/policy.d.ts +25 -0
  33. package/dist/lib/policy.js +48 -0
  34. package/dist/lib/policy.js.map +1 -0
  35. package/dist/lib/request-runtime.d.ts +26 -0
  36. package/dist/lib/request-runtime.js +323 -0
  37. package/dist/lib/request-runtime.js.map +1 -0
  38. package/dist/lib/sanitize.d.ts +1 -0
  39. package/dist/lib/sanitize.js +21 -0
  40. package/dist/lib/sanitize.js.map +1 -0
  41. package/dist/lib/session-capacity.d.ts +8 -0
  42. package/dist/lib/session-capacity.js +7 -0
  43. package/dist/lib/session-capacity.js.map +1 -0
  44. package/dist/server/build-server.d.ts +3 -0
  45. package/dist/server/build-server.js +13 -0
  46. package/dist/server/build-server.js.map +1 -0
  47. package/dist/tools/gitlab.d.ts +9 -0
  48. package/dist/tools/gitlab.js +2576 -0
  49. package/dist/tools/gitlab.js.map +1 -0
  50. package/dist/tools/health.d.ts +2 -0
  51. package/dist/tools/health.js +21 -0
  52. package/dist/tools/health.js.map +1 -0
  53. package/dist/tools/mr-code-context.d.ts +38 -0
  54. package/dist/tools/mr-code-context.js +330 -0
  55. package/dist/tools/mr-code-context.js.map +1 -0
  56. package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
  57. package/dist/types/context.js +2 -0
  58. package/dist/types/context.js.map +1 -0
  59. package/docs/configuration.md +6 -6
  60. package/docs/mcp-integration-testing-best-practices.md +981 -0
  61. package/package.json +13 -1
  62. package/.dockerignore +0 -7
  63. package/.editorconfig +0 -9
  64. package/.env.example +0 -75
  65. package/.github/workflows/nodejs.yml +0 -31
  66. package/.github/workflows/npm-publish.yml +0 -31
  67. package/.husky/pre-commit +0 -1
  68. package/.nvmrc +0 -1
  69. package/.prettierrc.json +0 -6
  70. package/Dockerfile +0 -20
  71. package/docker-compose.yml +0 -10
  72. package/eslint.config.js +0 -23
  73. package/scripts/get-oauth-token.example.sh +0 -15
  74. package/src/config/env.ts +0 -171
  75. package/src/http.ts +0 -620
  76. package/src/index.ts +0 -77
  77. package/src/lib/auth-context.ts +0 -19
  78. package/src/lib/gitlab-client.ts +0 -1810
  79. package/src/lib/logger.ts +0 -17
  80. package/src/lib/network.ts +0 -45
  81. package/src/lib/oauth.ts +0 -287
  82. package/src/lib/output.ts +0 -51
  83. package/src/lib/policy.ts +0 -78
  84. package/src/lib/request-runtime.ts +0 -376
  85. package/src/lib/sanitize.ts +0 -25
  86. package/src/lib/session-capacity.ts +0 -14
  87. package/src/server/build-server.ts +0 -17
  88. package/src/tools/gitlab.ts +0 -3135
  89. package/src/tools/health.ts +0 -27
  90. package/src/tools/mr-code-context.ts +0 -473
  91. package/tests/auth-context.test.ts +0 -102
  92. package/tests/gitlab-client.test.ts +0 -672
  93. package/tests/graphql-guard.test.ts +0 -121
  94. package/tests/integration/agent-loop.integration.test.ts +0 -558
  95. package/tests/integration/server.integration.test.ts +0 -543
  96. package/tests/mr-code-context.test.ts +0 -600
  97. package/tests/oauth.test.ts +0 -43
  98. package/tests/output.test.ts +0 -186
  99. package/tests/policy.test.ts +0 -324
  100. package/tests/request-runtime.test.ts +0 -252
  101. package/tests/sanitize.test.ts +0 -123
  102. package/tests/session-capacity.test.ts +0 -49
  103. package/tests/upload-reference.test.ts +0 -88
  104. package/tsconfig.build.json +0 -11
  105. package/tsconfig.json +0 -21
  106. package/vitest.config.ts +0 -12
@@ -0,0 +1,1025 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { getSessionAuth } from "./auth-context.js";
4
+ export class GitLabApiError extends Error {
5
+ status;
6
+ details;
7
+ constructor(message, status, details) {
8
+ super(message);
9
+ this.status = status;
10
+ this.details = details;
11
+ this.name = "GitLabApiError";
12
+ }
13
+ }
14
+ export class GitLabClient {
15
+ static DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
16
+ static DEFAULT_MAX_RESPONSE_BODY_BYTES = 25 * 1024 * 1024;
17
+ baseApiUrl;
18
+ apiUrls;
19
+ nextApiUrlIndex = 0;
20
+ defaultToken;
21
+ timeoutMs;
22
+ maxAttachmentBytes;
23
+ maxResponseBodyBytes;
24
+ beforeRequest;
25
+ constructor(baseApiUrl, defaultToken, options = {}) {
26
+ this.baseApiUrl = normalizeApiUrl(baseApiUrl);
27
+ const configuredApiUrls = options.apiUrls
28
+ ?.map((item) => normalizeApiUrl(item))
29
+ .filter((item) => item.length > 0) ?? [this.baseApiUrl];
30
+ this.apiUrls = configuredApiUrls.length > 0 ? configuredApiUrls : [this.baseApiUrl];
31
+ this.defaultToken = defaultToken;
32
+ this.timeoutMs = options.timeoutMs ?? 20_000;
33
+ this.maxAttachmentBytes =
34
+ options.maxAttachmentBytes ?? GitLabClient.DEFAULT_MAX_ATTACHMENT_BYTES;
35
+ this.maxResponseBodyBytes =
36
+ options.maxResponseBodyBytes ?? GitLabClient.DEFAULT_MAX_RESPONSE_BODY_BYTES;
37
+ this.beforeRequest = options.beforeRequest;
38
+ }
39
+ // projects
40
+ getProject(projectId, options) {
41
+ return this.get(`/projects/${encode(projectId)}`, options);
42
+ }
43
+ listProjects(options = {}) {
44
+ return this.get("/projects", options);
45
+ }
46
+ createRepository(payload, options = {}) {
47
+ return this.post("/projects", {
48
+ ...options,
49
+ body: JSON.stringify(payload),
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ ...(options.headers ?? {})
53
+ }
54
+ });
55
+ }
56
+ listProjectMembers(projectId, options = {}) {
57
+ return this.get(`/projects/${encode(projectId)}/members/all`, options);
58
+ }
59
+ listGroupProjects(groupId, options = {}) {
60
+ return this.get(`/groups/${encode(groupId)}/projects`, options);
61
+ }
62
+ forkRepository(projectId, payload = {}, options = {}) {
63
+ return this.post(`/projects/${encode(projectId)}/fork`, {
64
+ ...options,
65
+ body: JSON.stringify(payload),
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ ...(options.headers ?? {})
69
+ }
70
+ });
71
+ }
72
+ searchProjects(search, limit = 10, options = {}) {
73
+ return this.get("/projects", {
74
+ ...options,
75
+ query: {
76
+ search,
77
+ simple: true,
78
+ per_page: limit,
79
+ ...(options.query ?? {})
80
+ }
81
+ });
82
+ }
83
+ searchRepositories(search, options = {}) {
84
+ return this.get("/search", {
85
+ ...options,
86
+ query: {
87
+ scope: "projects",
88
+ search,
89
+ ...(options.query ?? {})
90
+ }
91
+ });
92
+ }
93
+ searchCodeBlobs(projectId, search, options = {}) {
94
+ return this.get(`/projects/${encode(projectId)}/search`, {
95
+ ...options,
96
+ query: {
97
+ scope: "blobs",
98
+ search,
99
+ ...(options.query ?? {})
100
+ }
101
+ });
102
+ }
103
+ // repository/files
104
+ getRepositoryTree(projectId, options = {}) {
105
+ return this.get(`/projects/${encode(projectId)}/repository/tree`, options);
106
+ }
107
+ getFileContents(projectId, filePath, ref, options = {}) {
108
+ return this.get(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
109
+ ...options,
110
+ query: {
111
+ ref,
112
+ ...(options.query ?? {})
113
+ }
114
+ });
115
+ }
116
+ createOrUpdateFile(projectId, filePath, payload, options = {}) {
117
+ return this.put(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
118
+ ...options,
119
+ body: JSON.stringify(payload),
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ ...(options.headers ?? {})
123
+ }
124
+ });
125
+ }
126
+ pushFiles(projectId, payload, options = {}) {
127
+ return this.post(`/projects/${encode(projectId)}/repository/commits`, {
128
+ ...options,
129
+ body: JSON.stringify(payload),
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ ...(options.headers ?? {})
133
+ }
134
+ });
135
+ }
136
+ createBranch(projectId, payload, options = {}) {
137
+ return this.post(`/projects/${encode(projectId)}/repository/branches`, {
138
+ ...options,
139
+ query: payload
140
+ });
141
+ }
142
+ getBranchDiffs(projectId, payload, options = {}) {
143
+ return this.get(`/projects/${encode(projectId)}/repository/compare`, {
144
+ ...options,
145
+ query: payload
146
+ });
147
+ }
148
+ listCommits(projectId, options = {}) {
149
+ return this.get(`/projects/${encode(projectId)}/repository/commits`, options);
150
+ }
151
+ getCommit(projectId, sha, options = {}) {
152
+ return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}`, options);
153
+ }
154
+ getCommitDiff(projectId, sha, options = {}) {
155
+ return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}/diff`, options);
156
+ }
157
+ // merge requests
158
+ listMergeRequests(projectId, options = {}) {
159
+ return this.get(`/projects/${encode(projectId)}/merge_requests`, options);
160
+ }
161
+ listGlobalMergeRequests(options = {}) {
162
+ return this.get("/merge_requests", options);
163
+ }
164
+ getMergeRequest(projectId, mergeRequestIid, options = {}) {
165
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`, options);
166
+ }
167
+ createMergeRequest(projectId, payload, options = {}) {
168
+ return this.post(`/projects/${encode(projectId)}/merge_requests`, {
169
+ ...options,
170
+ body: JSON.stringify(payload),
171
+ headers: {
172
+ "Content-Type": "application/json",
173
+ ...(options.headers ?? {})
174
+ }
175
+ });
176
+ }
177
+ updateMergeRequest(projectId, mergeRequestIid, payload, options = {}) {
178
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`, {
179
+ ...options,
180
+ body: JSON.stringify(payload),
181
+ headers: {
182
+ "Content-Type": "application/json",
183
+ ...(options.headers ?? {})
184
+ }
185
+ });
186
+ }
187
+ mergeMergeRequest(projectId, mergeRequestIid, payload = {}, options = {}) {
188
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/merge`, {
189
+ ...options,
190
+ body: JSON.stringify(payload),
191
+ headers: {
192
+ "Content-Type": "application/json",
193
+ ...(options.headers ?? {})
194
+ }
195
+ });
196
+ }
197
+ getMergeRequestDiffs(projectId, mergeRequestIid, options = {}) {
198
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/changes`, options);
199
+ }
200
+ listMergeRequestDiffs(projectId, mergeRequestIid, options = {}) {
201
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/diffs`, options);
202
+ }
203
+ listMergeRequestVersions(projectId, mergeRequestIid, options = {}) {
204
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/versions`, options);
205
+ }
206
+ getMergeRequestVersion(projectId, mergeRequestIid, versionId, options = {}) {
207
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/versions/${encode(versionId)}`, options);
208
+ }
209
+ approveMergeRequest(projectId, mergeRequestIid, payload = {}, options = {}) {
210
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approve`, {
211
+ ...options,
212
+ body: JSON.stringify(payload),
213
+ headers: {
214
+ "Content-Type": "application/json",
215
+ ...(options.headers ?? {})
216
+ }
217
+ });
218
+ }
219
+ unapproveMergeRequest(projectId, mergeRequestIid, options = {}) {
220
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/unapprove`, {
221
+ ...options,
222
+ body: JSON.stringify({}),
223
+ headers: {
224
+ "Content-Type": "application/json",
225
+ ...(options.headers ?? {})
226
+ }
227
+ });
228
+ }
229
+ getMergeRequestApprovalState(projectId, mergeRequestIid, options = {}) {
230
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approval_state`, options);
231
+ }
232
+ listMergeRequestDiscussions(projectId, mergeRequestIid, options = {}) {
233
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions`, options);
234
+ }
235
+ createMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, payload, options = {}) {
236
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes`, {
237
+ ...options,
238
+ body: JSON.stringify(payload),
239
+ headers: {
240
+ "Content-Type": "application/json",
241
+ ...(options.headers ?? {})
242
+ }
243
+ });
244
+ }
245
+ createMergeRequestThread(projectId, mergeRequestIid, payload, options = {}) {
246
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions`, {
247
+ ...options,
248
+ body: JSON.stringify(payload),
249
+ headers: {
250
+ "Content-Type": "application/json",
251
+ ...(options.headers ?? {})
252
+ }
253
+ });
254
+ }
255
+ updateMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, payload, options = {}) {
256
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`, {
257
+ ...options,
258
+ body: JSON.stringify(payload),
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ ...(options.headers ?? {})
262
+ }
263
+ });
264
+ }
265
+ deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, options = {}) {
266
+ return this.delete(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`, options);
267
+ }
268
+ resolveMergeRequestThread(projectId, mergeRequestIid, discussionId, noteId, resolved, options = {}) {
269
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`, {
270
+ ...options,
271
+ body: JSON.stringify({ resolved }),
272
+ headers: {
273
+ "Content-Type": "application/json",
274
+ ...(options.headers ?? {})
275
+ }
276
+ });
277
+ }
278
+ listMergeRequestNotes(projectId, mergeRequestIid, options = {}) {
279
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes`, options);
280
+ }
281
+ getMergeRequestNote(projectId, mergeRequestIid, noteId, options = {}) {
282
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes/${encode(noteId)}`, options);
283
+ }
284
+ createMergeRequestNote(projectId, mergeRequestIid, body, options = {}) {
285
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes`, {
286
+ ...options,
287
+ body: JSON.stringify({ body }),
288
+ headers: {
289
+ "Content-Type": "application/json",
290
+ ...(options.headers ?? {})
291
+ }
292
+ });
293
+ }
294
+ getDraftNote(projectId, mergeRequestIid, draftNoteId, options = {}) {
295
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`, options);
296
+ }
297
+ listDraftNotes(projectId, mergeRequestIid, options = {}) {
298
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes`, options);
299
+ }
300
+ createDraftNote(projectId, mergeRequestIid, payload, options = {}) {
301
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes`, {
302
+ ...options,
303
+ body: JSON.stringify({
304
+ note: payload.body,
305
+ position: payload.position,
306
+ resolve_discussion: payload.resolve_discussion
307
+ }),
308
+ headers: {
309
+ "Content-Type": "application/json",
310
+ ...(options.headers ?? {})
311
+ }
312
+ });
313
+ }
314
+ updateDraftNote(projectId, mergeRequestIid, draftNoteId, payload, options = {}) {
315
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`, {
316
+ ...options,
317
+ body: JSON.stringify({
318
+ note: payload.body,
319
+ position: payload.position,
320
+ resolve_discussion: payload.resolve_discussion
321
+ }),
322
+ headers: {
323
+ "Content-Type": "application/json",
324
+ ...(options.headers ?? {})
325
+ }
326
+ });
327
+ }
328
+ deleteDraftNote(projectId, mergeRequestIid, draftNoteId, options = {}) {
329
+ return this.delete(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`, options);
330
+ }
331
+ publishDraftNote(projectId, mergeRequestIid, draftNoteId, options = {}) {
332
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}/publish`, {
333
+ ...options,
334
+ body: JSON.stringify({}),
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ ...(options.headers ?? {})
338
+ }
339
+ });
340
+ }
341
+ bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
342
+ return this.post(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/bulk_publish`, {
343
+ ...options,
344
+ body: JSON.stringify({}),
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ ...(options.headers ?? {})
348
+ }
349
+ });
350
+ }
351
+ createNote(projectId, noteableType, noteableIid, body, options = {}) {
352
+ return this.post(`/projects/${encode(projectId)}/${noteableType}s/${encode(noteableIid)}/notes`, {
353
+ ...options,
354
+ body: JSON.stringify({ body }),
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ ...(options.headers ?? {})
358
+ }
359
+ });
360
+ }
361
+ updateMergeRequestNote(projectId, mergeRequestIid, noteId, body, options = {}) {
362
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes/${encode(noteId)}`, {
363
+ ...options,
364
+ body: JSON.stringify({ body }),
365
+ headers: {
366
+ "Content-Type": "application/json",
367
+ ...(options.headers ?? {})
368
+ }
369
+ });
370
+ }
371
+ deleteMergeRequestNote(projectId, mergeRequestIid, noteId, options = {}) {
372
+ return this.delete(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes/${encode(noteId)}`, options);
373
+ }
374
+ // issues
375
+ listIssues(projectId, options = {}) {
376
+ return this.get(`/projects/${encode(projectId)}/issues`, options);
377
+ }
378
+ listGlobalIssues(options = {}) {
379
+ return this.get("/issues", options);
380
+ }
381
+ getIssue(projectId, issueIid, options = {}) {
382
+ return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}`, options);
383
+ }
384
+ createIssue(projectId, payload, options = {}) {
385
+ return this.post(`/projects/${encode(projectId)}/issues`, {
386
+ ...options,
387
+ body: JSON.stringify(payload),
388
+ headers: {
389
+ "Content-Type": "application/json",
390
+ ...(options.headers ?? {})
391
+ }
392
+ });
393
+ }
394
+ updateIssue(projectId, issueIid, payload, options = {}) {
395
+ return this.put(`/projects/${encode(projectId)}/issues/${encode(issueIid)}`, {
396
+ ...options,
397
+ body: JSON.stringify(payload),
398
+ headers: {
399
+ "Content-Type": "application/json",
400
+ ...(options.headers ?? {})
401
+ }
402
+ });
403
+ }
404
+ deleteIssue(projectId, issueIid, options = {}) {
405
+ return this.delete(`/projects/${encode(projectId)}/issues/${encode(issueIid)}`, options);
406
+ }
407
+ myIssues(payload, options = {}) {
408
+ const { project_id: projectId, ...queryPayload } = payload;
409
+ const path = projectId ? `/projects/${encode(projectId)}/issues` : "/issues";
410
+ return this.get(path, {
411
+ ...options,
412
+ query: {
413
+ scope: queryPayload.scope ?? "assigned_to_me",
414
+ ...queryPayload,
415
+ ...(options.query ?? {})
416
+ }
417
+ });
418
+ }
419
+ listIssueDiscussions(projectId, issueIid, options = {}) {
420
+ return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions`, options);
421
+ }
422
+ createIssueNote(projectId, issueIid, payload, options = {}) {
423
+ const discussionPath = payload.discussion_id
424
+ ? `/discussions/${encode(payload.discussion_id)}/notes`
425
+ : "/notes";
426
+ return this.post(`/projects/${encode(projectId)}/issues/${encode(issueIid)}${discussionPath}`, {
427
+ ...options,
428
+ body: JSON.stringify({
429
+ body: payload.body,
430
+ created_at: payload.created_at
431
+ }),
432
+ headers: {
433
+ "Content-Type": "application/json",
434
+ ...(options.headers ?? {})
435
+ }
436
+ });
437
+ }
438
+ updateIssueNote(projectId, issueIid, discussionId, noteId, payload, options = {}) {
439
+ return this.put(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`, {
440
+ ...options,
441
+ body: JSON.stringify(payload),
442
+ headers: {
443
+ "Content-Type": "application/json",
444
+ ...(options.headers ?? {})
445
+ }
446
+ });
447
+ }
448
+ listIssueLinks(projectId, issueIid, options = {}) {
449
+ return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/links`, options);
450
+ }
451
+ getIssueLink(projectId, issueIid, issueLinkId, options = {}) {
452
+ return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/links/${encode(issueLinkId)}`, options);
453
+ }
454
+ createIssueLink(projectId, issueIid, payload, options = {}) {
455
+ return this.post(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/links`, {
456
+ ...options,
457
+ body: JSON.stringify(payload),
458
+ headers: {
459
+ "Content-Type": "application/json",
460
+ ...(options.headers ?? {})
461
+ }
462
+ });
463
+ }
464
+ deleteIssueLink(projectId, issueIid, issueLinkId, options = {}) {
465
+ return this.delete(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/links/${encode(issueLinkId)}`, options);
466
+ }
467
+ // wiki
468
+ listWikiPages(projectId, options = {}) {
469
+ return this.get(`/projects/${encode(projectId)}/wikis`, options);
470
+ }
471
+ getWikiPage(projectId, slug, options = {}) {
472
+ return this.get(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, options);
473
+ }
474
+ createWikiPage(projectId, payload, options = {}) {
475
+ return this.post(`/projects/${encode(projectId)}/wikis`, {
476
+ ...options,
477
+ body: JSON.stringify(payload),
478
+ headers: {
479
+ "Content-Type": "application/json",
480
+ ...(options.headers ?? {})
481
+ }
482
+ });
483
+ }
484
+ updateWikiPage(projectId, slug, payload, options = {}) {
485
+ return this.put(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, {
486
+ ...options,
487
+ body: JSON.stringify(payload),
488
+ headers: {
489
+ "Content-Type": "application/json",
490
+ ...(options.headers ?? {})
491
+ }
492
+ });
493
+ }
494
+ deleteWikiPage(projectId, slug, options = {}) {
495
+ return this.delete(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, options);
496
+ }
497
+ // pipelines
498
+ listPipelines(projectId, options = {}) {
499
+ return this.get(`/projects/${encode(projectId)}/pipelines`, options);
500
+ }
501
+ getPipeline(projectId, pipelineId, options = {}) {
502
+ return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}`, options);
503
+ }
504
+ listPipelineJobs(projectId, pipelineId, options = {}) {
505
+ return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/jobs`, options);
506
+ }
507
+ listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
508
+ return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/bridges`, options);
509
+ }
510
+ getPipelineJob(projectId, jobId, options = {}) {
511
+ return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}`, options);
512
+ }
513
+ getPipelineJobOutput(projectId, jobId, options = {}) {
514
+ return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/trace`, options);
515
+ }
516
+ createPipeline(projectId, payload, options = {}) {
517
+ return this.post(`/projects/${encode(projectId)}/pipeline`, {
518
+ ...options,
519
+ body: JSON.stringify(payload),
520
+ headers: {
521
+ "Content-Type": "application/json",
522
+ ...(options.headers ?? {})
523
+ }
524
+ });
525
+ }
526
+ retryPipeline(projectId, pipelineId, options = {}) {
527
+ return this.post(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/retry`, options);
528
+ }
529
+ cancelPipeline(projectId, pipelineId, options = {}) {
530
+ return this.post(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/cancel`, options);
531
+ }
532
+ retryPipelineJob(projectId, jobId, options = {}) {
533
+ return this.post(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/retry`, options);
534
+ }
535
+ cancelPipelineJob(projectId, jobId, options = {}) {
536
+ return this.post(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/cancel`, options);
537
+ }
538
+ playPipelineJob(projectId, jobId, options = {}) {
539
+ return this.post(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/play`, options);
540
+ }
541
+ // milestones
542
+ listMilestones(projectId, options = {}) {
543
+ return this.get(`/projects/${encode(projectId)}/milestones`, options);
544
+ }
545
+ getMilestone(projectId, milestoneId, options = {}) {
546
+ return this.get(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}`, options);
547
+ }
548
+ createMilestone(projectId, payload, options = {}) {
549
+ return this.post(`/projects/${encode(projectId)}/milestones`, {
550
+ ...options,
551
+ body: JSON.stringify(payload),
552
+ headers: {
553
+ "Content-Type": "application/json",
554
+ ...(options.headers ?? {})
555
+ }
556
+ });
557
+ }
558
+ updateMilestone(projectId, milestoneId, payload, options = {}) {
559
+ return this.put(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}`, {
560
+ ...options,
561
+ body: JSON.stringify(payload),
562
+ headers: {
563
+ "Content-Type": "application/json",
564
+ ...(options.headers ?? {})
565
+ }
566
+ });
567
+ }
568
+ deleteMilestone(projectId, milestoneId, options = {}) {
569
+ return this.delete(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}`, options);
570
+ }
571
+ getMilestoneIssues(projectId, milestoneId, options = {}) {
572
+ return this.get(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/issues`, options);
573
+ }
574
+ getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
575
+ return this.get(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/merge_requests`, options);
576
+ }
577
+ promoteMilestone(projectId, milestoneId, options = {}) {
578
+ return this.post(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/promote`, options);
579
+ }
580
+ getMilestoneBurndownEvents(projectId, milestoneId, options = {}) {
581
+ return this.get(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/burndown_events`, options);
582
+ }
583
+ // releases
584
+ listReleases(projectId, options = {}) {
585
+ return this.get(`/projects/${encode(projectId)}/releases`, options);
586
+ }
587
+ getRelease(projectId, tagName, options = {}) {
588
+ return this.get(`/projects/${encode(projectId)}/releases/${encode(tagName)}`, options);
589
+ }
590
+ createRelease(projectId, payload, options = {}) {
591
+ return this.post(`/projects/${encode(projectId)}/releases`, {
592
+ ...options,
593
+ body: JSON.stringify(payload),
594
+ headers: {
595
+ "Content-Type": "application/json",
596
+ ...(options.headers ?? {})
597
+ }
598
+ });
599
+ }
600
+ updateRelease(projectId, tagName, payload, options = {}) {
601
+ return this.put(`/projects/${encode(projectId)}/releases/${encode(tagName)}`, {
602
+ ...options,
603
+ body: JSON.stringify(payload),
604
+ headers: {
605
+ "Content-Type": "application/json",
606
+ ...(options.headers ?? {})
607
+ }
608
+ });
609
+ }
610
+ deleteRelease(projectId, tagName, options = {}) {
611
+ return this.delete(`/projects/${encode(projectId)}/releases/${encode(tagName)}`, options);
612
+ }
613
+ createReleaseEvidence(projectId, tagName, options = {}) {
614
+ return this.post(`/projects/${encode(projectId)}/releases/${encode(tagName)}/evidence`, options);
615
+ }
616
+ downloadReleaseAsset(projectId, tagName, directAssetPath, options = {}) {
617
+ const safePath = encodeSlashPath(directAssetPath);
618
+ return this.get(`/projects/${encode(projectId)}/releases/${encode(tagName)}/downloads/${safePath}`, options);
619
+ }
620
+ // labels
621
+ listLabels(projectId, options = {}) {
622
+ return this.get(`/projects/${encode(projectId)}/labels`, options);
623
+ }
624
+ getLabel(projectId, labelId, options = {}) {
625
+ return this.get(`/projects/${encode(projectId)}/labels/${encode(labelId)}`, options);
626
+ }
627
+ createLabel(projectId, payload, options = {}) {
628
+ return this.post(`/projects/${encode(projectId)}/labels`, {
629
+ ...options,
630
+ body: JSON.stringify(payload),
631
+ headers: {
632
+ "Content-Type": "application/json",
633
+ ...(options.headers ?? {})
634
+ }
635
+ });
636
+ }
637
+ updateLabel(projectId, payload, options = {}) {
638
+ return this.put(`/projects/${encode(projectId)}/labels`, {
639
+ ...options,
640
+ body: JSON.stringify(payload),
641
+ headers: {
642
+ "Content-Type": "application/json",
643
+ ...(options.headers ?? {})
644
+ }
645
+ });
646
+ }
647
+ deleteLabel(projectId, labelName, options = {}) {
648
+ return this.delete(`/projects/${encode(projectId)}/labels`, {
649
+ ...options,
650
+ query: {
651
+ name: labelName,
652
+ ...(options.query ?? {})
653
+ }
654
+ });
655
+ }
656
+ // namespaces/users/events
657
+ listNamespaces(options = {}) {
658
+ return this.get("/namespaces", options);
659
+ }
660
+ listGroupIterations(groupId, options = {}) {
661
+ return this.get(`/groups/${encode(groupId)}/iterations`, options);
662
+ }
663
+ getNamespace(namespaceIdOrPath, options = {}) {
664
+ return this.get(`/namespaces/${encode(namespaceIdOrPath)}`, options);
665
+ }
666
+ verifyNamespace(pathName, options = {}) {
667
+ return this.get(`/namespaces/${encode(pathName)}/exists`, options);
668
+ }
669
+ getUsers(options = {}) {
670
+ return this.get("/users", options);
671
+ }
672
+ listEvents(options = {}) {
673
+ return this.get("/events", options);
674
+ }
675
+ getProjectEvents(projectId, options = {}) {
676
+ return this.get(`/projects/${encode(projectId)}/events`, options);
677
+ }
678
+ // attachments / markdown
679
+ uploadMarkdown(projectId, content, filename, options = {}) {
680
+ const form = new FormData();
681
+ form.append("file", new Blob([content], { type: "text/markdown" }), filename);
682
+ return this.post(`/projects/${encode(projectId)}/uploads`, {
683
+ ...options,
684
+ body: form,
685
+ headers: {
686
+ Accept: "*/*",
687
+ ...(options.headers ?? {})
688
+ }
689
+ });
690
+ }
691
+ async uploadMarkdownFile(projectId, filePath, options = {}) {
692
+ const content = await fs.readFile(filePath);
693
+ const filename = path.basename(filePath);
694
+ const form = new FormData();
695
+ form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
696
+ return this.post(`/projects/${encode(projectId)}/uploads`, {
697
+ ...options,
698
+ body: form,
699
+ headers: {
700
+ Accept: "*/*",
701
+ ...(options.headers ?? {})
702
+ }
703
+ });
704
+ }
705
+ async downloadAttachment(urlOrPath, options = {}) {
706
+ const requestConfig = this.resolveRequestConfig(options);
707
+ const url = this.resolveAttachmentUrl(urlOrPath, requestConfig.apiUrl);
708
+ let headers = new Headers(options.headers);
709
+ let token = requestConfig.token;
710
+ let authHeader = requestConfig.authHeader;
711
+ let fetchImpl = fetch;
712
+ if (this.beforeRequest) {
713
+ const override = await this.beforeRequest({
714
+ url,
715
+ method: "GET",
716
+ headers,
717
+ token,
718
+ authHeader: requestConfig.authHeader
719
+ });
720
+ if (override?.headers) {
721
+ headers = override.headers;
722
+ }
723
+ if (override?.token !== undefined) {
724
+ token = override.token;
725
+ }
726
+ if (override?.authHeader !== undefined) {
727
+ authHeader = override.authHeader;
728
+ }
729
+ if (override?.fetchImpl) {
730
+ fetchImpl = override.fetchImpl;
731
+ }
732
+ }
733
+ this.attachAuth(headers, token, authHeader);
734
+ const response = await fetchImpl(url, {
735
+ method: "GET",
736
+ headers,
737
+ signal: AbortSignal.timeout(this.timeoutMs)
738
+ });
739
+ if (!response.ok) {
740
+ let details;
741
+ try {
742
+ details = await this.parseResponseBody(response);
743
+ }
744
+ catch (error) {
745
+ details = {
746
+ message: error instanceof Error ? error.message : "Failed to read GitLab error response"
747
+ };
748
+ }
749
+ throw new GitLabApiError(`GitLab attachment download failed: ${response.status} ${response.statusText}`, response.status, details);
750
+ }
751
+ assertContentLengthWithinLimit(response, this.maxAttachmentBytes, "Attachment");
752
+ const contentType = response.headers.get("content-type") ?? "application/octet-stream";
753
+ const disposition = response.headers.get("content-disposition") ?? "";
754
+ const fileName = extractFileName(disposition) ?? `attachment-${Date.now()}`;
755
+ const bytes = await readResponseBytesWithLimit(response, this.maxAttachmentBytes, "Attachment");
756
+ return {
757
+ fileName,
758
+ contentType,
759
+ base64: bytes.toString("base64")
760
+ };
761
+ }
762
+ // graphql
763
+ executeGraphql(query, variables, options = {}) {
764
+ const requestConfig = this.resolveRequestConfig(options);
765
+ const endpoint = buildGraphqlEndpoint(requestConfig.apiUrl);
766
+ return this.rawRequest(endpoint, {
767
+ method: "POST",
768
+ headers: {
769
+ "Content-Type": "application/json",
770
+ ...(options.headers ?? {})
771
+ },
772
+ body: JSON.stringify({ query, variables }),
773
+ token: requestConfig.token,
774
+ authHeader: requestConfig.authHeader
775
+ });
776
+ }
777
+ // generic methods
778
+ get(path, options = {}) {
779
+ return this.request("GET", path, options);
780
+ }
781
+ post(path, options = {}) {
782
+ return this.request("POST", path, options);
783
+ }
784
+ put(path, options = {}) {
785
+ return this.request("PUT", path, options);
786
+ }
787
+ delete(path, options = {}) {
788
+ return this.request("DELETE", path, options);
789
+ }
790
+ async request(method, path, options = {}) {
791
+ const config = this.resolveRequestConfig(options);
792
+ const url = new URL(path.replace(/^\//, ""), `${config.apiUrl}/`);
793
+ for (const [key, value] of Object.entries(options.query ?? {})) {
794
+ if (value !== undefined && value !== null) {
795
+ url.searchParams.set(key, String(value));
796
+ }
797
+ }
798
+ return this.rawRequest(url, {
799
+ method,
800
+ body: options.body,
801
+ headers: options.headers,
802
+ token: config.token,
803
+ authHeader: config.authHeader
804
+ });
805
+ }
806
+ async rawRequest(url, options) {
807
+ let headers = new Headers(options.headers);
808
+ if (!headers.has("Accept")) {
809
+ headers.set("Accept", "application/json");
810
+ }
811
+ let requestBody = options.body;
812
+ let token = options.token;
813
+ let authHeader = options.authHeader;
814
+ let fetchImpl = fetch;
815
+ if (this.beforeRequest) {
816
+ const override = await this.beforeRequest({
817
+ url,
818
+ method: options.method,
819
+ headers,
820
+ body: requestBody,
821
+ token,
822
+ authHeader: options.authHeader
823
+ });
824
+ if (override?.headers) {
825
+ headers = override.headers;
826
+ }
827
+ if (override?.body !== undefined) {
828
+ requestBody = override.body;
829
+ }
830
+ if (override?.token !== undefined) {
831
+ token = override.token;
832
+ }
833
+ if (override?.authHeader !== undefined) {
834
+ authHeader = override.authHeader;
835
+ }
836
+ if (override?.fetchImpl) {
837
+ fetchImpl = override.fetchImpl;
838
+ }
839
+ }
840
+ this.attachAuth(headers, token, authHeader);
841
+ const response = await fetchImpl(url, {
842
+ method: options.method,
843
+ body: requestBody,
844
+ headers,
845
+ signal: AbortSignal.timeout(this.timeoutMs)
846
+ });
847
+ let body;
848
+ try {
849
+ body = await this.parseResponseBody(response);
850
+ }
851
+ catch (error) {
852
+ if (response.ok) {
853
+ throw error;
854
+ }
855
+ throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, {
856
+ message: error instanceof Error ? error.message : "Failed to read GitLab error response"
857
+ });
858
+ }
859
+ if (!response.ok) {
860
+ throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, body);
861
+ }
862
+ return body;
863
+ }
864
+ async parseResponseBody(response) {
865
+ assertContentLengthWithinLimit(response, this.maxResponseBodyBytes, "Response body");
866
+ const text = await readResponseTextWithLimit(response, this.maxResponseBodyBytes, "Response body");
867
+ const contentType = response.headers.get("content-type") ?? "";
868
+ if (contentType.includes("application/json")) {
869
+ try {
870
+ return JSON.parse(text);
871
+ }
872
+ catch {
873
+ return text;
874
+ }
875
+ }
876
+ return text;
877
+ }
878
+ resolveRequestConfig(options) {
879
+ const sessionAuth = getSessionAuth();
880
+ const apiUrl = options.apiUrl ?? sessionAuth?.apiUrl ?? this.pickApiUrl();
881
+ const token = options.token ?? sessionAuth?.token ?? this.defaultToken;
882
+ const authHeader = options.authHeader ?? sessionAuth?.header;
883
+ return {
884
+ apiUrl: normalizeApiUrl(apiUrl),
885
+ token,
886
+ authHeader
887
+ };
888
+ }
889
+ pickApiUrl() {
890
+ if (this.apiUrls.length <= 1) {
891
+ return this.baseApiUrl;
892
+ }
893
+ const index = this.nextApiUrlIndex % this.apiUrls.length;
894
+ this.nextApiUrlIndex = (this.nextApiUrlIndex + 1) % this.apiUrls.length;
895
+ return this.apiUrls[index] ?? this.baseApiUrl;
896
+ }
897
+ resolveAbsoluteUrl(raw, apiUrl) {
898
+ if (/^https?:\/\//i.test(raw)) {
899
+ return new URL(raw);
900
+ }
901
+ const base = new URL(apiUrl);
902
+ return new URL(raw.replace(/^\//, ""), `${base.origin}/`);
903
+ }
904
+ resolveAttachmentUrl(raw, apiUrl) {
905
+ const base = new URL(apiUrl);
906
+ const resolved = this.resolveAbsoluteUrl(raw, apiUrl);
907
+ if (resolved.origin !== base.origin) {
908
+ throw new Error(`Refusing to download cross-origin attachment URL '${resolved.origin}'. Only '${base.origin}' is allowed.`);
909
+ }
910
+ if (!resolved.pathname.includes("/uploads/")) {
911
+ throw new Error(`Refusing to download non-upload path '${resolved.pathname}'. Only GitLab upload URLs containing '/uploads/' are allowed.`);
912
+ }
913
+ return resolved;
914
+ }
915
+ attachAuth(headers, token, authHeader) {
916
+ if (!token) {
917
+ return;
918
+ }
919
+ if (authHeader === "authorization") {
920
+ headers.set("Authorization", `Bearer ${token}`);
921
+ return;
922
+ }
923
+ headers.set("PRIVATE-TOKEN", token);
924
+ }
925
+ }
926
+ export function getEffectiveSessionAuth(defaultToken, defaultApiUrl) {
927
+ const auth = getSessionAuth();
928
+ return {
929
+ token: auth?.token ?? defaultToken,
930
+ apiUrl: auth?.apiUrl ?? defaultApiUrl,
931
+ header: auth?.header,
932
+ sessionId: auth?.sessionId,
933
+ updatedAt: auth?.updatedAt ?? Date.now()
934
+ };
935
+ }
936
+ function encode(value) {
937
+ return encodeURIComponent(value);
938
+ }
939
+ function encodeSlashPath(pathValue) {
940
+ const trimmed = pathValue.replace(/^\/+/, "").trim();
941
+ if (!trimmed) {
942
+ return "";
943
+ }
944
+ return trimmed
945
+ .split("/")
946
+ .filter((segment) => segment.length > 0)
947
+ .map((segment) => encode(segment))
948
+ .join("/");
949
+ }
950
+ function normalizeApiUrl(rawUrl) {
951
+ const url = new URL(rawUrl);
952
+ const pathname = url.pathname.replace(/\/+$/, "");
953
+ if (pathname.endsWith("/api/v4")) {
954
+ url.pathname = pathname;
955
+ return url.toString();
956
+ }
957
+ url.pathname = `${pathname}/api/v4`.replace(/\/\//g, "/");
958
+ return url.toString();
959
+ }
960
+ function buildGraphqlEndpoint(apiUrl) {
961
+ const url = new URL(apiUrl);
962
+ const prefix = url.pathname.replace(/\/api\/v4\/?$/, "");
963
+ return new URL(`${prefix || "/"}/api/graphql`, url.origin);
964
+ }
965
+ function extractFileName(contentDisposition) {
966
+ const quoted = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(contentDisposition);
967
+ if (!quoted) {
968
+ return undefined;
969
+ }
970
+ return decodeURIComponent(quoted[1] ?? "");
971
+ }
972
+ function parseContentLength(value) {
973
+ if (!value) {
974
+ return undefined;
975
+ }
976
+ const parsed = Number.parseInt(value, 10);
977
+ if (!Number.isFinite(parsed) || parsed < 0) {
978
+ return undefined;
979
+ }
980
+ return parsed;
981
+ }
982
+ function assertContentLengthWithinLimit(response, maxBytes, label) {
983
+ const declaredContentLength = parseContentLength(response.headers.get("content-length"));
984
+ if (declaredContentLength !== undefined && declaredContentLength > maxBytes) {
985
+ throw new Error(`${label} size ${declaredContentLength} bytes exceeds limit ${maxBytes} bytes`);
986
+ }
987
+ }
988
+ async function readResponseTextWithLimit(response, maxBytes, label) {
989
+ const bytes = await readResponseBytesWithLimit(response, maxBytes, label);
990
+ return bytes.toString("utf8");
991
+ }
992
+ async function readResponseBytesWithLimit(response, maxBytes, label) {
993
+ if (!response.body) {
994
+ const bytes = Buffer.from(await response.arrayBuffer());
995
+ if (bytes.length > maxBytes) {
996
+ throw new Error(`${label} size ${bytes.length} bytes exceeds limit ${maxBytes} bytes`);
997
+ }
998
+ return bytes;
999
+ }
1000
+ const reader = response.body.getReader();
1001
+ const chunks = [];
1002
+ let total = 0;
1003
+ try {
1004
+ while (true) {
1005
+ const { done, value } = await reader.read();
1006
+ if (done) {
1007
+ break;
1008
+ }
1009
+ if (!value) {
1010
+ continue;
1011
+ }
1012
+ total += value.byteLength;
1013
+ if (total > maxBytes) {
1014
+ await reader.cancel();
1015
+ throw new Error(`${label} size ${total} bytes exceeds limit ${maxBytes} bytes`);
1016
+ }
1017
+ chunks.push(Buffer.from(value));
1018
+ }
1019
+ }
1020
+ finally {
1021
+ reader.releaseLock();
1022
+ }
1023
+ return Buffer.concat(chunks, total);
1024
+ }
1025
+ //# sourceMappingURL=gitlab-client.js.map