@foundation0/git 1.2.1 → 1.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/mcp/src/cli.ts +9 -5
- package/mcp/src/redaction.ts +207 -0
- package/mcp/src/server.ts +8 -6
- package/package.json +1 -1
- package/src/git-service-api.ts +40 -3
- package/src/issue-dependencies.ts +12 -4
- package/src/platform/config.ts +6 -4
package/mcp/src/cli.ts
CHANGED
|
@@ -41,14 +41,18 @@ if (hasFlag('--help') || hasFlag('-h')) {
|
|
|
41
41
|
process.exit(0)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const owner = getArgValue('--default-owner'
|
|
45
|
-
const repo = getArgValue('--default-repo'
|
|
46
|
-
const host = getArgValue('--gitea-host', process.env.GITEA_HOST
|
|
44
|
+
const owner = getArgValue('--default-owner')?.trim()
|
|
45
|
+
const repo = getArgValue('--default-repo')?.trim()
|
|
46
|
+
const host = getArgValue('--gitea-host', process.env.GITEA_HOST)?.trim()
|
|
47
47
|
const token = process.env.GITEA_TOKEN
|
|
48
48
|
const serverName = getArgValue('--server-name', 'f0-git-mcp')
|
|
49
49
|
const serverVersion = getArgValue('--server-version', '1.0.0')
|
|
50
50
|
const toolsPrefix = getArgValue('--tools-prefix') ?? process.env.MCP_TOOLS_PREFIX
|
|
51
51
|
|
|
52
|
+
if (!host) {
|
|
53
|
+
throw new Error('GITEA_HOST is required. Set process.env.GITEA_HOST or pass --gitea-host.')
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
if (isInsecureHttpUrl(host) && !hasFlag('--allow-insecure-http')) {
|
|
53
57
|
throw new Error(
|
|
54
58
|
'Refusing to send requests to an insecure http:// Gitea host. Use https:// or pass --allow-insecure-http if you really need this for local testing.',
|
|
@@ -63,8 +67,8 @@ void runGitMcpServer({
|
|
|
63
67
|
giteaHost: host,
|
|
64
68
|
giteaToken: token,
|
|
65
69
|
},
|
|
66
|
-
defaultOwner: owner,
|
|
67
|
-
defaultRepo: repo,
|
|
70
|
+
...(owner ? { defaultOwner: owner } : {}),
|
|
71
|
+
...(repo ? { defaultRepo: repo } : {}),
|
|
68
72
|
toolsPrefix,
|
|
69
73
|
}).catch((error) => {
|
|
70
74
|
console.error('Failed to start MCP git server', error)
|
|
@@ -0,0 +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
|
+
}
|
package/mcp/src/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
11
|
+
import { redactSecretsForMcpOutput, redactSecretsInText } from './redaction'
|
|
11
12
|
|
|
12
13
|
type ToolInvocationPayload = {
|
|
13
14
|
args?: unknown[]
|
|
@@ -299,7 +300,7 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
299
300
|
index,
|
|
300
301
|
tool,
|
|
301
302
|
isError: false,
|
|
302
|
-
data,
|
|
303
|
+
data: redactSecretsForMcpOutput(data),
|
|
303
304
|
} as BatchResult
|
|
304
305
|
} catch (error) {
|
|
305
306
|
if (continueOnError) {
|
|
@@ -307,7 +308,7 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
307
308
|
index,
|
|
308
309
|
tool,
|
|
309
310
|
isError: true,
|
|
310
|
-
data: error instanceof Error ? error.message : String(error),
|
|
311
|
+
data: redactSecretsInText(error instanceof Error ? error.message : String(error)),
|
|
311
312
|
} as BatchResult
|
|
312
313
|
}
|
|
313
314
|
throw error
|
|
@@ -320,7 +321,7 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
320
321
|
content: [
|
|
321
322
|
{
|
|
322
323
|
type: 'text',
|
|
323
|
-
text: JSON.stringify(results, null, 2),
|
|
324
|
+
text: JSON.stringify(redactSecretsForMcpOutput(results), null, 2),
|
|
324
325
|
},
|
|
325
326
|
],
|
|
326
327
|
}
|
|
@@ -330,7 +331,7 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
330
331
|
content: [
|
|
331
332
|
{
|
|
332
333
|
type: 'text',
|
|
333
|
-
text: error instanceof Error ? error.message : String(error),
|
|
334
|
+
text: redactSecretsInText(error instanceof Error ? error.message : String(error)),
|
|
334
335
|
},
|
|
335
336
|
],
|
|
336
337
|
}
|
|
@@ -345,11 +346,12 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
345
346
|
|
|
346
347
|
try {
|
|
347
348
|
const result = await invokeTool(tool, request.params.arguments)
|
|
349
|
+
const sanitized = redactSecretsForMcpOutput(result)
|
|
348
350
|
return {
|
|
349
351
|
content: [
|
|
350
352
|
{
|
|
351
353
|
type: 'text',
|
|
352
|
-
text: JSON.stringify(
|
|
354
|
+
text: JSON.stringify(sanitized, null, 2),
|
|
353
355
|
},
|
|
354
356
|
],
|
|
355
357
|
}
|
|
@@ -359,7 +361,7 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
359
361
|
content: [
|
|
360
362
|
{
|
|
361
363
|
type: 'text',
|
|
362
|
-
text: error instanceof Error ? error.message : String(error),
|
|
364
|
+
text: redactSecretsInText(error instanceof Error ? error.message : String(error)),
|
|
363
365
|
},
|
|
364
366
|
],
|
|
365
367
|
}
|
package/package.json
CHANGED
package/src/git-service-api.ts
CHANGED
|
@@ -218,6 +218,22 @@ const buildUrl = (
|
|
|
218
218
|
return url.toString()
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
const unresolvedPathParamPattern = /^\{[^{}]+\}$/
|
|
222
|
+
|
|
223
|
+
const assertResolvedMappedPath = (
|
|
224
|
+
mappedPath: string[],
|
|
225
|
+
featurePath: string[],
|
|
226
|
+
): void => {
|
|
227
|
+
const unresolved = mappedPath.filter((segment) => unresolvedPathParamPattern.test(segment))
|
|
228
|
+
if (unresolved.length === 0) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Missing required path arguments for "${featurePath.join('.')}". Unresolved parameters: ${unresolved.join(', ')}`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
221
237
|
const canUseAbortSignalTimeout = (): boolean =>
|
|
222
238
|
typeof AbortSignal !== 'undefined' && typeof (AbortSignal as unknown as { timeout?: unknown }).timeout === 'function'
|
|
223
239
|
|
|
@@ -369,6 +385,7 @@ const createMethod = (
|
|
|
369
385
|
|
|
370
386
|
return segment
|
|
371
387
|
})
|
|
388
|
+
assertResolvedMappedPath(hydratedPath, feature.path)
|
|
372
389
|
|
|
373
390
|
const requestBody = buildRequestBody(mapping.method, bodyOptions, unhandled)
|
|
374
391
|
const headers = {
|
|
@@ -530,8 +547,8 @@ export const createGitServiceApi = (options: GitServiceApiFactoryOptions = {}):
|
|
|
530
547
|
const log = options.log
|
|
531
548
|
|
|
532
549
|
const defaults = {
|
|
533
|
-
defaultOwner: options.defaultOwner
|
|
534
|
-
defaultRepo: options.defaultRepo
|
|
550
|
+
defaultOwner: options.defaultOwner,
|
|
551
|
+
defaultRepo: options.defaultRepo,
|
|
535
552
|
}
|
|
536
553
|
|
|
537
554
|
const root: GitServiceApi = {}
|
|
@@ -562,4 +579,24 @@ export const createGitServiceApi = (options: GitServiceApiFactoryOptions = {}):
|
|
|
562
579
|
return root
|
|
563
580
|
}
|
|
564
581
|
|
|
565
|
-
|
|
582
|
+
const createUnavailableGitServiceApi = (error: Error): GitServiceApi => {
|
|
583
|
+
return new Proxy(
|
|
584
|
+
{},
|
|
585
|
+
{
|
|
586
|
+
get: (): never => {
|
|
587
|
+
throw error
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
) as GitServiceApi
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export const gitServiceApi: GitServiceApi = (() => {
|
|
594
|
+
try {
|
|
595
|
+
return createGitServiceApi()
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
598
|
+
return createUnavailableGitServiceApi(
|
|
599
|
+
new Error(`Failed to initialize gitServiceApi singleton: ${message}`),
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
})()
|
|
@@ -2,7 +2,6 @@ import type { GitServiceApiExecutionResult } from './git-service-api'
|
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
3
|
import crypto from 'node:crypto'
|
|
4
4
|
|
|
5
|
-
const DEFAULT_GITEA_HOST = 'https://gitea.example.com'
|
|
6
5
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000
|
|
7
6
|
|
|
8
7
|
const parseRequestTimeoutMs = (value: unknown): number | null => {
|
|
@@ -274,6 +273,14 @@ const resolveGiteaApiBase = (host: string): string => {
|
|
|
274
273
|
return trimmed.endsWith('/api/v1') ? trimmed : `${trimmed}/api/v1`
|
|
275
274
|
}
|
|
276
275
|
|
|
276
|
+
const resolveRequiredGiteaHost = (host: string | undefined): string => {
|
|
277
|
+
const resolved = host?.trim() ?? process.env.GITEA_HOST?.trim()
|
|
278
|
+
if (!resolved) {
|
|
279
|
+
throw new Error('GITEA_HOST is required. Pass host explicitly or set process.env.GITEA_HOST.')
|
|
280
|
+
}
|
|
281
|
+
return resolved
|
|
282
|
+
}
|
|
283
|
+
|
|
277
284
|
const buildIssueDependenciesUrl = (host: string, owner: string, repo: string, issueNumber: number): string => {
|
|
278
285
|
return `${resolveGiteaApiBase(host)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}/dependencies`
|
|
279
286
|
}
|
|
@@ -283,11 +290,12 @@ export async function callIssueDependenciesApi(
|
|
|
283
290
|
owner: string,
|
|
284
291
|
repo: string,
|
|
285
292
|
issueNumber: number,
|
|
286
|
-
host: string
|
|
293
|
+
host: string | undefined,
|
|
287
294
|
token: string | undefined,
|
|
288
295
|
payload?: GitIssueDependencyPayload
|
|
289
296
|
): Promise<GitServiceApiExecutionResult<unknown>> {
|
|
290
|
-
const
|
|
297
|
+
const resolvedHost = resolveRequiredGiteaHost(host)
|
|
298
|
+
const requestUrl = buildIssueDependenciesUrl(resolvedHost, owner, repo, issueNumber)
|
|
291
299
|
const headers = {
|
|
292
300
|
Accept: 'application/json',
|
|
293
301
|
...(token ? { Authorization: `token ${token}` } : {}),
|
|
@@ -343,7 +351,7 @@ export async function callIssueDependenciesApi(
|
|
|
343
351
|
method,
|
|
344
352
|
query: [],
|
|
345
353
|
headers: [],
|
|
346
|
-
apiBase: resolveGiteaApiBase(
|
|
354
|
+
apiBase: resolveGiteaApiBase(resolvedHost),
|
|
347
355
|
swaggerPath: '/repos/{owner}/{repo}/issues/{index}/dependencies',
|
|
348
356
|
mapped: true,
|
|
349
357
|
},
|
package/src/platform/config.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface GitPlatformConfig {
|
|
|
8
8
|
giteaSwaggerPath?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const DEFAULT_GITEA_HOST = 'https://gitea.example.com'
|
|
12
11
|
const DEFAULT_PLATFORM: PlatformName = 'GITEA'
|
|
13
12
|
const DEFAULT_GITEA_SWAGGER_PATH = '/swagger.v1.json'
|
|
14
13
|
|
|
@@ -34,8 +33,7 @@ export const getGitPlatformConfig = (overrides: Partial<GitPlatformConfig> = {})
|
|
|
34
33
|
|
|
35
34
|
const giteaHost =
|
|
36
35
|
overrides.giteaHost ??
|
|
37
|
-
process.env.GITEA_HOST
|
|
38
|
-
DEFAULT_GITEA_HOST
|
|
36
|
+
process.env.GITEA_HOST
|
|
39
37
|
|
|
40
38
|
const giteaToken =
|
|
41
39
|
overrides.giteaToken ??
|
|
@@ -50,9 +48,13 @@ export const getGitPlatformConfig = (overrides: Partial<GitPlatformConfig> = {})
|
|
|
50
48
|
process.env.GITEA_SWAGGER_PATH ??
|
|
51
49
|
DEFAULT_GITEA_SWAGGER_PATH
|
|
52
50
|
|
|
51
|
+
if (!giteaHost || giteaHost.trim().length === 0) {
|
|
52
|
+
throw new Error('GITEA_HOST is required. Set process.env.GITEA_HOST or pass config.giteaHost explicitly.')
|
|
53
|
+
}
|
|
54
|
+
|
|
53
55
|
return {
|
|
54
56
|
platform,
|
|
55
|
-
giteaHost,
|
|
57
|
+
giteaHost: giteaHost.trim(),
|
|
56
58
|
giteaToken,
|
|
57
59
|
giteaApiVersion,
|
|
58
60
|
giteaSwaggerPath,
|