@zereight/mcp-gitlab 2.1.64 → 2.1.66

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/build/index.js CHANGED
@@ -89,7 +89,7 @@ import { formatPrometheusMetrics } from "./server/metrics.js";
89
89
  import { SERVER_VERSION } from "./server/version.js";
90
90
  import { isInitializationRequestBody, isUnauthenticatedDiscoveryRequestBody, readAcceptHeader, readMcpSessionIdHeader, redactSessionIdForLog, } from "./server/request-helpers.js";
91
91
  export { readMcpSessionIdHeader } from "./server/request-helpers.js";
92
- import { normalizeGitLabApiUrl } from "./utils/url.js";
92
+ import { encodeGitLabPath, encodeGitLabPathSegment, normalizeGitLabApiUrl, } from "./utils/url.js";
93
93
  import { estimateMergeCommitCount, filterDiffsByPatterns, openSafeOutputWriteStream, readSafeExistingFile, summarizeWebhookEvents, } from "./utils/helpers.js";
94
94
  import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation, } from "./utils/graphql-query.js";
95
95
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
@@ -98,9 +98,11 @@ import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
98
98
  import { createMaskingPolicyResolver, getManagedMaskingProjectIds, } from "./masking/index.js";
99
99
  import { checkForNewVersion } from "./utils/version-check.js";
100
100
  import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
101
+ import { fetchWithValidatedRedirects } from "./utils/safe-redirect-fetch.js";
101
102
  import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.js";
102
103
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
103
104
  import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
105
+ import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
104
106
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
105
107
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
106
108
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -288,6 +290,18 @@ function createServer() {
288
290
  },
289
291
  };
290
292
  }
293
+ if (modified.inputSchema && typeof modified.inputSchema === "object") {
294
+ const properties = modified.inputSchema.properties && typeof modified.inputSchema.properties === "object"
295
+ ? { ...modified.inputSchema.properties }
296
+ : {};
297
+ if (!(JMESPATH_TOOL_ARGUMENT in properties)) {
298
+ properties[JMESPATH_TOOL_ARGUMENT] = {
299
+ type: "string",
300
+ description: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
301
+ };
302
+ modified.inputSchema = { ...modified.inputSchema, properties };
303
+ }
304
+ }
291
305
  return modified;
292
306
  });
293
307
  // <<< END: Remove $schema for Gemini compatibility >>>
@@ -302,15 +316,24 @@ function createServer() {
302
316
  const toolName = request.params.name;
303
317
  const start = Date.now();
304
318
  let maskingEngine;
305
- const logCompletion = (result) => {
319
+ const jmespathExpression = readJmespathExpression(request.params.arguments);
320
+ const logCompletion = (result, completionOptions) => {
306
321
  const durationMs = Date.now() - start;
307
322
  logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
308
323
  const isPlainTextResult = toolName === "get_pipeline_job_output" ||
309
324
  toolName === "get_job_artifact_file" ||
310
325
  (toolName === "download_release_asset" && !IS_REMOTE);
326
+ const filtered = applyJmespathToToolResult(result, jmespathExpression, {
327
+ ...completionOptions,
328
+ maskValue: maskingEngine
329
+ ? (value) => maskingEngine.maskValue(value)
330
+ : undefined,
331
+ });
311
332
  return maskingEngine
312
- ? maskingEngine.maskToolResult(result, { textFormat: isPlainTextResult ? "plain" : "json" })
313
- : result;
333
+ ? maskingEngine.maskToolResult(filtered, {
334
+ textFormat: isPlainTextResult ? "plain" : "json",
335
+ })
336
+ : filtered;
314
337
  };
315
338
  const logError = (error) => {
316
339
  const durationMs = Date.now() - start;
@@ -369,6 +392,12 @@ function createServer() {
369
392
  maskingEngine = sessionContext
370
393
  ? sessionAuthStore.run(sessionContext, selectMaskingPolicy)
371
394
  : selectMaskingPolicy();
395
+ const jmespathSyntaxError = jmespathExpression
396
+ ? getJmespathSyntaxError(jmespathExpression)
397
+ : undefined;
398
+ if (jmespathSyntaxError) {
399
+ return logCompletion({ content: [{ type: "text", text: jmespathSyntaxError }], isError: true }, { skipJmespath: true });
400
+ }
372
401
  // Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
373
402
  if (toolName === "discover_tools") {
374
403
  const category = request.params.arguments?.category?.trim()?.toLowerCase();
@@ -476,12 +505,15 @@ function createServer() {
476
505
  text: `Tool "${toolName}" requires confirmation. This tool is marked as requiring approval before execution. Re-call with _confirmed: true to proceed.`,
477
506
  },
478
507
  ],
479
- });
508
+ }, { skipJmespath: true });
480
509
  }
481
510
  // Strip _confirmed from args before forwarding to handler
482
511
  const { _confirmed, ...cleanArgs } = request.params.arguments || {};
483
512
  request.params.arguments = cleanArgs;
484
513
  }
514
+ if (request.params.arguments && typeof request.params.arguments === "object") {
515
+ request.params.arguments = omitJmespathArgument(request.params.arguments);
516
+ }
485
517
  if (sessionContext) {
486
518
  // Policy selection and the tool handler must use the same request-local
487
519
  // API URL and project allowlist.
@@ -829,7 +861,7 @@ async function ensureValidOAuthToken() {
829
861
  throw error;
830
862
  }
831
863
  }
