@zereight/mcp-gitlab 2.2.1 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +49 -21
- package/README.md +110 -76
- package/README.pt-BR.md +48 -20
- package/README.zh-CN.md +48 -20
- package/build/auth-cli.js +15 -5
- package/build/cli-boolean-flags.js +34 -0
- package/build/config.js +12 -30
- package/build/deprecated-env.js +158 -28
- package/build/index.js +328 -42
- package/build/oauth-proxy.js +2 -1
- package/build/oauth.js +5 -4
- package/build/schemas.js +97 -0
- package/build/tools/registry.js +54 -3
- package/build/tools/tool-descriptions.js +5 -1
- package/build/utils/health-check-transport-error.js +115 -0
- package/build/utils/logger.js +92 -16
- package/build/utils/redact-sensitive.js +18 -8
- package/build/utils/redact-url.js +141 -0
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const SENSITIVE_QUERY_NAMES = new Set([
|
|
2
|
+
"access_token",
|
|
3
|
+
"private_token",
|
|
4
|
+
"job_token",
|
|
5
|
+
"password",
|
|
6
|
+
"token",
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* Removes userinfo from a GitLab API URL before it is returned to the client.
|
|
10
|
+
* A password with an unencoded slash is not a valid WHATWG URL, so that form
|
|
11
|
+
* uses the same text redaction as transport error messages.
|
|
12
|
+
*/
|
|
13
|
+
export function redactGitLabUrlCredentials(url) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url);
|
|
16
|
+
const hadUserinfo = parsed.username.length > 0 || parsed.password.length > 0;
|
|
17
|
+
if (hadUserinfo) {
|
|
18
|
+
parsed.username = "";
|
|
19
|
+
parsed.password = "";
|
|
20
|
+
}
|
|
21
|
+
const serialized = hadUserinfo ? parsed.toString() : url;
|
|
22
|
+
return redactSensitiveQuery(serialized);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return redactSensitiveQuery(redactUrlUserinfoInText(url));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Log text can contain several URLs. A parseable URL keeps `[REDACTED]@`
|
|
30
|
+
* where userinfo was. redactGitLabUrlCredentials strips that userinfo instead,
|
|
31
|
+
* because the value is returned to the client as a URL.
|
|
32
|
+
*/
|
|
33
|
+
export function redactUrlSecretsInText(text) {
|
|
34
|
+
return redactSensitiveQuery(redactUrlUserinfoInText(redactParseableUrlUserinfo(text)));
|
|
35
|
+
}
|
|
36
|
+
function redactSensitiveQuery(url) {
|
|
37
|
+
return url.replace(/([?&])([^=&\s#]+)=[^&\s#]+/g, (match, separator, rawName) => {
|
|
38
|
+
if (!isSensitiveQueryName(rawName)) {
|
|
39
|
+
return match;
|
|
40
|
+
}
|
|
41
|
+
return `${separator}${rawName}=[REDACTED]`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isSensitiveQueryName(rawName) {
|
|
45
|
+
return SENSITIVE_QUERY_NAMES.has(decodeQueryName(rawName).toLowerCase());
|
|
46
|
+
}
|
|
47
|
+
function decodeQueryName(rawName) {
|
|
48
|
+
try {
|
|
49
|
+
return decodeURIComponent(rawName);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return rawName;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function redactUrlUserinfoInText(message) {
|
|
56
|
+
const withoutCompactUserinfo = message.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s]+/gi, urlToken => redactCompactUserinfo(urlToken));
|
|
57
|
+
return withoutCompactUserinfo.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^\s/:@.]+(?::[^\s/:@.]+)+(?:\s+[^\s/:@.]+)+)@/gi, (match, scheme, userinfo, offset, source) => {
|
|
58
|
+
if (looksLikeHostPort(userinfo, source.slice(offset + match.length))) {
|
|
59
|
+
return match;
|
|
60
|
+
}
|
|
61
|
+
return `${scheme}[REDACTED]@`;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function redactParseableUrlUserinfo(text) {
|
|
65
|
+
const trimmed = text.trim();
|
|
66
|
+
const parsed = tryParseUrl(trimmed);
|
|
67
|
+
if (parsed === null || (parsed.username.length === 0 && parsed.password.length === 0)) {
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
const split = splitAuthorityUserinfo(trimmed);
|
|
71
|
+
if (split === null) {
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
const leadingLength = text.length - text.trimStart().length;
|
|
75
|
+
const trailing = text.slice(text.trimEnd().length);
|
|
76
|
+
return `${text.slice(0, leadingLength)}${split.scheme}[REDACTED]@${split.afterAt}${trailing}`;
|
|
77
|
+
}
|
|
78
|
+
function tryParseUrl(value) {
|
|
79
|
+
try {
|
|
80
|
+
return new URL(value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function splitAuthorityUserinfo(url) {
|
|
87
|
+
// WHATWG special schemes still expose userinfo for "https:user:pass@host".
|
|
88
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*:)(\/\/)?/i.exec(url);
|
|
89
|
+
if (schemeMatch === null) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const schemeEnd = schemeMatch[0].length;
|
|
93
|
+
const rest = url.slice(schemeEnd);
|
|
94
|
+
const slash = rest.indexOf("/");
|
|
95
|
+
const query = rest.indexOf("?");
|
|
96
|
+
const hash = rest.indexOf("#");
|
|
97
|
+
const ends = [slash, query, hash].filter(index => index >= 0);
|
|
98
|
+
const authorityEnd = ends.length === 0 ? rest.length : Math.min(...ends);
|
|
99
|
+
const authority = rest.slice(0, authorityEnd);
|
|
100
|
+
const atIndex = authority.lastIndexOf("@");
|
|
101
|
+
if (atIndex <= 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
scheme: url.slice(0, schemeEnd),
|
|
106
|
+
afterAt: `${authority.slice(atIndex + 1)}${rest.slice(authorityEnd)}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function redactCompactUserinfo(urlToken) {
|
|
110
|
+
const schemeIndex = urlToken.indexOf("://");
|
|
111
|
+
const rest = urlToken.slice(schemeIndex + 3);
|
|
112
|
+
const atIndex = rest.lastIndexOf("@");
|
|
113
|
+
if (atIndex <= 0) {
|
|
114
|
+
return urlToken;
|
|
115
|
+
}
|
|
116
|
+
return `${urlToken.slice(0, schemeIndex + 3)}[REDACTED]@${rest.slice(atIndex + 1)}`;
|
|
117
|
+
}
|
|
118
|
+
// "user:pa ss@host" is a password. "localhost:3000 a@b.com" is a host, then an email.
|
|
119
|
+
// A numeric port counts only when one token follows it and the text after "@" is an email domain.
|
|
120
|
+
function looksLikeHostPort(userinfo, afterAt) {
|
|
121
|
+
const colon = userinfo.indexOf(":");
|
|
122
|
+
if (colon <= 0) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const host = userinfo.slice(0, colon);
|
|
126
|
+
const portMatch = /^([^\s]+)/.exec(userinfo.slice(colon + 1));
|
|
127
|
+
const port = portMatch?.[1] ?? "";
|
|
128
|
+
if (port.length === 0) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (/^\d+$/.test(port)) {
|
|
132
|
+
const remainder = userinfo.slice(colon + 1 + port.length).trim();
|
|
133
|
+
const firstTokenEnd = afterAt.search(/\s/);
|
|
134
|
+
const firstToken = firstTokenEnd === -1 ? afterAt : afterAt.slice(0, firstTokenEnd);
|
|
135
|
+
return (remainder.length > 0 &&
|
|
136
|
+
!/\s/.test(remainder) &&
|
|
137
|
+
firstToken.length > 0 &&
|
|
138
|
+
!/[/:@]/.test(firstToken));
|
|
139
|
+
}
|
|
140
|
+
return host.toLowerCase() === "localhost";
|
|
141
|
+
}
|
package/package.json
CHANGED