@zereight/mcp-gitlab 2.1.55 → 2.1.57

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.
package/README.ko.md CHANGED
@@ -17,7 +17,7 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
17
17
 
18
18
  ### 왜 이 GitLab MCP를 사용하나요?
19
19
 
20
- - **217개 도구 + `discover_tools`** — 작은 toolset으로 시작하고, 런타임에 카테고리 활성화
20
+ - **232개 도구 + `discover_tools`** — 작은 toolset으로 시작하고, 런타임에 카테고리 활성화
21
21
  - **MR 2단계 리뷰** — `list_merge_request_changed_files` → 배치 `get_merge_request_file_diff`
22
22
  - **Agent Skill 내장** — `skills/gitlab-mcp/` 워크플로우 가이드
23
23
  - **유연한 인증** — Personal Access Token, 로컬 OAuth2 브라우저 플로우, MCP OAuth 프록시, 요청별 원격 인증
@@ -30,9 +30,9 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
30
30
  | | @zereight/mcp-gitlab | GitLab MCP A (커뮤니티 CQRS형) |
31
31
  |---|----------------------|--------------------------------|
32
32
  | **적합한 경우** | AI 에이전트 워크플로우 | 엔터프라이즈 멀티 인스턴스 / 그룹형 도구 |
33
- | **도구 모델** | ~217개 세분화 도구 + `discover_tools` | ~50–60개 `browse_*` / `manage_*` 그룹 도구 |
33
+ | **도구 모델** | ~232개 세분화 도구 + `discover_tools` | ~50–60개 `browse_*` / `manage_*` 그룹 도구 |
34
34
  | **MR 리뷰** | 2단계 배치 diff | 서버마다 다름 |
35
- | **Node.js** | >=18 | 보통 >=24 |
35
+ | **Node.js** | >=18.17 | 보통 >=24 |
36
36
  | **라이선스** | MIT | 서버마다 다름 |
37
37
 
38
38
  [전체 비교 →](./docs/comparison/community-gitlab-mcp-a.md)
@@ -98,7 +98,7 @@ npm install -g @zereight/mcp-gitlab
98
98
 
99
99
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
100
100
 
101
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.54`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
101
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.56`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
102
102
 
103
103
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
104
104
 
@@ -288,7 +288,7 @@ MCP 클라이언트 설정:
288
288
  | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
289
289
  | `ENABLE_DYNAMIC_API_URL` | 선택 | 요청별 `X-GitLab-API-URL` 헤더 허용 |
290
290
  | `GITLAB_ALLOWED_HOSTS` | 선택 | 허용할 `X-GitLab-API-URL` 호스트의 쉼표 구분 목록; `GITLAB_API_URL` 호스트는 항상 허용 |
291
- | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | 선택 | 인증 없이 `initialize`, `notifications/initialized`, `tools/list`만 허용(도구 호출은 여전히 인증 필요) |
291
+ | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | 선택 | 인증 없이 `initialize`, `notifications/initialized`, `tools/list`, `server/discover`만 허용(도구 호출은 여전히 인증 필요) |
292
292
  | `MCP_SERVER_URL` / `MCP_ALLOWED_HOSTS` / `MCP_ALLOWED_ORIGINS` | 선택 | DNS rebinding 방지를 위한 허용 `/mcp` 호스트/오리진 값 |
293
293
  | `MCP_TRUST_PROXY` | 선택 | 리버스 프록시 뒤에서 `Forwarded` / `X-Forwarded-*` 헤더 신뢰(다운로드 URL, Express `req.ip`, `/mcp` IP rate limit, OAuth rate limit) |
294
294
 
package/README.md CHANGED
@@ -22,7 +22,7 @@ Supports PAT, OAuth, read-only mode, dynamic API URLs, and remote authorization
22
22
 
23
23
  ### Why use this GitLab MCP?
24
24
 
25
- - **229 tools + `discover_tools`** — start with a small toolset; activate more at runtime without CQRS-style grouping
25
+ - **232 tools + `discover_tools`** — start with a small toolset; activate more at runtime without CQRS-style grouping
26
26
  - **MR 2-step review** — `list_merge_request_changed_files` → batched `get_merge_request_file_diff`
27
27
  - **Agent Skill built in** — workflow guidance in `skills/gitlab-mcp/`
