@zereight/mcp-gitlab 2.1.63 → 2.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.ko.md +20 -5
  2. package/README.md +19 -6
  3. package/README.zh-CN.md +19 -6
  4. package/build/downloads/proxy.js +14 -7
  5. package/build/index.js +413 -455
  6. package/build/schemas.js +2 -2
  7. package/build/scripts/generate-tool-coverage.js +58 -0
  8. package/build/scripts/generate-tool-docs.js +6 -1
  9. package/build/scripts/tool-coverage/coverage.js +339 -0
  10. package/build/scripts/tool-coverage/coverage.test.js +142 -0
  11. package/build/test/path-segment-encoding.test.js +73 -17
  12. package/build/test/sse-session-limits.test.js +273 -0
  13. package/build/test/test-blank-filters.js +156 -0
  14. package/build/test/test-download-attachment.js +11 -0
  15. package/build/test/test-empty-toolsets.js +535 -0
  16. package/build/test/test-geteffectiveprojectid.js +1 -1
  17. package/build/test/test-job-artifacts.js +22 -0
  18. package/build/test/test-list-issues.js +47 -0
  19. package/build/test/test-list-merge-requests.js +76 -0
  20. package/build/test/test-list-milestones.js +145 -0
  21. package/build/test/test-permission-mode.js +103 -12
  22. package/build/test/test-remote-downloads.js +14 -1
  23. package/build/test/utils/graphql-query.test.js +72 -0
  24. package/build/test/utils/safe-redirect-fetch.test.js +463 -0
  25. package/build/test/utils/tool-args.test.js +89 -1
  26. package/build/test/utils/url.test.js +128 -0
  27. package/build/test-note.js +2 -1
  28. package/build/test-resolve-issue-note.js +4 -3
  29. package/build/tools/registry.js +16 -2
  30. package/build/utils/graphql-query.js +31 -9
  31. package/build/utils/safe-redirect-fetch.js +321 -0
  32. package/build/utils/tool-args.js +47 -3
  33. package/build/utils/url.js +102 -0
  34. package/package.json +2 -1
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,10 @@ 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
- import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
104
+ import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
104
105
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
105
106
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
106
107
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -829,7 +830,7 @@ async function ensureValidOAuthToken() {
829
830
  throw error;
830
831
  }
831
832
  }