832
- // Permission mode gate: readonly exposes only read tools; modify blocks delete tools; full allows all
864
+ // Permission mode gate: readonly exposes only read tools; modify blocks delete/teardown tools; full allows all
833
865
  function isToolAllowedByPermissionMode(toolName) {
834
866
  switch (GITLAB_PERMISSION_MODE) {
835
867
  case "readonly":
@@ -1165,6 +1197,21 @@ const getFetchConfig = () => {
1165
1197
  dispatcher,
1166
1198
  };
1167
1199
  };
1200
+ /**
1201
+ * Options for the download sinks that stream a file through `fetchWithValidatedRedirects`.
1202
+ *
1203
+ * GitLab answers a release asset, a job artifact or an uploaded file with a redirect to
1204
+ * its object storage, so those hops leave the origin and must be validated. The client
1205
+ * split matters as much as the headers: the wrapped client adds the cookie jar and a
1206
+ * fresh `Authorization` on a `401`, so a hop whose credentials were withheld has to use
1207
+ * plain undici instead.
1208
+ */
1209
+ const downloadRedirectOptions = () => ({
1210
+ ...getFetchConfig(),
1211
+ fetchImpl: fetch,
1212
+ unauthenticatedFetchImpl: undiciFetch,
1213
+ isTrustedRedirectHost: isTrustedGitLabRedirectHost,
1214
+ });
1168
1215
  // Compute at startup
1169
1216
  const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
1170
1217
  const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
@@ -1195,23 +1242,6 @@ function parseAllowedGitLabApiUrls(value) {
1195
1242
  .map(toAllowedGitLabApiUrl)
1196
1243
  .filter((entry) => Boolean(entry));
1197
1244
  }
1198
- function encodeGitLabPathSegment(value) {
1199
- const segment = String(value);
1200
- let decodedSegment;
1201
- try {
1202
- decodedSegment = decodeURIComponent(segment);
1203
- }
1204
- catch {
1205
- decodedSegment = segment;
1206
- }
1207
- if (decodedSegment === "." || decodedSegment === "..") {
1208
- throw new Error("GitLab URL path segments cannot be '.' or '..'");
1209
- }
1210
- return encodeURIComponent(decodedSegment);
1211
- }
1212
- function encodeGitLabPath(value) {
1213
- return value.split("/").map(encodeGitLabPathSegment).join("/");
1214
- }
1215
1245
  function resolveTrustedGitLabApiUrl(value) {
1216
1246
  const parsed = new URL(normalizeGitLabApiUrl(value));
1217
1247
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
@@ -1238,6 +1268,17 @@ for (const { host, apiUrl } of [
1238
1268
  GITLAB_ALLOWED_API_URLS_BY_HOST.set(host, apiUrl);
1239
1269
  }
1240
1270
  }
1271
+ /**
1272
+ * Redirects from the GitLab API to operator-declared GitLab hosts are followed even
1273
+ * when the host resolves to a private address (self-hosted instances and their
1274
+ * storage may live on an internal network), and those hosts keep receiving the
1275
+ * request credentials. Every other redirect target must resolve to a public
1276
+ * address and is fetched without credentials.
1277
+ *
1278
+ * The lookup key is `URL.host`, which keeps a non-default port, matching how this
1279
+ * map is built from GITLAB_API_URL / GITLAB_ALLOWED_HOSTS.
1280
+ */
1281
+ const isTrustedGitLabRedirectHost = (host) => GITLAB_ALLOWED_API_URLS_BY_HOST.has(host);
1241
1282
  const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
1242
1283
  const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(",")
1243
1284
  .map(id => id.trim())
@@ -1364,7 +1405,7 @@ function rejectIfProjectScopedDeployment(toolName) {
1364
1405
  async function forkProject(projectId, namespace) {
1365
1406
  projectId = decodeURIComponent(projectId); // Decode project ID
1366
1407
  const effectiveProjectId = getEffectiveProjectId(projectId);
1367
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/fork`);
1408
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/fork`);
1368
1409
  if (namespace) {
1369
1410
  url.searchParams.append("namespace", namespace);
1370
1411
  }
@@ -1391,7 +1432,7 @@ async function forkProject(projectId, namespace) {
1391
1432
  async function createBranch(projectId, options) {
1392
1433
  projectId = decodeURIComponent(projectId); // Decode project ID
1393
1434
  const effectiveProjectId = getEffectiveProjectId(projectId);
1394
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/branches`);
1435
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/branches`);
1395
1436
  const response = await fetch(url.toString(), {
1396
1437
  ...getFetchConfig(),
1397
1438
  method: "POST",
@@ -1413,7 +1454,7 @@ async function createBranch(projectId, options) {
1413
1454
  async function getDefaultBranchRef(projectId) {
1414
1455
  projectId = decodeURIComponent(projectId); // Decode project ID
1415
1456
  const effectiveProjectId = getEffectiveProjectId(projectId);
1416
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
1457
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
1417
1458
  const response = await fetch(url.toString(), {
1418
1459
  ...getFetchConfig(),
1419
1460
  });
@@ -1433,12 +1474,12 @@ async function getDefaultBranchRef(projectId) {
1433
1474
  async function getFileContents(projectId, filePath, ref) {
1434
1475
  const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
1435
1476
  const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1436
- const encodedPath = encodeURIComponent(filePath);
1477
+ const encodedPath = encodeGitLabPathSegment(filePath);
1437
1478
  // Fall back to default branch if ref is not provided
1438
1479
  if (!ref) {
1439
1480
  ref = await getDefaultBranchRef(decodedProjectId);
1440
1481
  }
1441
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
1482
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/files/${encodedPath}`);
1442
1483
  url.searchParams.append("ref", ref);
1443
1484
  const response = await fetch(url.toString(), {
1444
1485
  ...getFetchConfig(),
@@ -1464,7 +1505,7 @@ async function getFileContents(projectId, filePath, ref) {
1464
1505
  async function createIssue(projectId, options) {
1465
1506
  projectId = decodeURIComponent(projectId); // Decode project ID
1466
1507
  const effectiveProjectId = getEffectiveProjectId(projectId);
1467
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
1508
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/issues`);
1468
1509
  // Build request body, converting labels array to comma-separated string
1469
1510
  const body = { ...options };
1470
1511
  if (body.labels && Array.isArray(body.labels)) {
@@ -1499,7 +1540,7 @@ async function listIssues(projectId, options = {}) {
1499
1540
  // resolve the default project (or fail for a multi-project allowlist)
1500
1541
  const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
1501
1542
  const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1502
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
1543
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/issues`);
1503
1544
  }
1504
1545
  else {
1505
1546
  url = new URL(`${getEffectiveApiUrl()}/issues`);
@@ -1600,7 +1641,7 @@ async function listMergeRequests(projectId, options = {}) {
1600
1641
  // In strict scope, never fall back to the instance-wide endpoint;
1601
1642
  // resolve the default project (or fail for a multi-project allowlist)
1602
1643
  const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1603
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/merge_requests`);
1644
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/merge_requests`);
1604
1645
  }
1605
1646
  else {
1606
1647
  url = new URL(`${getEffectiveApiUrl()}/merge_requests`);
@@ -1622,7 +1663,7 @@ async function listMergeRequests(projectId, options = {}) {
1622
1663
  */
1623
1664
  async function listGroupMergeRequests(groupId, options = {}) {
1624
1665
  const decodedGroupId = decodeURIComponent(groupId);
1625
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(decodedGroupId)}/merge_requests`);
1666
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(decodedGroupId)}/merge_requests`);
1626
1667
  appendMergeRequestFilters(url, options);
1627
1668
  const response = await fetch(url.toString(), {
1628
1669
  ...getFetchConfig(),
@@ -1641,7 +1682,7 @@ async function listGroupMergeRequests(groupId, options = {}) {
1641
1682
  */
1642
1683
  async function getIssue(projectId, issueIid) {
1643
1684
  projectId = decodeURIComponent(projectId); // Decode project ID
1644
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1685
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1645
1686
  const response = await fetch(url.toString(), {
1646
1687
  ...getFetchConfig(),
1647
1688
  });
@@ -1660,7 +1701,7 @@ async function getIssue(projectId, issueIid) {
1660
1701
  */
1661
1702
  async function updateIssue(projectId, issueIid, options) {
1662
1703
  projectId = decodeURIComponent(projectId); // Decode project ID
1663
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1704
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1664
1705
  // Convert labels array to comma-separated string if present
1665
1706
  const body = { ...options };
1666
1707
  if (body.labels && Array.isArray(body.labels)) {
@@ -1685,7 +1726,7 @@ async function updateIssue(projectId, issueIid, options) {
1685
1726
  */
1686
1727
  async function deleteIssue(projectId, issueIid) {
1687
1728
  projectId = decodeURIComponent(projectId); // Decode project ID
1688
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1729
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1689
1730
  const response = await fetch(url.toString(), {
1690
1731
  ...getFetchConfig(),
1691
1732
  method: "DELETE",
@@ -2298,7 +2339,7 @@ async function resolveProjectOrGroupPath(projectId) {
2298
2339
  }
2299
2340
  const effectiveProjectId = getEffectiveProjectId(requestedProjectId);
2300
2341
  if (explicitKind === "group") {
2301
- const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
2342
+ const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(effectiveProjectId)}`);
2302
2343
  const groupResponse = await fetch(groupUrl.toString(), {
2303
2344
  ...getFetchConfig(),
2304
2345
  });
@@ -2306,7 +2347,7 @@ async function resolveProjectOrGroupPath(projectId) {
2306
2347
  const group = await groupResponse.json();
2307
2348
  return { path: group.full_path, kind: "group" };
2308
2349
  }
2309
- const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
2350
+ const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
2310
2351
  const projectResponse = await fetch(projectUrl.toString(), {
2311
2352
  ...getFetchConfig(),
2312
2353
  });
@@ -2318,7 +2359,7 @@ async function resolveProjectOrGroupPath(projectId) {
2318
2359
  !explicitKind &&
2319
2360
  getEffectiveAllowedProjectIds().length === 0 &&
2320
2361
  !/^\d+$/.test(effectiveProjectId)) {
2321
- const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
2362
+ const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(effectiveProjectId)}`);
2322
2363
  const groupResponse = await fetch(groupUrl.toString(), {
2323
2364
  ...getFetchConfig(),
2324
2365
  });
@@ -3059,7 +3100,7 @@ async function updateWorkItem(projectId, iid, options) {
3059
3100
  */
3060
3101
  async function listIssueLinks(projectId, issueIid) {
3061
3102
  projectId = decodeURIComponent(projectId); // Decode project ID
3062
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3103
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3063
3104
  const response = await fetch(url.toString(), {
3064
3105
  ...getFetchConfig(),
3065
3106
  });
@@ -3078,7 +3119,7 @@ async function listIssueLinks(projectId, issueIid) {
3078
3119
  */
3079
3120
  async function getIssueLink(projectId, issueIid, issueLinkId) {
3080
3121
  projectId = decodeURIComponent(projectId); // Decode project ID
3081
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3122
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3082
3123
  const response = await fetch(url.toString(), {
3083
3124
  ...getFetchConfig(),
3084
3125
  });
@@ -3100,7 +3141,7 @@ async function getIssueLink(projectId, issueIid, issueLinkId) {
3100
3141
  async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
3101
3142
  projectId = decodeURIComponent(projectId); // Decode project ID
3102
3143
  targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
3103
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3144
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3104
3145
  const response = await fetch(url.toString(), {
3105
3146
  ...getFetchConfig(),
3106
3147
  method: "POST",
@@ -3125,7 +3166,7 @@ async function createIssueLink(projectId, issueIid, targetProjectId, targetIssue
3125
3166
  */
3126
3167
  async function deleteIssueLink(projectId, issueIid, issueLinkId) {
3127
3168
  projectId = decodeURIComponent(projectId); // Decode project ID
3128
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3169
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3129
3170
  const response = await fetch(url.toString(), {
3130
3171
  ...getFetchConfig(),
3131
3172
  method: "DELETE",
@@ -3142,7 +3183,7 @@ async function deleteIssueLink(projectId, issueIid, issueLinkId) {
3142
3183
  */
3143
3184
  async function createMergeRequest(projectId, options) {
3144
3185
  projectId = decodeURIComponent(projectId); // Decode project ID
3145
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`);
3186
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests`);
3146
3187
  const response = await fetch(url.toString(), {
3147
3188
  ...getFetchConfig(),
3148
3189
  method: "POST",
@@ -3184,7 +3225,7 @@ async function createMergeRequest(projectId, options) {
3184
3225
  */
3185
3226
  async function listDiscussions(projectId, resourceType, resourceIid, options = {}) {
3186
3227
  projectId = decodeURIComponent(projectId); // Decode project ID
3187
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${resourceType}/${encodeGitLabPathSegment(resourceIid)}/discussions`);
3228
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/${resourceType}/${encodeGitLabPathSegment(resourceIid)}/discussions`);
3188
3229
  // Add query parameters for pagination and sorting
3189
3230
  if (options.page) {
3190
3231
  url.searchParams.append("page", options.page.toString());
@@ -3248,7 +3289,7 @@ async function listIssueDiscussions(projectId, issueIid, options = {}) {
3248
3289
  }
3249
3290
  async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId) {
3250
3291
  projectId = decodeURIComponent(projectId); // Decode project ID
3251
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3292
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3252
3293
  const response = await fetch(url.toString(), {
3253
3294
  ...getFetchConfig(),
3254
3295
  method: "DELETE",
@@ -3272,7 +3313,7 @@ async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3272
3313
  */
3273
3314
  async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
3274
3315
  projectId = decodeURIComponent(projectId); // Decode project ID
3275
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3316
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3276
3317
  // Only one of body or resolved can be sent according to GitLab API
3277
3318
  const payload = {};
3278
3319
  if (body !== undefined) {
@@ -3318,7 +3359,7 @@ async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3318
3359
  */
3319
3360
  async function updateIssueNote(projectId, issueIid, discussionId, noteId, body, resolved) {
3320
3361
  projectId = decodeURIComponent(projectId); // Decode project ID
3321
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3362
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3322
3363
  // Only one of body or resolved can be sent according to GitLab API
3323
3364
  const payload = {};
3324
3365
  if (body !== undefined) {
@@ -3347,7 +3388,7 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3347
3388
  */
3348
3389
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3349
3390
  projectId = decodeURIComponent(projectId); // Decode project ID
3350
- const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3391
+ const basePath = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3351
3392
  const url = new URL(discussionId ? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes` : `${basePath}/notes`);
3352
3393
  const payload = { body };
3353
3394
  if (createdAt) {
@@ -3375,7 +3416,7 @@ async function createIssueNote(projectId, issueIid, discussionId, body, createdA
3375
3416
  */
3376
3417
  async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
3377
3418
  projectId = decodeURIComponent(projectId); // Decode project ID
3378
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`);
3419
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`);
3379
3420
  const payload = { body };
3380
3421
  if (createdAt) {
3381
3422
  payload.created_at = createdAt;
@@ -3391,7 +3432,7 @@ async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3391
3432
  }
3392
3433
  async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3393
3434
  projectId = decodeURIComponent(projectId); // Decode project ID
3394
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3435
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3395
3436
  const payload = {
3396
3437
  id: projectId,
3397
3438
  merge_request_iid: mergeRequestIid,
@@ -3408,7 +3449,7 @@ async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3408
3449
  }
3409
3450
  async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3410
3451
  projectId = decodeURIComponent(projectId); // Decode project ID
3411
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3452
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3412
3453
  const response = await fetch(url.toString(), {
3413
3454
  ...getFetchConfig(),
3414
3455
  method: "DELETE",
@@ -3421,7 +3462,7 @@ async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3421
3462
  // --- Emoji Reactions (REST) ---
3422
3463
  function buildAwardEmojiPath(entity, projectId, entityIid, opts) {
3423
3464
  projectId = decodeURIComponent(projectId);
3424
- const pp = encodeURIComponent(getEffectiveProjectId(projectId));
3465
+ const pp = encodeGitLabPathSegment(getEffectiveProjectId(projectId));
3425
3466
  let path = `${getEffectiveApiUrl()}/projects/${pp}/${entity}/${encodeGitLabPathSegment(entityIid)}`;
3426
3467
  if (opts?.noteId) {
3427
3468
  path = opts.discussionId
@@ -3456,7 +3497,7 @@ async function deleteRestAwardEmoji(path) {
3456
3497
  }
3457
3498
  async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3458
3499
  projectId = decodeURIComponent(projectId); // Decode project ID
3459
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3500
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3460
3501
  const response = await fetch(url.toString(), {
3461
3502
  ...getFetchConfig(),
3462
3503
  method: "GET",
@@ -3467,7 +3508,7 @@ async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3467
3508
  }
3468
3509
  async function getMergeRequestDiscussion(projectId, mergeRequestIid, discussionId) {
3469
3510
  projectId = decodeURIComponent(projectId); // Decode project ID
3470
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
3511
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
3471
3512
  const response = await fetch(url.toString(), {
3472
3513
  ...getFetchConfig(),
3473
3514
  method: "GET",
@@ -3478,7 +3519,7 @@ async function getMergeRequestDiscussion(projectId, mergeRequestIid, discussionI
3478
3519
  }
3479
3520
  async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by, per_page, page) {
3480
3521
  projectId = decodeURIComponent(projectId); // Decode project ID
3481
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3522
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3482
3523
  if (sort) {
3483
3524
  url.searchParams.append("sort", sort);
3484
3525
  }
@@ -3501,7 +3542,7 @@ async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by,
3501
3542
  }
3502
3543
  async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body) {
3503
3544
  projectId = decodeURIComponent(projectId); // Decode project ID
3504
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3545
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3505
3546
  const payload = {
3506
3547
  id: projectId,
3507
3548
  merge_request_iid: mergeRequestIid,
@@ -3531,8 +3572,8 @@ async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body)
3531
3572
  */
3532
3573
  async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id, encoding) {
3533
3574
  projectId = decodeURIComponent(projectId); // Decode project ID
3534
- const encodedPath = encodeURIComponent(filePath);
3535
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
3575
+ const encodedPath = encodeGitLabPathSegment(filePath);
3576
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
3536
3577
  const resolvedEncoding = encoding ?? GITLAB_REPO_FILE_ENCODING;
3537
3578
  const body = {
3538
3579
  branch,
@@ -3603,7 +3644,7 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
3603
3644
  */
3604
3645
  async function createCommit(projectId, message, branch, actions) {
3605
3646
  projectId = decodeURIComponent(projectId); // Decode project ID
3606
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`);
3647
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits`);
3607
3648
  const response = await fetch(url.toString(), {
3608
3649
  ...getFetchConfig(),
3609
3650
  method: "POST",
@@ -3672,7 +3713,7 @@ async function searchBlobs(params) {
3672
3713
  let basePath;
3673
3714
  if (params.project_id) {
3674
3715
  const decodedProjectId = decodeURIComponent(params.project_id);
3675
- const projectId = encodeURIComponent(getEffectiveProjectId(decodedProjectId));
3716
+ const projectId = encodeGitLabPathSegment(getEffectiveProjectId(decodedProjectId));
3676
3717
  basePath = `${getEffectiveApiUrl()}/projects/${projectId}/search`;
3677
3718
  }
3678
3719
  else if (params.group_id) {
@@ -3750,11 +3791,11 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3750
3791
  projectId = decodeURIComponent(projectId); // Decode project ID
3751
3792
  let url;
3752
3793
  if (mergeRequestIid) {
3753
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3794
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3754
3795
  url.searchParams.append("include_diverged_commits_count", "true");
3755
3796
  }
3756
3797
  else if (branchName) {
3757
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
3798
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
3758
3799
  }
3759
3800
  else {
3760
3801
  throw new Error("Either mergeRequestIid or branchName must be provided");
@@ -3772,7 +3813,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3772
3813
  return GitLabMergeRequestSchema.parse(data);
3773
3814
  }
3774
3815
  async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3775
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/commits`);
3816
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/commits`);
3776
3817
  url.searchParams.append("per_page", "100");
3777
3818
  let totalCount = 0;
3778
3819
  let page = 1;
@@ -3800,7 +3841,7 @@ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3800
3841
  }
3801
3842
  async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
3802
3843
  projectId = decodeURIComponent(projectId);
3803
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3844
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3804
3845
  appendFilterParams(url.searchParams, options);
3805
3846
  const response = await fetch(url.toString(), {
3806
3847
  ...getFetchConfig(),
@@ -3810,7 +3851,7 @@ async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {
3810
3851
  return z.array(GitLabMergeRequestPipelineSchema).parse(data);
3811
3852
  }
3812
3853
  async function getProjectMergeMethod(projectId) {
3813
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
3854
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}`);
3814
3855
  const response = await fetch(url.toString(), {
3815
3856
  ...getFetchConfig(),
3816
3857
  });
@@ -3997,7 +4038,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
3997
4038
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3998
4039
  mergeRequestIid = mergeRequest.iid;
3999
4040
  }
4000
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4041
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4001
4042
  if (view) {
4002
4043
  url.searchParams.append("view", view);
4003
4044
  }
@@ -4027,7 +4068,7 @@ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, pag
4027
4068
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4028
4069
  mergeRequestIid = mergeRequest.iid;
4029
4070
  }
4030
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4071
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4031
4072
  if (page) {
4032
4073
  url.searchParams.append("page", page.toString());
4033
4074
  }
@@ -4063,7 +4104,7 @@ async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchNa
4063
4104
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4064
4105
  mergeRequestIid = mergeRequest.iid;
4065
4106
  }
4066
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4107
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4067
4108
  const response = await fetch(url.toString(), { ...getFetchConfig() });
4068
4109
  await handleGitLabError(response);
4069
4110
  const data = (await response.json());
@@ -4104,7 +4145,7 @@ async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, br
4104
4145
  let page = 1;
4105
4146
  const perPage = 20;
4106
4147
  while (remaining.size > 0) {
4107
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4148
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4108
4149
  url.searchParams.append("page", page.toString());
4109
4150
  url.searchParams.append("per_page", perPage.toString());
4110
4151
  if (unidiff) {
@@ -4147,7 +4188,7 @@ async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, br
4147
4188
  */
4148
4189
  async function getBranchDiffs(projectId, from, to, straight) {
4149
4190
  projectId = decodeURIComponent(projectId); // Decode project ID
4150
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/compare`);
4191
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/compare`);
4151
4192
  url.searchParams.append("from", from);
4152
4193
  url.searchParams.append("to", to);
4153
4194
  if (straight !== undefined) {
@@ -4182,7 +4223,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
4182
4223
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4183
4224
  mergeRequestIid = mergeRequest.iid;
4184
4225
  }
4185
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
4226
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
4186
4227
  const response = await fetch(url.toString(), {
4187
4228
  ...getFetchConfig(),
4188
4229
  method: "PUT",
@@ -4202,7 +4243,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
4202
4243
  */
4203
4244
  async function mergeMergeRequest(projectId, options, mergeRequestIid) {
4204
4245
  projectId = decodeURIComponent(projectId); // Decode project ID
4205
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/merge`);
4246
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/merge`);
4206
4247
  const response = await fetch(url.toString(), {
4207
4248
  ...getFetchConfig(),
4208
4249
  method: "PUT",
@@ -4222,7 +4263,7 @@ async function mergeMergeRequest(projectId, options, mergeRequestIid) {
4222
4263
  */
4223
4264
  async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPassword) {
4224
4265
  projectId = decodeURIComponent(projectId);
4225
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approve`);
4266
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approve`);
4226
4267
  const body = {};
4227
4268
  if (sha) {
4228
4269
  body.sha = sha;
@@ -4247,7 +4288,7 @@ async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPass
4247
4288
  */
4248
4289
  async function unapproveMergeRequest(projectId, mergeRequestIid) {
4249
4290
  projectId = decodeURIComponent(projectId);
4250
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/unapprove`);
4291
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/unapprove`);
4251
4292
  const response = await fetch(url.toString(), {
4252
4293
  ...getFetchConfig(),
4253
4294
  method: "POST",
@@ -4265,7 +4306,7 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
4265
4306
  */
4266
4307
  async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4267
4308
  projectId = decodeURIComponent(projectId);
4268
- const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approval_state`);
4309
+ const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approval_state`);
4269
4310
  const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
4270
4311
  ...getFetchConfig(),
4271
4312
  method: "GET",
@@ -4313,7 +4354,7 @@ async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4313
4354
  */
4314
4355
  async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4315
4356
  projectId = decodeURIComponent(projectId);
4316
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/conflicts`);
4357
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/conflicts`);
4317
4358
  const response = await fetch(url.toString(), {
4318
4359
  ...getFetchConfig(),
4319
4360
  method: "GET",
@@ -4322,7 +4363,7 @@ async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4322
4363
  return (await response.json());
4323
4364
  }
4324
4365
  async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
4325
- const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approvals`);
4366
+ const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approvals`);
4326
4367
  const approvalsResponse = await fetch(approvalsUrl.toString(), {
4327
4368
  ...getFetchConfig(),
4328
4369
  method: "GET",
@@ -4382,7 +4423,7 @@ async function createNote(projectId, noteableType, // specifies 'issue' or 'merg
4382
4423
  noteableIid, body) {
4383
4424
  projectId = decodeURIComponent(projectId); // Decode project ID
4384
4425
  // ⚙️ Response type can be adjusted according to the GitLab API documentation
4385
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${noteableType}s/${encodeGitLabPathSegment(noteableIid)}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
4426
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/${noteableType}s/${encodeGitLabPathSegment(noteableIid)}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
4386
4427
  );
4387
4428
  const response = await fetch(url.toString(), {
4388
4429
  ...getFetchConfig(),
@@ -4403,7 +4444,7 @@ noteableIid, body) {
4403
4444
  */
4404
4445
  async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4405
4446
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
4406
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/merge_requests/${encodeGitLabPathSegment(merge_request_iid)}/draft_notes/${encodeGitLabPathSegment(draft_note_id)}`, { ...getFetchConfig() });
4447
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/merge_requests/${encodeGitLabPathSegment(merge_request_iid)}/draft_notes/${encodeGitLabPathSegment(draft_note_id)}`, { ...getFetchConfig() });
4407
4448
  if (!response.ok) {
4408
4449
  const errorText = await response.text();
4409
4450
  throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
@@ -4413,7 +4454,7 @@ async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4413
4454
  }
4414
4455
  async function listDraftNotes(projectId, mergeRequestIid) {
4415
4456
  projectId = decodeURIComponent(projectId);
4416
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4457
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4417
4458
  const response = await fetch(url.toString(), {
4418
4459
  ...getFetchConfig(),
4419
4460
  method: "GET",
@@ -4437,7 +4478,7 @@ async function listDraftNotes(projectId, mergeRequestIid) {
4437
4478
  */
4438
4479
  async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscussionId, position, resolveDiscussion) {
4439
4480
  projectId = decodeURIComponent(projectId);
4440
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4481
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4441
4482
  const requestBody = { note: body };
4442
4483
  if (inReplyToDiscussionId) {
4443
4484
  requestBody.in_reply_to_discussion_id = inReplyToDiscussionId;
@@ -4472,7 +4513,7 @@ async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscus
4472
4513
  */
4473
4514
  async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
4474
4515
  projectId = decodeURIComponent(projectId);
4475
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4516
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4476
4517
  const requestBody = {};
4477
4518
  if (body !== undefined) {
4478
4519
  requestBody.note = body;
@@ -4504,7 +4545,7 @@ async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, po
4504
4545
  */
4505
4546
  async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4506
4547
  projectId = decodeURIComponent(projectId);
4507
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4548
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4508
4549
  const response = await fetch(url.toString(), {
4509
4550
  ...getFetchConfig(),
4510
4551
  method: "DELETE",
@@ -4523,7 +4564,7 @@ async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4523
4564
  */
4524
4565
  async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4525
4566
  projectId = decodeURIComponent(projectId);
4526
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}/publish`);
4567
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}/publish`);
4527
4568
  const response = await fetch(url.toString(), {
4528
4569
  ...getFetchConfig(),
4529
4570
  method: "PUT",
@@ -4594,7 +4635,7 @@ async function fetchGitLabInstanceVersion() {
4594
4635
  */
4595
4636
  async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
4596
4637
  projectId = decodeURIComponent(projectId);
4597
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4638
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4598
4639
  const body = buildBulkPublishDraftNotesBody(options);
4599
4640
  if (needsGitLab19_2BulkPublish(body)) {
4600
4641
  await assertGitLabVersionAtLeast({
@@ -4632,7 +4673,7 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
4632
4673
  }
4633
4674
  async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionId, resolved) {
4634
4675
  projectId = decodeURIComponent(projectId);
4635
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
4676
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
4636
4677
  if (resolved !== undefined) {
4637
4678
  url.searchParams.append("resolved", resolved ? "true" : "false");
4638
4679
  }
@@ -4664,7 +4705,7 @@ async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionI
4664
4705
  */
4665
4706
  async function createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
4666
4707
  projectId = decodeURIComponent(projectId); // Decode project ID
4667
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions`);
4708
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions`);
4668
4709
  const payload = { body };
4669
4710
  if (position) {
4670
4711
  payload.position = position;
@@ -4691,7 +4732,7 @@ async function createMergeRequestThread(projectId, mergeRequestIid, body, positi
4691
4732
  */
4692
4733
  async function listMergeRequestVersions(projectId, mergeRequestIid) {
4693
4734
  projectId = decodeURIComponent(projectId); // Decode project ID
4694
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions`);
4735
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions`);
4695
4736
  const response = await fetch(url.toString(), {
4696
4737
  ...getFetchConfig(),
4697
4738
  });
@@ -4710,7 +4751,7 @@ async function listMergeRequestVersions(projectId, mergeRequestIid) {
4710
4751
  */
4711
4752
  async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, unidiff) {
4712
4753
  projectId = decodeURIComponent(projectId); // Decode project ID
4713
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions/${encodeGitLabPathSegment(versionId)}`);
4754
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions/${encodeGitLabPathSegment(versionId)}`);
4714
4755
  if (unidiff !== undefined) {
4715
4756
  url.searchParams.append("unidiff", String(unidiff));
4716
4757
  }
@@ -4752,7 +4793,7 @@ async function listProjects(options = {}) {
4752
4793
  async function listLabels(projectId, options = {}) {
4753
4794
  projectId = decodeURIComponent(projectId); // Decode project ID
4754
4795
  // Construct the URL with project path
4755
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`);
4796
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels`);
4756
4797
  // Add query parameters
4757
4798
  appendFilterParams(url.searchParams, options);
4758
4799
  // Make the API request
@@ -4775,7 +4816,7 @@ async function listLabels(projectId, options = {}) {
4775
4816
  */
4776
4817
  async function getLabel(projectId, labelId, includeAncestorGroups) {
4777
4818
  projectId = decodeURIComponent(projectId); // Decode project ID
4778
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`);
4819
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels/${encodeGitLabPathSegment(String(labelId))}`);
4779
4820
  // Add query parameters
4780
4821
  if (includeAncestorGroups !== undefined) {
4781
4822
  url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
@@ -4800,7 +4841,7 @@ async function getLabel(projectId, labelId, includeAncestorGroups) {
4800
4841
  async function createLabel(projectId, options) {
4801
4842
  projectId = decodeURIComponent(projectId); // Decode project ID
4802
4843
  // Make the API request
4803
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`, {
4844
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels`, {
4804
4845
  ...getFetchConfig(),
4805
4846
  method: "POST",
4806
4847
  body: JSON.stringify(options),
@@ -4822,7 +4863,7 @@ async function createLabel(projectId, options) {
4822
4863
  async function updateLabel(projectId, labelId, options) {
4823
4864
  projectId = decodeURIComponent(projectId); // Decode project ID
4824
4865
  // Make the API request
4825
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
4866
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels/${encodeGitLabPathSegment(String(labelId))}`, {
4826
4867
  ...getFetchConfig(),
4827
4868
  method: "PUT",
4828
4869
  body: JSON.stringify(options),
@@ -4842,7 +4883,7 @@ async function updateLabel(projectId, labelId, options) {
4842
4883
  async function deleteLabel(projectId, labelId) {
4843
4884
  projectId = decodeURIComponent(projectId); // Decode project ID
4844
4885
  // Make the API request
4845
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
4886
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels/${encodeGitLabPathSegment(String(labelId))}`, {
4846
4887
  ...getFetchConfig(),
4847
4888
  method: "DELETE",
4848
4889
  });
@@ -4856,7 +4897,7 @@ async function deleteLabel(projectId, labelId) {
4856
4897
  * @returns {Promise<GitLabProject[]>} Array of projects in the group
4857
4898
  */
4858
4899
  async function listGroupProjects(options) {
4859
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(options.group_id)}/projects`);
4900
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(options.group_id)}/projects`);
4860
4901
  // Add optional parameters to URL
4861
4902
  if (options.include_subgroups)
4862
4903
  url.searchParams.append("include_subgroups", "true");
@@ -4906,11 +4947,11 @@ async function listGroupProjects(options) {
4906
4947
  function buildWebhookBaseUrl(projectId, groupId) {
4907
4948
  if (projectId) {
4908
4949
  projectId = decodeURIComponent(projectId);
4909
- return `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/hooks`;
4950
+ return `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/hooks`;
4910
4951
  }
4911
4952
  rejectIfStrictProjectScope("group webhooks");
4912
4953
  const decodedGroupId = decodeURIComponent(groupId);
4913
- return `${getEffectiveApiUrl()}/groups/${encodeURIComponent(decodedGroupId)}/hooks`;
4954
+ return `${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(decodedGroupId)}/hooks`;
4914
4955
  }
4915
4956
  /**
4916
4957
  * List webhooks for a project or group
@@ -5025,7 +5066,7 @@ async function getWebhookEvent(options) {
5025
5066
  */
5026
5067
  async function listWikiPages(projectId, options = {}) {
5027
5068
  projectId = decodeURIComponent(projectId); // Decode project ID
5028
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`);
5069
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis`);
5029
5070
  if (options.page)
5030
5071
  url.searchParams.append("page", options.page.toString());
5031
5072
  if (options.per_page)
@@ -5046,7 +5087,7 @@ async function listWikiPages(projectId, options = {}) {
5046
5087
  */
5047
5088
  async function getWikiPage(projectId, slug, renderHtml) {
5048
5089
  projectId = decodeURIComponent(projectId); // Decode project ID
5049
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`);
5090
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis/${encodeGitLabPathSegment(slug)}`);
5050
5091
  if (renderHtml)
5051
5092
  url.searchParams.append("render_html", renderHtml.toString());
5052
5093
  const response = await fetch(url.toString(), { ...getFetchConfig() });
@@ -5062,7 +5103,7 @@ async function createWikiPage(projectId, title, content, format) {
5062
5103
  const body = { title, content };
5063
5104
  if (format)
5064
5105
  body.format = format;
5065
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`, {
5106
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis`, {
5066
5107
  ...getFetchConfig(),
5067
5108
  method: "POST",
5068
5109
  body: JSON.stringify(body),
@@ -5090,7 +5131,7 @@ async function updateWikiPage(projectId, slug, title, content, format) {
5090
5131
  body.content = content;
5091
5132
  if (format)
5092
5133
  body.format = format;
5093
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
5134
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis/${encodeGitLabPathSegment(slug)}`, {
5094
5135
  ...getFetchConfig(),
5095
5136
  method: "PUT",
5096
5137
  body: JSON.stringify(body),
@@ -5104,7 +5145,7 @@ async function updateWikiPage(projectId, slug, title, content, format) {
5104
5145
  */
5105
5146
  async function deleteWikiPage(projectId, slug) {
5106
5147
  projectId = decodeURIComponent(projectId); // Decode project ID
5107
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
5148
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis/${encodeGitLabPathSegment(slug)}`, {
5108
5149
  ...getFetchConfig(),
5109
5150
  method: "DELETE",
5110
5151
  });
@@ -5115,7 +5156,7 @@ async function deleteWikiPage(projectId, slug) {
5115
5156
  */
5116
5157
  async function listGroupWikiPages(groupId, options = {}) {
5117
5158
  groupId = decodeURIComponent(groupId); // Decode group ID
5118
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis`);
5159
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis`);
5119
5160
  if (options.page)
5120
5161
  url.searchParams.append("page", options.page.toString());
5121
5162
  if (options.per_page)
@@ -5136,7 +5177,7 @@ async function listGroupWikiPages(groupId, options = {}) {
5136
5177
  */
5137
5178
  async function getGroupWikiPage(groupId, slug, renderHtml) {
5138
5179
  groupId = decodeURIComponent(groupId); // Decode group ID
5139
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`);
5180
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis/${encodeGitLabPathSegment(slug)}`);
5140
5181
  if (renderHtml)
5141
5182
  url.searchParams.append("render_html", renderHtml.toString());
5142
5183
  const response = await fetch(url.toString(), { ...getFetchConfig() });
@@ -5152,7 +5193,7 @@ async function createGroupWikiPage(groupId, title, content, format) {
5152
5193
  const body = { title, content };
5153
5194
  if (format)
5154
5195
  body.format = format;
5155
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis`, {
5196
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis`, {
5156
5197
  ...getFetchConfig(),
5157
5198
  method: "POST",
5158
5199
  body: JSON.stringify(body),
@@ -5180,7 +5221,7 @@ async function updateGroupWikiPage(groupId, slug, title, content, format) {
5180
5221
  body.content = content;
5181
5222
  if (format)
5182
5223
  body.format = format;
5183
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, {
5224
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis/${encodeGitLabPathSegment(slug)}`, {
5184
5225
  ...getFetchConfig(),
5185
5226
  method: "PUT",
5186
5227
  body: JSON.stringify(body),
@@ -5194,7 +5235,7 @@ async function updateGroupWikiPage(groupId, slug, title, content, format) {
5194
5235
  */
5195
5236
  async function deleteGroupWikiPage(groupId, slug) {
5196
5237
  groupId = decodeURIComponent(groupId); // Decode group ID
5197
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, {
5238
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis/${encodeGitLabPathSegment(slug)}`, {
5198
5239
  ...getFetchConfig(),
5199
5240
  method: "DELETE",
5200
5241
  });
@@ -5209,7 +5250,7 @@ async function deleteGroupWikiPage(groupId, slug) {
5209
5250
  */
5210
5251
  async function listPipelines(projectId, options = {}) {
5211
5252
  projectId = decodeURIComponent(projectId); // Decode project ID
5212
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines`);
5253
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines`);
5213
5254
  // Add all query parameters
5214
5255
  appendFilterParams(url.searchParams, options);
5215
5256
  const response = await fetch(url.toString(), {
@@ -5228,7 +5269,7 @@ async function listPipelines(projectId, options = {}) {
5228
5269
  */
5229
5270
  async function getPipeline(projectId, pipelineId, signal) {
5230
5271
  projectId = decodeURIComponent(projectId); // Decode project ID
5231
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`);
5272
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`);
5232
5273
  const response = await fetch(url.toString(), {
5233
5274
  ...getFetchConfig(),
5234
5275
  signal,
@@ -5242,7 +5283,7 @@ async function getPipeline(projectId, pipelineId, signal) {
5242
5283
  }
5243
5284
  async function getPipelineVariables(projectId, pipelineId, page, perPage) {
5244
5285
  projectId = decodeURIComponent(projectId);
5245
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/variables`);
5286
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/variables`);
5246
5287
  if (page !== undefined)
5247
5288
  url.searchParams.set("page", String(page));
5248
5289
  if (perPage !== undefined)
@@ -5254,7 +5295,7 @@ async function getPipelineVariables(projectId, pipelineId, page, perPage) {
5254
5295
  async function getPipelineReport(projectId, pipelineId, summary = false, page, perPage) {
5255
5296
  projectId = decodeURIComponent(projectId);
5256
5297
  const endpoint = summary ? "test_report_summary" : "test_report";
5257
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/${endpoint}`);
5298
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/${endpoint}`);
5258
5299
  if (page !== undefined)
5259
5300
  url.searchParams.set("page", String(page));
5260
5301
  if (perPage !== undefined)
@@ -5265,13 +5306,13 @@ async function getPipelineReport(projectId, pipelineId, summary = false, page, p
5265
5306
  }
5266
5307
  async function deletePipeline(projectId, pipelineId) {
5267
5308
  projectId = decodeURIComponent(projectId);
5268
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`, { ...getFetchConfig(), method: "DELETE" });
5309
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`, { ...getFetchConfig(), method: "DELETE" });
5269
5310
  await handleGitLabError(response);
5270
5311
  return response.status === 204 ? { deleted: true } : response.json();
5271
5312
  }
5272
5313
  async function updatePipelineMetadata(projectId, pipelineId, name) {
5273
5314
  projectId = decodeURIComponent(projectId);
5274
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/metadata`, { ...getFetchConfig(), method: "PUT", body: JSON.stringify({ name }) });
5315
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/metadata`, { ...getFetchConfig(), method: "PUT", body: JSON.stringify({ name }) });
5275
5316
  await handleGitLabError(response);
5276
5317
  return response.json();
5277
5318
  }
@@ -5284,7 +5325,7 @@ async function updatePipelineMetadata(projectId, pipelineId, name) {
5284
5325
  */
5285
5326
  async function listDeployments(projectId, options = {}) {
5286
5327
  projectId = decodeURIComponent(projectId);
5287
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
5328
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/deployments`);
5288
5329
  appendFilterParams(url.searchParams, options);
5289
5330
  const response = await fetch(url.toString(), {
5290
5331
  ...getFetchConfig(),
@@ -5302,7 +5343,7 @@ async function listDeployments(projectId, options = {}) {
5302
5343
  */
5303
5344
  async function getDeployment(projectId, deploymentId) {
5304
5345
  projectId = decodeURIComponent(projectId);
5305
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${encodeGitLabPathSegment(deploymentId)}`);
5346
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/deployments/${encodeGitLabPathSegment(deploymentId)}`);
5306
5347
  const response = await fetch(url.toString(), {
5307
5348
  ...getFetchConfig(),
5308
5349
  });
@@ -5315,7 +5356,7 @@ async function getDeployment(projectId, deploymentId) {
5315
5356
  }
5316
5357
  async function deploymentRequest(projectId, path = "", method = "GET", body, query) {
5317
5358
  projectId = decodeURIComponent(projectId);
5318
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments${path}`);
5359
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/deployments${path}`);
5319
5360
  for (const [key, value] of Object.entries(query ?? {})) {
5320
5361
  if (value !== undefined)
5321
5362
  url.searchParams.set(key, String(value));
@@ -5336,7 +5377,7 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
5336
5377
  */
5337
5378
  async function listEnvironments(projectId, options = {}) {
5338
5379
  projectId = decodeURIComponent(projectId);
5339
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
5380
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments`);
5340
5381
  appendFilterParams(url.searchParams, options);
5341
5382
  const response = await fetch(url.toString(), {
5342
5383
  ...getFetchConfig(),
@@ -5354,7 +5395,7 @@ async function listEnvironments(projectId, options = {}) {
5354
5395
  */
5355
5396
  async function getEnvironment(projectId, environmentId) {
5356
5397
  projectId = decodeURIComponent(projectId);
5357
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${encodeGitLabPathSegment(environmentId)}`);
5398
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments/${encodeGitLabPathSegment(environmentId)}`);
5358
5399
  const response = await fetch(url.toString(), {
5359
5400
  ...getFetchConfig(),
5360
5401
  });
@@ -5367,7 +5408,7 @@ async function getEnvironment(projectId, environmentId) {
5367
5408
  }
5368
5409
  async function environmentRequest(projectId, path, method, body, query) {
5369
5410
  projectId = decodeURIComponent(projectId);
5370
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments${path}`);
5411
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments${path}`);
5371
5412
  for (const [key, value] of Object.entries(query ?? {}))
5372
5413
  if (value !== undefined)
5373
5414
  url.searchParams.set(key, String(value));
@@ -5377,13 +5418,13 @@ async function environmentRequest(projectId, path, method, body, query) {
5377
5418
  }
5378
5419
  async function pipelineTriggerRequest(projectId, path = "", method = "GET", body) {
5379
5420
  projectId = decodeURIComponent(projectId);
5380
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/triggers${path}`, { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
5421
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/triggers${path}`, { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
5381
5422
  await handleGitLabError(response);
5382
5423
  return response.status === 204 ? { success: true } : response.json();
5383
5424
  }
5384
5425
  async function triggerPipeline(projectId, token, ref, variables, inputs) {
5385
5426
  projectId = decodeURIComponent(projectId);
5386
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/trigger/pipeline`);
5427
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/trigger/pipeline`);
5387
5428
  url.searchParams.set("token", token);
5388
5429
  url.searchParams.set("ref", ref);
5389
5430
  const body = {};
@@ -5405,7 +5446,7 @@ async function triggerPipeline(projectId, token, ref, variables, inputs) {
5405
5446
  */
5406
5447
  async function listPipelineJobs(projectId, pipelineId, options = {}) {
5407
5448
  projectId = decodeURIComponent(projectId); // Decode project ID
5408
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
5449
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
5409
5450
  // Add all query parameters
5410
5451
  appendFilterParams(url.searchParams, options);
5411
5452
  const response = await fetch(url.toString(), {
@@ -5428,7 +5469,7 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
5428
5469
  */
5429
5470
  async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
5430
5471
  projectId = decodeURIComponent(projectId); // Decode project ID
5431
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
5472
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
5432
5473
  // Add all query parameters
5433
5474
  appendFilterParams(url.searchParams, options);
5434
5475
  const response = await fetch(url.toString(), {
@@ -5443,7 +5484,7 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
5443
5484
  }
5444
5485
  async function getPipelineJob(projectId, jobId, signal) {
5445
5486
  projectId = decodeURIComponent(projectId); // Decode project ID
5446
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}`);
5487
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}`);
5447
5488
  const response = await fetch(url.toString(), {
5448
5489
  ...getFetchConfig(),
5449
5490
  signal,
@@ -5467,7 +5508,7 @@ async function getPipelineJob(projectId, jobId, signal) {
5467
5508
  const MAX_JOB_TRACE_LINES = 1000;
5468
5509
  async function getPipelineJobOutput(projectId, jobId, limit, offset) {
5469
5510
  projectId = decodeURIComponent(projectId); // Decode project ID
5470
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/trace`);
5511
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/trace`);
5471
5512
  const response = await fetch(url.toString(), {
5472
5513
  ...getFetchConfig(),
5473
5514
  headers: {
@@ -5502,7 +5543,7 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
5502
5543
  }
5503
5544
  async function validateCiLint(projectId, options) {
5504
5545
  projectId = decodeURIComponent(projectId);
5505
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
5546
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/ci/lint`);
5506
5547
  const response = await fetch(url.toString(), {
5507
5548
  ...getFetchConfig(),
5508
5549
  method: "POST",
@@ -5514,7 +5555,7 @@ async function validateCiLint(projectId, options) {
5514
5555
  }
5515
5556
  async function validateProjectCiLint(projectId, options) {
5516
5557
  projectId = decodeURIComponent(projectId);
5517
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
5558
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/ci/lint`);
5518
5559
  appendFilterParams(url.searchParams, options);
5519
5560
  const response = await fetch(url.toString(), {
5520
5561
  ...getFetchConfig(),
@@ -5533,7 +5574,7 @@ async function validateProjectCiLint(projectId, options) {
5533
5574
  */
5534
5575
  async function listJobArtifacts(projectId, jobId, options = {}) {
5535
5576
  projectId = decodeURIComponent(projectId);
5536
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
5577
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
5537
5578
  appendFilterParams(url.searchParams, options);
5538
5579
  const response = await fetch(url.toString(), {
5539
5580
  ...getFetchConfig(),
@@ -5556,10 +5597,8 @@ async function listJobArtifacts(projectId, jobId, options = {}) {
5556
5597
  async function downloadJobArtifacts(projectId, jobId, localPath) {
5557
5598
  projectId = decodeURIComponent(projectId);
5558
5599
  const effectiveProjectId = getEffectiveProjectId(projectId);
5559
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts`);
5560
- const response = await fetch(url.toString(), {
5561
- ...getFetchConfig(),
5562
- });
5600
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts`);
5601
+ const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
5563
5602
  if (response.status === 404) {
5564
5603
  throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
5565
5604
  }
@@ -5583,14 +5622,9 @@ async function downloadJobArtifacts(projectId, jobId, localPath) {
5583
5622
  async function getJobArtifactFile(projectId, jobId, artifactPath) {
5584
5623
  projectId = decodeURIComponent(projectId);
5585
5624
  const effectiveProjectId = getEffectiveProjectId(projectId);
5586
- const encodedArtifactPath = artifactPath
5587
- .split("/")
5588
- .map(segment => encodeGitLabPathSegment(segment))
5589
- .join("/");
5590
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
5591
- const response = await fetch(url.toString(), {
5592
- ...getFetchConfig(),
5593
- });
5625
+ const encodedArtifactPath = encodeGitLabPath(artifactPath);
5626
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
5627
+ const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
5594
5628
  if (response.status === 404) {
5595
5629
  throw new Error(`Artifact file not found: ${artifactPath}`);
5596
5630
  }
@@ -5608,7 +5642,7 @@ async function getJobArtifactFile(projectId, jobId, artifactPath) {
5608
5642
  */
5609
5643
  async function createPipeline(projectId, ref, variables, inputs) {
5610
5644
  projectId = decodeURIComponent(projectId); // Decode project ID
5611
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`);
5645
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline`);
5612
5646
  const body = { ref };
5613
5647
  if (variables && variables.length > 0) {
5614
5648
  body.variables = variables;
@@ -5634,7 +5668,7 @@ async function createPipeline(projectId, ref, variables, inputs) {
5634
5668
  */
5635
5669
  async function retryPipeline(projectId, pipelineId) {
5636
5670
  projectId = decodeURIComponent(projectId); // Decode project ID
5637
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/retry`);
5671
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/retry`);
5638
5672
  const response = await fetch(url.toString(), {
5639
5673
  ...getFetchConfig(),
5640
5674
  method: "POST",
@@ -5652,7 +5686,7 @@ async function retryPipeline(projectId, pipelineId) {
5652
5686
  */
5653
5687
  async function cancelPipeline(projectId, pipelineId) {
5654
5688
  projectId = decodeURIComponent(projectId); // Decode project ID
5655
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/cancel`);
5689
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/cancel`);
5656
5690
  const response = await fetch(url.toString(), {
5657
5691
  ...getFetchConfig(),
5658
5692
  method: "POST",
@@ -5670,7 +5704,7 @@ async function cancelPipeline(projectId, pipelineId) {
5670
5704
  */
5671
5705
  async function listPipelineSchedules(projectId, options = {}) {
5672
5706
  projectId = decodeURIComponent(projectId); // Decode project ID
5673
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5707
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5674
5708
  appendFilterParams(url.searchParams, options);
5675
5709
  const response = await fetch(url.toString(), {
5676
5710
  ...getFetchConfig(),
@@ -5688,7 +5722,7 @@ async function listPipelineSchedules(projectId, options = {}) {
5688
5722
  */
5689
5723
  async function getPipelineSchedule(projectId, pipelineScheduleId) {
5690
5724
  projectId = decodeURIComponent(projectId); // Decode project ID
5691
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5725
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5692
5726
  const response = await fetch(url.toString(), {
5693
5727
  ...getFetchConfig(),
5694
5728
  });
@@ -5706,7 +5740,7 @@ async function getPipelineSchedule(projectId, pipelineScheduleId) {
5706
5740
  */
5707
5741
  async function listPipelineSchedulePipelines(projectId, pipelineScheduleId, options = {}) {
5708
5742
  projectId = decodeURIComponent(projectId); // Decode project ID
5709
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/pipelines`);
5743
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/pipelines`);
5710
5744
  appendFilterParams(url.searchParams, options);
5711
5745
  const response = await fetch(url.toString(), {
5712
5746
  ...getFetchConfig(),
@@ -5724,7 +5758,7 @@ async function listPipelineSchedulePipelines(projectId, pipelineScheduleId, opti
5724
5758
  */
5725
5759
  async function createPipelineSchedule(projectId, options) {
5726
5760
  projectId = decodeURIComponent(projectId); // Decode project ID
5727
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5761
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5728
5762
  const response = await fetch(url.toString(), {
5729
5763
  ...getFetchConfig(),
5730
5764
  method: "POST",
@@ -5744,7 +5778,7 @@ async function createPipelineSchedule(projectId, options) {
5744
5778
  */
5745
5779
  async function updatePipelineSchedule(projectId, pipelineScheduleId, options) {
5746
5780
  projectId = decodeURIComponent(projectId); // Decode project ID
5747
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5781
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5748
5782
  const response = await fetch(url.toString(), {
5749
5783
  ...getFetchConfig(),
5750
5784
  method: "PUT",
@@ -5763,7 +5797,7 @@ async function updatePipelineSchedule(projectId, pipelineScheduleId, options) {
5763
5797
  */
5764
5798
  async function deletePipelineSchedule(projectId, pipelineScheduleId) {
5765
5799
  projectId = decodeURIComponent(projectId); // Decode project ID
5766
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5800
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5767
5801
  const response = await fetch(url.toString(), {
5768
5802
  ...getFetchConfig(),
5769
5803
  method: "DELETE",
@@ -5779,7 +5813,7 @@ async function deletePipelineSchedule(projectId, pipelineScheduleId) {
5779
5813
  */
5780
5814
  async function playPipelineSchedule(projectId, pipelineScheduleId) {
5781
5815
  projectId = decodeURIComponent(projectId); // Decode project ID
5782
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/play`);
5816
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/play`);
5783
5817
  const response = await fetch(url.toString(), {
5784
5818
  ...getFetchConfig(),
5785
5819
  method: "POST",
@@ -5797,7 +5831,7 @@ async function playPipelineSchedule(projectId, pipelineScheduleId) {
5797
5831
  */
5798
5832
  async function takeOwnershipPipelineSchedule(projectId, pipelineScheduleId) {
5799
5833
  projectId = decodeURIComponent(projectId); // Decode project ID
5800
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/take_ownership`);
5834
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/take_ownership`);
5801
5835
  const response = await fetch(url.toString(), {
5802
5836
  ...getFetchConfig(),
5803
5837
  method: "POST",
@@ -5816,7 +5850,7 @@ async function takeOwnershipPipelineSchedule(projectId, pipelineScheduleId) {
5816
5850
  */
5817
5851
  async function getPipelineScheduleVariable(projectId, pipelineScheduleId, key) {
5818
5852
  projectId = decodeURIComponent(projectId); // Decode project ID
5819
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5853
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5820
5854
  const response = await fetch(url.toString(), {
5821
5855
  ...getFetchConfig(),
5822
5856
  });
@@ -5834,7 +5868,7 @@ async function getPipelineScheduleVariable(projectId, pipelineScheduleId, key) {
5834
5868
  */
5835
5869
  async function createPipelineScheduleVariable(projectId, pipelineScheduleId, options) {
5836
5870
  projectId = decodeURIComponent(projectId); // Decode project ID
5837
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables`);
5871
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables`);
5838
5872
  const response = await fetch(url.toString(), {
5839
5873
  ...getFetchConfig(),
5840
5874
  method: "POST",
@@ -5855,7 +5889,7 @@ async function createPipelineScheduleVariable(projectId, pipelineScheduleId, opt
5855
5889
  */
5856
5890
  async function updatePipelineScheduleVariable(projectId, pipelineScheduleId, key, options) {
5857
5891
  projectId = decodeURIComponent(projectId); // Decode project ID
5858
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5892
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5859
5893
  const response = await fetch(url.toString(), {
5860
5894
  ...getFetchConfig(),
5861
5895
  method: "PUT",
@@ -5875,7 +5909,7 @@ async function updatePipelineScheduleVariable(projectId, pipelineScheduleId, key
5875
5909
  */
5876
5910
  async function deletePipelineScheduleVariable(projectId, pipelineScheduleId, key) {
5877
5911
  projectId = decodeURIComponent(projectId); // Decode project ID
5878
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5912
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5879
5913
  const response = await fetch(url.toString(), {
5880
5914
  ...getFetchConfig(),
5881
5915
  method: "DELETE",
@@ -5892,7 +5926,7 @@ async function deletePipelineScheduleVariable(projectId, pipelineScheduleId, key
5892
5926
  */
5893
5927
  async function playPipelineJob(projectId, jobId, variables, jobInputs) {
5894
5928
  projectId = decodeURIComponent(projectId);
5895
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/play`);
5929
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/play`);
5896
5930
  const body = {};
5897
5931
  if (variables && variables.length > 0) {
5898
5932
  body.job_variables_attributes = variables;
@@ -5926,7 +5960,7 @@ async function playPipelineJobs(projectId, jobIds, variables, timeoutSeconds = 3
5926
5960
  */
5927
5961
  async function retryPipelineJob(projectId, jobId, jobInputs) {
5928
5962
  projectId = decodeURIComponent(projectId);
5929
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/retry`);
5963
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/retry`);
5930
5964
  const response = await fetch(url.toString(), {
5931
5965
  ...getFetchConfig(),
5932
5966
  method: "POST",
@@ -5938,7 +5972,7 @@ async function retryPipelineJob(projectId, jobId, jobInputs) {
5938
5972
  }
5939
5973
  async function erasePipelineJob(projectId, jobId) {
5940
5974
  projectId = decodeURIComponent(projectId);
5941
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/erase`, { ...getFetchConfig(), method: "POST" });
5975
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/erase`, { ...getFetchConfig(), method: "POST" });
5942
5976
  await handleGitLabError(response);
5943
5977
  return response.status === 204 ? { erased: true } : response.json();
5944
5978
  }
@@ -5974,7 +6008,7 @@ async function waitForStatus(fetchStatus, terminalStatuses, timeoutSeconds = 300
5974
6008
  */
5975
6009
  async function cancelPipelineJob(projectId, jobId, force) {
5976
6010
  projectId = decodeURIComponent(projectId);
5977
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/cancel`);
6011
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/cancel`);
5978
6012
  if (force !== undefined) {
5979
6013
  url.searchParams.append("force", force.toString());
5980
6014
  }
@@ -6006,7 +6040,7 @@ async function getRepositoryTree(options) {
6006
6040
  queryParams.append("page_token", options.page_token);
6007
6041
  if (options.pagination)
6008
6042
  queryParams.append("pagination", options.pagination);
6009
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, { ...getFetchConfig() });
6043
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, { ...getFetchConfig() });
6010
6044
  if (response.status === 404) {
6011
6045
  throw new Error("Repository or path not found");
6012
6046
  }
@@ -6028,7 +6062,7 @@ async function getRepositoryTree(options) {
6028
6062
  */
6029
6063
  async function listProjectMilestones(projectId, options) {
6030
6064
  projectId = decodeURIComponent(projectId);
6031
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
6065
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones`);
6032
6066
  Object.entries(options).forEach(([key, value]) => {
6033
6067
  // Drop blank entries inside arrays so they behave like the scalar blank guard below
6034
6068
  const normalized = dropBlankArrayEntries(value);
@@ -6059,7 +6093,7 @@ async function listProjectMilestones(projectId, options) {
6059
6093
  */
6060
6094
  async function getProjectMilestone(projectId, milestoneId) {
6061
6095
  projectId = decodeURIComponent(projectId);
6062
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6096
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6063
6097
  const response = await fetch(url.toString(), {
6064
6098
  ...getFetchConfig(),
6065
6099
  });
@@ -6075,7 +6109,7 @@ async function getProjectMilestone(projectId, milestoneId) {
6075
6109
  */
6076
6110
  async function createProjectMilestone(projectId, options) {
6077
6111
  projectId = decodeURIComponent(projectId);
6078
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
6112
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones`);
6079
6113
  const response = await fetch(url.toString(), {
6080
6114
  ...getFetchConfig(),
6081
6115
  method: "POST",
@@ -6094,7 +6128,7 @@ async function createProjectMilestone(projectId, options) {
6094
6128
  */
6095
6129
  async function editProjectMilestone(projectId, milestoneId, options) {
6096
6130
  projectId = decodeURIComponent(projectId);
6097
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6131
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6098
6132
  const response = await fetch(url.toString(), {
6099
6133
  ...getFetchConfig(),
6100
6134
  method: "PUT",
@@ -6112,7 +6146,7 @@ async function editProjectMilestone(projectId, milestoneId, options) {
6112
6146
  */
6113
6147
  async function deleteProjectMilestone(projectId, milestoneId) {
6114
6148
  projectId = decodeURIComponent(projectId);
6115
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6149
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6116
6150
  const response = await fetch(url.toString(), {
6117
6151
  ...getFetchConfig(),
6118
6152
  method: "DELETE",
@@ -6127,7 +6161,7 @@ async function deleteProjectMilestone(projectId, milestoneId) {
6127
6161
  */
6128
6162
  async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6129
6163
  projectId = decodeURIComponent(projectId);
6130
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6164
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6131
6165
  appendFilterParams(url.searchParams, options);
6132
6166
  const response = await fetch(url.toString(), {
6133
6167
  ...getFetchConfig(),
@@ -6144,7 +6178,7 @@ async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6144
6178
  */
6145
6179
  async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
6146
6180
  projectId = decodeURIComponent(projectId);
6147
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6181
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6148
6182
  appendFilterParams(url.searchParams, options);
6149
6183
  const response = await fetch(url.toString(), {
6150
6184
  ...getFetchConfig(),
@@ -6161,7 +6195,7 @@ async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
6161
6195
  */
6162
6196
  async function promoteProjectMilestone(projectId, milestoneId) {
6163
6197
  projectId = decodeURIComponent(projectId);
6164
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/promote`);
6198
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/promote`);
6165
6199
  const response = await fetch(url.toString(), {
6166
6200
  ...getFetchConfig(),
6167
6201
  method: "POST",
@@ -6178,7 +6212,7 @@ async function promoteProjectMilestone(projectId, milestoneId) {
6178
6212
  */
6179
6213
  async function getMilestoneBurndownEvents(projectId, milestoneId, options = {}) {
6180
6214
  projectId = decodeURIComponent(projectId);
6181
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6215
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6182
6216
  appendFilterParams(url.searchParams, options);
6183
6217
  const response = await fetch(url.toString(), {
6184
6218
  ...getFetchConfig(),
@@ -6192,7 +6226,7 @@ async function getMilestoneBurndownEvents(projectId, milestoneId, options = {})
6192
6226
  */
6193
6227
  async function listGroupMilestones(groupId, options) {
6194
6228
  groupId = decodeURIComponent(groupId);
6195
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
6229
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones`);
6196
6230
  Object.entries(options).forEach(([key, value]) => {
6197
6231
  // Drop blank entries inside arrays so they behave like the scalar blank guard below
6198
6232
  const normalized = dropBlankArrayEntries(value);
@@ -6220,7 +6254,7 @@ async function listGroupMilestones(groupId, options) {
6220
6254
  */
6221
6255
  async function getGroupMilestone(groupId, milestoneId) {
6222
6256
  groupId = decodeURIComponent(groupId);
6223
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6257
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6224
6258
  const response = await fetch(url.toString(), {
6225
6259
  ...getFetchConfig(),
6226
6260
  });
@@ -6233,7 +6267,7 @@ async function getGroupMilestone(groupId, milestoneId) {
6233
6267
  */
6234
6268
  async function createGroupMilestone(groupId, options) {
6235
6269
  groupId = decodeURIComponent(groupId);
6236
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
6270
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones`);
6237
6271
  const response = await fetch(url.toString(), {
6238
6272
  ...getFetchConfig(),
6239
6273
  method: "POST",
@@ -6248,7 +6282,7 @@ async function createGroupMilestone(groupId, options) {
6248
6282
  */
6249
6283
  async function editGroupMilestone(groupId, milestoneId, options) {
6250
6284
  groupId = decodeURIComponent(groupId);
6251
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6285
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6252
6286
  const response = await fetch(url.toString(), {
6253
6287
  ...getFetchConfig(),
6254
6288
  method: "PUT",
@@ -6263,7 +6297,7 @@ async function editGroupMilestone(groupId, milestoneId, options) {
6263
6297
  */
6264
6298
  async function deleteGroupMilestone(groupId, milestoneId) {
6265
6299
  groupId = decodeURIComponent(groupId);
6266
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6300
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6267
6301
  const response = await fetch(url.toString(), {
6268
6302
  ...getFetchConfig(),
6269
6303
  method: "DELETE",
@@ -6275,7 +6309,7 @@ async function deleteGroupMilestone(groupId, milestoneId) {
6275
6309
  */
6276
6310
  async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6277
6311
  groupId = decodeURIComponent(groupId);
6278
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6312
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6279
6313
  appendFilterParams(url.searchParams, options);
6280
6314
  const response = await fetch(url.toString(), {
6281
6315
  ...getFetchConfig(),
@@ -6289,7 +6323,7 @@ async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6289
6323
  */
6290
6324
  async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}) {
6291
6325
  groupId = decodeURIComponent(groupId);
6292
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6326
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6293
6327
  appendFilterParams(url.searchParams, options);
6294
6328
  const response = await fetch(url.toString(), {
6295
6329
  ...getFetchConfig(),
@@ -6303,7 +6337,7 @@ async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}
6303
6337
  */
6304
6338
  async function getGroupMilestoneBurndownEvents(groupId, milestoneId, options = {}) {
6305
6339
  groupId = decodeURIComponent(groupId);
6306
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6340
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6307
6341
  appendFilterParams(url.searchParams, options);
6308
6342
  const response = await fetch(url.toString(), {
6309
6343
  ...getFetchConfig(),
@@ -6374,7 +6408,7 @@ async function getUsers(usernames) {
6374
6408
  */
6375
6409
  async function listCommits(projectId, options = {}) {
6376
6410
  projectId = decodeURIComponent(projectId);
6377
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`);
6411
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits`);
6378
6412
  // Add query parameters
6379
6413
  if (options.ref_name)
6380
6414
  url.searchParams.append("ref_name", options.ref_name);
@@ -6418,7 +6452,7 @@ async function listCommits(projectId, options = {}) {
6418
6452
  */
6419
6453
  async function getCommit(projectId, sha, stats) {
6420
6454
  projectId = decodeURIComponent(projectId);
6421
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}`);
6455
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits/${encodeGitLabPathSegment(sha)}`);
6422
6456
  if (stats) {
6423
6457
  url.searchParams.append("stats", "true");
6424
6458
  }
@@ -6440,7 +6474,7 @@ async function getCommit(projectId, sha, stats) {
6440
6474
  */
6441
6475
  async function getCommitDiff(projectId, sha, full_diff) {
6442
6476
  projectId = decodeURIComponent(projectId);
6443
- const baseUrl = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/diff`;
6477
+ const baseUrl = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits/${encodeGitLabPathSegment(sha)}/diff`;
6444
6478
  let allDiffs = [];
6445
6479
  let page = 1;
6446
6480
  while (true) {
@@ -6479,7 +6513,7 @@ async function getCommitDiff(projectId, sha, full_diff) {
6479
6513
  */
6480
6514
  async function getFileBlame(projectId, options) {
6481
6515
  projectId = decodeURIComponent(projectId);
6482
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodeURIComponent(options.file_path)}/blame`);
6516
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/files/${encodeGitLabPathSegment(options.file_path)}/blame`);
6483
6517
  url.searchParams.append("ref", options.ref);
6484
6518
  if (options.range_start !== undefined && options.range_end !== undefined) {
6485
6519
  url.searchParams.append("range[start]", options.range_start.toString());
@@ -6502,7 +6536,7 @@ async function getFileBlame(projectId, options) {
6502
6536
  */
6503
6537
  async function listCommitStatuses(projectId, sha, options = {}) {
6504
6538
  projectId = decodeURIComponent(projectId);
6505
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/statuses`);
6539
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits/${encodeGitLabPathSegment(sha)}/statuses`);
6506
6540
  appendFilterParams(url.searchParams, options);
6507
6541
  const response = await fetch(url.toString(), {
6508
6542
  ...getFetchConfig(),
@@ -6524,7 +6558,7 @@ async function createCommitStatus(projectId, sha, options) {
6524
6558
  throw new Error("Use either name or context when creating a commit status, not both.");
6525
6559
  }
6526
6560
  projectId = decodeURIComponent(projectId);
6527
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/statuses/${encodeURIComponent(sha)}`);
6561
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/statuses/${encodeGitLabPathSegment(sha)}`);
6528
6562
  appendFilterParams(url.searchParams, options);
6529
6563
  const response = await fetch(url.toString(), {
6530
6564
  ...getFetchConfig(),
@@ -6614,7 +6648,7 @@ async function listProjectMembers(projectId, options = {}) {
6614
6648
  projectId = decodeURIComponent(projectId);
6615
6649
  const effectiveProjectId = getEffectiveProjectId(projectId);
6616
6650
  const membersPath = options.include_inheritance ? "members/all" : "members";
6617
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/${membersPath}`);
6651
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/${membersPath}`);
6618
6652
  // Add query parameters
6619
6653
  if (options.query)
6620
6654
  url.searchParams.append("query", options.query);
@@ -6644,7 +6678,7 @@ async function listProjectMembers(projectId, options = {}) {
6644
6678
  async function listGroupMembers(groupId, options = {}) {
6645
6679
  groupId = decodeURIComponent(groupId);
6646
6680
  const membersPath = options.include_inheritance ? "members/all" : "members";
6647
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/${membersPath}`);
6681
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/${membersPath}`);
6648
6682
  if (options.query)
6649
6683
  url.searchParams.append("query", options.query);
6650
6684
  if (options.user_ids) {
@@ -6671,7 +6705,7 @@ async function listGroupMembers(groupId, options = {}) {
6671
6705
  */
6672
6706
  async function listGroupIterations(groupId, options = {}) {
6673
6707
  groupId = decodeURIComponent(groupId);
6674
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/iterations`);
6708
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/iterations`);
6675
6709
  // クエリパラメータの追加
6676
6710
  if (options.state)
6677
6711
  url.searchParams.append("state", options.state);
@@ -6701,7 +6735,7 @@ async function listGroupIterations(groupId, options = {}) {
6701
6735
  // --- CI/CD Variables ---
6702
6736
  async function listProjectVariables(projectId, options = {}) {
6703
6737
  projectId = decodeURIComponent(projectId);
6704
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`);
6738
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables`);
6705
6739
  Object.entries(options).forEach(([key, value]) => {
6706
6740
  if (key === "filter" && typeof value === "object" && value !== null) {
6707
6741
  Object.entries(value).forEach(([filterKey, filterValue]) => {
@@ -6718,7 +6752,7 @@ async function listProjectVariables(projectId, options = {}) {
6718
6752
  }
6719
6753
  async function getProjectVariable(projectId, key, filter) {
6720
6754
  projectId = decodeURIComponent(projectId);
6721
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
6755
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables/${encodeGitLabPathSegment(key)}`);
6722
6756
  if (filter?.environment_scope) {
6723
6757
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6724
6758
  }