28
28
  - **Flexible auth** — Personal Access Token, local OAuth2 browser flow, MCP OAuth proxy, and per-request remote authorization
@@ -35,9 +35,9 @@ Supports PAT, OAuth, read-only mode, dynamic API URLs, and remote authorization
35
35
  | | @zereight/mcp-gitlab | GitLab MCP A (community CQRS-style) |
36
36
  |---|----------------------|-------------------------------------|
37
37
  | **Best for** | AI agent workflows | Enterprise multi-instance / grouped tools |
38
- | **Tool model** | ~229 granular tools + `discover_tools` | ~50–60 grouped `browse_*` / `manage_*` tools |
38
+ | **Tool model** | ~232 granular tools + `discover_tools` | ~50–60 grouped `browse_*` / `manage_*` tools |
39
39
  | **MR review** | 2-step batched diff | Varies |
40
- | **Node.js** | >=18 | Often >=24 |
40
+ | **Node.js** | >=18.17 | Often >=24 |
41
41
  | **License** | MIT | Varies |
42
42
 
43
43
  [Full comparison →](./docs/comparison/community-gitlab-mcp-a.md)
@@ -103,7 +103,7 @@ npm install -g @zereight/mcp-gitlab
103
103
 
104
104
  The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
105
105
 
106
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.54`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
106
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.56`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
107
107
 
108
108
  #### Using CLI Arguments (for clients with env var issues)
109
109
 
@@ -316,7 +316,7 @@ the token to GitLab on behalf of the caller.
316
316
  | `STREAMABLE_HTTP` | ✅ | Must be `true` |
317
317
  | `ENABLE_DYNAMIC_API_URL` | optional | Allow per-request GitLab URL via `X-GitLab-API-URL` header |
318
318
  | `GITLAB_ALLOWED_HOSTS` | optional | Comma-separated allowed `X-GitLab-API-URL` hosts; `GITLAB_API_URL` hosts are always allowed |
319
- | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | optional | Allow unauthenticated `initialize`, `notifications/initialized`, and `tools/list` only (tool calls still require auth) |
319
+ | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | optional | Allow unauthenticated `initialize`, `notifications/initialized`, `tools/list`, and `server/discover` only (tool calls still require auth) |
320
320
  | `MCP_SERVER_URL` / `MCP_ALLOWED_HOSTS` / `MCP_ALLOWED_ORIGINS` | optional | Allowed public `/mcp` host/origin values for DNS rebinding protection |