832
- // Permission mode gate: readonly exposes only read tools; modify blocks delete tools; full allows all
833
+ // Permission mode gate: readonly exposes only read tools; modify blocks delete/teardown tools; full allows all
833
834
  function isToolAllowedByPermissionMode(toolName) {
834
835
  switch (GITLAB_PERMISSION_MODE) {
835
836
  case "readonly":
@@ -1165,6 +1166,21 @@ const getFetchConfig = () => {
1165
1166
  dispatcher,
1166
1167
  };
1167
1168
  };
1169
+ /**
1170
+ * Options for the download sinks that stream a file through `fetchWithValidatedRedirects`.
1171
+ *
1172
+ * GitLab answers a release asset, a job artifact or an uploaded file with a redirect to
1173
+ * its object storage, so those hops leave the origin and must be validated. The client
1174
+ * split matters as much as the headers: the wrapped client adds the cookie jar and a
1175
+ * fresh `Authorization` on a `401`, so a hop whose credentials were withheld has to use
1176
+ * plain undici instead.
1177
+ */
1178
+ const downloadRedirectOptions = () => ({
1179
+ ...getFetchConfig(),
1180
+ fetchImpl: fetch,
1181
+ unauthenticatedFetchImpl: undiciFetch,
1182
+ isTrustedRedirectHost: isTrustedGitLabRedirectHost,
1183
+ });
1168
1184
  // Compute at startup
1169
1185
  const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
1170
1186
  const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
@@ -1195,23 +1211,6 @@ function parseAllowedGitLabApiUrls(value) {
1195
1211
  .map(toAllowedGitLabApiUrl)
1196
1212
  .filter((entry) => Boolean(entry));
1197
1213
  }
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
1214
  function resolveTrustedGitLabApiUrl(value) {
1216
1215
  const parsed = new URL(normalizeGitLabApiUrl(value));
1217
1216
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
@@ -1238,6 +1237,17 @@ for (const { host, apiUrl } of [
1238
1237
  GITLAB_ALLOWED_API_URLS_BY_HOST.set(host, apiUrl);
1239
1238
  }
1240
1239
  }
1240
+ /**
1241
+ * Redirects from the GitLab API to operator-declared GitLab hosts are followed even
1242
+ * when the host resolves to a private address (self-hosted instances and their
1243
+ * storage may live on an internal network), and those hosts keep receiving the
1244
+ * request credentials. Every other redirect target must resolve to a public
1245
+ * address and is fetched without credentials.
1246
+ *
1247
+ * The lookup key is `URL.host`, which keeps a non-default port, matching how this
1248
+ * map is built from GITLAB_API_URL / GITLAB_ALLOWED_HOSTS.
1249
+ */
1250
+ const isTrustedGitLabRedirectHost = (host) => GITLAB_ALLOWED_API_URLS_BY_HOST.has(host);
1241
1251
  const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
1242
1252
  const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(",")
1243
1253
  .map(id => id.trim())
@@ -1364,7 +1374,7 @@ function rejectIfProjectScopedDeployment(toolName) {
1364
1374
  async function forkProject(projectId, namespace) {
1365
1375
  projectId = decodeURIComponent(projectId); // Decode project ID
1366
1376
  const effectiveProjectId = getEffectiveProjectId(projectId);
1367
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/fork`);
1377
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/fork`);
1368
1378
  if (namespace) {
1369
1379
  url.searchParams.append("namespace", namespace);
1370
1380
  }
@@ -1391,7 +1401,7 @@ async function forkProject(projectId, namespace) {
1391
1401
  async function createBranch(projectId, options) {
1392
1402
  projectId = decodeURIComponent(projectId); // Decode project ID
1393
1403
  const effectiveProjectId = getEffectiveProjectId(projectId);
1394
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/branches`);
1404
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/branches`);
1395
1405
  const response = await fetch(url.toString(), {
1396
1406
  ...getFetchConfig(),
1397
1407
  method: "POST",
@@ -1413,7 +1423,7 @@ async function createBranch(projectId, options) {
1413
1423
  async function getDefaultBranchRef(projectId) {
1414
1424
  projectId = decodeURIComponent(projectId); // Decode project ID
1415
1425
  const effectiveProjectId = getEffectiveProjectId(projectId);
1416
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
1426
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
1417
1427
  const response = await fetch(url.toString(), {
1418
1428
  ...getFetchConfig(),
1419
1429
  });
@@ -1433,12 +1443,12 @@ async function getDefaultBranchRef(projectId) {
1433
1443
  async function getFileContents(projectId, filePath, ref) {
1434
1444
  const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
1435
1445
  const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1436
- const encodedPath = encodeURIComponent(filePath);
1446
+ const encodedPath = encodeGitLabPathSegment(filePath);
1437
1447
  // Fall back to default branch if ref is not provided
1438
1448
  if (!ref) {
1439
1449
  ref = await getDefaultBranchRef(decodedProjectId);
1440
1450
  }
1441
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
1451
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/files/${encodedPath}`);
1442
1452
  url.searchParams.append("ref", ref);
1443
1453
  const response = await fetch(url.toString(), {
1444
1454
  ...getFetchConfig(),
@@ -1464,7 +1474,7 @@ async function getFileContents(projectId, filePath, ref) {
1464
1474
  async function createIssue(projectId, options) {
1465
1475
  projectId = decodeURIComponent(projectId); // Decode project ID
1466
1476
  const effectiveProjectId = getEffectiveProjectId(projectId);
1467
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
1477
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/issues`);
1468
1478
  // Build request body, converting labels array to comma-separated string
1469
1479
  const body = { ...options };
1470
1480
  if (body.labels && Array.isArray(body.labels)) {
@@ -1499,30 +1509,33 @@ async function listIssues(projectId, options = {}) {
1499
1509
  // resolve the default project (or fail for a multi-project allowlist)
1500
1510
  const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
1501
1511
  const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1502
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
1512
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/issues`);
1503
1513
  }
1504
1514
  else {
1505
1515
  url = new URL(`${getEffectiveApiUrl()}/issues`);
1506
1516
  }
1507
1517
  // Add all query parameters
1508
1518
  Object.entries(options).forEach(([key, value]) => {
1509
- if (value !== undefined) {
1510
- const keys = ["labels", "assignee_username"];
1511
- if (keys.includes(key)) {
1512
- if (Array.isArray(value)) {
1513
- // Handle array of labels
1514
- value.forEach(label => {
1515
- url.searchParams.append(`${key}[]`, label.toString());
1516
- });
1517
- }
1518
- else if (value) {
1519
- url.searchParams.append(`${key}[]`, value.toString());
1520
- }
1519
+ // Drop blank entries inside arrays so ["", "bug"] behaves like the scalar guard below
1520
+ const normalized = dropBlankArrayEntries(value);
1521
+ if (isBlankFilterValue(normalized)) {
1522
+ return;
1523
+ }
1524
+ const keys = ["labels", "assignee_username"];
1525
+ if (keys.includes(key)) {
1526
+ if (Array.isArray(normalized)) {
1527
+ // Handle array of labels
1528
+ normalized.forEach(label => {
1529
+ url.searchParams.append(`${key}[]`, String(label));
1530
+ });
1521
1531
  }
1522
1532
  else {
1523
- url.searchParams.append(key, String(value));
1533
+ url.searchParams.append(`${key}[]`, String(normalized));
1524
1534
  }
1525
1535
  }
1536
+ else {
1537
+ url.searchParams.append(key, String(normalized));
1538
+ }
1526
1539
  });
1527
1540
  const response = await fetch(url.toString(), {
1528
1541
  ...getFetchConfig(),
@@ -1533,11 +1546,7 @@ async function listIssues(projectId, options = {}) {
1533
1546
  }
1534
1547
  async function listTodos(options = {}) {
1535
1548
  const url = new URL(`${getEffectiveApiUrl()}/todos`);
1536
- Object.entries(options).forEach(([key, value]) => {
1537
- if (value !== undefined) {
1538
- url.searchParams.append(key, String(value));
1539
- }
1540
- });
1549
+ appendFilterParams(url.searchParams, options);
1541
1550
  const response = await fetch(url.toString(), {
1542
1551
  ...getFetchConfig(),
1543
1552
  });
@@ -1566,20 +1575,22 @@ async function markAllTodosDone() {
1566
1575
  */
1567
1576
  function appendMergeRequestFilters(url, options) {
1568
1577
  Object.entries(options).forEach(([key, value]) => {
1569
- if (value === undefined) {
1578
+ // Drop blank entries inside arrays so ["", "bug"] behaves like the scalar guard below
1579
+ const normalized = dropBlankArrayEntries(value);
1580
+ if (isBlankFilterValue(normalized)) {
1570
1581
  return;
1571
1582
  }
1572
- if (key === "labels" && Array.isArray(value)) {
1573
- url.searchParams.append(key, value.join(","));
1583
+ if (key === "labels" && Array.isArray(normalized)) {
1584
+ url.searchParams.append(key, normalized.join(","));
1574
1585
  }
1575
- else if (key === "approved_by_usernames" && Array.isArray(value)) {
1586
+ else if (key === "approved_by_usernames" && Array.isArray(normalized)) {
1576
1587
  // GitLab expects array-bracket form: approved_by_usernames[]=alice&approved_by_usernames[]=bob
1577
- for (const v of value) {
1588
+ for (const v of normalized) {
1578
1589
  url.searchParams.append(`${key}[]`, String(v));
1579
1590
  }
1580
1591
  }
1581
1592
  else {
1582
- url.searchParams.append(key, String(value));
1593
+ url.searchParams.append(key, String(normalized));
1583
1594
  }
1584
1595
  });
1585
1596
  }
@@ -1599,7 +1610,7 @@ async function listMergeRequests(projectId, options = {}) {
1599
1610
  // In strict scope, never fall back to the instance-wide endpoint;
1600
1611
  // resolve the default project (or fail for a multi-project allowlist)
1601
1612
  const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1602
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/merge_requests`);
1613
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/merge_requests`);
1603
1614
  }
1604
1615
  else {
1605
1616
  url = new URL(`${getEffectiveApiUrl()}/merge_requests`);
@@ -1621,7 +1632,7 @@ async function listMergeRequests(projectId, options = {}) {
1621
1632
  */
1622
1633
  async function listGroupMergeRequests(groupId, options = {}) {
1623
1634
  const decodedGroupId = decodeURIComponent(groupId);
1624
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(decodedGroupId)}/merge_requests`);
1635
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(decodedGroupId)}/merge_requests`);
1625
1636
  appendMergeRequestFilters(url, options);
1626
1637
  const response = await fetch(url.toString(), {
1627
1638
  ...getFetchConfig(),
@@ -1640,7 +1651,7 @@ async function listGroupMergeRequests(groupId, options = {}) {
1640
1651
  */
1641
1652
  async function getIssue(projectId, issueIid) {
1642
1653
  projectId = decodeURIComponent(projectId); // Decode project ID
1643
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1654
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1644
1655
  const response = await fetch(url.toString(), {
1645
1656
  ...getFetchConfig(),
1646
1657
  });
@@ -1659,7 +1670,7 @@ async function getIssue(projectId, issueIid) {
1659
1670
  */
1660
1671
  async function updateIssue(projectId, issueIid, options) {
1661
1672
  projectId = decodeURIComponent(projectId); // Decode project ID
1662
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1673
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1663
1674
  // Convert labels array to comma-separated string if present
1664
1675
  const body = { ...options };
1665
1676
  if (body.labels && Array.isArray(body.labels)) {
@@ -1684,7 +1695,7 @@ async function updateIssue(projectId, issueIid, options) {
1684
1695
  */
1685
1696
  async function deleteIssue(projectId, issueIid) {
1686
1697
  projectId = decodeURIComponent(projectId); // Decode project ID
1687
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1698
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1688
1699
  const response = await fetch(url.toString(), {
1689
1700
  ...getFetchConfig(),
1690
1701
  method: "DELETE",
@@ -2297,7 +2308,7 @@ async function resolveProjectOrGroupPath(projectId) {
2297
2308
  }
2298
2309
  const effectiveProjectId = getEffectiveProjectId(requestedProjectId);
2299
2310
  if (explicitKind === "group") {
2300
- const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
2311
+ const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(effectiveProjectId)}`);
2301
2312
  const groupResponse = await fetch(groupUrl.toString(), {
2302
2313
  ...getFetchConfig(),
2303
2314
  });
@@ -2305,7 +2316,7 @@ async function resolveProjectOrGroupPath(projectId) {
2305
2316
  const group = await groupResponse.json();
2306
2317
  return { path: group.full_path, kind: "group" };
2307
2318
  }
2308
- const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
2319
+ const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
2309
2320
  const projectResponse = await fetch(projectUrl.toString(), {
2310
2321
  ...getFetchConfig(),
2311
2322
  });
@@ -2317,7 +2328,7 @@ async function resolveProjectOrGroupPath(projectId) {
2317
2328
  !explicitKind &&
2318
2329
  getEffectiveAllowedProjectIds().length === 0 &&
2319
2330
  !/^\d+$/.test(effectiveProjectId)) {
2320
- const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
2331
+ const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(effectiveProjectId)}`);
2321
2332
  const groupResponse = await fetch(groupUrl.toString(), {
2322
2333
  ...getFetchConfig(),
2323
2334
  });
@@ -3058,7 +3069,7 @@ async function updateWorkItem(projectId, iid, options) {
3058
3069
  */
3059
3070
  async function listIssueLinks(projectId, issueIid) {
3060
3071
  projectId = decodeURIComponent(projectId); // Decode project ID
3061
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3072
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3062
3073
  const response = await fetch(url.toString(), {
3063
3074
  ...getFetchConfig(),
3064
3075
  });
@@ -3077,7 +3088,7 @@ async function listIssueLinks(projectId, issueIid) {
3077
3088
  */
3078
3089
  async function getIssueLink(projectId, issueIid, issueLinkId) {
3079
3090
  projectId = decodeURIComponent(projectId); // Decode project ID
3080
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3091
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3081
3092
  const response = await fetch(url.toString(), {
3082
3093
  ...getFetchConfig(),
3083
3094
  });
@@ -3099,7 +3110,7 @@ async function getIssueLink(projectId, issueIid, issueLinkId) {
3099
3110
  async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
3100
3111
  projectId = decodeURIComponent(projectId); // Decode project ID
3101
3112
  targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
3102
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3113
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
3103
3114
  const response = await fetch(url.toString(), {
3104
3115
  ...getFetchConfig(),
3105
3116
  method: "POST",
@@ -3124,7 +3135,7 @@ async function createIssueLink(projectId, issueIid, targetProjectId, targetIssue
3124
3135
  */
3125
3136
  async function deleteIssueLink(projectId, issueIid, issueLinkId) {
3126
3137
  projectId = decodeURIComponent(projectId); // Decode project ID
3127
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3138
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
3128
3139
  const response = await fetch(url.toString(), {
3129
3140
  ...getFetchConfig(),
3130
3141
  method: "DELETE",
@@ -3141,7 +3152,7 @@ async function deleteIssueLink(projectId, issueIid, issueLinkId) {
3141
3152
  */
3142
3153
  async function createMergeRequest(projectId, options) {
3143
3154
  projectId = decodeURIComponent(projectId); // Decode project ID
3144
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`);
3155
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests`);
3145
3156
  const response = await fetch(url.toString(), {
3146
3157
  ...getFetchConfig(),
3147
3158
  method: "POST",
@@ -3183,7 +3194,7 @@ async function createMergeRequest(projectId, options) {
3183
3194
  */
3184
3195
  async function listDiscussions(projectId, resourceType, resourceIid, options = {}) {
3185
3196
  projectId = decodeURIComponent(projectId); // Decode project ID
3186
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${resourceType}/${encodeGitLabPathSegment(resourceIid)}/discussions`);
3197
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/${resourceType}/${encodeGitLabPathSegment(resourceIid)}/discussions`);
3187
3198
  // Add query parameters for pagination and sorting
3188
3199
  if (options.page) {
3189
3200
  url.searchParams.append("page", options.page.toString());
@@ -3247,7 +3258,7 @@ async function listIssueDiscussions(projectId, issueIid, options = {}) {
3247
3258
  }
3248
3259
  async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId) {
3249
3260
  projectId = decodeURIComponent(projectId); // Decode project ID
3250
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3261
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3251
3262
  const response = await fetch(url.toString(), {
3252
3263
  ...getFetchConfig(),
3253
3264
  method: "DELETE",
@@ -3271,7 +3282,7 @@ async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3271
3282
  */
3272
3283
  async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
3273
3284
  projectId = decodeURIComponent(projectId); // Decode project ID
3274
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3285
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3275
3286
  // Only one of body or resolved can be sent according to GitLab API
3276
3287
  const payload = {};
3277
3288
  if (body !== undefined) {
@@ -3317,7 +3328,7 @@ async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3317
3328
  */
3318
3329
  async function updateIssueNote(projectId, issueIid, discussionId, noteId, body, resolved) {
3319
3330
  projectId = decodeURIComponent(projectId); // Decode project ID
3320
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3331
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3321
3332
  // Only one of body or resolved can be sent according to GitLab API
3322
3333
  const payload = {};
3323
3334
  if (body !== undefined) {
@@ -3346,7 +3357,7 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3346
3357
  */
3347
3358
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3348
3359
  projectId = decodeURIComponent(projectId); // Decode project ID
3349
- const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3360
+ const basePath = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3350
3361
  const url = new URL(discussionId ? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes` : `${basePath}/notes`);
3351
3362
  const payload = { body };
3352
3363
  if (createdAt) {
@@ -3374,7 +3385,7 @@ async function createIssueNote(projectId, issueIid, discussionId, body, createdA
3374
3385
  */
3375
3386
  async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
3376
3387
  projectId = decodeURIComponent(projectId); // Decode project ID
3377
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`);
3388
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`);
3378
3389
  const payload = { body };
3379
3390
  if (createdAt) {
3380
3391
  payload.created_at = createdAt;
@@ -3390,7 +3401,7 @@ async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3390
3401
  }
3391
3402
  async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3392
3403
  projectId = decodeURIComponent(projectId); // Decode project ID
3393
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3404
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3394
3405
  const payload = {
3395
3406
  id: projectId,
3396
3407
  merge_request_iid: mergeRequestIid,
@@ -3407,7 +3418,7 @@ async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3407
3418
  }
3408
3419
  async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3409
3420
  projectId = decodeURIComponent(projectId); // Decode project ID
3410
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3421
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3411
3422
  const response = await fetch(url.toString(), {
3412
3423
  ...getFetchConfig(),
3413
3424
  method: "DELETE",
@@ -3420,7 +3431,7 @@ async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3420
3431
  // --- Emoji Reactions (REST) ---
3421
3432
  function buildAwardEmojiPath(entity, projectId, entityIid, opts) {
3422
3433
  projectId = decodeURIComponent(projectId);
3423
- const pp = encodeURIComponent(getEffectiveProjectId(projectId));
3434
+ const pp = encodeGitLabPathSegment(getEffectiveProjectId(projectId));
3424
3435
  let path = `${getEffectiveApiUrl()}/projects/${pp}/${entity}/${encodeGitLabPathSegment(entityIid)}`;
3425
3436
  if (opts?.noteId) {
3426
3437
  path = opts.discussionId
@@ -3455,7 +3466,7 @@ async function deleteRestAwardEmoji(path) {
3455
3466
  }
3456
3467
  async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3457
3468
  projectId = decodeURIComponent(projectId); // Decode project ID
3458
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3469
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3459
3470
  const response = await fetch(url.toString(), {
3460
3471
  ...getFetchConfig(),
3461
3472
  method: "GET",
@@ -3466,7 +3477,7 @@ async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3466
3477
  }
3467
3478
  async function getMergeRequestDiscussion(projectId, mergeRequestIid, discussionId) {
3468
3479
  projectId = decodeURIComponent(projectId); // Decode project ID
3469
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
3480
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
3470
3481
  const response = await fetch(url.toString(), {
3471
3482
  ...getFetchConfig(),
3472
3483
  method: "GET",
@@ -3477,7 +3488,7 @@ async function getMergeRequestDiscussion(projectId, mergeRequestIid, discussionI
3477
3488
  }
3478
3489
  async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by, per_page, page) {
3479
3490
  projectId = decodeURIComponent(projectId); // Decode project ID
3480
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3491
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3481
3492
  if (sort) {
3482
3493
  url.searchParams.append("sort", sort);
3483
3494
  }
@@ -3500,7 +3511,7 @@ async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by,
3500
3511
  }
3501
3512
  async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body) {
3502
3513
  projectId = decodeURIComponent(projectId); // Decode project ID
3503
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3514
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3504
3515
  const payload = {
3505
3516
  id: projectId,
3506
3517
  merge_request_iid: mergeRequestIid,
@@ -3530,8 +3541,8 @@ async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body)
3530
3541
  */
3531
3542
  async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id, encoding) {
3532
3543
  projectId = decodeURIComponent(projectId); // Decode project ID
3533
- const encodedPath = encodeURIComponent(filePath);
3534
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
3544
+ const encodedPath = encodeGitLabPathSegment(filePath);
3545
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
3535
3546
  const resolvedEncoding = encoding ?? GITLAB_REPO_FILE_ENCODING;
3536
3547
  const body = {
3537
3548
  branch,
@@ -3602,7 +3613,7 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
3602
3613
  */
3603
3614
  async function createCommit(projectId, message, branch, actions) {
3604
3615
  projectId = decodeURIComponent(projectId); // Decode project ID
3605
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`);
3616
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits`);
3606
3617
  const response = await fetch(url.toString(), {
3607
3618
  ...getFetchConfig(),
3608
3619
  method: "POST",
@@ -3671,7 +3682,7 @@ async function searchBlobs(params) {
3671
3682
  let basePath;
3672
3683
  if (params.project_id) {
3673
3684
  const decodedProjectId = decodeURIComponent(params.project_id);
3674
- const projectId = encodeURIComponent(getEffectiveProjectId(decodedProjectId));
3685
+ const projectId = encodeGitLabPathSegment(getEffectiveProjectId(decodedProjectId));
3675
3686
  basePath = `${getEffectiveApiUrl()}/projects/${projectId}/search`;
3676
3687
  }
3677
3688
  else if (params.group_id) {
@@ -3749,11 +3760,11 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3749
3760
  projectId = decodeURIComponent(projectId); // Decode project ID
3750
3761
  let url;
3751
3762
  if (mergeRequestIid) {
3752
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3763
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3753
3764
  url.searchParams.append("include_diverged_commits_count", "true");
3754
3765
  }
3755
3766
  else if (branchName) {
3756
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
3767
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
3757
3768
  }
3758
3769
  else {
3759
3770
  throw new Error("Either mergeRequestIid or branchName must be provided");
@@ -3771,7 +3782,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3771
3782
  return GitLabMergeRequestSchema.parse(data);
3772
3783
  }
3773
3784
  async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3774
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/commits`);
3785
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/commits`);
3775
3786
  url.searchParams.append("per_page", "100");
3776
3787
  let totalCount = 0;
3777
3788
  let page = 1;
@@ -3799,12 +3810,8 @@ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3799
3810
  }
3800
3811
  async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
3801
3812
  projectId = decodeURIComponent(projectId);
3802
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3803
- Object.entries(options).forEach(([key, value]) => {
3804
- if (value !== undefined) {
3805
- url.searchParams.append(key, value.toString());
3806
- }
3807
- });
3813
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3814
+ appendFilterParams(url.searchParams, options);
3808
3815
  const response = await fetch(url.toString(), {
3809
3816
  ...getFetchConfig(),
3810
3817
  });
@@ -3813,7 +3820,7 @@ async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {
3813
3820
  return z.array(GitLabMergeRequestPipelineSchema).parse(data);
3814
3821
  }
3815
3822
  async function getProjectMergeMethod(projectId) {
3816
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
3823
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}`);
3817
3824
  const response = await fetch(url.toString(), {
3818
3825
  ...getFetchConfig(),
3819
3826
  });
@@ -4000,7 +4007,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
4000
4007
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4001
4008
  mergeRequestIid = mergeRequest.iid;
4002
4009
  }
4003
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4010
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4004
4011
  if (view) {
4005
4012
  url.searchParams.append("view", view);
4006
4013
  }
@@ -4030,7 +4037,7 @@ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, pag
4030
4037
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4031
4038
  mergeRequestIid = mergeRequest.iid;
4032
4039
  }
4033
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4040
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4034
4041
  if (page) {
4035
4042
  url.searchParams.append("page", page.toString());
4036
4043
  }
@@ -4066,7 +4073,7 @@ async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchNa
4066
4073
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4067
4074
  mergeRequestIid = mergeRequest.iid;
4068
4075
  }
4069
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4076
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
4070
4077
  const response = await fetch(url.toString(), { ...getFetchConfig() });
4071
4078
  await handleGitLabError(response);
4072
4079
  const data = (await response.json());
@@ -4107,7 +4114,7 @@ async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, br
4107
4114
  let page = 1;
4108
4115
  const perPage = 20;
4109
4116
  while (remaining.size > 0) {
4110
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4117
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
4111
4118
  url.searchParams.append("page", page.toString());
4112
4119
  url.searchParams.append("per_page", perPage.toString());
4113
4120
  if (unidiff) {
@@ -4150,7 +4157,7 @@ async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, br
4150
4157
  */
4151
4158
  async function getBranchDiffs(projectId, from, to, straight) {
4152
4159
  projectId = decodeURIComponent(projectId); // Decode project ID
4153
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/compare`);
4160
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/compare`);
4154
4161
  url.searchParams.append("from", from);
4155
4162
  url.searchParams.append("to", to);
4156
4163
  if (straight !== undefined) {
@@ -4185,7 +4192,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
4185
4192
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
4186
4193
  mergeRequestIid = mergeRequest.iid;
4187
4194
  }
4188
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
4195
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
4189
4196
  const response = await fetch(url.toString(), {
4190
4197
  ...getFetchConfig(),
4191
4198
  method: "PUT",
@@ -4205,7 +4212,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
4205
4212
  */
4206
4213
  async function mergeMergeRequest(projectId, options, mergeRequestIid) {
4207
4214
  projectId = decodeURIComponent(projectId); // Decode project ID
4208
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/merge`);
4215
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/merge`);
4209
4216
  const response = await fetch(url.toString(), {
4210
4217
  ...getFetchConfig(),
4211
4218
  method: "PUT",
@@ -4225,7 +4232,7 @@ async function mergeMergeRequest(projectId, options, mergeRequestIid) {
4225
4232
  */
4226
4233
  async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPassword) {
4227
4234
  projectId = decodeURIComponent(projectId);
4228
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approve`);
4235
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approve`);
4229
4236
  const body = {};
4230
4237
  if (sha) {
4231
4238
  body.sha = sha;
@@ -4250,7 +4257,7 @@ async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPass
4250
4257
  */
4251
4258
  async function unapproveMergeRequest(projectId, mergeRequestIid) {
4252
4259
  projectId = decodeURIComponent(projectId);
4253
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/unapprove`);
4260
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/unapprove`);
4254
4261
  const response = await fetch(url.toString(), {
4255
4262
  ...getFetchConfig(),
4256
4263
  method: "POST",
@@ -4268,7 +4275,7 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
4268
4275
  */
4269
4276
  async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4270
4277
  projectId = decodeURIComponent(projectId);
4271
- const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approval_state`);
4278
+ const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approval_state`);
4272
4279
  const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
4273
4280
  ...getFetchConfig(),
4274
4281
  method: "GET",
@@ -4316,7 +4323,7 @@ async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4316
4323
  */
4317
4324
  async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4318
4325
  projectId = decodeURIComponent(projectId);
4319
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/conflicts`);
4326
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/conflicts`);
4320
4327
  const response = await fetch(url.toString(), {
4321
4328
  ...getFetchConfig(),
4322
4329
  method: "GET",
@@ -4325,7 +4332,7 @@ async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4325
4332
  return (await response.json());
4326
4333
  }
4327
4334
  async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
4328
- const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approvals`);
4335
+ const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approvals`);
4329
4336
  const approvalsResponse = await fetch(approvalsUrl.toString(), {
4330
4337
  ...getFetchConfig(),
4331
4338
  method: "GET",
@@ -4385,7 +4392,7 @@ async function createNote(projectId, noteableType, // specifies 'issue' or 'merg
4385
4392
  noteableIid, body) {
4386
4393
  projectId = decodeURIComponent(projectId); // Decode project ID
4387
4394
  // ⚙️ Response type can be adjusted according to the GitLab API documentation
4388
- 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
4395
+ 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
4389
4396
  );
4390
4397
  const response = await fetch(url.toString(), {
4391
4398
  ...getFetchConfig(),
@@ -4406,7 +4413,7 @@ noteableIid, body) {
4406
4413
  */
4407
4414
  async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4408
4415
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
4409
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/merge_requests/${encodeGitLabPathSegment(merge_request_iid)}/draft_notes/${encodeGitLabPathSegment(draft_note_id)}`, { ...getFetchConfig() });
4416
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/merge_requests/${encodeGitLabPathSegment(merge_request_iid)}/draft_notes/${encodeGitLabPathSegment(draft_note_id)}`, { ...getFetchConfig() });
4410
4417
  if (!response.ok) {
4411
4418
  const errorText = await response.text();
4412
4419
  throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
@@ -4416,7 +4423,7 @@ async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4416
4423
  }
4417
4424
  async function listDraftNotes(projectId, mergeRequestIid) {
4418
4425
  projectId = decodeURIComponent(projectId);
4419
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4426
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4420
4427
  const response = await fetch(url.toString(), {
4421
4428
  ...getFetchConfig(),
4422
4429
  method: "GET",
@@ -4440,7 +4447,7 @@ async function listDraftNotes(projectId, mergeRequestIid) {
4440
4447
  */
4441
4448
  async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscussionId, position, resolveDiscussion) {
4442
4449
  projectId = decodeURIComponent(projectId);
4443
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4450
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4444
4451
  const requestBody = { note: body };
4445
4452
  if (inReplyToDiscussionId) {
4446
4453
  requestBody.in_reply_to_discussion_id = inReplyToDiscussionId;
@@ -4475,7 +4482,7 @@ async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscus
4475
4482
  */
4476
4483
  async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
4477
4484
  projectId = decodeURIComponent(projectId);
4478
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4485
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4479
4486
  const requestBody = {};
4480
4487
  if (body !== undefined) {
4481
4488
  requestBody.note = body;
@@ -4507,7 +4514,7 @@ async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, po
4507
4514
  */
4508
4515
  async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4509
4516
  projectId = decodeURIComponent(projectId);
4510
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4517
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4511
4518
  const response = await fetch(url.toString(), {
4512
4519
  ...getFetchConfig(),
4513
4520
  method: "DELETE",
@@ -4526,7 +4533,7 @@ async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4526
4533
  */
4527
4534
  async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4528
4535
  projectId = decodeURIComponent(projectId);
4529
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}/publish`);
4536
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}/publish`);
4530
4537
  const response = await fetch(url.toString(), {
4531
4538
  ...getFetchConfig(),
4532
4539
  method: "PUT",
@@ -4597,7 +4604,7 @@ async function fetchGitLabInstanceVersion() {
4597
4604
  */
4598
4605
  async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
4599
4606
  projectId = decodeURIComponent(projectId);
4600
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4607
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4601
4608
  const body = buildBulkPublishDraftNotesBody(options);
4602
4609
  if (needsGitLab19_2BulkPublish(body)) {
4603
4610
  await assertGitLabVersionAtLeast({
@@ -4635,7 +4642,7 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
4635
4642
  }
4636
4643
  async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionId, resolved) {
4637
4644
  projectId = decodeURIComponent(projectId);
4638
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
4645
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
4639
4646
  if (resolved !== undefined) {
4640
4647
  url.searchParams.append("resolved", resolved ? "true" : "false");
4641
4648
  }
@@ -4667,7 +4674,7 @@ async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionI
4667
4674
  */
4668
4675
  async function createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
4669
4676
  projectId = decodeURIComponent(projectId); // Decode project ID
4670
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions`);
4677
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions`);
4671
4678
  const payload = { body };
4672
4679
  if (position) {
4673
4680
  payload.position = position;
@@ -4694,7 +4701,7 @@ async function createMergeRequestThread(projectId, mergeRequestIid, body, positi
4694
4701
  */
4695
4702
  async function listMergeRequestVersions(projectId, mergeRequestIid) {
4696
4703
  projectId = decodeURIComponent(projectId); // Decode project ID
4697
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions`);
4704
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions`);
4698
4705
  const response = await fetch(url.toString(), {
4699
4706
  ...getFetchConfig(),
4700
4707
  });
@@ -4713,7 +4720,7 @@ async function listMergeRequestVersions(projectId, mergeRequestIid) {
4713
4720
  */
4714
4721
  async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, unidiff) {
4715
4722
  projectId = decodeURIComponent(projectId); // Decode project ID
4716
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions/${encodeGitLabPathSegment(versionId)}`);
4723
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions/${encodeGitLabPathSegment(versionId)}`);
4717
4724
  if (unidiff !== undefined) {
4718
4725
  url.searchParams.append("unidiff", String(unidiff));
4719
4726
  }
@@ -4734,16 +4741,7 @@ async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, uni
4734
4741
  async function listProjects(options = {}) {
4735
4742
  // Construct the query parameters
4736
4743
  const params = new URLSearchParams();
4737
- for (const [key, value] of Object.entries(options)) {
4738
- if (value !== undefined && value !== null) {
4739
- if (typeof value === "boolean") {
4740
- params.append(key, value ? "true" : "false");
4741
- }
4742
- else {
4743
- params.append(key, String(value));
4744
- }
4745
- }
4746
- }
4744
+ appendFilterParams(params, options);
4747
4745
  // Make the API request
4748
4746
  const response = await fetch(`${getEffectiveApiUrl()}/projects?${params.toString()}`, {
4749
4747
  ...getFetchConfig(),
@@ -4764,18 +4762,9 @@ async function listProjects(options = {}) {
4764
4762
  async function listLabels(projectId, options = {}) {
4765
4763
  projectId = decodeURIComponent(projectId); // Decode project ID
4766
4764
  // Construct the URL with project path
4767
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`);
4765
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels`);
4768
4766
  // Add query parameters
4769
- Object.entries(options).forEach(([key, value]) => {
4770
- if (value !== undefined) {
4771
- if (typeof value === "boolean") {
4772
- url.searchParams.append(key, value ? "true" : "false");
4773
- }
4774
- else {
4775
- url.searchParams.append(key, String(value));
4776
- }
4777
- }
4778
- });
4767
+ appendFilterParams(url.searchParams, options);
4779
4768
  // Make the API request
4780
4769
  const response = await fetch(url.toString(), {
4781
4770
  ...getFetchConfig(),
@@ -4796,7 +4785,7 @@ async function listLabels(projectId, options = {}) {
4796
4785
  */
4797
4786
  async function getLabel(projectId, labelId, includeAncestorGroups) {
4798
4787
  projectId = decodeURIComponent(projectId); // Decode project ID
4799
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`);
4788
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels/${encodeGitLabPathSegment(String(labelId))}`);
4800
4789
  // Add query parameters
4801
4790
  if (includeAncestorGroups !== undefined) {
4802
4791
  url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
@@ -4821,7 +4810,7 @@ async function getLabel(projectId, labelId, includeAncestorGroups) {
4821
4810
  async function createLabel(projectId, options) {
4822
4811
  projectId = decodeURIComponent(projectId); // Decode project ID
4823
4812
  // Make the API request
4824
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`, {
4813
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels`, {
4825
4814
  ...getFetchConfig(),
4826
4815
  method: "POST",
4827
4816
  body: JSON.stringify(options),
@@ -4843,7 +4832,7 @@ async function createLabel(projectId, options) {
4843
4832
  async function updateLabel(projectId, labelId, options) {
4844
4833
  projectId = decodeURIComponent(projectId); // Decode project ID
4845
4834
  // Make the API request
4846
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
4835
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels/${encodeGitLabPathSegment(String(labelId))}`, {
4847
4836
  ...getFetchConfig(),
4848
4837
  method: "PUT",
4849
4838
  body: JSON.stringify(options),
@@ -4863,7 +4852,7 @@ async function updateLabel(projectId, labelId, options) {
4863
4852
  async function deleteLabel(projectId, labelId) {
4864
4853
  projectId = decodeURIComponent(projectId); // Decode project ID
4865
4854
  // Make the API request
4866
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
4855
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/labels/${encodeGitLabPathSegment(String(labelId))}`, {
4867
4856
  ...getFetchConfig(),
4868
4857
  method: "DELETE",
4869
4858
  });
@@ -4877,7 +4866,7 @@ async function deleteLabel(projectId, labelId) {
4877
4866
  * @returns {Promise<GitLabProject[]>} Array of projects in the group
4878
4867
  */
4879
4868
  async function listGroupProjects(options) {
4880
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(options.group_id)}/projects`);
4869
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(options.group_id)}/projects`);
4881
4870
  // Add optional parameters to URL
4882
4871
  if (options.include_subgroups)
4883
4872
  url.searchParams.append("include_subgroups", "true");
@@ -4927,11 +4916,11 @@ async function listGroupProjects(options) {
4927
4916
  function buildWebhookBaseUrl(projectId, groupId) {
4928
4917
  if (projectId) {
4929
4918
  projectId = decodeURIComponent(projectId);
4930
- return `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/hooks`;
4919
+ return `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/hooks`;
4931
4920
  }
4932
4921
  rejectIfStrictProjectScope("group webhooks");
4933
4922
  const decodedGroupId = decodeURIComponent(groupId);
4934
- return `${getEffectiveApiUrl()}/groups/${encodeURIComponent(decodedGroupId)}/hooks`;
4923
+ return `${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(decodedGroupId)}/hooks`;
4935
4924
  }
4936
4925
  /**
4937
4926
  * List webhooks for a project or group
@@ -5046,7 +5035,7 @@ async function getWebhookEvent(options) {
5046
5035
  */
5047
5036
  async function listWikiPages(projectId, options = {}) {
5048
5037
  projectId = decodeURIComponent(projectId); // Decode project ID
5049
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`);
5038
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis`);
5050
5039
  if (options.page)
5051
5040
  url.searchParams.append("page", options.page.toString());
5052
5041
  if (options.per_page)
@@ -5067,7 +5056,7 @@ async function listWikiPages(projectId, options = {}) {
5067
5056
  */
5068
5057
  async function getWikiPage(projectId, slug, renderHtml) {
5069
5058
  projectId = decodeURIComponent(projectId); // Decode project ID
5070
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`);
5059
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis/${encodeGitLabPathSegment(slug)}`);
5071
5060
  if (renderHtml)
5072
5061
  url.searchParams.append("render_html", renderHtml.toString());
5073
5062
  const response = await fetch(url.toString(), { ...getFetchConfig() });
@@ -5083,7 +5072,7 @@ async function createWikiPage(projectId, title, content, format) {
5083
5072
  const body = { title, content };
5084
5073
  if (format)
5085
5074
  body.format = format;
5086
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`, {
5075
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis`, {
5087
5076
  ...getFetchConfig(),
5088
5077
  method: "POST",
5089
5078
  body: JSON.stringify(body),
@@ -5111,7 +5100,7 @@ async function updateWikiPage(projectId, slug, title, content, format) {
5111
5100
  body.content = content;
5112
5101
  if (format)
5113
5102
  body.format = format;
5114
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
5103
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis/${encodeGitLabPathSegment(slug)}`, {
5115
5104
  ...getFetchConfig(),
5116
5105
  method: "PUT",
5117
5106
  body: JSON.stringify(body),
@@ -5125,7 +5114,7 @@ async function updateWikiPage(projectId, slug, title, content, format) {
5125
5114
  */
5126
5115
  async function deleteWikiPage(projectId, slug) {
5127
5116
  projectId = decodeURIComponent(projectId); // Decode project ID
5128
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
5117
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/wikis/${encodeGitLabPathSegment(slug)}`, {
5129
5118
  ...getFetchConfig(),
5130
5119
  method: "DELETE",
5131
5120
  });
@@ -5136,7 +5125,7 @@ async function deleteWikiPage(projectId, slug) {
5136
5125
  */
5137
5126
  async function listGroupWikiPages(groupId, options = {}) {
5138
5127
  groupId = decodeURIComponent(groupId); // Decode group ID
5139
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis`);
5128
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis`);
5140
5129
  if (options.page)
5141
5130
  url.searchParams.append("page", options.page.toString());
5142
5131
  if (options.per_page)
@@ -5157,7 +5146,7 @@ async function listGroupWikiPages(groupId, options = {}) {
5157
5146
  */
5158
5147
  async function getGroupWikiPage(groupId, slug, renderHtml) {
5159
5148
  groupId = decodeURIComponent(groupId); // Decode group ID
5160
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`);
5149
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis/${encodeGitLabPathSegment(slug)}`);
5161
5150
  if (renderHtml)
5162
5151
  url.searchParams.append("render_html", renderHtml.toString());
5163
5152
  const response = await fetch(url.toString(), { ...getFetchConfig() });
@@ -5173,7 +5162,7 @@ async function createGroupWikiPage(groupId, title, content, format) {
5173
5162
  const body = { title, content };
5174
5163
  if (format)
5175
5164
  body.format = format;
5176
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis`, {
5165
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis`, {
5177
5166
  ...getFetchConfig(),
5178
5167
  method: "POST",
5179
5168
  body: JSON.stringify(body),
@@ -5201,7 +5190,7 @@ async function updateGroupWikiPage(groupId, slug, title, content, format) {
5201
5190
  body.content = content;
5202
5191
  if (format)
5203
5192
  body.format = format;
5204
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, {
5193
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis/${encodeGitLabPathSegment(slug)}`, {
5205
5194
  ...getFetchConfig(),
5206
5195
  method: "PUT",
5207
5196
  body: JSON.stringify(body),
@@ -5215,7 +5204,7 @@ async function updateGroupWikiPage(groupId, slug, title, content, format) {
5215
5204
  */
5216
5205
  async function deleteGroupWikiPage(groupId, slug) {
5217
5206
  groupId = decodeURIComponent(groupId); // Decode group ID
5218
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, {
5207
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/wikis/${encodeGitLabPathSegment(slug)}`, {
5219
5208
  ...getFetchConfig(),
5220
5209
  method: "DELETE",
5221
5210
  });
@@ -5230,13 +5219,9 @@ async function deleteGroupWikiPage(groupId, slug) {
5230
5219
  */
5231
5220
  async function listPipelines(projectId, options = {}) {
5232
5221
  projectId = decodeURIComponent(projectId); // Decode project ID
5233
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines`);
5222
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines`);
5234
5223
  // Add all query parameters
5235
- Object.entries(options).forEach(([key, value]) => {
5236
- if (value !== undefined) {
5237
- url.searchParams.append(key, value.toString());
5238
- }
5239
- });
5224
+ appendFilterParams(url.searchParams, options);
5240
5225
  const response = await fetch(url.toString(), {
5241
5226
  ...getFetchConfig(),
5242
5227
  });
@@ -5253,7 +5238,7 @@ async function listPipelines(projectId, options = {}) {
5253
5238
  */
5254
5239
  async function getPipeline(projectId, pipelineId, signal) {
5255
5240
  projectId = decodeURIComponent(projectId); // Decode project ID
5256
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`);
5241
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`);
5257
5242
  const response = await fetch(url.toString(), {
5258
5243
  ...getFetchConfig(),
5259
5244
  signal,
@@ -5267,7 +5252,7 @@ async function getPipeline(projectId, pipelineId, signal) {
5267
5252
  }
5268
5253
  async function getPipelineVariables(projectId, pipelineId, page, perPage) {
5269
5254
  projectId = decodeURIComponent(projectId);
5270
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/variables`);
5255
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/variables`);
5271
5256
  if (page !== undefined)
5272
5257
  url.searchParams.set("page", String(page));
5273
5258
  if (perPage !== undefined)
@@ -5279,7 +5264,7 @@ async function getPipelineVariables(projectId, pipelineId, page, perPage) {
5279
5264
  async function getPipelineReport(projectId, pipelineId, summary = false, page, perPage) {
5280
5265
  projectId = decodeURIComponent(projectId);
5281
5266
  const endpoint = summary ? "test_report_summary" : "test_report";
5282
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/${endpoint}`);
5267
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/${endpoint}`);
5283
5268
  if (page !== undefined)
5284
5269
  url.searchParams.set("page", String(page));
5285
5270
  if (perPage !== undefined)
@@ -5290,13 +5275,13 @@ async function getPipelineReport(projectId, pipelineId, summary = false, page, p
5290
5275
  }
5291
5276
  async function deletePipeline(projectId, pipelineId) {
5292
5277
  projectId = decodeURIComponent(projectId);
5293
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`, { ...getFetchConfig(), method: "DELETE" });
5278
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`, { ...getFetchConfig(), method: "DELETE" });
5294
5279
  await handleGitLabError(response);
5295
5280
  return response.status === 204 ? { deleted: true } : response.json();
5296
5281
  }
5297
5282
  async function updatePipelineMetadata(projectId, pipelineId, name) {
5298
5283
  projectId = decodeURIComponent(projectId);
5299
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/metadata`, { ...getFetchConfig(), method: "PUT", body: JSON.stringify({ name }) });
5284
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/metadata`, { ...getFetchConfig(), method: "PUT", body: JSON.stringify({ name }) });
5300
5285
  await handleGitLabError(response);
5301
5286
  return response.json();
5302
5287
  }
@@ -5309,12 +5294,8 @@ async function updatePipelineMetadata(projectId, pipelineId, name) {
5309
5294
  */
5310
5295
  async function listDeployments(projectId, options = {}) {
5311
5296
  projectId = decodeURIComponent(projectId);
5312
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
5313
- Object.entries(options).forEach(([key, value]) => {
5314
- if (value !== undefined) {
5315
- url.searchParams.append(key, value.toString());
5316
- }
5317
- });
5297
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/deployments`);
5298
+ appendFilterParams(url.searchParams, options);
5318
5299
  const response = await fetch(url.toString(), {
5319
5300
  ...getFetchConfig(),
5320
5301
  });