@@ -6729,7 +6763,7 @@ async function getProjectVariable(projectId, key, filter) {
6729
6763
  }
6730
6764
  async function createProjectVariable(projectId, options) {
6731
6765
  projectId = decodeURIComponent(projectId);
6732
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`, { ...getFetchConfig(), method: "POST", body: JSON.stringify(options) });
6766
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables`, { ...getFetchConfig(), method: "POST", body: JSON.stringify(options) });
6733
6767
  await handleGitLabError(response);
6734
6768
  const data = await response.json();
6735
6769
  return GitLabCiVariableSchema.parse(data);
@@ -6737,7 +6771,7 @@ async function createProjectVariable(projectId, options) {
6737
6771
  async function updateProjectVariable(projectId, key, options) {
6738
6772
  projectId = decodeURIComponent(projectId);
6739
6773
  const { filter, ...body } = options;
6740
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
6774
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables/${encodeGitLabPathSegment(key)}`);
6741
6775
  if (filter?.environment_scope) {
6742
6776
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6743
6777
  }
@@ -6752,7 +6786,7 @@ async function updateProjectVariable(projectId, key, options) {
6752
6786
  }
6753
6787
  async function deleteProjectVariable(projectId, key, filter) {
6754
6788
  projectId = decodeURIComponent(projectId);
6755
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
6789
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables/${encodeGitLabPathSegment(key)}`);
6756
6790
  if (filter?.environment_scope) {
6757
6791
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6758
6792
  }
@@ -6760,7 +6794,7 @@ async function deleteProjectVariable(projectId, key, filter) {
6760
6794
  await handleGitLabError(response);
6761
6795
  }
6762
6796
  async function listGroupVariables(groupId, options = {}) {
6763
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6797
+ const encoded = encodeGitLabPathSegment(groupId);
6764
6798
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables`);
6765
6799
  Object.entries(options).forEach(([key, value]) => {
6766
6800
  if (key === "filter" && typeof value === "object" && value !== null) {
@@ -6777,8 +6811,8 @@ async function listGroupVariables(groupId, options = {}) {
6777
6811
  return z.array(GitLabCiVariableSchema).parse(data);
6778
6812
  }
6779
6813
  async function getGroupVariable(groupId, key, filter) {
6780
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6781
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
6814
+ const encoded = encodeGitLabPathSegment(groupId);
6815
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeGitLabPathSegment(key)}`);
6782
6816
  if (filter?.environment_scope) {
6783
6817
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6784
6818
  }
@@ -6788,7 +6822,7 @@ async function getGroupVariable(groupId, key, filter) {
6788
6822
  return GitLabCiVariableSchema.parse(data);
6789
6823
  }
6790
6824
  async function createGroupVariable(groupId, options) {
6791
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6825
+ const encoded = encodeGitLabPathSegment(groupId);
6792
6826
  const response = await fetch(`${getEffectiveApiUrl()}/groups/${encoded}/variables`, {
6793
6827
  ...getFetchConfig(),
6794
6828
  method: "POST",
@@ -6799,9 +6833,9 @@ async function createGroupVariable(groupId, options) {
6799
6833
  return GitLabCiVariableSchema.parse(data);
6800
6834
  }
6801
6835
  async function updateGroupVariable(groupId, key, options) {
6802
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6836
+ const encoded = encodeGitLabPathSegment(groupId);
6803
6837
  const { filter, ...body } = options;
6804
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
6838
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeGitLabPathSegment(key)}`);
6805
6839
  if (filter?.environment_scope) {
6806
6840
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6807
6841
  }
@@ -6815,8 +6849,8 @@ async function updateGroupVariable(groupId, key, options) {
6815
6849
  return GitLabCiVariableSchema.parse(data);
6816
6850
  }
6817
6851
  async function deleteGroupVariable(groupId, key, filter) {
6818
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6819
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
6852
+ const encoded = encodeGitLabPathSegment(groupId);
6853
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeGitLabPathSegment(key)}`);
6820
6854
  if (filter?.environment_scope) {
6821
6855
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6822
6856
  }
@@ -6902,7 +6936,7 @@ async function listDependencyProxyBlobs(groupPath, options = {}) {
6902
6936
  };
6903
6937
  }
6904
6938
  async function purgeDependencyProxyCache(groupId) {
6905
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6939
+ const encoded = encodeGitLabPathSegment(groupId);
6906
6940
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/dependency_proxy/cache`);
6907
6941
  const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
6908
6942
  await handleGitLabError(response);
@@ -6932,7 +6966,7 @@ async function resolveProjectFullPath(projectId) {
6932
6966
  if (!/^\d+$/.test(decoded)) {
6933
6967
  return decoded;
6934
6968
  }
6935
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(decoded)}`);
6969
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(decoded)}`);
6936
6970
  const response = await fetch(url.toString(), { ...getFetchConfig() });
6937
6971
  await handleGitLabError(response);
6938
6972
  const project = (await response.json());
@@ -7243,7 +7277,7 @@ async function markdownUpload(projectId, filePath, content, filename) {
7243
7277
  // Create form data
7244
7278
  const form = new FormData();
7245
7279
  form.append("file", new File([fileBuffer], fileName, { type: "application/octet-stream" }));
7246
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
7280
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/uploads`);
7247
7281
  const defaultFetchConfig = getFetchConfig();
