@foundation0/git 1.3.0 → 1.3.2
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.md +291 -291
- package/gitea-swagger.json +28627 -28627
- package/mcp/README.md +262 -247
- package/mcp/cli.mjs +37 -37
- package/mcp/src/cli.ts +76 -76
- package/mcp/src/client.ts +143 -134
- package/mcp/src/index.ts +7 -7
- package/mcp/src/redaction.ts +207 -207
- package/mcp/src/server.ts +2313 -814
- package/package.json +3 -1
- package/src/actions-api.ts +860 -637
- package/src/api.ts +69 -69
- package/src/ci-api.ts +544 -544
- package/src/git-service-api.ts +822 -754
- package/src/git-service-feature-spec.generated.ts +5341 -5341
- package/src/index.ts +55 -55
- package/src/issue-dependencies.ts +533 -533
- package/src/label-management.ts +587 -587
- package/src/platform/config.ts +62 -62
- package/src/platform/gitea-adapter.ts +460 -460
- package/src/platform/gitea-rules.ts +129 -129
- package/src/platform/index.ts +44 -44
- package/src/repository.ts +151 -151
- package/src/spec-mock.ts +45 -45
package/mcp/src/redaction.ts
CHANGED
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
const REDACTED = '[REDACTED]'
|
|
2
|
-
|
|
3
|
-
const normalizeKey = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
4
|
-
|
|
5
|
-
const SENSITIVE_OBJECT_KEYS = new Set(
|
|
6
|
-
[
|
|
7
|
-
'authorization',
|
|
8
|
-
'proxyauthorization',
|
|
9
|
-
'cookie',
|
|
10
|
-
'setcookie',
|
|
11
|
-
'xapikey',
|
|
12
|
-
'xauthtoken',
|
|
13
|
-
'xaccesstoken',
|
|
14
|
-
'apikey',
|
|
15
|
-
'accesstoken',
|
|
16
|
-
'refreshtoken',
|
|
17
|
-
'idtoken',
|
|
18
|
-
'clientsecret',
|
|
19
|
-
'secret',
|
|
20
|
-
'password',
|
|
21
|
-
'passphrase',
|
|
22
|
-
'privatekey',
|
|
23
|
-
'token',
|
|
24
|
-
'session',
|
|
25
|
-
'sessionid',
|
|
26
|
-
].map(normalizeKey),
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
const SENSITIVE_QUERY_KEYS = new Set(
|
|
30
|
-
[
|
|
31
|
-
'access_token',
|
|
32
|
-
'refresh_token',
|
|
33
|
-
'id_token',
|
|
34
|
-
'token',
|
|
35
|
-
'api_key',
|
|
36
|
-
'apikey',
|
|
37
|
-
'key',
|
|
38
|
-
'auth',
|
|
39
|
-
'authorization',
|
|
40
|
-
'client_secret',
|
|
41
|
-
].map(normalizeKey),
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
45
|
-
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
46
|
-
|
|
47
|
-
const isSensitiveObjectKey = (key: string): boolean => SENSITIVE_OBJECT_KEYS.has(normalizeKey(key))
|
|
48
|
-
|
|
49
|
-
const isSensitiveQueryKey = (key: string): boolean => SENSITIVE_QUERY_KEYS.has(normalizeKey(key))
|
|
50
|
-
|
|
51
|
-
const redactQueryEntry = (entry: string): string => {
|
|
52
|
-
const separatorIndex = entry.indexOf('=')
|
|
53
|
-
if (separatorIndex < 0) return entry
|
|
54
|
-
|
|
55
|
-
const key = entry.slice(0, separatorIndex).trim()
|
|
56
|
-
if (!key || !isSensitiveQueryKey(key)) return entry
|
|
57
|
-
|
|
58
|
-
return `${key}=${REDACTED}`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const redactHeaderLine = (entry: string): string => {
|
|
62
|
-
const separatorIndex = entry.indexOf(':')
|
|
63
|
-
if (separatorIndex < 0) return entry
|
|
64
|
-
|
|
65
|
-
const name = entry.slice(0, separatorIndex).trim()
|
|
66
|
-
if (!name || !isSensitiveObjectKey(name)) return entry
|
|
67
|
-
|
|
68
|
-
return `${name}: ${REDACTED}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const redactHeaderRecord = (headers: Record<string, unknown>): Record<string, unknown> => {
|
|
72
|
-
const next: Record<string, unknown> = {}
|
|
73
|
-
|
|
74
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
75
|
-
if (isSensitiveObjectKey(key)) {
|
|
76
|
-
next[key] = REDACTED
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
next[key] = value
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return next
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const redactUrl = (value: string): string => {
|
|
87
|
-
const trimmed = value.trim()
|
|
88
|
-
if (!trimmed) return value
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const url = new URL(trimmed)
|
|
92
|
-
for (const key of Array.from(url.searchParams.keys())) {
|
|
93
|
-
if (isSensitiveQueryKey(key)) {
|
|
94
|
-
url.searchParams.set(key, REDACTED)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return url.toString()
|
|
98
|
-
} catch {
|
|
99
|
-
return value
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const looksLikeHeaderList = (value: unknown[]): value is string[] =>
|
|
104
|
-
value.length === 0 || value.every((entry) => typeof entry === 'string' && entry.includes(':'))
|
|
105
|
-
|
|
106
|
-
const looksLikeQueryList = (value: unknown[]): value is string[] =>
|
|
107
|
-
value.length === 0 || value.every((entry) => typeof entry === 'string' && entry.includes('='))
|
|
108
|
-
|
|
109
|
-
const shouldTreatAsUrlKey = (key: string): boolean => normalizeKey(key) === 'url'
|
|
110
|
-
|
|
111
|
-
const shouldTreatAsHeadersKey = (key: string): boolean => normalizeKey(key) === 'headers'
|
|
112
|
-
|
|
113
|
-
const shouldTreatAsQueryKey = (key: string): boolean => normalizeKey(key) === 'query'
|
|
114
|
-
|
|
115
|
-
export const redactSecretsForMcpOutput = (value: unknown): unknown => {
|
|
116
|
-
const seen = new WeakMap<object, unknown>()
|
|
117
|
-
|
|
118
|
-
const redact = (current: unknown, keyHint?: string): unknown => {
|
|
119
|
-
if (current === null || current === undefined) {
|
|
120
|
-
return current
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (typeof current === 'string') {
|
|
124
|
-
if (keyHint && shouldTreatAsUrlKey(keyHint)) {
|
|
125
|
-
return redactUrl(current)
|
|
126
|
-
}
|
|
127
|
-
return current
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (typeof current !== 'object') {
|
|
131
|
-
return current
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (seen.has(current as object)) {
|
|
135
|
-
return seen.get(current as object)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (Array.isArray(current)) {
|
|
139
|
-
if (keyHint && shouldTreatAsHeadersKey(keyHint) && looksLikeHeaderList(current)) {
|
|
140
|
-
return current.map(redactHeaderLine)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (keyHint && shouldTreatAsQueryKey(keyHint) && looksLikeQueryList(current)) {
|
|
144
|
-
return current.map(redactQueryEntry)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const next = current.map((entry) => redact(entry))
|
|
148
|
-
seen.set(current, next)
|
|
149
|
-
return next
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!isRecord(current)) {
|
|
153
|
-
return current
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (keyHint && shouldTreatAsHeadersKey(keyHint)) {
|
|
157
|
-
const next = redactHeaderRecord(current)
|
|
158
|
-
seen.set(current, next)
|
|
159
|
-
return next
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const next: Record<string, unknown> = {}
|
|
163
|
-
seen.set(current, next)
|
|
164
|
-
|
|
165
|
-
for (const [key, entryValue] of Object.entries(current)) {
|
|
166
|
-
if (isSensitiveObjectKey(key)) {
|
|
167
|
-
next[key] = REDACTED
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (shouldTreatAsHeadersKey(key) && isRecord(entryValue)) {
|
|
172
|
-
next[key] = redactHeaderRecord(entryValue)
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (shouldTreatAsHeadersKey(key) && Array.isArray(entryValue) && looksLikeHeaderList(entryValue)) {
|
|
177
|
-
next[key] = entryValue.map(redactHeaderLine)
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (shouldTreatAsQueryKey(key) && Array.isArray(entryValue) && looksLikeQueryList(entryValue)) {
|
|
182
|
-
next[key] = entryValue.map(redactQueryEntry)
|
|
183
|
-
continue
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
next[key] = redact(entryValue, key)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return next
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return redact(value)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export const redactSecretsInText = (text: string): string => {
|
|
196
|
-
if (!text) return text
|
|
197
|
-
|
|
198
|
-
const redactedHeaders = text.replace(
|
|
199
|
-
/(Authorization|Proxy-Authorization|Cookie|Set-Cookie|X-API-Key|X-Auth-Token|X-Access-Token)\s*:\s*([^\r\n]*)/gi,
|
|
200
|
-
(_match, name: string) => `${name}: ${REDACTED}`,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
return redactedHeaders.replace(
|
|
204
|
-
/(access_token|refresh_token|id_token|token|api_key|apikey|client_secret)\s*=\s*([^&\s]+)/gi,
|
|
205
|
-
(_match, key: string) => `${key}=${REDACTED}`,
|
|
206
|
-
)
|
|
207
|
-
}
|
|
1
|
+
const REDACTED = '[REDACTED]'
|
|
2
|
+
|
|
3
|
+
const normalizeKey = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
4
|
+
|
|
5
|
+
const SENSITIVE_OBJECT_KEYS = new Set(
|
|
6
|
+
[
|
|
7
|
+
'authorization',
|
|
8
|
+
'proxyauthorization',
|
|
9
|
+
'cookie',
|
|
10
|
+
'setcookie',
|
|
11
|
+
'xapikey',
|
|
12
|
+
'xauthtoken',
|
|
13
|
+
'xaccesstoken',
|
|
14
|
+
'apikey',
|
|
15
|
+
'accesstoken',
|
|
16
|
+
'refreshtoken',
|
|
17
|
+
'idtoken',
|
|
18
|
+
'clientsecret',
|
|
19
|
+
'secret',
|
|
20
|
+
'password',
|
|
21
|
+
'passphrase',
|
|
22
|
+
'privatekey',
|
|
23
|
+
'token',
|
|
24
|
+
'session',
|
|
25
|
+
'sessionid',
|
|
26
|
+
].map(normalizeKey),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const SENSITIVE_QUERY_KEYS = new Set(
|
|
30
|
+
[
|
|
31
|
+
'access_token',
|
|
32
|
+
'refresh_token',
|
|
33
|
+
'id_token',
|
|
34
|
+
'token',
|
|
35
|
+
'api_key',
|
|
36
|
+
'apikey',
|
|
37
|
+
'key',
|
|
38
|
+
'auth',
|
|
39
|
+
'authorization',
|
|
40
|
+
'client_secret',
|
|
41
|
+
].map(normalizeKey),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
45
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
46
|
+
|
|
47
|
+
const isSensitiveObjectKey = (key: string): boolean => SENSITIVE_OBJECT_KEYS.has(normalizeKey(key))
|
|
48
|
+
|
|
49
|
+
const isSensitiveQueryKey = (key: string): boolean => SENSITIVE_QUERY_KEYS.has(normalizeKey(key))
|
|
50
|
+
|
|
51
|
+
const redactQueryEntry = (entry: string): string => {
|
|
52
|
+
const separatorIndex = entry.indexOf('=')
|
|
53
|
+
if (separatorIndex < 0) return entry
|
|
54
|
+
|
|
55
|
+
const key = entry.slice(0, separatorIndex).trim()
|
|
56
|
+
if (!key || !isSensitiveQueryKey(key)) return entry
|
|
57
|
+
|
|
58
|
+
return `${key}=${REDACTED}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const redactHeaderLine = (entry: string): string => {
|
|
62
|
+
const separatorIndex = entry.indexOf(':')
|
|
63
|
+
if (separatorIndex < 0) return entry
|
|
64
|
+
|
|
65
|
+
const name = entry.slice(0, separatorIndex).trim()
|
|
66
|
+
if (!name || !isSensitiveObjectKey(name)) return entry
|
|
67
|
+
|
|
68
|
+
return `${name}: ${REDACTED}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const redactHeaderRecord = (headers: Record<string, unknown>): Record<string, unknown> => {
|
|
72
|
+
const next: Record<string, unknown> = {}
|
|
73
|
+
|
|
74
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
75
|
+
if (isSensitiveObjectKey(key)) {
|
|
76
|
+
next[key] = REDACTED
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
next[key] = value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return next
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const redactUrl = (value: string): string => {
|
|
87
|
+
const trimmed = value.trim()
|
|
88
|
+
if (!trimmed) return value
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(trimmed)
|
|
92
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
93
|
+
if (isSensitiveQueryKey(key)) {
|
|
94
|
+
url.searchParams.set(key, REDACTED)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return url.toString()
|
|
98
|
+
} catch {
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const looksLikeHeaderList = (value: unknown[]): value is string[] =>
|
|
104
|
+
value.length === 0 || value.every((entry) => typeof entry === 'string' && entry.includes(':'))
|
|
105
|
+
|
|
106
|
+
const looksLikeQueryList = (value: unknown[]): value is string[] =>
|
|
107
|
+
value.length === 0 || value.every((entry) => typeof entry === 'string' && entry.includes('='))
|
|
108
|
+
|
|
109
|
+
const shouldTreatAsUrlKey = (key: string): boolean => normalizeKey(key) === 'url'
|
|
110
|
+
|
|
111
|
+
const shouldTreatAsHeadersKey = (key: string): boolean => normalizeKey(key) === 'headers'
|
|
112
|
+
|
|
113
|
+
const shouldTreatAsQueryKey = (key: string): boolean => normalizeKey(key) === 'query'
|
|
114
|
+
|
|
115
|
+
export const redactSecretsForMcpOutput = (value: unknown): unknown => {
|
|
116
|
+
const seen = new WeakMap<object, unknown>()
|
|
117
|
+
|
|
118
|
+
const redact = (current: unknown, keyHint?: string): unknown => {
|
|
119
|
+
if (current === null || current === undefined) {
|
|
120
|
+
return current
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof current === 'string') {
|
|
124
|
+
if (keyHint && shouldTreatAsUrlKey(keyHint)) {
|
|
125
|
+
return redactUrl(current)
|
|
126
|
+
}
|
|
127
|
+
return current
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof current !== 'object') {
|
|
131
|
+
return current
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (seen.has(current as object)) {
|
|
135
|
+
return seen.get(current as object)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (Array.isArray(current)) {
|
|
139
|
+
if (keyHint && shouldTreatAsHeadersKey(keyHint) && looksLikeHeaderList(current)) {
|
|
140
|
+
return current.map(redactHeaderLine)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (keyHint && shouldTreatAsQueryKey(keyHint) && looksLikeQueryList(current)) {
|
|
144
|
+
return current.map(redactQueryEntry)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const next = current.map((entry) => redact(entry))
|
|
148
|
+
seen.set(current, next)
|
|
149
|
+
return next
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!isRecord(current)) {
|
|
153
|
+
return current
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (keyHint && shouldTreatAsHeadersKey(keyHint)) {
|
|
157
|
+
const next = redactHeaderRecord(current)
|
|
158
|
+
seen.set(current, next)
|
|
159
|
+
return next
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const next: Record<string, unknown> = {}
|
|
163
|
+
seen.set(current, next)
|
|
164
|
+
|
|
165
|
+
for (const [key, entryValue] of Object.entries(current)) {
|
|
166
|
+
if (isSensitiveObjectKey(key)) {
|
|
167
|
+
next[key] = REDACTED
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (shouldTreatAsHeadersKey(key) && isRecord(entryValue)) {
|
|
172
|
+
next[key] = redactHeaderRecord(entryValue)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (shouldTreatAsHeadersKey(key) && Array.isArray(entryValue) && looksLikeHeaderList(entryValue)) {
|
|
177
|
+
next[key] = entryValue.map(redactHeaderLine)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (shouldTreatAsQueryKey(key) && Array.isArray(entryValue) && looksLikeQueryList(entryValue)) {
|
|
182
|
+
next[key] = entryValue.map(redactQueryEntry)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
next[key] = redact(entryValue, key)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return next
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return redact(value)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const redactSecretsInText = (text: string): string => {
|
|
196
|
+
if (!text) return text
|
|
197
|
+
|
|
198
|
+
const redactedHeaders = text.replace(
|
|
199
|
+
/(Authorization|Proxy-Authorization|Cookie|Set-Cookie|X-API-Key|X-Auth-Token|X-Access-Token)\s*:\s*([^\r\n]*)/gi,
|
|
200
|
+
(_match, name: string) => `${name}: ${REDACTED}`,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return redactedHeaders.replace(
|
|
204
|
+
/(access_token|refresh_token|id_token|token|api_key|apikey|client_secret)\s*=\s*([^&\s]+)/gi,
|
|
205
|
+
(_match, key: string) => `${key}=${REDACTED}`,
|
|
206
|
+
)
|
|
207
|
+
}
|