@@ -5331,7 +5312,7 @@ async function listDeployments(projectId, options = {}) {
5331
5312
  */
5332
5313
  async function getDeployment(projectId, deploymentId) {
5333
5314
  projectId = decodeURIComponent(projectId);
5334
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${encodeGitLabPathSegment(deploymentId)}`);
5315
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/deployments/${encodeGitLabPathSegment(deploymentId)}`);
5335
5316
  const response = await fetch(url.toString(), {
5336
5317
  ...getFetchConfig(),
5337
5318
  });
@@ -5344,7 +5325,7 @@ async function getDeployment(projectId, deploymentId) {
5344
5325
  }
5345
5326
  async function deploymentRequest(projectId, path = "", method = "GET", body, query) {
5346
5327
  projectId = decodeURIComponent(projectId);
5347
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments${path}`);
5328
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/deployments${path}`);
5348
5329
  for (const [key, value] of Object.entries(query ?? {})) {
5349
5330
  if (value !== undefined)
5350
5331
  url.searchParams.set(key, String(value));
@@ -5365,12 +5346,8 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
5365
5346
  */
5366
5347
  async function listEnvironments(projectId, options = {}) {
5367
5348
  projectId = decodeURIComponent(projectId);
5368
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
5369
- Object.entries(options).forEach(([key, value]) => {
5370
- if (value !== undefined) {
5371
- url.searchParams.append(key, value.toString());
5372
- }
5373
- });
5349
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments`);
5350
+ appendFilterParams(url.searchParams, options);
5374
5351
  const response = await fetch(url.toString(), {
5375
5352
  ...getFetchConfig(),
5376
5353
  });
@@ -5387,7 +5364,7 @@ async function listEnvironments(projectId, options = {}) {
5387
5364
  */
5388
5365
  async function getEnvironment(projectId, environmentId) {
5389
5366
  projectId = decodeURIComponent(projectId);
5390
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${encodeGitLabPathSegment(environmentId)}`);
5367
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments/${encodeGitLabPathSegment(environmentId)}`);
5391
5368
  const response = await fetch(url.toString(), {
5392
5369
  ...getFetchConfig(),
5393
5370
  });
@@ -5400,7 +5377,7 @@ async function getEnvironment(projectId, environmentId) {
5400
5377
  }
5401
5378
  async function environmentRequest(projectId, path, method, body, query) {
5402
5379
  projectId = decodeURIComponent(projectId);
5403
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments${path}`);
5380
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments${path}`);
5404
5381
  for (const [key, value] of Object.entries(query ?? {}))
5405
5382
  if (value !== undefined)
5406
5383
  url.searchParams.set(key, String(value));
@@ -5410,13 +5387,13 @@ async function environmentRequest(projectId, path, method, body, query) {
5410
5387
  }
5411
5388
  async function pipelineTriggerRequest(projectId, path = "", method = "GET", body) {
5412
5389
  projectId = decodeURIComponent(projectId);
5413
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/triggers${path}`, { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
5390
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/triggers${path}`, { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
5414
5391
  await handleGitLabError(response);
5415
5392
  return response.status === 204 ? { success: true } : response.json();
5416
5393
  }
5417
5394
  async function triggerPipeline(projectId, token, ref, variables, inputs) {
5418
5395
  projectId = decodeURIComponent(projectId);
5419
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/trigger/pipeline`);
5396
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/trigger/pipeline`);
5420
5397
  url.searchParams.set("token", token);
5421
5398
  url.searchParams.set("ref", ref);
5422
5399
  const body = {};
@@ -5438,18 +5415,9 @@ async function triggerPipeline(projectId, token, ref, variables, inputs) {
5438
5415
  */
5439
5416
  async function listPipelineJobs(projectId, pipelineId, options = {}) {
5440
5417
  projectId = decodeURIComponent(projectId); // Decode project ID
5441
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
5418
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
5442
5419
  // Add all query parameters
5443
- Object.entries(options).forEach(([key, value]) => {
5444
- if (value !== undefined) {
5445
- if (typeof value === "boolean") {
5446
- url.searchParams.append(key, value ? "true" : "false");
5447
- }
5448
- else {
5449
- url.searchParams.append(key, value.toString());
5450
- }
5451
- }
5452
- });
5420
+ appendFilterParams(url.searchParams, options);
5453
5421
  const response = await fetch(url.toString(), {
5454
5422
  ...getFetchConfig(),
5455
5423
  });
@@ -5470,18 +5438,9 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
5470
5438
  */
5471
5439
  async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
5472
5440
  projectId = decodeURIComponent(projectId); // Decode project ID
5473
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
5441
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
5474
5442
  // Add all query parameters
5475
- Object.entries(options).forEach(([key, value]) => {
5476
- if (value !== undefined) {
5477
- if (typeof value === "boolean") {
5478
- url.searchParams.append(key, value ? "true" : "false");
5479
- }
5480
- else {
5481
- url.searchParams.append(key, value.toString());
5482
- }
5483
- }
5484
- });
5443
+ appendFilterParams(url.searchParams, options);
5485
5444
  const response = await fetch(url.toString(), {
5486
5445
  ...getFetchConfig(),
5487
5446
  });
@@ -5494,7 +5453,7 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
5494
5453
  }
5495
5454
  async function getPipelineJob(projectId, jobId, signal) {
5496
5455
  projectId = decodeURIComponent(projectId); // Decode project ID
5497
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}`);
5456
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}`);
5498
5457
  const response = await fetch(url.toString(), {
5499
5458
  ...getFetchConfig(),
5500
5459
  signal,
@@ -5518,7 +5477,7 @@ async function getPipelineJob(projectId, jobId, signal) {
5518
5477
  const MAX_JOB_TRACE_LINES = 1000;
5519
5478
  async function getPipelineJobOutput(projectId, jobId, limit, offset) {
5520
5479
  projectId = decodeURIComponent(projectId); // Decode project ID
5521
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/trace`);
5480
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/trace`);
5522
5481
  const response = await fetch(url.toString(), {
5523
5482
  ...getFetchConfig(),
5524
5483
  headers: {
@@ -5553,7 +5512,7 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
5553
5512
  }
5554
5513
  async function validateCiLint(projectId, options) {
5555
5514
  projectId = decodeURIComponent(projectId);
5556
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
5515
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/ci/lint`);
5557
5516
  const response = await fetch(url.toString(), {
5558
5517
  ...getFetchConfig(),
5559
5518
  method: "POST",
@@ -5565,17 +5524,8 @@ async function validateCiLint(projectId, options) {
5565
5524
  }
5566
5525
  async function validateProjectCiLint(projectId, options) {
5567
5526
  projectId = decodeURIComponent(projectId);
5568
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
5569
- Object.entries(options).forEach(([key, value]) => {
5570
- if (value !== undefined) {
5571
- if (typeof value === "boolean") {
5572
- url.searchParams.append(key, value ? "true" : "false");
5573
- }
5574
- else {
5575
- url.searchParams.append(key, value.toString());
5576
- }
5577
- }
5578
- });
5527
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/ci/lint`);
5528
+ appendFilterParams(url.searchParams, options);
5579
5529
  const response = await fetch(url.toString(), {
5580
5530
  ...getFetchConfig(),
5581
5531
  });
@@ -5593,17 +5543,8 @@ async function validateProjectCiLint(projectId, options) {
5593
5543
  */
5594
5544
  async function listJobArtifacts(projectId, jobId, options = {}) {
5595
5545
  projectId = decodeURIComponent(projectId);
5596
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
5597
- Object.entries(options).forEach(([key, value]) => {
5598
- if (value !== undefined) {
5599
- if (typeof value === "boolean") {
5600
- url.searchParams.append(key, value ? "true" : "false");
5601
- }
5602
- else {
5603
- url.searchParams.append(key, value.toString());
5604
- }
5605
- }
5606
- });
5546
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
5547
+ appendFilterParams(url.searchParams, options);
5607
5548
  const response = await fetch(url.toString(), {
5608
5549
  ...getFetchConfig(),
5609
5550
  });
@@ -5625,10 +5566,8 @@ async function listJobArtifacts(projectId, jobId, options = {}) {
5625
5566
  async function downloadJobArtifacts(projectId, jobId, localPath) {
5626
5567
  projectId = decodeURIComponent(projectId);
5627
5568
  const effectiveProjectId = getEffectiveProjectId(projectId);
5628
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts`);
5629
- const response = await fetch(url.toString(), {
5630
- ...getFetchConfig(),
5631
- });
5569
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts`);
5570
+ const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
5632
5571
  if (response.status === 404) {
5633
5572
  throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
5634
5573
  }
@@ -5652,14 +5591,9 @@ async function downloadJobArtifacts(projectId, jobId, localPath) {
5652
5591
  async function getJobArtifactFile(projectId, jobId, artifactPath) {
5653
5592
  projectId = decodeURIComponent(projectId);
5654
5593
  const effectiveProjectId = getEffectiveProjectId(projectId);
5655
- const encodedArtifactPath = artifactPath
5656
- .split("/")
5657
- .map(segment => encodeGitLabPathSegment(segment))
5658
- .join("/");
5659
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
5660
- const response = await fetch(url.toString(), {
5661
- ...getFetchConfig(),
5662
- });
5594
+ const encodedArtifactPath = encodeGitLabPath(artifactPath);
5595
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
5596
+ const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
5663
5597
  if (response.status === 404) {
5664
5598
  throw new Error(`Artifact file not found: ${artifactPath}`);
5665
5599
  }
@@ -5677,7 +5611,7 @@ async function getJobArtifactFile(projectId, jobId, artifactPath) {
5677
5611
  */
5678
5612
  async function createPipeline(projectId, ref, variables, inputs) {
5679
5613
  projectId = decodeURIComponent(projectId); // Decode project ID
5680
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`);
5614
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline`);
5681
5615
  const body = { ref };
5682
5616
  if (variables && variables.length > 0) {
5683
5617
  body.variables = variables;
@@ -5703,7 +5637,7 @@ async function createPipeline(projectId, ref, variables, inputs) {
5703
5637
  */
5704
5638
  async function retryPipeline(projectId, pipelineId) {
5705
5639
  projectId = decodeURIComponent(projectId); // Decode project ID
5706
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/retry`);
5640
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/retry`);
5707
5641
  const response = await fetch(url.toString(), {
5708
5642
  ...getFetchConfig(),
5709
5643
  method: "POST",
@@ -5721,7 +5655,7 @@ async function retryPipeline(projectId, pipelineId) {
5721
5655
  */
5722
5656
  async function cancelPipeline(projectId, pipelineId) {
5723
5657
  projectId = decodeURIComponent(projectId); // Decode project ID
5724
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/cancel`);
5658
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/cancel`);
5725
5659
  const response = await fetch(url.toString(), {
5726
5660
  ...getFetchConfig(),
5727
5661
  method: "POST",
@@ -5739,12 +5673,8 @@ async function cancelPipeline(projectId, pipelineId) {
5739
5673
  */
5740
5674
  async function listPipelineSchedules(projectId, options = {}) {
5741
5675
  projectId = decodeURIComponent(projectId); // Decode project ID
5742
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5743
- Object.entries(options).forEach(([key, value]) => {
5744
- if (value !== undefined) {
5745
- url.searchParams.append(key, value.toString());
5746
- }
5747
- });
5676
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5677
+ appendFilterParams(url.searchParams, options);
5748
5678
  const response = await fetch(url.toString(), {
5749
5679
  ...getFetchConfig(),
5750
5680
  });
@@ -5761,7 +5691,7 @@ async function listPipelineSchedules(projectId, options = {}) {
5761
5691
  */
5762
5692
  async function getPipelineSchedule(projectId, pipelineScheduleId) {
5763
5693
  projectId = decodeURIComponent(projectId); // Decode project ID
5764
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5694
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5765
5695
  const response = await fetch(url.toString(), {
5766
5696
  ...getFetchConfig(),
5767
5697
  });
@@ -5779,12 +5709,8 @@ async function getPipelineSchedule(projectId, pipelineScheduleId) {
5779
5709
  */
5780
5710
  async function listPipelineSchedulePipelines(projectId, pipelineScheduleId, options = {}) {
5781
5711
  projectId = decodeURIComponent(projectId); // Decode project ID
5782
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/pipelines`);
5783
- Object.entries(options).forEach(([key, value]) => {
5784
- if (value !== undefined) {
5785
- url.searchParams.append(key, value.toString());
5786
- }
5787
- });
5712
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/pipelines`);
5713
+ appendFilterParams(url.searchParams, options);
5788
5714
  const response = await fetch(url.toString(), {
5789
5715
  ...getFetchConfig(),
5790
5716
  });
@@ -5801,7 +5727,7 @@ async function listPipelineSchedulePipelines(projectId, pipelineScheduleId, opti
5801
5727
  */
5802
5728
  async function createPipelineSchedule(projectId, options) {
5803
5729
  projectId = decodeURIComponent(projectId); // Decode project ID
5804
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5730
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5805
5731
  const response = await fetch(url.toString(), {
5806
5732
  ...getFetchConfig(),
5807
5733
  method: "POST",
@@ -5821,7 +5747,7 @@ async function createPipelineSchedule(projectId, options) {
5821
5747
  */
5822
5748
  async function updatePipelineSchedule(projectId, pipelineScheduleId, options) {
5823
5749
  projectId = decodeURIComponent(projectId); // Decode project ID
5824
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5750
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5825
5751
  const response = await fetch(url.toString(), {
5826
5752
  ...getFetchConfig(),
5827
5753
  method: "PUT",
@@ -5840,7 +5766,7 @@ async function updatePipelineSchedule(projectId, pipelineScheduleId, options) {
5840
5766
  */
5841
5767
  async function deletePipelineSchedule(projectId, pipelineScheduleId) {
5842
5768
  projectId = decodeURIComponent(projectId); // Decode project ID
5843
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5769
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}`);
5844
5770
  const response = await fetch(url.toString(), {
5845
5771
  ...getFetchConfig(),
5846
5772
  method: "DELETE",
@@ -5856,7 +5782,7 @@ async function deletePipelineSchedule(projectId, pipelineScheduleId) {
5856
5782
  */
5857
5783
  async function playPipelineSchedule(projectId, pipelineScheduleId) {
5858
5784
  projectId = decodeURIComponent(projectId); // Decode project ID
5859
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/play`);
5785
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/play`);
5860
5786
  const response = await fetch(url.toString(), {
5861
5787
  ...getFetchConfig(),
5862
5788
  method: "POST",
@@ -5874,7 +5800,7 @@ async function playPipelineSchedule(projectId, pipelineScheduleId) {
5874
5800
  */
5875
5801
  async function takeOwnershipPipelineSchedule(projectId, pipelineScheduleId) {
5876
5802
  projectId = decodeURIComponent(projectId); // Decode project ID
5877
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/take_ownership`);
5803
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/take_ownership`);
5878
5804
  const response = await fetch(url.toString(), {
5879
5805
  ...getFetchConfig(),
5880
5806
  method: "POST",
@@ -5893,7 +5819,7 @@ async function takeOwnershipPipelineSchedule(projectId, pipelineScheduleId) {
5893
5819
  */
5894
5820
  async function getPipelineScheduleVariable(projectId, pipelineScheduleId, key) {
5895
5821
  projectId = decodeURIComponent(projectId); // Decode project ID
5896
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5822
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5897
5823
  const response = await fetch(url.toString(), {
5898
5824
  ...getFetchConfig(),
5899
5825
  });
@@ -5911,7 +5837,7 @@ async function getPipelineScheduleVariable(projectId, pipelineScheduleId, key) {
5911
5837
  */
5912
5838
  async function createPipelineScheduleVariable(projectId, pipelineScheduleId, options) {
5913
5839
  projectId = decodeURIComponent(projectId); // Decode project ID
5914
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables`);
5840
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables`);
5915
5841
  const response = await fetch(url.toString(), {
5916
5842
  ...getFetchConfig(),
5917
5843
  method: "POST",
@@ -5932,7 +5858,7 @@ async function createPipelineScheduleVariable(projectId, pipelineScheduleId, opt
5932
5858
  */
5933
5859
  async function updatePipelineScheduleVariable(projectId, pipelineScheduleId, key, options) {
5934
5860
  projectId = decodeURIComponent(projectId); // Decode project ID
5935
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5861
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5936
5862
  const response = await fetch(url.toString(), {
5937
5863
  ...getFetchConfig(),
5938
5864
  method: "PUT",
@@ -5952,7 +5878,7 @@ async function updatePipelineScheduleVariable(projectId, pipelineScheduleId, key
5952
5878
  */
5953
5879
  async function deletePipelineScheduleVariable(projectId, pipelineScheduleId, key) {
5954
5880
  projectId = decodeURIComponent(projectId); // Decode project ID
5955
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5881
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/variables/${encodeGitLabPathSegment(key)}`);
5956
5882
  const response = await fetch(url.toString(), {
5957
5883
  ...getFetchConfig(),
5958
5884
  method: "DELETE",
@@ -5969,7 +5895,7 @@ async function deletePipelineScheduleVariable(projectId, pipelineScheduleId, key
5969
5895
  */
5970
5896
  async function playPipelineJob(projectId, jobId, variables, jobInputs) {
5971
5897
  projectId = decodeURIComponent(projectId);
5972
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/play`);
5898
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/play`);
5973
5899
  const body = {};
5974
5900
  if (variables && variables.length > 0) {
5975
5901
  body.job_variables_attributes = variables;
@@ -6003,7 +5929,7 @@ async function playPipelineJobs(projectId, jobIds, variables, timeoutSeconds = 3
6003
5929
  */
6004
5930
  async function retryPipelineJob(projectId, jobId, jobInputs) {
6005
5931
  projectId = decodeURIComponent(projectId);
6006
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/retry`);
5932
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/retry`);
6007
5933
  const response = await fetch(url.toString(), {
6008
5934
  ...getFetchConfig(),
6009
5935
  method: "POST",
@@ -6015,7 +5941,7 @@ async function retryPipelineJob(projectId, jobId, jobInputs) {
6015
5941
  }
6016
5942
  async function erasePipelineJob(projectId, jobId) {
6017
5943
  projectId = decodeURIComponent(projectId);
6018
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/erase`, { ...getFetchConfig(), method: "POST" });
5944
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/erase`, { ...getFetchConfig(), method: "POST" });
6019
5945
  await handleGitLabError(response);
6020
5946
  return response.status === 204 ? { erased: true } : response.json();
6021
5947
  }
@@ -6051,7 +5977,7 @@ async function waitForStatus(fetchStatus, terminalStatuses, timeoutSeconds = 300
6051
5977
  */
6052
5978
  async function cancelPipelineJob(projectId, jobId, force) {
6053
5979
  projectId = decodeURIComponent(projectId);
6054
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/cancel`);
5980
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/cancel`);
6055
5981
  if (force !== undefined) {
6056
5982
  url.searchParams.append("force", force.toString());
6057
5983
  }
@@ -6083,7 +6009,7 @@ async function getRepositoryTree(options) {
6083
6009
  queryParams.append("page_token", options.page_token);
6084
6010
  if (options.pagination)
6085
6011
  queryParams.append("pagination", options.pagination);
6086
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, { ...getFetchConfig() });
6012
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, { ...getFetchConfig() });
6087
6013
  if (response.status === 404) {
6088
6014
  throw new Error("Repository or path not found");
6089
6015
  }
@@ -6105,17 +6031,20 @@ async function getRepositoryTree(options) {
6105
6031
  */
6106
6032
  async function listProjectMilestones(projectId, options) {
6107
6033
  projectId = decodeURIComponent(projectId);
6108
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
6034
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones`);
6109
6035
  Object.entries(options).forEach(([key, value]) => {
6110
- if (value !== undefined) {
6111
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
6112
- value.forEach(iid => {
6113
- url.searchParams.append("iids[]", iid.toString());
6114
- });
6115
- }
6116
- else if (value !== undefined) {
6117
- url.searchParams.append(key, value.toString());
6118
- }
6036
+ // Drop blank entries inside arrays so they behave like the scalar blank guard below
6037
+ const normalized = dropBlankArrayEntries(value);
6038
+ if (isBlankFilterValue(normalized)) {
6039
+ return;
6040
+ }
6041
+ if (key === "iids" && Array.isArray(normalized)) {
6042
+ normalized.forEach(iid => {
6043
+ url.searchParams.append("iids[]", String(iid));
6044
+ });
6045
+ }
6046
+ else {
6047
+ url.searchParams.append(key, String(normalized));
6119
6048
  }
6120
6049
  });
6121
6050
  const response = await fetch(url.toString(), {
@@ -6133,7 +6062,7 @@ async function listProjectMilestones(projectId, options) {
6133
6062
  */
6134
6063
  async function getProjectMilestone(projectId, milestoneId) {
6135
6064
  projectId = decodeURIComponent(projectId);
6136
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6065
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6137
6066
  const response = await fetch(url.toString(), {
6138
6067
  ...getFetchConfig(),
6139
6068
  });
@@ -6149,7 +6078,7 @@ async function getProjectMilestone(projectId, milestoneId) {
6149
6078
  */
6150
6079
  async function createProjectMilestone(projectId, options) {
6151
6080
  projectId = decodeURIComponent(projectId);
6152
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
6081
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones`);
6153
6082
  const response = await fetch(url.toString(), {
6154
6083
  ...getFetchConfig(),
6155
6084
  method: "POST",
@@ -6168,7 +6097,7 @@ async function createProjectMilestone(projectId, options) {
6168
6097
  */
6169
6098
  async function editProjectMilestone(projectId, milestoneId, options) {
6170
6099
  projectId = decodeURIComponent(projectId);
6171
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6100
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6172
6101
  const response = await fetch(url.toString(), {
6173
6102
  ...getFetchConfig(),
6174
6103
  method: "PUT",
@@ -6186,7 +6115,7 @@ async function editProjectMilestone(projectId, milestoneId, options) {
6186
6115
  */
6187
6116
  async function deleteProjectMilestone(projectId, milestoneId) {
6188
6117
  projectId = decodeURIComponent(projectId);
6189
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6118
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6190
6119
  const response = await fetch(url.toString(), {
6191
6120
  ...getFetchConfig(),
6192
6121
  method: "DELETE",
@@ -6201,12 +6130,8 @@ async function deleteProjectMilestone(projectId, milestoneId) {
6201
6130
  */
6202
6131
  async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6203
6132
  projectId = decodeURIComponent(projectId);
6204
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6205
- Object.entries(options).forEach(([key, value]) => {
6206
- if (value !== undefined) {
6207
- url.searchParams.append(key, String(value));
6208
- }
6209
- });
6133
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6134
+ appendFilterParams(url.searchParams, options);
6210
6135
  const response = await fetch(url.toString(), {
6211
6136
  ...getFetchConfig(),
6212
6137
  });
@@ -6222,12 +6147,8 @@ async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6222
6147
  */
6223
6148
  async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
6224
6149
  projectId = decodeURIComponent(projectId);
6225
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6226
- Object.entries(options).forEach(([key, value]) => {
6227
- if (value !== undefined) {
6228
- url.searchParams.append(key, String(value));
6229
- }
6230
- });
6150
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6151
+ appendFilterParams(url.searchParams, options);
6231
6152
  const response = await fetch(url.toString(), {
6232
6153
  ...getFetchConfig(),
6233
6154
  });
@@ -6243,7 +6164,7 @@ async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
6243
6164
  */
6244
6165
  async function promoteProjectMilestone(projectId, milestoneId) {
6245
6166
  projectId = decodeURIComponent(projectId);
6246
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/promote`);
6167
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/promote`);
6247
6168
  const response = await fetch(url.toString(), {
6248
6169
  ...getFetchConfig(),
6249
6170
  method: "POST",
@@ -6260,12 +6181,8 @@ async function promoteProjectMilestone(projectId, milestoneId) {
6260
6181
  */
6261
6182
  async function getMilestoneBurndownEvents(projectId, milestoneId, options = {}) {
6262
6183
  projectId = decodeURIComponent(projectId);
6263
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6264
- Object.entries(options).forEach(([key, value]) => {
6265
- if (value !== undefined) {
6266
- url.searchParams.append(key, String(value));
6267
- }
6268
- });
6184
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6185
+ appendFilterParams(url.searchParams, options);
6269
6186
  const response = await fetch(url.toString(), {
6270
6187
  ...getFetchConfig(),
6271
6188
  });
@@ -6278,17 +6195,20 @@ async function getMilestoneBurndownEvents(projectId, milestoneId, options = {})
6278
6195
  */
6279
6196
  async function listGroupMilestones(groupId, options) {
6280
6197
  groupId = decodeURIComponent(groupId);
6281
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
6198
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones`);
6282
6199
  Object.entries(options).forEach(([key, value]) => {
6283
- if (value !== undefined) {
6284
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
6285
- value.forEach(iid => {
6286
- url.searchParams.append("iids[]", iid.toString());
6287
- });
6288
- }
6289
- else {
6290
- url.searchParams.append(key, value.toString());
6291
- }
6200
+ // Drop blank entries inside arrays so they behave like the scalar blank guard below
6201
+ const normalized = dropBlankArrayEntries(value);
6202
+ if (isBlankFilterValue(normalized)) {
6203
+ return;
6204
+ }
6205
+ if (key === "iids" && Array.isArray(normalized)) {
6206
+ normalized.forEach(iid => {
6207
+ url.searchParams.append("iids[]", String(iid));
6208
+ });
6209
+ }
6210
+ else {
6211
+ url.searchParams.append(key, String(normalized));
6292
6212
  }
6293
6213
  });
6294
6214
  const response = await fetch(url.toString(), {
@@ -6303,7 +6223,7 @@ async function listGroupMilestones(groupId, options) {
6303
6223
  */
6304
6224
  async function getGroupMilestone(groupId, milestoneId) {
6305
6225
  groupId = decodeURIComponent(groupId);
6306
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6226
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6307
6227
  const response = await fetch(url.toString(), {
6308
6228
  ...getFetchConfig(),
6309
6229
  });
@@ -6316,7 +6236,7 @@ async function getGroupMilestone(groupId, milestoneId) {
6316
6236
  */
6317
6237
  async function createGroupMilestone(groupId, options) {
6318
6238
  groupId = decodeURIComponent(groupId);
6319
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
6239
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones`);
6320
6240
  const response = await fetch(url.toString(), {
6321
6241
  ...getFetchConfig(),
6322
6242
  method: "POST",
@@ -6331,7 +6251,7 @@ async function createGroupMilestone(groupId, options) {
6331
6251
  */
6332
6252
  async function editGroupMilestone(groupId, milestoneId, options) {
6333
6253
  groupId = decodeURIComponent(groupId);
6334
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6254
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6335
6255
  const response = await fetch(url.toString(), {
6336
6256
  ...getFetchConfig(),
6337
6257
  method: "PUT",
@@ -6346,7 +6266,7 @@ async function editGroupMilestone(groupId, milestoneId, options) {
6346
6266
  */
6347
6267
  async function deleteGroupMilestone(groupId, milestoneId) {
6348
6268
  groupId = decodeURIComponent(groupId);
6349
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6269
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
6350
6270
  const response = await fetch(url.toString(), {
6351
6271
  ...getFetchConfig(),
6352
6272
  method: "DELETE",
@@ -6358,12 +6278,8 @@ async function deleteGroupMilestone(groupId, milestoneId) {
6358
6278
  */
6359
6279
  async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6360
6280
  groupId = decodeURIComponent(groupId);
6361
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6362
- Object.entries(options).forEach(([key, value]) => {
6363
- if (value !== undefined) {
6364
- url.searchParams.append(key, String(value));
6365
- }
6366
- });
6281
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6282
+ appendFilterParams(url.searchParams, options);
6367
6283
  const response = await fetch(url.toString(), {
6368
6284
  ...getFetchConfig(),
6369
6285
  });
@@ -6376,12 +6292,8 @@ async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6376
6292
  */
6377
6293
  async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}) {
6378
6294
  groupId = decodeURIComponent(groupId);
6379
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6380
- Object.entries(options).forEach(([key, value]) => {
6381
- if (value !== undefined) {
6382
- url.searchParams.append(key, String(value));
6383
- }
6384
- });
6295
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6296
+ appendFilterParams(url.searchParams, options);
6385
6297
  const response = await fetch(url.toString(), {
6386
6298
  ...getFetchConfig(),
6387
6299
  });
@@ -6394,12 +6306,8 @@ async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}
6394
6306
  */
6395
6307
  async function getGroupMilestoneBurndownEvents(groupId, milestoneId, options = {}) {
6396
6308
  groupId = decodeURIComponent(groupId);
6397
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6398
- Object.entries(options).forEach(([key, value]) => {
6399
- if (value !== undefined) {
6400
- url.searchParams.append(key, String(value));
6401
- }
6402
- });
6309
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6310
+ appendFilterParams(url.searchParams, options);
6403
6311
  const response = await fetch(url.toString(), {
6404
6312
  ...getFetchConfig(),
6405
6313
  });
@@ -6469,7 +6377,7 @@ async function getUsers(usernames) {
6469
6377
  */
6470
6378
  async function listCommits(projectId, options = {}) {
6471
6379
  projectId = decodeURIComponent(projectId);
6472
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`);
6380
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits`);
6473
6381
  // Add query parameters
6474
6382
  if (options.ref_name)
6475
6383
  url.searchParams.append("ref_name", options.ref_name);
@@ -6513,7 +6421,7 @@ async function listCommits(projectId, options = {}) {
6513
6421
  */
6514
6422
  async function getCommit(projectId, sha, stats) {
6515
6423
  projectId = decodeURIComponent(projectId);
6516
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}`);
6424
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits/${encodeGitLabPathSegment(sha)}`);
6517
6425
  if (stats) {
6518
6426
  url.searchParams.append("stats", "true");
6519
6427
  }
@@ -6535,7 +6443,7 @@ async function getCommit(projectId, sha, stats) {
6535
6443
  */
6536
6444
  async function getCommitDiff(projectId, sha, full_diff) {
6537
6445
  projectId = decodeURIComponent(projectId);
6538
- const baseUrl = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/diff`;
6446
+ const baseUrl = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits/${encodeGitLabPathSegment(sha)}/diff`;
6539
6447
  let allDiffs = [];
6540
6448
  let page = 1;
6541
6449
  while (true) {
@@ -6574,7 +6482,7 @@ async function getCommitDiff(projectId, sha, full_diff) {
6574
6482
  */
6575
6483
  async function getFileBlame(projectId, options) {
6576
6484
  projectId = decodeURIComponent(projectId);
6577
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodeURIComponent(options.file_path)}/blame`);
6485
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/files/${encodeGitLabPathSegment(options.file_path)}/blame`);
6578
6486
  url.searchParams.append("ref", options.ref);
6579
6487
  if (options.range_start !== undefined && options.range_end !== undefined) {
6580
6488
  url.searchParams.append("range[start]", options.range_start.toString());
@@ -6597,12 +6505,8 @@ async function getFileBlame(projectId, options) {
6597
6505
  */
6598
6506
  async function listCommitStatuses(projectId, sha, options = {}) {
6599
6507
  projectId = decodeURIComponent(projectId);
6600
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/statuses`);
6601
- Object.entries(options).forEach(([key, value]) => {
6602
- if (value !== undefined) {
6603
- url.searchParams.append(key, value.toString());
6604
- }
6605
- });
6508
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/commits/${encodeGitLabPathSegment(sha)}/statuses`);
6509
+ appendFilterParams(url.searchParams, options);
6606
6510
  const response = await fetch(url.toString(), {
6607
6511
  ...getFetchConfig(),
6608
6512
  });
@@ -6623,12 +6527,8 @@ async function createCommitStatus(projectId, sha, options) {
6623
6527
  throw new Error("Use either name or context when creating a commit status, not both.");
6624
6528
  }
6625
6529
  projectId = decodeURIComponent(projectId);
6626
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/statuses/${encodeURIComponent(sha)}`);
6627
- Object.entries(options).forEach(([key, value]) => {
6628
- if (value !== undefined) {
6629
- url.searchParams.append(key, value.toString());
6630
- }
6631
- });
6530
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/statuses/${encodeGitLabPathSegment(sha)}`);
6531
+ appendFilterParams(url.searchParams, options);
6632
6532
  const response = await fetch(url.toString(), {
6633
6533
  ...getFetchConfig(),
6634
6534
  method: "POST",
@@ -6717,7 +6617,7 @@ async function listProjectMembers(projectId, options = {}) {
6717
6617
  projectId = decodeURIComponent(projectId);
6718
6618
  const effectiveProjectId = getEffectiveProjectId(projectId);
6719
6619
  const membersPath = options.include_inheritance ? "members/all" : "members";
6720
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/${membersPath}`);
6620
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/${membersPath}`);
6721
6621
  // Add query parameters
6722
6622
  if (options.query)
6723
6623
  url.searchParams.append("query", options.query);
@@ -6747,7 +6647,7 @@ async function listProjectMembers(projectId, options = {}) {
6747
6647
  async function listGroupMembers(groupId, options = {}) {
6748
6648
  groupId = decodeURIComponent(groupId);
6749
6649
  const membersPath = options.include_inheritance ? "members/all" : "members";
6750
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/${membersPath}`);
6650
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/${membersPath}`);
6751
6651
  if (options.query)
6752
6652
  url.searchParams.append("query", options.query);
6753
6653
  if (options.user_ids) {
@@ -6774,7 +6674,7 @@ async function listGroupMembers(groupId, options = {}) {
6774
6674
  */
6775
6675
  async function listGroupIterations(groupId, options = {}) {
6776
6676
  groupId = decodeURIComponent(groupId);
6777
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/iterations`);
6677
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(groupId)}/iterations`);
6778
6678
  // クエリパラメータの追加
6779
6679
  if (options.state)
6780
6680
  url.searchParams.append("state", options.state);
@@ -6804,21 +6704,15 @@ async function listGroupIterations(groupId, options = {}) {
6804
6704
  // --- CI/CD Variables ---
6805
6705
  async function listProjectVariables(projectId, options = {}) {
6806
6706
  projectId = decodeURIComponent(projectId);
6807
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`);
6707
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables`);
6808
6708
  Object.entries(options).forEach(([key, value]) => {
6809
- if (value === undefined)
6810
- return;
6811
6709
  if (key === "filter" && typeof value === "object" && value !== null) {
6812
- Object.entries(value).forEach(([fKey, fVal]) => {
6813
- url.searchParams.append(`filter[${fKey}]`, fVal);
6710
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
6711
+ appendFilterParam(url.searchParams, `filter[${filterKey}]`, filterValue);
6814
6712
  });
6713
+ return;
6815
6714
  }
6816
- else if (typeof value === "boolean") {
6817
- url.searchParams.append(key, value ? "true" : "false");
6818
- }
6819
- else {
6820
- url.searchParams.append(key, String(value));
6821
- }
6715
+ appendFilterParam(url.searchParams, key, value);
6822
6716
  });
6823
6717
  const response = await fetch(url.toString(), getFetchConfig());
6824
6718
  await handleGitLabError(response);
@@ -6827,7 +6721,7 @@ async function listProjectVariables(projectId, options = {}) {
6827
6721
  }
6828
6722
  async function getProjectVariable(projectId, key, filter) {
6829
6723
  projectId = decodeURIComponent(projectId);
6830
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
6724
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables/${encodeGitLabPathSegment(key)}`);
6831
6725
  if (filter?.environment_scope) {
6832
6726
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6833
6727
  }
@@ -6838,7 +6732,7 @@ async function getProjectVariable(projectId, key, filter) {
6838
6732
  }
6839
6733
  async function createProjectVariable(projectId, options) {
6840
6734
  projectId = decodeURIComponent(projectId);
6841
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`, { ...getFetchConfig(), method: "POST", body: JSON.stringify(options) });
6735
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables`, { ...getFetchConfig(), method: "POST", body: JSON.stringify(options) });
6842
6736
  await handleGitLabError(response);
6843
6737
  const data = await response.json();
6844
6738
  return GitLabCiVariableSchema.parse(data);
@@ -6846,7 +6740,7 @@ async function createProjectVariable(projectId, options) {
6846
6740
  async function updateProjectVariable(projectId, key, options) {
6847
6741
  projectId = decodeURIComponent(projectId);
6848
6742
  const { filter, ...body } = options;
6849
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
6743
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables/${encodeGitLabPathSegment(key)}`);
6850
6744
  if (filter?.environment_scope) {
6851
6745
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6852
6746
  }
@@ -6861,7 +6755,7 @@ async function updateProjectVariable(projectId, key, options) {
6861
6755
  }
6862
6756
  async function deleteProjectVariable(projectId, key, filter) {
6863
6757
  projectId = decodeURIComponent(projectId);
6864
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
6758
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/variables/${encodeGitLabPathSegment(key)}`);
6865
6759
  if (filter?.environment_scope) {
6866
6760
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6867
6761
  }
@@ -6869,22 +6763,16 @@ async function deleteProjectVariable(projectId, key, filter) {
6869
6763
  await handleGitLabError(response);
6870
6764
  }
6871
6765
  async function listGroupVariables(groupId, options = {}) {
6872
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6766
+ const encoded = encodeGitLabPathSegment(groupId);
6873
6767
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables`);
6874
6768
  Object.entries(options).forEach(([key, value]) => {
6875
- if (value === undefined)
6876
- return;
6877
6769
  if (key === "filter" && typeof value === "object" && value !== null) {
6878
- Object.entries(value).forEach(([fKey, fVal]) => {
6879
- url.searchParams.append(`filter[${fKey}]`, fVal);
6770
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
6771
+ appendFilterParam(url.searchParams, `filter[${filterKey}]`, filterValue);
6880
6772
  });
6773
+ return;
6881
6774
  }
6882
- else if (typeof value === "boolean") {
6883
- url.searchParams.append(key, value ? "true" : "false");
6884
- }
6885
- else {
6886
- url.searchParams.append(key, String(value));
6887
- }
6775
+ appendFilterParam(url.searchParams, key, value);
6888
6776
  });
6889
6777
  const response = await fetch(url.toString(), getFetchConfig());
6890
6778
  await handleGitLabError(response);
@@ -6892,8 +6780,8 @@ async function listGroupVariables(groupId, options = {}) {
6892
6780
  return z.array(GitLabCiVariableSchema).parse(data);
6893
6781
  }
6894
6782
  async function getGroupVariable(groupId, key, filter) {
6895
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6896
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
6783
+ const encoded = encodeGitLabPathSegment(groupId);
6784
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeGitLabPathSegment(key)}`);
6897
6785
  if (filter?.environment_scope) {
6898
6786
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6899
6787
  }
@@ -6903,7 +6791,7 @@ async function getGroupVariable(groupId, key, filter) {
6903
6791
  return GitLabCiVariableSchema.parse(data);
6904
6792
  }
6905
6793
  async function createGroupVariable(groupId, options) {
6906
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6794
+ const encoded = encodeGitLabPathSegment(groupId);
6907
6795
  const response = await fetch(`${getEffectiveApiUrl()}/groups/${encoded}/variables`, {
6908
6796
  ...getFetchConfig(),
6909
6797
  method: "POST",
@@ -6914,9 +6802,9 @@ async function createGroupVariable(groupId, options) {
6914
6802
  return GitLabCiVariableSchema.parse(data);
6915
6803
  }
6916
6804
  async function updateGroupVariable(groupId, key, options) {
6917
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6805
+ const encoded = encodeGitLabPathSegment(groupId);
6918
6806
  const { filter, ...body } = options;
6919
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
6807
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeGitLabPathSegment(key)}`);
6920
6808
  if (filter?.environment_scope) {
6921
6809
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6922
6810
  }
@@ -6930,8 +6818,8 @@ async function updateGroupVariable(groupId, key, options) {
6930
6818
  return GitLabCiVariableSchema.parse(data);
6931
6819
  }
6932
6820
  async function deleteGroupVariable(groupId, key, filter) {
6933
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6934
- const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
6821
+ const encoded = encodeGitLabPathSegment(groupId);
6822
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeGitLabPathSegment(key)}`);
6935
6823
  if (filter?.environment_scope) {
6936
6824
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
6937
6825
  }
@@ -7017,7 +6905,7 @@ async function listDependencyProxyBlobs(groupPath, options = {}) {
7017
6905
  };
7018
6906
  }
7019
6907
  async function purgeDependencyProxyCache(groupId) {
7020
- const encoded = encodeURIComponent(decodeURIComponent(groupId));
6908
+ const encoded = encodeGitLabPathSegment(groupId);
7021
6909
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/dependency_proxy/cache`);
7022
6910
  const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
7023
6911
  await handleGitLabError(response);
@@ -7047,7 +6935,7 @@ async function resolveProjectFullPath(projectId) {
7047
6935
  if (!/^\d+$/.test(decoded)) {
7048
6936
  return decoded;
7049
6937
  }
7050
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(decoded)}`);
6938
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(decoded)}`);
7051
6939
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7052
6940
  await handleGitLabError(response);
7053
6941
  const project = (await response.json());
@@ -7358,7 +7246,7 @@ async function markdownUpload(projectId, filePath, content, filename) {
7358
7246
  // Create form data
7359
7247
  const form = new FormData();
7360
7248
  form.append("file", new File([fileBuffer], fileName, { type: "application/octet-stream" }));
7361
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
7249
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/uploads`);
7362
7250
  const defaultFetchConfig = getFetchConfig();
7363
7251
  delete defaultFetchConfig.headers["Content-Type"];
7364
7252
  const response = await fetch(url.toString(), {
@@ -7399,11 +7287,8 @@ function getSafeAttachmentFilename(filename) {
7399
7287
  async function downloadAttachment(projectId, secret, filename, localPath) {
7400
7288
  const effectiveProjectId = getEffectiveProjectId(projectId);
7401
7289
  const safeFilename = getSafeAttachmentFilename(filename);
7402
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
7403
- const response = await fetch(url.toString(), {
7404
- ...getFetchConfig(),
7405
- method: "GET",
7406
- });
7290
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
7291
+ const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
7407
7292
  if (!response.ok) {
7408
7293
  await handleGitLabError(response);
7409
7294
  }
@@ -7431,11 +7316,7 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
7431
7316
  async function listEvents(options = {}) {
7432
7317
  const url = new URL(`${getEffectiveApiUrl()}/events`);
7433
7318
  // Add all query parameters
7434
- Object.entries(options).forEach(([key, value]) => {
7435
- if (value !== undefined) {
7436
- url.searchParams.append(key, value.toString());
7437
- }
7438
- });
7319
+ appendFilterParams(url.searchParams, options);
7439
7320
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7440
7321
  if (!response.ok) {
7441
7322
  await handleGitLabError(response);
@@ -7451,13 +7332,9 @@ async function listEvents(options = {}) {
7451
7332
  */
7452
7333
  async function getProjectEvents(projectId, options = {}) {
7453
7334
  const effectiveProjectId = getEffectiveProjectId(projectId);
7454
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/events`);
7335
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/events`);
7455
7336
  // Add all query parameters
7456
- Object.entries(options).forEach(([key, value]) => {
7457
- if (value !== undefined) {
7458
- url.searchParams.append(key, value.toString());
7459
- }
7460
- });
7337
+ appendFilterParams(url.searchParams, options);
7461
7338
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7462
7339
  if (!response.ok) {
7463
7340
  await handleGitLabError(response);
@@ -7474,13 +7351,9 @@ async function getProjectEvents(projectId, options = {}) {
7474
7351
  */
7475
7352
  async function listReleases(projectId, options = {}) {
7476
7353
  const effectiveProjectId = getEffectiveProjectId(projectId);
7477
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases`);
7354
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases`);
7478
7355
  // Add query parameters
7479
- Object.entries(options).forEach(([key, value]) => {
7480
- if (value !== undefined) {
7481
- url.searchParams.append(key, value.toString());
7482
- }
7483
- });
7356
+ appendFilterParams(url.searchParams, options);
7484
7357
  const response = await fetch(url.toString(), {
7485
7358
  ...getFetchConfig(),
7486
7359
  });
@@ -7498,7 +7371,7 @@ async function listReleases(projectId, options = {}) {
7498
7371
  */
7499
7372
  async function getRelease(projectId, tagName, includeHtmlDescription) {
7500
7373
  const effectiveProjectId = getEffectiveProjectId(projectId);
7501
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`);
7374
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`);
7502
7375
  if (includeHtmlDescription !== undefined) {
7503
7376
  url.searchParams.append("include_html_description", includeHtmlDescription.toString());
7504
7377
  }
@@ -7518,7 +7391,7 @@ async function getRelease(projectId, tagName, includeHtmlDescription) {
7518
7391
  */
7519
7392
  async function createRelease(projectId, options) {
7520
7393
  const effectiveProjectId = getEffectiveProjectId(projectId);
7521
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases`, {
7394
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases`, {
7522
7395
  ...getFetchConfig(),
7523
7396
  method: "POST",
7524
7397
  body: JSON.stringify(options),
@@ -7537,7 +7410,7 @@ async function createRelease(projectId, options) {
7537
7410
  */
7538
7411
  async function updateRelease(projectId, tagName, options) {
7539
7412
  const effectiveProjectId = getEffectiveProjectId(projectId);
7540
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7413
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7541
7414
  ...getFetchConfig(),
7542
7415
  method: "PUT",
7543
7416
  body: JSON.stringify(options),
@@ -7555,7 +7428,7 @@ async function updateRelease(projectId, tagName, options) {
7555
7428
  */
7556
7429
  async function deleteRelease(projectId, tagName) {
7557
7430
  const effectiveProjectId = getEffectiveProjectId(projectId);
7558
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7431
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
7559
7432
  ...getFetchConfig(),
7560
7433
  method: "DELETE",
7561
7434
  });
@@ -7571,7 +7444,7 @@ async function deleteRelease(projectId, tagName) {
7571
7444
  */
7572
7445
  async function createReleaseEvidence(projectId, tagName) {
7573
7446
  const effectiveProjectId = getEffectiveProjectId(projectId);
7574
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/evidence`, {
7447
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/evidence`, {
7575
7448
  ...getFetchConfig(),
7576
7449
  method: "POST",
7577
7450
  });
@@ -7587,9 +7460,7 @@ async function createReleaseEvidence(projectId, tagName) {
7587
7460
  */
7588
7461
  async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
7589
7462
  const effectiveProjectId = getEffectiveProjectId(projectId);
7590
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/downloads/${encodeGitLabPath(directAssetPath)}`, {
7591
- ...getFetchConfig(),
7592
- });
7463
+ const response = await fetchWithValidatedRedirects(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/downloads/${encodeGitLabPath(directAssetPath)}`, downloadRedirectOptions());
7593
7464
  await handleGitLabError(response);
7594
7465
  return await response.text();
7595
7466
  }
@@ -7602,12 +7473,8 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
7602
7473
  */
7603
7474
  async function listTags(projectId, options = {}) {
7604
7475
  const effectiveProjectId = getEffectiveProjectId(projectId);
7605
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`);
7606
- Object.entries(options).forEach(([key, value]) => {
7607
- if (value !== undefined) {
7608
- url.searchParams.append(key, value.toString());
7609
- }
7610
- });
7476
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags`);
7477
+ appendFilterParams(url.searchParams, options);
7611
7478
  const response = await fetch(url.toString(), {
7612
7479
  ...getFetchConfig(),
7613
7480
  });
@@ -7624,7 +7491,7 @@ async function listTags(projectId, options = {}) {
7624
7491
  */
7625
7492
  async function getTag(projectId, tagName) {
7626
7493
  const effectiveProjectId = getEffectiveProjectId(projectId);
7627
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}`, {
7494
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags/${encodeGitLabPathSegment(tagName)}`, {
7628
7495
  ...getFetchConfig(),
7629
7496
  });
7630
7497
  await handleGitLabError(response);
@@ -7640,7 +7507,7 @@ async function getTag(projectId, tagName) {
7640
7507
  */
7641
7508
  async function createTag(projectId, options) {
7642
7509
  const effectiveProjectId = getEffectiveProjectId(projectId);
7643
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`, {
7510
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags`, {
7644
7511
  ...getFetchConfig(),
7645
7512
  method: "POST",
7646
7513
  body: JSON.stringify(options),
@@ -7657,7 +7524,7 @@ async function createTag(projectId, options) {
7657
7524
  */
7658
7525
  async function deleteTag(projectId, tagName) {
7659
7526
  const effectiveProjectId = getEffectiveProjectId(projectId);
7660
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}`, {
7527
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags/${encodeGitLabPathSegment(tagName)}`, {
7661
7528
  ...getFetchConfig(),
7662
7529
  method: "DELETE",
7663
7530
  });
@@ -7672,7 +7539,7 @@ async function deleteTag(projectId, tagName) {
7672
7539
  */
7673
7540
  async function getTagSignature(projectId, tagName) {
7674
7541
  const effectiveProjectId = getEffectiveProjectId(projectId);
7675
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}/signature`, {
7542
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/repository/tags/${encodeGitLabPathSegment(tagName)}/signature`, {
7676
7543
  ...getFetchConfig(),
7677
7544
  });
7678
7545
  await handleGitLabError(response);
@@ -7736,7 +7603,7 @@ async function handleToolCall(params) {
7736
7603
  if (!isToolAllowedByPermissionMode(params.name)) {
7737
7604
  throw new Error(GITLAB_PERMISSION_MODE === "readonly"
7738
7605
  ? `${params.name} is not allowed in read-only mode`
7739
- : `${params.name} is not allowed in modify mode (delete operations are disabled)`);
7606
+ : `${params.name} is not allowed in modify mode (delete and teardown operations are disabled)`);
7740
7607
  }
7741
7608
  logger.info({ tool: params.name, event: "tool_call_start" }, `tool_call_start: ${params.name}`);
7742
7609
  switch (params.name) {
@@ -7749,7 +7616,7 @@ async function handleToolCall(params) {
7749
7616
  }
7750
7617
  if (GITLAB_PERMISSION_MODE === "modify" &&
7751
7618
  graphqlQueryContainsDeleteOperation(args.query)) {
7752
- throw new Error("execute_graphql does not allow delete mutations in modify mode");
7619
+ throw new Error("execute_graphql does not allow destructive mutations in modify mode");
7753
7620
  }
7754
7621
  const apiUrl = new URL(getEffectiveApiUrl());
7755
7622
  // Build GraphQL endpoint preserving any instance subpath (e.g. /gitlab)
@@ -8339,7 +8206,7 @@ async function handleToolCall(params) {
8339
8206
  }
8340
8207
  case "get_namespace": {
8341
8208
  const args = GetNamespaceSchema.parse(params.arguments);
8342
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`);
8209
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeGitLabPathSegment(args.namespace_id)}`);
8343
8210
  const response = await fetch(url.toString(), {
8344
8211
  ...getFetchConfig(),
8345
8212
  });
@@ -8352,7 +8219,7 @@ async function handleToolCall(params) {
8352
8219
  }
8353
8220
  case "verify_namespace": {
8354
8221
  const args = VerifyNamespaceSchema.parse(params.arguments);
8355
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
8222
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeGitLabPathSegment(args.path)}/exists`);
8356
8223
  if (args.parent_id !== undefined)
8357
8224
  url.searchParams.set("parent_id", String(args.parent_id));
8358
8225
  const response = await fetch(url.toString(), {
@@ -8369,7 +8236,7 @@ async function handleToolCall(params) {
8369
8236
  const args = GetProjectSchema.parse(params.arguments);
8370
8237
  const options = params.arguments;
8371
8238
  const effectiveProjectId = getEffectiveProjectId(args.project_id);
8372
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
8239
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
8373
8240
  if (options.license)
8374
8241
  url.searchParams.append("license", "true");
8375
8242
  if (options.statistics)
@@ -8398,7 +8265,7 @@ async function handleToolCall(params) {
8398
8265
  const { project_id, ...updates } = UpdateProjectSchema.parse(params.arguments);
8399
8266
  const effectiveProjectId = getEffectiveProjectId(project_id);
8400
8267
  const body = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== undefined));
8401
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`, {
8268
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`, {
8402
8269
  ...getFetchConfig(),
8403
8270
  method: "PUT",
8404
8271
  body: JSON.stringify(body),
@@ -8437,7 +8304,7 @@ async function handleToolCall(params) {
8437
8304
  }
8438
8305
  case "get_user": {
8439
8306
  const args = GetUserSchema.parse(params.arguments);
8440
- const url = new URL(`${getEffectiveApiUrl()}/users/${encodeURIComponent(args.user_id)}`);
8307
+ const url = new URL(`${getEffectiveApiUrl()}/users/${encodeGitLabPathSegment(args.user_id)}`);
8441
8308
  const response = await fetch(url.toString(), {
8442
8309
  ...getFetchConfig(),
8443
8310
  });
@@ -8847,8 +8714,8 @@ async function handleToolCall(params) {
8847
8714
  };
8848
8715
  }
8849
8716
  case "list_labels": {
8850
- const args = ListLabelsSchema.parse(params.arguments);
8851
- const labels = await listLabels(args.project_id, args);
8717
+ const { project_id, ...options } = ListLabelsSchema.parse(params.arguments);
8718
+ const labels = await listLabels(project_id, options);
8852
8719
  return {
8853
8720
  content: [{ type: "text", text: JSON.stringify(labels) }],
8854
8721
  };
@@ -10317,7 +10184,7 @@ async function handleToolCall(params) {
10317
10184
  case "get_branch": {
10318
10185
  const args = GetBranchSchema.parse(params.arguments);
10319
10186
  const projectId = decodeURIComponent(args.project_id);
10320
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches/${encodeURIComponent(args.branch_name)}`);
10187
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/branches/${encodeGitLabPathSegment(args.branch_name)}`);
10321
10188
  const response = await fetch(url.toString(), {
10322
10189
  ...getFetchConfig(),
10323
10190
  });
@@ -10331,7 +10198,7 @@ async function handleToolCall(params) {
10331
10198
  case "list_branches": {
10332
10199
  const args = ListBranchesSchema.parse(params.arguments);
10333
10200
  const projectId = decodeURIComponent(args.project_id);
10334
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches`);
10201
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/branches`);
10335
10202
  if (args.search) {
10336
10203
  url.searchParams.append("search", args.search);
10337
10204
  }
@@ -10354,7 +10221,7 @@ async function handleToolCall(params) {
10354
10221
  case "delete_branch": {
10355
10222
  const args = DeleteBranchSchema.parse(params.arguments);
10356
10223
  const projectId = decodeURIComponent(args.project_id);
10357
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches/${encodeURIComponent(args.branch_name)}`);
10224
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/repository/branches/${encodeGitLabPathSegment(args.branch_name)}`);
10358
10225
  const response = await fetch(url.toString(), {
10359
10226
  ...getFetchConfig(),
10360
10227
  method: "DELETE",
@@ -10368,7 +10235,7 @@ async function handleToolCall(params) {
10368
10235
  const args = ListProtectedBranchesSchema.parse(params.arguments);
10369
10236
  const projectId = decodeURIComponent(args.project_id);
10370
10237
  const effectiveProjectId = getEffectiveProjectId(projectId);
10371
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches`);
10238
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches`);
10372
10239
  if (args.search)
10373
10240
  url.searchParams.append("search", args.search);
10374
10241
  if (args.page)
@@ -10388,7 +10255,7 @@ async function handleToolCall(params) {
10388
10255
  const args = GetProtectedBranchSchema.parse(params.arguments);
10389
10256
  const projectId = decodeURIComponent(args.project_id);
10390
10257
  const effectiveProjectId = getEffectiveProjectId(projectId);
10391
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches/${encodeURIComponent(args.branch_name)}`);
10258
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches/${encodeGitLabPathSegment(args.branch_name)}`);
10392
10259
  const response = await fetch(url.toString(), {
10393
10260
  ...getFetchConfig(),
10394
10261
  });
@@ -10402,7 +10269,7 @@ async function handleToolCall(params) {
10402
10269
  const args = ProtectBranchSchema.parse(params.arguments);
10403
10270
  const projectId = decodeURIComponent(args.project_id);
10404
10271
  const effectiveProjectId = getEffectiveProjectId(projectId);
10405
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches`);
10272
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches`);
10406
10273
  const body = { name: args.branch_name };
10407
10274
  if (args.push_access_level !== undefined)
10408
10275
  body.push_access_level = args.push_access_level;
@@ -10429,7 +10296,7 @@ async function handleToolCall(params) {
10429
10296
  const args = UnprotectBranchSchema.parse(params.arguments);
10430
10297
  const projectId = decodeURIComponent(args.project_id);
10431
10298
  const effectiveProjectId = getEffectiveProjectId(projectId);
10432
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches/${encodeURIComponent(args.branch_name)}`);
10299
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_branches/${encodeGitLabPathSegment(args.branch_name)}`);
10433
10300
  const response = await fetch(url.toString(), {
10434
10301
  ...getFetchConfig(),
10435
10302
  method: "DELETE",
@@ -10443,7 +10310,7 @@ async function handleToolCall(params) {
10443
10310
  const args = UpdateDefaultBranchSchema.parse(params.arguments);
10444
10311
  const projectId = decodeURIComponent(args.project_id);
10445
10312
  const effectiveProjectId = getEffectiveProjectId(projectId);
10446
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
10313
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}`);
10447
10314
  const response = await fetch(url.toString(), {
10448
10315
  ...getFetchConfig(),
10449
10316
  method: "PUT",
@@ -10500,6 +10367,7 @@ function buildDownloadProxyDeps() {
10500
10367
  getDispatcherForUrl: clientPool.getDispatcherForUrl.bind(clientPool),
10501
10368
  fetch: undiciFetch,
10502
10369
  logger,
10370
+ isTrustedRedirectHost: isTrustedGitLabRedirectHost,
10503
10371
  };
10504
10372
  }
10505
10373
  /**
@@ -10561,9 +10429,68 @@ async function startSSEServer() {
10561
10429
  }
10562
10430
  next();
10563
10431
  };
10564
- const transports = {};
10432
+ // Session IDs are untrusted map keys (POST /messages?sessionId=). A null
10433
+ // prototype prevents inherited names such as "constructor" from masquerading
10434
+ // as live transports, matching the Streamable HTTP transport.
10435
+ const transports = Object.create(null);
10436
+ const sessionLastActivity = new Map();
10565
10437
  let shuttingDown = false;
10566
- app.get("/sse", rejectDnsRebinding, requireSseAuth, async (req, res) => {
10438
+ // Mirror the Streamable HTTP controls so the SSE transport cannot be used to
10439
+ // exhaust the process: a session cap, a per-IP creation rate limit and an idle
10440
+ // timeout that closes connections which never send anything.
10441
+ const maxSessions = Number.parseInt(process.env.MAX_SESSIONS || "1000", 10);
10442
+ const maxSseConnectionsPerMinute = Number.parseInt(process.env.MAX_REQUESTS_PER_MINUTE || "60", 10);
10443
+ const idleTimeoutMs = Math.max(SESSION_TIMEOUT_SECONDS, 1) * 1000;
10444
+ const sseConnectionRateLimit = rateLimit({
10445
+ windowMs: 60_000,
10446
+ limit: maxSseConnectionsPerMinute,
10447
+ keyGenerator: (req) => ipKeyGenerator(normalizeProxyClientIpForRateLimit(req.ip ?? "")),
10448
+ standardHeaders: true,
10449
+ legacyHeaders: false,
10450
+ handler: (_req, res) => {
10451
+ res.status(429).json({
10452
+ error: "Rate limit exceeded",
10453
+ message: `Maximum ${maxSseConnectionsPerMinute} SSE connections per minute allowed`,
10454
+ });
10455
+ },
10456
+ });
10457
+ /**
10458
+ * Reclaim SSE sessions that have not received a POST /messages request within
10459
+ * the idle timeout.
10460
+ *
10461
+ * On /sse it is correct to idle-close an open GET: the stream alone is not
10462
+ * proof of a live client, so a client that connects and never sends anything
10463
+ * would otherwise hold a MAX_SESSIONS slot forever. This intentionally
10464
+ * differs from the Streamable HTTP GET /mcp handler, which pauses inactivity
10465
+ * expiry while its stream is open so list_changed can still be pushed - do
10466
+ * not copy that rule here.
10467
+ */
10468
+ const closeIdleSseSessions = () => {
10469
+ const now = Date.now();
10470
+ for (const [sessionId, lastActivity] of sessionLastActivity) {
10471
+ if (now - lastActivity < idleTimeoutMs)
10472
+ continue;
10473
+ const transport = transports[sessionId];
10474
+ delete transports[sessionId];
10475
+ sessionLastActivity.delete(sessionId);
10476
+ logger.info({ sessionId, event: "sse_session_idle_closed" }, `SSE session ${sessionId} closed after ${SESSION_TIMEOUT_SECONDS}s of inactivity`);
10477
+ if (transport) {
10478
+ void transport.close().catch(error => {
10479
+ logger.error({ err: error }, "Error closing idle SSE transport");
10480
+ });
10481
+ }
10482
+ }
10483
+ };
10484
+ const idleSweep = setInterval(closeIdleSseSessions, Math.min(idleTimeoutMs, 60_000));
10485
+ idleSweep.unref();
10486
+ app.get("/sse", rejectDnsRebinding, requireSseAuth, sseConnectionRateLimit, async (req, res) => {
10487
+ if (Object.keys(transports).length >= maxSessions) {
10488
+ res.status(503).json({
10489
+ error: "Server capacity reached",
10490
+ message: `Maximum ${maxSessions} concurrent sessions allowed. Please try again later.`,
10491
+ });
10492
+ return;
10493
+ }
10567
10494
  const serverInstance = createServer();
10568
10495
  const effectiveHosts = getEffectiveAllowedHosts(req);
10569
10496
  const transport = new SSEServerTransport("/messages", res, {
@@ -10571,16 +10498,38 @@ async function startSSEServer() {
10571
10498
  allowedHosts: [...effectiveHosts],
10572
10499
  allowedOrigins: [...effectiveHosts].flatMap(host => [`http://${host}`, `https://${host}`]),
10573
10500
  });
10574
- transports[transport.sessionId] = transport;
10501
+ const sessionId = transport.sessionId;
10502
+ transports[sessionId] = transport;
10503
+ sessionLastActivity.set(sessionId, Date.now());
10575
10504
  res.on("close", () => {
10576
- delete transports[transport.sessionId];
10505
+ delete transports[sessionId];
10506
+ sessionLastActivity.delete(sessionId);
10577
10507
  });
10578
- await serverInstance.connect(transport);
10508
+ try {
10509
+ await serverInstance.connect(transport);
10510
+ }
10511
+ catch (error) {
10512
+ logger.error({ err: error, sessionId }, "SSE connection error");
10513
+ // Release the capacity slot immediately instead of waiting for the
10514
+ // client to disconnect or the idle sweep to run.
10515
+ delete transports[sessionId];
10516
+ sessionLastActivity.delete(sessionId);
10517
+ if (!res.headersSent) {
10518
+ res.status(500).json({
10519
+ error: "Internal server error",
10520
+ message: error instanceof Error ? error.message : "Unknown error",
10521
+ });
10522
+ }
10523
+ else {
10524
+ res.end();
10525
+ }
10526
+ }
10579
10527
  });
10580
10528
  app.post("/messages", rejectDnsRebinding, requireSseAuth, async (req, res) => {
10581
10529
  const sessionId = req.query.sessionId;
10582
10530
  const transport = transports[sessionId];
10583
10531
  if (transport) {
10532
+ sessionLastActivity.set(sessionId, Date.now());
10584
10533
  await transport.handlePostMessage(req, res);
10585
10534
  }
10586
10535
  else {
@@ -10589,10 +10538,18 @@ async function startSSEServer() {
10589
10538
  });
10590
10539
  registerDownloadProxy(app, buildDownloadProxyDeps());
10591
10540
  app.get("/health", (_, res) => {
10592
- res.status(200).json({
10593
- status: "healthy",
10541
+ const activeSessions = Object.keys(transports).length;
10542
+ const isHealthy = activeSessions < maxSessions;
10543
+ if (!isHealthy) {
10544
+ logger.warn({ activeSessions, maxSessions }, "Health check degraded: active SSE session capacity reached");
10545
+ }
10546
+ res.status(isHealthy ? 200 : 503).json({
10547
+ status: isHealthy ? "healthy" : "degraded",
10594
10548
  version: SERVER_VERSION,
10595
10549
  transport: TransportMode.SSE,
10550
+ activeSessions,
10551
+ maxSessions,
10552
+ uptime: process.uptime(),
10596
10553
  });
10597
10554
  });
10598
10555
  const httpServer = app.listen(Number(PORT), HOST, () => {
@@ -10604,6 +10561,7 @@ async function startSSEServer() {
10604
10561
  return;
10605
10562
  shuttingDown = true;
10606
10563
  logger.info(`${signal} received, shutting down SSE server...`);
10564
+ clearInterval(idleSweep);
10607
10565
  httpServer.close(() => logger.info("SSE HTTP server closed"));
10608
10566
  await Promise.allSettled(Object.values(transports).map(async (transport) => {
10609
10567
  try {