7248
7282
  delete defaultFetchConfig.headers["Content-Type"];
7249
7283
  const response = await fetch(url.toString(), {
@@ -7284,11 +7318,8 @@ function getSafeAttachmentFilename(filename) {
7284
7318
  async function downloadAttachment(projectId, secret, filename, localPath) {
7285
7319
  const effectiveProjectId = getEffectiveProjectId(projectId);
7286
7320
  const safeFilename = getSafeAttachmentFilename(filename);
7287
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
7288
- const response = await fetch(url.toString(), {
7289
- ...getFetchConfig(),
7290
- method: "GET",
7291
- });
7321
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
7322
+ const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
7292
7323
  if (!response.ok) {
7293
7324
  await handleGitLabError(response);
7294
7325
  }
@@ -7332,7 +7363,7 @@ async function listEvents(options = {}) {
7332
7363
  */
7333
7364
  async function getProjectEvents(projectId, options = {}) {
7334
7365
  const effectiveProjectId = getEffectiveProjectId(projectId);
7335
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/events`);
7366
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/events`);
7336
7367
  // Add all query parameters
7337
7368
  appendFilterParams(url.searchParams, options);
7338
7369
  const response = await fetch(url.toString(), { ...getFetchConfig() });
@@ -7351,7 +7382,7 @@ async function getProjectEvents(projectId, options = {}) {
7351
7382
  */
7352
7383
  async function listReleases(projectId, options = {}) {
7353
7384
  const effectiveProjectId = getEffectiveProjectId(projectId);
7354
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases`);
7385
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases`);
7355
7386
  // Add query parameters
7356
7387
  appendFilterParams(url.searchParams, options);
7357
7388
  const response = await fetch(url.toString(), {
@@ -7371,7 +7402,7 @@ async function listReleases(projectId, options = {}) {
7371
7402
  */
7372
7403
  async function getRelease(projectId, tagName, includeHtmlDescription) {
7373
7404
  const effectiveProjectId = getEffectiveProjectId(projectId);
7374
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`);
7405
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`);
7375
7406
  if (includeHtmlDescription !== undefined) {
7376
7407
  url.searchParams.append("include_html_description", includeHtmlDescription.toString());
7377
7408
  }
@@ -7391,7 +7422,7 @@ async function getRelease(projectId, tagName, includeHtmlDescription) {
7391
7422
  */
7392
7423
  async function createRelease(projectId, options) {
7393
7424
  const effectiveProjectId = getEffectiveProjectId(projectId);
7394
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases`, {
7425
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases`, {
7395
7426
  ...getFetchConfig(),
7396
7427
  method: "POST",
7397
7428
  body: JSON.stringify(options),
@@ -7410,7 +7441,7 @@ async function createRelease(projectId, options) {
7410
7441
  */
7411
7442
  async function updateRelease(projectId, tagName, options) {
7412
7443
  const effectiveProjectId = getEffectiveProjectId(projectId);
7413
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7444
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7414
7445
  ...getFetchConfig(),
7415
7446
  method: "PUT",
7416
7447
  body: JSON.stringify(options),
@@ -7428,7 +7459,7 @@ async function updateRelease(projectId, tagName, options) {
7428
7459
  */
7429
7460
  async function deleteRelease(projectId, tagName) {
7430
7461
  const effectiveProjectId = getEffectiveProjectId(projectId);
7431
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7462
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7432
7463
  ...getFetchConfig(),
7433
7464
  method: "DELETE",
7434
7465
  });
@@ -7444,7 +7475,7 @@ async function deleteRelease(projectId, tagName) {
7444
7475
  */
7445
7476
  async function createReleaseEvidence(projectId, tagName) {
7446
7477
  const effectiveProjectId = getEffectiveProjectId(projectId);
7447
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/evidence`, {
7478
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/evidence`, {
7448
7479
  ...getFetchConfig(),
7449
7480
  method: "POST",
7450
7481
  });
@@ -7460,9 +7491,7 @@ async function createReleaseEvidence(projectId, tagName) {
7460
7491
  */
7461
7492
  async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
7462
7493
  const effectiveProjectId = getEffectiveProjectId(projectId);
7463
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/downloads/${encodeGitLabPath(directAssetPath)}`, {
7464
- ...getFetchConfig(),
7465
- });
7494
+ const response = await fetchWithValidatedRedirects(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/downloads/${encodeGitLabPath(directAssetPath)}`, downloadRedirectOptions());
7466
7495
  await handleGitLabError(response);
7467
7496
  return await response.text();
7468
7497
  }
@@ -7475,7 +7504,7 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
7475
7504
  */
7476
7505
  async function listTags(projectId, options = {}) {
7477
7506
  const effectiveProjectId = getEffectiveProjectId(projectId);
7478
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`);
7507
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags`);
7479
7508
  appendFilterParams(url.searchParams, options);
7480
7509
  const response = await fetch(url.toString(), {
7481
7510
  ...getFetchConfig(),
@@ -7493,7 +7522,7 @@ async function listTags(projectId, options = {}) {
7493
7522
  */
7494
7523
  async function getTag(projectId, tagName) {
7495
7524
  const effectiveProjectId = getEffectiveProjectId(projectId);
7496
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}`, {
7525
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags/${encodeGitLabPathSegment(tagName)}`, {
7497
7526
  ...getFetchConfig(),
7498
7527
  });
7499
7528
  await handleGitLabError(response);
@@ -7509,7 +7538,7 @@ async function getTag(projectId, tagName) {
7509
7538
  */
7510
7539
  async function createTag(projectId, options) {
7511
7540
  const effectiveProjectId = getEffectiveProjectId(projectId);
7512
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`, {
7541
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags`, {
7513
7542
  ...getFetchConfig(),
7514
7543
  method: "POST",
7515
7544
  body: JSON.stringify(options),
@@ -7526,7 +7555,7 @@ async function createTag(projectId, options) {
7526
7555
  */
7527
7556
  async function deleteTag(projectId, tagName) {
7528
7557
  const effectiveProjectId = getEffectiveProjectId(projectId);
7529
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}`, {
7558
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags/${encodeGitLabPathSegment(tagName)}`, {
7530
7559
  ...getFetchConfig(),
7531
7560
  method: "DELETE",
7532
7561
  });
@@ -7541,7 +7570,7 @@ async function deleteTag(projectId, tagName) {
7541
7570
  */
7542
7571
  async function getTagSignature(projectId, tagName) {
7543
7572
  const effectiveProjectId = getEffectiveProjectId(projectId);
7544
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}/signature`, {
7573
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags/${encodeGitLabPathSegment(tagName)}/signature`, {
7545
7574
  ...getFetchConfig(),
7546
7575
  });
7547
7576
  await handleGitLabError(response);
@@ -7605,7 +7634,7 @@ async function handleToolCall(params) {
7605
7634
  if (!isToolAllowedByPermissionMode(params.name)) {
7606
7635
  throw new Error(GITLAB_PERMISSION_MODE === "readonly"
7607
7636
  ? `${params.name} is not allowed in read-only mode`
7608
- : `${params.name} is not allowed in modify mode (delete operations are disabled)`);
7637
+ : `${params.name} is not allowed in modify mode (delete and teardown operations are disabled)`);
7609
7638
  }
7610
7639
  logger.info({ tool: params.name, event: "tool_call_start" }, `tool_call_start: ${params.name}`);
7611
7640
  switch (params.name) {
@@ -7618,7 +7647,7 @@ async function handleToolCall(params) {
7618
7647
  }
7619
7648
  if (GITLAB_PERMISSION_MODE === "modify" &&
7620
7649
  graphqlQueryContainsDeleteOperation(args.query)) {
7621
- throw new Error("execute_graphql does not allow delete mutations in modify mode");
7650
+ throw new Error("execute_graphql does not allow destructive mutations in modify mode");
7622
7651
  }
7623
7652
  const apiUrl = new URL(getEffectiveApiUrl());
7624
7653
  // Build GraphQL endpoint preserving any instance subpath (e.g. /gitlab)
@@ -8208,7 +8237,7 @@ async function handleToolCall(params) {
8208
8237
  }
8209
8238
  case "get_namespace": {
8210
8239
  const args = GetNamespaceSchema.parse(params.arguments);
8211
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`);
8240
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeGitLabPathSegment(args.namespace_id)}`);
8212
8241
  const response = await fetch(url.toString(), {
8213
8242
  ...getFetchConfig(),
8214
8243
  });
@@ -8221,7 +8250,7 @@ async function handleToolCall(params) {
8221
8250
  }
8222
8251
  case "verify_namespace": {
8223
8252
  const args = VerifyNamespaceSchema.parse(params.arguments);
8224
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
8253
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeGitLabPathSegment(args.path)}/exists`);
8225
8254
  if (args.parent_id !== undefined)
8226
8255
  url.searchParams.set("parent_id", String(args.parent_id));
8227
8256
  const response = await fetch(url.toString(), {
@@ -8238,7 +8267,7 @@ async function handleToolCall(params) {
8238
8267
  const args = GetProjectSchema.parse(params.arguments);
8239
8268
  const options = params.arguments;
8240
8269
  const effectiveProjectId = getEffectiveProjectId(args.project_id);
8241
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
8270
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
8242
8271
  if (options.license)
8243
8272
  url.searchParams.append("license", "true");
8244
8273
  if (options.statistics)
@@ -8267,7 +8296,7 @@ async function handleToolCall(params) {
8267
8296
  const { project_id, ...updates } = UpdateProjectSchema.parse(params.arguments);
8268
8297
  const effectiveProjectId = getEffectiveProjectId(project_id);
8269
8298
  const body = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== undefined));
8270
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`, {
8299
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`, {
8271
8300
  ...getFetchConfig(),
8272
8301
  method: "PUT",
8273
8302
  body: JSON.stringify(body),
@@ -8306,7 +8335,7 @@ async function handleToolCall(params) {
8306
8335
  }
8307
8336
  case "get_user": {
8308
8337
  const args = GetUserSchema.parse(params.arguments);
8309
- const url = new URL(`${getEffectiveApiUrl()}/users/${encodeURIComponent(args.user_id)}`);
8338
+ const url = new URL(`${getEffectiveApiUrl()}/users/${encodeGitLabPathSegment(args.user_id)}`);
8310
8339
  const response = await fetch(url.toString(), {
8311
8340
  ...getFetchConfig(),
8312
8341
  });
@@ -10186,7 +10215,7 @@ async function handleToolCall(params) {
10186
10215
  case "get_branch": {
10187
10216
  const args = GetBranchSchema.parse(params.arguments);
10188
10217
  const projectId = decodeURIComponent(args.project_id);
10189
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches/${encodeURIComponent(args.branch_name)}`);
10218
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/branches/${encodeGitLabPathSegment(args.branch_name)}`);
10190
10219
  const response = await fetch(url.toString(), {
10191
10220
  ...getFetchConfig(),
10192
10221
  });
@@ -10200,7 +10229,7 @@ async function handleToolCall(params) {
10200
10229
  case "list_branches": {
10201
10230
  const args = ListBranchesSchema.parse(params.arguments);
10202
10231
  const projectId = decodeURIComponent(args.project_id);
10203
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches`);
10232
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/branches`);
10204
10233
  if (args.search) {
10205
10234
  url.searchParams.append("search", args.search);
10206
10235
  }
@@ -10223,7 +10252,7 @@ async function handleToolCall(params) {
10223
10252
  case "delete_branch": {
10224
10253
  const args = DeleteBranchSchema.parse(params.arguments);
10225
10254
  const projectId = decodeURIComponent(args.project_id);
10226
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches/${encodeURIComponent(args.branch_name)}`);
10255
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/branches/${encodeGitLabPathSegment(args.branch_name)}`);
10227
10256
  const response = await fetch(url.toString(), {
10228
10257
  ...getFetchConfig(),
10229
10258
  method: "DELETE",
@@ -10237,7 +10266,7 @@ async function handleToolCall(params) {
10237
10266
  const args = ListProtectedBranchesSchema.parse(params.arguments);
10238
10267
  const projectId = decodeURIComponent(args.project_id);
10239
10268
  const effectiveProjectId = getEffectiveProjectId(projectId);
10240
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches`);
10269
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches`);
10241
10270
  if (args.search)
10242
10271
  url.searchParams.append("search", args.search);
10243
10272
  if (args.page)
@@ -10257,7 +10286,7 @@ async function handleToolCall(params) {
10257
10286
  const args = GetProtectedBranchSchema.parse(params.arguments);
10258
10287
  const projectId = decodeURIComponent(args.project_id);
10259
10288
  const effectiveProjectId = getEffectiveProjectId(projectId);
10260
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches/${encodeURIComponent(args.branch_name)}`);
10289
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches/${encodeGitLabPathSegment(args.branch_name)}`);
10261
10290
  const response = await fetch(url.toString(), {
10262
10291
  ...getFetchConfig(),
10263
10292
  });
@@ -10271,7 +10300,7 @@ async function handleToolCall(params) {
10271
10300
  const args = ProtectBranchSchema.parse(params.arguments);
10272
10301
  const projectId = decodeURIComponent(args.project_id);
10273
10302
  const effectiveProjectId = getEffectiveProjectId(projectId);
10274
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches`);
10303
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches`);
10275
10304
  const body = { name: args.branch_name };
10276
10305
  if (args.push_access_level !== undefined)
10277
10306
  body.push_access_level = args.push_access_level;
@@ -10298,7 +10327,7 @@ async function handleToolCall(params) {
10298
10327
  const args = UnprotectBranchSchema.parse(params.arguments);
10299
10328
  const projectId = decodeURIComponent(args.project_id);
10300
10329
  const effectiveProjectId = getEffectiveProjectId(projectId);
10301
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches/${encodeURIComponent(args.branch_name)}`);
10330
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches/${encodeGitLabPathSegment(args.branch_name)}`);
10302
10331
  const response = await fetch(url.toString(), {
10303
10332
  ...getFetchConfig(),
10304
10333
  method: "DELETE",
@@ -10312,7 +10341,7 @@ async function handleToolCall(params) {
10312
10341
  const args = UpdateDefaultBranchSchema.parse(params.arguments);
10313
10342
  const projectId = decodeURIComponent(args.project_id);
10314
10343
  const effectiveProjectId = getEffectiveProjectId(projectId);
10315
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
10344
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
10316
10345
  const response = await fetch(url.toString(), {
10317
10346
  ...getFetchConfig(),
10318
10347
  method: "PUT",
@@ -10369,6 +10398,7 @@ function buildDownloadProxyDeps() {
10369
10398
  getDispatcherForUrl: clientPool.getDispatcherForUrl.bind(clientPool),
10370
10399
  fetch: undiciFetch,
10371
10400
  logger,
10401
+ isTrustedRedirectHost: isTrustedGitLabRedirectHost,
10372
10402
  };
10373
10403
  }
10374
10404
  /**
@@ -10430,9 +10460,68 @@ async function startSSEServer() {
10430
10460
  }
10431
10461
  next();
10432
10462
  };
10433
- const transports = {};
10463
+ // Session IDs are untrusted map keys (POST /messages?sessionId=). A null
10464
+ // prototype prevents inherited names such as "constructor" from masquerading
10465
+ // as live transports, matching the Streamable HTTP transport.
10466
+ const transports = Object.create(null);
10467
+ const sessionLastActivity = new Map();
10434
10468
  let shuttingDown = false;
10435
- app.get("/sse", rejectDnsRebinding, requireSseAuth, async (req, res) => {
10469
+ // Mirror the Streamable HTTP controls so the SSE transport cannot be used to
10470
+ // exhaust the process: a session cap, a per-IP creation rate limit and an idle
10471
+ // timeout that closes connections which never send anything.
10472
+ const maxSessions = Number.parseInt(process.env.MAX_SESSIONS || "1000", 10);
10473
+ const maxSseConnectionsPerMinute = Number.parseInt(process.env.MAX_REQUESTS_PER_MINUTE || "60", 10);
10474
+ const idleTimeoutMs = Math.max(SESSION_TIMEOUT_SECONDS, 1) * 1000;
10475
+ const sseConnectionRateLimit = rateLimit({
10476
+ windowMs: 60_000,
10477
+ limit: maxSseConnectionsPerMinute,
10478
+ keyGenerator: (req) => ipKeyGenerator(normalizeProxyClientIpForRateLimit(req.ip ?? "")),
10479
+ standardHeaders: true,
10480
+ legacyHeaders: false,
10481
+ handler: (_req, res) => {
10482
+ res.status(429).json({
10483
+ error: "Rate limit exceeded",
10484
+ message: `Maximum ${maxSseConnectionsPerMinute} SSE connections per minute allowed`,
10485
+ });
10486
+ },
10487
+ });
10488
+ /**
10489
+ * Reclaim SSE sessions that have not received a POST /messages request within
10490
+ * the idle timeout.
10491
+ *
10492
+ * On /sse it is correct to idle-close an open GET: the stream alone is not
10493
+ * proof of a live client, so a client that connects and never sends anything
10494
+ * would otherwise hold a MAX_SESSIONS slot forever. This intentionally
10495
+ * differs from the Streamable HTTP GET /mcp handler, which pauses inactivity
10496
+ * expiry while its stream is open so list_changed can still be pushed - do
10497
+ * not copy that rule here.
10498
+ */
10499
+ const closeIdleSseSessions = () => {
10500
+ const now = Date.now();
10501
+ for (const [sessionId, lastActivity] of sessionLastActivity) {
10502
+ if (now - lastActivity < idleTimeoutMs)
10503
+ continue;
10504
+ const transport = transports[sessionId];
10505
+ delete transports[sessionId];
10506
+ sessionLastActivity.delete(sessionId);
10507
+ logger.info({ sessionId, event: "sse_session_idle_closed" }, `SSE session ${sessionId} closed after ${SESSION_TIMEOUT_SECONDS}s of inactivity`);
10508
+ if (transport) {
10509
+ void transport.close().catch(error => {
10510
+ logger.error({ err: error }, "Error closing idle SSE transport");
10511
+ });
10512
+ }
10513
+ }
10514
+ };
10515
+ const idleSweep = setInterval(closeIdleSseSessions, Math.min(idleTimeoutMs, 60_000));
10516
+ idleSweep.unref();
10517
+ app.get("/sse", rejectDnsRebinding, requireSseAuth, sseConnectionRateLimit, async (req, res) => {
10518
+ if (Object.keys(transports).length >= maxSessions) {
10519
+ res.status(503).json({
10520
+ error: "Server capacity reached",
10521
+ message: `Maximum ${maxSessions} concurrent sessions allowed. Please try again later.`,
10522
+ });
10523
+ return;
10524
+ }
10436
10525
  const serverInstance = createServer();
10437
10526
  const effectiveHosts = getEffectiveAllowedHosts(req);
10438
10527
  const transport = new SSEServerTransport("/messages", res, {
@@ -10440,16 +10529,38 @@ async function startSSEServer() {
10440
10529
  allowedHosts: [...effectiveHosts],
10441
10530
  allowedOrigins: [...effectiveHosts].flatMap(host => [`http://${host}`, `https://${host}`]),
10442
10531
  });
10443
- transports[transport.sessionId] = transport;
10532
+ const sessionId = transport.sessionId;
10533
+ transports[sessionId] = transport;
10534
+ sessionLastActivity.set(sessionId, Date.now());
10444
10535
  res.on("close", () => {
10445
- delete transports[transport.sessionId];
10536
+ delete transports[sessionId];
10537
+ sessionLastActivity.delete(sessionId);
10446
10538
  });
10447
- await serverInstance.connect(transport);
10539
+ try {
10540
+ await serverInstance.connect(transport);
10541
+ }
10542
+ catch (error) {
10543
+ logger.error({ err: error, sessionId }, "SSE connection error");
10544
+ // Release the capacity slot immediately instead of waiting for the
10545
+ // client to disconnect or the idle sweep to run.
10546
+ delete transports[sessionId];
10547
+ sessionLastActivity.delete(sessionId);
10548
+ if (!res.headersSent) {
10549
+ res.status(500).json({
10550
+ error: "Internal server error",
10551
+ message: error instanceof Error ? error.message : "Unknown error",
10552
+ });
10553
+ }
10554
+ else {
10555
+ res.end();
10556
+ }
10557
+ }
10448
10558
  });
10449
10559
  app.post("/messages", rejectDnsRebinding, requireSseAuth, async (req, res) => {
10450
10560
  const sessionId = req.query.sessionId;
10451
10561
  const transport = transports[sessionId];
10452
10562
  if (transport) {
10563
+ sessionLastActivity.set(sessionId, Date.now());
10453
10564
  await transport.handlePostMessage(req, res);
10454
10565
  }
10455
10566
  else {
@@ -10458,10 +10569,18 @@ async function startSSEServer() {
10458
10569
  });
10459
10570
  registerDownloadProxy(app, buildDownloadProxyDeps());
10460
10571
  app.get("/health", (_, res) => {
10461
- res.status(200).json({
10462
- status: "healthy",
10572
+ const activeSessions = Object.keys(transports).length;
10573
+ const isHealthy = activeSessions < maxSessions;
10574
+ if (!isHealthy) {
10575
+ logger.warn({ activeSessions, maxSessions }, "Health check degraded: active SSE session capacity reached");
10576
+ }
10577
+ res.status(isHealthy ? 200 : 503).json({
10578
+ status: isHealthy ? "healthy" : "degraded",
10463
10579
  version: SERVER_VERSION,
10464
10580
  transport: TransportMode.SSE,
10581
+ activeSessions,
10582
+ maxSessions,
10583
+ uptime: process.uptime(),
10465
10584
  });
10466
10585
  });
10467
10586
  const httpServer = app.listen(Number(PORT), HOST, () => {
@@ -10473,6 +10592,7 @@ async function startSSEServer() {
10473
10592
  return;
10474
10593
  shuttingDown = true;
10475
10594
  logger.info(`${signal} received, shutting down SSE server...`);
10595
+ clearInterval(idleSweep);
10476
10596
  httpServer.close(() => logger.info("SSE HTTP server closed"));
10477
10597
  await Promise.allSettled(Object.values(transports).map(async (transport) => {
10478
10598
  try {