321
321
  | `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers behind a reverse proxy (download URLs, Express `req.ip`, `/mcp` IP rate limits, OAuth rate limits) |
322
322
 
@@ -777,30 +777,33 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
777
777
  204. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
778
778
  205. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
779
779
  206. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
780
- 207. `list_webhook_events` - List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details
781
- 208. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
782
- 209. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
783
- 210. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
784
- 211. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
785
- 212. `list_project_variables` - List CI/CD variables for a project with optional environment scope filter
786
- 213. `get_project_variable` - Get a single CI/CD variable from a project by key, with optional environment scope filter
787
- 214. `create_project_variable` - Create a new CI/CD variable in a project
788
- 215. `update_project_variable` - Update an existing CI/CD variable in a project, with optional filter to disambiguate by environment scope
789
- 216. `delete_project_variable` - Delete a CI/CD variable from a project, with optional filter to disambiguate by environment scope
790
- 217. `list_group_variables` - List CI/CD variables for a group with optional environment scope filter
791
- 218. `get_group_variable` - Get a single CI/CD variable from a group by key, with optional environment scope filter
792
- 219. `create_group_variable` - Create a new CI/CD variable in a group
793
- 220. `update_group_variable` - Update an existing CI/CD variable in a group, with optional filter to disambiguate by environment scope
794
- 221. `delete_group_variable` - Delete a CI/CD variable from a group, with optional filter to disambiguate by environment scope
795
- 222. `get_dependency_proxy_settings` - Get dependency proxy settings for a group (enabled status, blob count, total size, image prefix, TTL policy)
796
- 223. `update_dependency_proxy_settings` - Update dependency proxy settings for a group (enable/disable, credentials for authenticated Docker Hub pulls)
797
- 224. `list_dependency_proxy_blobs` - List cached dependency proxy blobs for a group with cursor-based pagination
798
- 225. `purge_dependency_proxy_cache` - Schedule purge of all cached dependency proxy blobs for a group
799
- 226. `list_project_vulnerabilities` - List vulnerabilities for a project with optional state, severity, and report type filters (GraphQL-backed, cursor pagination)
800
- 227. `get_vulnerability` - Get full details of a specific vulnerability
801
- 228. `dismiss_vulnerability` - Dismiss a vulnerability with a reason (acceptable_risk, false_positive, used_in_tests, mitigating_control, not_applicable) and optional comment
802
- 229. `confirm_vulnerability` - Confirm a vulnerability as a real finding requiring remediation
803
- 230. `discover_tools` - Discover and activate additional tool categories for this session. Available categories: merge_requests, issues, repositories, branches, projects, labels, ci, groups, pipelines, milestones, wiki, releases, tags, users, workitems, webhooks, search, variables, dependency_proxy, vulnerabilities. Already-active categories are listed in the response.
780
+ 207. `create_webhook` - Create a webhook on a GitLab project or group
781
+ 208. `update_webhook` - Update an existing project or group webhook
782
+ 209. `delete_webhook` - Delete a project or group webhook
783
+ 210. `list_webhook_events` - List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details
784
+ 211. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
785
+ 212. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
786
+ 213. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
787
+ 214. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
788
+ 215. `list_project_variables` - List CI/CD variables for a project with optional environment scope filter
789
+ 216. `get_project_variable` - Get a single CI/CD variable from a project by key, with optional environment scope filter
790
+ 217. `create_project_variable` - Create a new CI/CD variable in a project
791
+ 218. `update_project_variable` - Update an existing CI/CD variable in a project, with optional filter to disambiguate by environment scope
792
+ 219. `delete_project_variable` - Delete a CI/CD variable from a project, with optional filter to disambiguate by environment scope
793
+ 220. `list_group_variables` - List CI/CD variables for a group with optional environment scope filter
794
+ 221. `get_group_variable` - Get a single CI/CD variable from a group by key, with optional environment scope filter
795
+ 222. `create_group_variable` - Create a new CI/CD variable in a group
796
+ 223. `update_group_variable` - Update an existing CI/CD variable in a group, with optional filter to disambiguate by environment scope
797
+ 224. `delete_group_variable` - Delete a CI/CD variable from a group, with optional filter to disambiguate by environment scope
798
+ 225. `get_dependency_proxy_settings` - Get dependency proxy settings for a group (enabled status, blob count, total size, image prefix, TTL policy)
799
+ 226. `update_dependency_proxy_settings` - Update dependency proxy settings for a group (enable/disable, credentials for authenticated Docker Hub pulls)
800
+ 227. `list_dependency_proxy_blobs` - List cached dependency proxy blobs for a group with cursor-based pagination
801
+ 228. `purge_dependency_proxy_cache` - Schedule purge of all cached dependency proxy blobs for a group
802
+ 229. `list_project_vulnerabilities` - List vulnerabilities for a project with optional state, severity, and report type filters (GraphQL-backed, cursor pagination)
803
+ 230. `get_vulnerability` - Get full details of a specific vulnerability
804
+ 231. `dismiss_vulnerability` - Dismiss a vulnerability with a reason (acceptable_risk, false_positive, used_in_tests, mitigating_control, not_applicable) and optional comment
805
+ 232. `confirm_vulnerability` - Confirm a vulnerability as a real finding requiring remediation
806
+ 233. `discover_tools` - Discover and activate additional tool categories for this session. Available categories: merge_requests, issues, repositories, branches, projects, labels, ci, groups, pipelines, milestones, wiki, releases, tags, users, workitems, webhooks, search, variables, dependency_proxy, vulnerabilities. Already-active categories are listed in the response.
804
807
 
805
808
  <!-- TOOLS-END -->
806
809
 
package/README.zh-CN.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ### 为什么使用这个 GitLab MCP?
19
19
 
20
- - **217 个工具 + `discover_tools`** — 从小型 toolset 开始,运行时按需激活类别
20
+ - **232 个工具 + `discover_tools`** — 从小型 toolset 开始,运行时按需激活类别
21
21
  - **MR 两步审查** — `list_merge_request_changed_files` → 批量 `get_merge_request_file_diff`
22
22
  - **内置 Agent Skill** — `skills/gitlab-mcp/` 工作流指南
23
23
  - **认证灵活** — Personal Access Token、本地 OAuth2 浏览器流程、MCP OAuth 代理、按请求远程授权
@@ -30,9 +30,9 @@
30
30
  | | @zereight/mcp-gitlab | GitLab MCP A(社区 CQRS 型) |
31
31
  |---|----------------------|------------------------------|
32
32
  | **更适合** | AI 代理工作流 | 企业多实例 / 分组工具 |
33
- | **工具模型** | ~217 个细粒度工具 + `discover_tools` | ~50–60 个 `browse_*` / `manage_*` 分组工具 |
33
+ | **工具模型** | ~232 个细粒度工具 + `discover_tools` | ~50–60 个 `browse_*` / `manage_*` 分组工具 |
34
34
  | **MR 审查** | 两步批量 diff | 因服务器而异 |
35
- | **Node.js** | >=18 | 通常 >=24 |
35
+ | **Node.js** | >=18.17 | 通常 >=24 |
36
36
  | **许可证** | MIT | 因服务器而异 |
37
37
 
38
38
  [完整对比 →](./docs/comparison/community-gitlab-mcp-a.md)
@@ -98,7 +98,7 @@ npm install -g @zereight/mcp-gitlab
98
98
 
99
99
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
100
100
 
101
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.54`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
101
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.56`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
102
102
 
103
103
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
104
104
 
@@ -289,7 +289,7 @@ MCP 客户端配置:
289
289
  | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
290
290
  | `ENABLE_DYNAMIC_API_URL` | 可选 | 允许按请求通过 `X-GitLab-API-URL` 请求头指定 GitLab URL |
291
291
  | `GITLAB_ALLOWED_HOSTS` | 可选 | 允许的 `X-GitLab-API-URL` 主机逗号分隔列表;`GITLAB_API_URL` 中的主机始终允许 |
292
- | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | 可选 | 仅允许未认证的 `initialize`、`notifications/initialized`、`tools/list`(工具调用仍需认证) |
292
+ | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | 可选 | 仅允许未认证的 `initialize`、`notifications/initialized`、`tools/list`、`server/discover`(工具调用仍需认证) |
293
293
  | `MCP_SERVER_URL` / `MCP_ALLOWED_HOSTS` / `MCP_ALLOWED_ORIGINS` | 可选 | 用于 DNS rebinding 防护的允许 `/mcp` 主机/来源值 |
294
294
  | `MCP_TRUST_PROXY` | 可选 | 在反向代理后信任 `Forwarded` / `X-Forwarded-*` 请求头(下载 URL、Express `req.ip`、`/mcp` IP 速率限制、OAuth 速率限制) |
295
295
 
@@ -5,34 +5,55 @@
5
5
  * importing index.ts (which has heavy side effects: starts the MCP server,
6
6
  * reads env vars, etc.).
7
7
  */
8
- import { Headers } from "node-fetch";
8
+ function isTupleArray(headers) {
9
+ if (!Array.isArray(headers))
10
+ return false;
11
+ return headers.every(entry => Array.isArray(entry) &&
12
+ entry.length >= 2 &&
13
+ typeof entry[0] === "string" &&
14
+ typeof entry[1] === "string");
15
+ }
16
+ function hasForEach(headers) {
17
+ return "forEach" in headers && typeof headers.forEach === "function";
18
+ }
19
+ function isPlainStringHeaders(headers) {
20
+ return Object.values(headers).every(value => typeof value === "string");
21
+ }
9
22
  /**
10
23
  * Convert various header representations to a plain Record<string, string>.
11
24
  */
12
25
  export function headersToPlainObject(headers) {
13
- if (!headers)
26
+ if (!headers || typeof headers !== "object")
14
27
  return {};
15
- if (headers instanceof Headers) {
28
+ if (isTupleArray(headers)) {
29
+ return Object.fromEntries(headers);
30
+ }
31
+ if (hasForEach(headers)) {
16
32
  const obj = {};
17
- headers.forEach((value, key) => { obj[key] = value; });
33
+ headers.forEach((value, key) => {
34
+ obj[key] = value;
35
+ });
18
36
  return obj;
19
37
  }
20
- if (Array.isArray(headers)) {
21
- return Object.fromEntries(headers);
38
+ if (isPlainStringHeaders(headers)) {
39
+ return headers;
22
40
  }
23
- return headers;
41
+ return {};
24
42
  }
25
43
  /**
26
- * Detect request bodies that cannot be replayed (streams, FormData).
44
+ * Detect request bodies that cannot be replayed (streams).
27
45
  */
46
+ function hasCallableMethod(body, name) {
47
+ return name in body && typeof Reflect.get(body, name) === "function";
48
+ }
28
49
  export function isNonReplayableBody(body) {
29
- if (!body)
50
+ if (!body || typeof body !== "object")
30
51
  return false;
31
- // Stream-like objects (has .pipe or .read)
32
- if (typeof body.pipe === "function" || typeof body.read === "function")
52
+ if (hasCallableMethod(body, "pipe"))
33
53
  return true;
34
- // form-data instances (duck-type check since FormData is dynamically imported)
35
- if (typeof body.getBuffer === "function" && typeof body.getBoundary === "function")
54
+ if (hasCallableMethod(body, "read"))
55
+ return true;
56
+ if (hasCallableMethod(body, "getReader"))
36
57
  return true;
37
58
  return false;
38
59
  }
@@ -41,24 +62,19 @@ export function isNonReplayableBody(body) {
41
62
  * On a 401, force-refreshes the OAuth token and retries the request once.
42
63
  * The retry calls baseFetch directly (not the wrapper), so infinite loops are impossible.
43
64
  * In non-OAuth mode, the wrapper is a transparent pass-through.
44
- *
45
- * When called without `config`, falls back to module globals in index.ts.
46
- * When called with `config` (tests), uses injected dependencies.
47
65
  */
48
66
  export function wrapWithAuthRetry(baseFetch, config) {
49
67
  let refreshLock = null;
50
68
  const log = config.logger ?? { info: () => { }, error: () => { } };
51
- return (async (url, options) => {
69
+ const wrapped = async (url, options) => {
52
70
  const response = await baseFetch(url, options);
53
71
  if (response.status === 401 && config.isOAuthEnabled()) {
54
- // Skip retry for non-replayable bodies (streams, FormData) since the first request consumed them
55
72
  if (isNonReplayableBody(options?.body)) {
56
73
  log.info("Received 401 but request body is not replayable (stream/FormData), skipping retry.");
57
74
  return response;
58
75
  }
59
76
  log.info("Received 401, force-refreshing OAuth token and retrying...");
60
77
  try {
61
- // Mutex: coalesce concurrent refresh attempts into a single in-flight request
62
78
  if (!refreshLock) {
63
79
  refreshLock = config.refreshToken(true).finally(() => {
64
80
  refreshLock = null;
@@ -77,5 +93,6 @@ export function wrapWithAuthRetry(baseFetch, config) {
77
93
  }
78
94
  }
79
95
  return response;
80
- });
96
+ };
97
+ return wrapped;
81
98
  }
@@ -1,3 +1,5 @@
1
+ import { Readable } from "node:stream";
2
+ import { pipeline } from "node:stream/promises";
1
3
  import { decryptDownloadToken } from "../utils/download-token.js";
2
4
  const DEFAULT_DOWNLOAD_TIMEOUT_MS = 120_000;
3
5
  function canonicalizeQueryParams(params) {
@@ -156,10 +158,9 @@ export function registerDownloadProxy(app, deps) {
156
158
  const controller = new AbortController();
157
159
  const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
158
160
  try {
159
- const agent = deps.getAgentFunctionForUrl(apiUrl);
160
161
  const gitlabResponse = await deps.fetch(gitlabUrl, {
161
162
  headers,
162
- agent,
163
+ dispatcher: deps.getDispatcherForUrl(apiUrl),
163
164
  signal: controller.signal,
164
165
  });
165
166
  if (!gitlabResponse.ok) {
@@ -177,12 +178,11 @@ export function registerDownloadProxy(app, deps) {
177
178
  res.setHeader("Content-Disposition", contentDisposition);
178
179
  if (contentLength)
179
180
  res.setHeader("Content-Length", contentLength);
180
- if (gitlabResponse.body) {
181
- gitlabResponse.body.pipe(res);
182
- }
183
- else {
181
+ if (!gitlabResponse.body) {
184
182
  res.status(502).json({ error: "No response body from GitLab" });
183
+ return;
185
184
  }
185
+ await pipeline(Readable.fromWeb(gitlabResponse.body), res);
186
186
  }
187
187
  catch (error) {
188
188
  deps.logger.error({ err: error }, "Download proxy error");