gitlab-mcp 1.1.0 → 1.2.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -1
  3. package/dist/config/dotenv.d.ts +2 -0
  4. package/dist/config/dotenv.js +40 -0
  5. package/dist/config/dotenv.js.map +1 -0
  6. package/dist/config/env.d.ts +55 -0
  7. package/dist/config/env.js +164 -0
  8. package/dist/config/env.js.map +1 -0
  9. package/dist/http-app.d.ts +45 -0
  10. package/dist/http-app.js +550 -0
  11. package/dist/http-app.js.map +1 -0
  12. package/dist/http.d.ts +2 -0
  13. package/dist/http.js +65 -0
  14. package/dist/http.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +65 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/auth-context.d.ts +9 -0
  19. package/dist/lib/auth-context.js +9 -0
  20. package/dist/lib/auth-context.js.map +1 -0
  21. package/dist/lib/gitlab-client.d.ts +331 -0
  22. package/dist/lib/gitlab-client.js +1025 -0
  23. package/dist/lib/gitlab-client.js.map +1 -0
  24. package/dist/lib/logger.d.ts +2 -0
  25. package/dist/lib/logger.js +13 -0
  26. package/dist/lib/logger.js.map +1 -0
  27. package/dist/lib/network.d.ts +3 -0
  28. package/dist/lib/network.js +38 -0
  29. package/dist/lib/network.js.map +1 -0
  30. package/dist/lib/oauth.d.ts +29 -0
  31. package/dist/lib/oauth.js +220 -0
  32. package/dist/lib/oauth.js.map +1 -0
  33. package/dist/lib/output.d.ts +14 -0
  34. package/dist/lib/output.js +38 -0
  35. package/dist/lib/output.js.map +1 -0
  36. package/dist/lib/policy.d.ts +25 -0
  37. package/dist/lib/policy.js +48 -0
  38. package/dist/lib/policy.js.map +1 -0
  39. package/dist/lib/request-runtime.d.ts +26 -0
  40. package/dist/lib/request-runtime.js +323 -0
  41. package/dist/lib/request-runtime.js.map +1 -0
  42. package/dist/lib/sanitize.d.ts +1 -0
  43. package/dist/lib/sanitize.js +21 -0
  44. package/dist/lib/sanitize.js.map +1 -0
  45. package/dist/lib/session-capacity.d.ts +8 -0
  46. package/dist/lib/session-capacity.js +7 -0
  47. package/dist/lib/session-capacity.js.map +1 -0
  48. package/dist/server/build-server.d.ts +3 -0
  49. package/dist/server/build-server.js +13 -0
  50. package/dist/server/build-server.js.map +1 -0
  51. package/dist/tools/gitlab.d.ts +9 -0
  52. package/dist/tools/gitlab.js +2576 -0
  53. package/dist/tools/gitlab.js.map +1 -0
  54. package/dist/tools/health.d.ts +2 -0
  55. package/dist/tools/health.js +21 -0
  56. package/dist/tools/health.js.map +1 -0
  57. package/dist/tools/mr-code-context.d.ts +38 -0
  58. package/dist/tools/mr-code-context.js +330 -0
  59. package/dist/tools/mr-code-context.js.map +1 -0
  60. package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
  61. package/dist/types/context.js +2 -0
  62. package/dist/types/context.js.map +1 -0
  63. package/docs/architecture.md +10 -10
  64. package/docs/configuration.md +12 -7
  65. package/docs/mcp-integration-testing-best-practices.md +981 -0
  66. package/package.json +13 -1
  67. package/.dockerignore +0 -7
  68. package/.editorconfig +0 -9
  69. package/.env.example +0 -75
  70. package/.github/workflows/nodejs.yml +0 -31
  71. package/.github/workflows/npm-publish.yml +0 -31
  72. package/.husky/pre-commit +0 -1
  73. package/.nvmrc +0 -1
  74. package/.prettierrc.json +0 -6
  75. package/Dockerfile +0 -20
  76. package/docker-compose.yml +0 -10
  77. package/eslint.config.js +0 -23
  78. package/scripts/get-oauth-token.example.sh +0 -15
  79. package/src/config/env.ts +0 -171
  80. package/src/http.ts +0 -620
  81. package/src/index.ts +0 -77
  82. package/src/lib/auth-context.ts +0 -19
  83. package/src/lib/gitlab-client.ts +0 -1810
  84. package/src/lib/logger.ts +0 -17
  85. package/src/lib/network.ts +0 -45
  86. package/src/lib/oauth.ts +0 -287
  87. package/src/lib/output.ts +0 -51
  88. package/src/lib/policy.ts +0 -78
  89. package/src/lib/request-runtime.ts +0 -376
  90. package/src/lib/sanitize.ts +0 -25
  91. package/src/lib/session-capacity.ts +0 -14
  92. package/src/server/build-server.ts +0 -17
  93. package/src/tools/gitlab.ts +0 -3135
  94. package/src/tools/health.ts +0 -27
  95. package/src/tools/mr-code-context.ts +0 -473
  96. package/tests/auth-context.test.ts +0 -102
  97. package/tests/gitlab-client.test.ts +0 -672
  98. package/tests/graphql-guard.test.ts +0 -121
  99. package/tests/integration/agent-loop.integration.test.ts +0 -558
  100. package/tests/integration/server.integration.test.ts +0 -543
  101. package/tests/mr-code-context.test.ts +0 -600
  102. package/tests/oauth.test.ts +0 -43
  103. package/tests/output.test.ts +0 -186
  104. package/tests/policy.test.ts +0 -324
  105. package/tests/request-runtime.test.ts +0 -252
  106. package/tests/sanitize.test.ts +0 -123
  107. package/tests/session-capacity.test.ts +0 -49
  108. package/tests/upload-reference.test.ts +0 -88
  109. package/tsconfig.build.json +0 -11
  110. package/tsconfig.json +0 -21
  111. 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