@amonstack/gitea-mcp 0.3.0 → 0.3.1
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 +40 -11
- package/README.zh-CN.md +31 -9
- package/dist/cli.js +9 -5
- package/dist/cli.js.map +1 -1
- package/dist/credentials.d.ts +145 -0
- package/dist/credentials.js +141 -0
- package/dist/credentials.js.map +1 -0
- package/dist/git-config.d.ts +43 -19
- package/dist/git-config.js +127 -27
- package/dist/git-config.js.map +1 -1
- package/dist/gitea-client.d.ts +54 -2
- package/dist/gitea-client.js +109 -6
- package/dist/gitea-client.js.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.js +16 -5
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +1 -0
- package/dist/tools.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ projects. Set them only to override the discovery.
|
|
|
59
59
|
| Variable | Required | Description |
|
|
60
60
|
|----------|:--------:|-------------|
|
|
61
61
|
| `GITEA_BASE_URL` | No | Gitea instance URL (e.g. `https://gitea.example.com`). Auto-detected from the project's git remote when omitted. |
|
|
62
|
-
| `GITEA_TOKEN` | No | Gitea API access token.
|
|
62
|
+
| `GITEA_TOKEN` | No | Gitea API access token. One of several auth candidates; tried after a `.git/config [gitea]` token and before the git credential store (see [Token discovery](#token-discovery)). |
|
|
63
63
|
| `GITEA_DEFAULT_OWNER` | No | Default repository owner — skip passing `owner` on every call |
|
|
64
64
|
| `GITEA_DEFAULT_REPO` | No | Default repository name — skip passing `repo` on every call |
|
|
65
65
|
|
|
@@ -80,21 +80,48 @@ repository, or set `GITEA_BASE_URL` / `GITEA_TOKEN` explicitly.
|
|
|
80
80
|
|
|
81
81
|
### Token discovery
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
`gitea-mcp` collects authentication **candidates** from three sources, in this
|
|
84
|
+
priority order:
|
|
84
85
|
|
|
85
|
-
1. A `[gitea "<baseUrl>"]` section in `.git/config
|
|
86
|
+
1. A `[gitea "<baseUrl>"]` section in `.git/config` (a bare `[gitea]` section is
|
|
87
|
+
a host-wide fallback):
|
|
86
88
|
```ini
|
|
87
89
|
[gitea "https://gitea.example.com"]
|
|
88
90
|
token = <your-token>
|
|
89
91
|
```
|
|
90
|
-
|
|
91
|
-
2. The
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
Always sent as `Authorization: token <token>`.
|
|
93
|
+
2. The `GITEA_TOKEN` environment variable — also sent as `Authorization: token`.
|
|
94
|
+
3. The git credential store (`~/.git-credentials`, or
|
|
95
|
+
`$XDG_CONFIG_HOME/git/credentials`) — every line whose host matches the
|
|
96
|
+
instance, e.g. `https://alice:s3cret@gitea.example.com`. When several lines
|
|
97
|
+
match, the one whose path best matches `owner/repo` is tried first.
|
|
98
|
+
|
|
99
|
+
A credential-store entry's `password` field may hold a real PAT, an account
|
|
100
|
+
password, or an OAuth token — git stores whatever was typed at the prompt, and
|
|
101
|
+
the server cannot tell them apart statically. So each credential-store entry is
|
|
102
|
+
tried under **two authentication schemes**:
|
|
103
|
+
|
|
104
|
+
- `Authorization: Basic <base64(user:pass)>` — works for account passwords and
|
|
105
|
+
PATs alike (Gitea checks that the username matches the secret's owner).
|
|
106
|
+
- `Authorization: token <secret>` — works only for real PATs.
|
|
107
|
+
|
|
108
|
+
The order is chosen by a username heuristic: a convention username
|
|
109
|
+
(`oauth2`, `x-oauth-basic`, or empty) tries `token` first; a real-looking
|
|
110
|
+
username (e.g. `alice`) tries `basic` first.
|
|
111
|
+
|
|
112
|
+
**Fault tolerance.** On `401`/`403` the server advances to the next
|
|
113
|
+
scheme/candidate and retries the same request; once a combination succeeds it is
|
|
114
|
+
locked for the rest of the session (no re-probing). Non-auth errors (`404`,
|
|
115
|
+
`500`, network) propagate immediately and do **not** trigger a retry.
|
|
116
|
+
|
|
117
|
+
**Diagnostics.** The `gitea_status` tool (see [Repository Helpers](#repository-helpers))
|
|
118
|
+
returns a redacted view of the current state — which candidate is active, which
|
|
119
|
+
are exhausted, the last status seen — without ever exposing the secret. Use it
|
|
120
|
+
to troubleshoot a `401` instead of guessing.
|
|
121
|
+
|
|
122
|
+
If no source resolves a credential, the server still starts anonymously. Public
|
|
123
|
+
repositories may be read; private repos and write operations return `401` — use
|
|
124
|
+
the `gitea-configure` skill to guide setup, or set `GITEA_TOKEN`.
|
|
98
125
|
|
|
99
126
|
When `GITEA_DEFAULT_OWNER` and `GITEA_DEFAULT_REPO` are set, you can omit the
|
|
100
127
|
`owner` and `repo` parameters in tool calls. The `resolve_repo` tool can also
|
|
@@ -234,6 +261,7 @@ gitea-mcp
|
|
|
234
261
|
|------|-------------|
|
|
235
262
|
| `list_my_repos` | List repositories accessible to the authenticated user |
|
|
236
263
|
| `resolve_repo` | Detect `baseUrl`, `owner`, and `repo` from the project's git remotes (`upstream` preferred, then `origin`) |
|
|
264
|
+
| `gitea_status` | Inspect credential-handling state — active candidate, exhausted candidates, last error (redacted; secrets never exposed) |
|
|
237
265
|
|
|
238
266
|
## AI Guidance & Skills
|
|
239
267
|
|
|
@@ -290,6 +318,7 @@ npm ci
|
|
|
290
318
|
| `make test` | Run unit tests |
|
|
291
319
|
| `make test-watch` | Run tests in watch mode |
|
|
292
320
|
| `make test-integration` | Run integration tests (needs live Gitea instance) |
|
|
321
|
+
| `make scan` | Scan for leaked secrets with gitleaks (part of `make verify`) |
|
|
293
322
|
| `make dev` | Run directly with tsx |
|
|
294
323
|
|
|
295
324
|
For the full architecture — module layout, dependency graph, core patterns, and
|
package/README.zh-CN.md
CHANGED
|
@@ -56,7 +56,7 @@ node dist/cli.js
|
|
|
56
56
|
| 变量 | 必填 | 说明 |
|
|
57
57
|
|------|:----:|------|
|
|
58
58
|
| `GITEA_BASE_URL` | 否 | Gitea 实例地址(如 `https://gitea.example.com`)。未设置时从项目 git 远程地址自动推导。 |
|
|
59
|
-
| `GITEA_TOKEN` | 否 | Gitea API
|
|
59
|
+
| `GITEA_TOKEN` | 否 | Gitea API 访问令牌。是多个认证候选之一;排在 `.git/config [gitea]` 令牌之后、git 凭据存储之前(见[令牌发现](#令牌发现))。 |
|
|
60
60
|
| `GITEA_DEFAULT_OWNER` | 否 | 默认仓库所有者,免去每次传入 `owner` 参数 |
|
|
61
61
|
| `GITEA_DEFAULT_REPO` | 否 | 默认仓库名称,免去每次传入 `repo` 参数 |
|
|
62
62
|
|
|
@@ -76,20 +76,40 @@ node dist/cli.js
|
|
|
76
76
|
|
|
77
77
|
### 令牌发现
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
`gitea-mcp` 从三个来源收集认证**候选**,按以下优先级排序:
|
|
80
80
|
|
|
81
|
-
1. `.git/config` 中的 `[gitea "<baseUrl>"]`
|
|
81
|
+
1. `.git/config` 中的 `[gitea "<baseUrl>"]` 段(不带地址的 `[gitea]` 段作为全局兜底):
|
|
82
82
|
```ini
|
|
83
83
|
[gitea "https://gitea.example.com"]
|
|
84
84
|
token = <your-token>
|
|
85
85
|
```
|
|
86
|
-
|
|
87
|
-
2.
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
始终以 `Authorization: token <token>` 发送。
|
|
87
|
+
2. `GITEA_TOKEN` 环境变量 —— 同样以 `Authorization: token` 发送。
|
|
88
|
+
3. git 凭据存储(`~/.git-credentials`,或 `$XDG_CONFIG_HOME/git/credentials`)—— host 匹配实例
|
|
89
|
+
的每一行,如 `https://alice:s3cret@gitea.example.com`。多行同时匹配时,path 与 `owner/repo`
|
|
90
|
+
最贴近的那一行优先尝试。
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
凭据存储条目的 `password` 字段可能是真正的 PAT、账户登录密码,或 OAuth token —— git 存的是
|
|
93
|
+
用户在密码提示里输入的任何内容,服务端无法静态区分。因此每个凭据存储条目会以**两种认证方案**
|
|
94
|
+
依次尝试:
|
|
95
|
+
|
|
96
|
+
- `Authorization: Basic <base64(user:pass)>` —— 账户密码和 PAT 都能通过(Gitea 会校验用户名与
|
|
97
|
+
密钥属主一致)。
|
|
98
|
+
- `Authorization: token <secret>` —— 仅对真正的 PAT 有效。
|
|
99
|
+
|
|
100
|
+
尝试顺序由用户名启发式决定:约定用户名(`oauth2`、`x-oauth-basic`、空)先试 `token`;真实用户名
|
|
101
|
+
(如 `alice`)先试 `basic`。
|
|
102
|
+
|
|
103
|
+
**容错。** 遇到 `401`/`403`,服务端会切换到下一个方案/候选并重试同一请求;一旦某组凭据成功,
|
|
104
|
+
它会被锁定到本会话结束(不再重复探测)。非认证类错误(`404`、`500`、网络错误)立即向上抛出,
|
|
105
|
+
**不会**触发重试。
|
|
106
|
+
|
|
107
|
+
**诊断。** `gitea_status` 工具(见[仓库辅助](#仓库辅助-repository-helpers))返回当前状态的脱敏
|
|
108
|
+
视图 —— 哪个候选处于 active、哪些已耗尽、最近一次的状态码 —— 永不暴露密钥本身。排查 `401` 时
|
|
109
|
+
用它代替盲猜。
|
|
110
|
+
|
|
111
|
+
若所有来源都未解析到凭据,服务器仍会以匿名方式启动。公开仓库可读;私有仓库和写操作返回
|
|
112
|
+
`401` —— 此时使用 `gitea-configure` 技能引导配置,或设置 `GITEA_TOKEN`。
|
|
93
113
|
|
|
94
114
|
设置 `GITEA_DEFAULT_OWNER` 和 `GITEA_DEFAULT_REPO` 后,调用工具时可以省略
|
|
95
115
|
`owner` 和 `repo` 参数。也可以使用 `resolve_repo` 工具自动从本地 git 仓库
|
|
@@ -227,6 +247,7 @@ gitea-mcp
|
|
|
227
247
|
|------|------|
|
|
228
248
|
| `list_my_repos` | 列出当前用户可访问的仓库 |
|
|
229
249
|
| `resolve_repo` | 从项目 git 远程地址检测 `baseUrl`、`owner`、`repo`(优先 `upstream`,回退 `origin`) |
|
|
250
|
+
| `gitea_status` | 查看认证处理状态 —— active 候选、已耗尽候选、最近一次错误(脱敏;永不暴露密钥) |
|
|
230
251
|
|
|
231
252
|
## AI 引导与技能
|
|
232
253
|
|
|
@@ -279,6 +300,7 @@ npm ci
|
|
|
279
300
|
| `make test` | 运行单元测试 |
|
|
280
301
|
| `make test-watch` | 监听模式运行测试 |
|
|
281
302
|
| `make test-integration` | 运行集成测试(需要可用的 Gitea 实例) |
|
|
303
|
+
| `make scan` | 用 gitleaks 扫描泄露的密钥(属于 `make verify`) |
|
|
282
304
|
| `make dev` | 通过 tsx 直接运行 |
|
|
283
305
|
|
|
284
306
|
完整的架构说明(模块布局、依赖关系、核心模式,以及新增工具的指引)请参阅
|
package/dist/cli.js
CHANGED
|
@@ -13,10 +13,14 @@ if (argv[0] === "init") {
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
else {
|
|
16
|
-
// Resolve baseUrl/owner/repo/
|
|
17
|
-
// (`.git/config` remotes + credential store).
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// Resolve baseUrl/owner/repo/credentials from env first, then the local git
|
|
17
|
+
// context (`.git/config` remotes + credential store). Discovery collects ALL
|
|
18
|
+
// credential candidates (config token, env token, credential-store entries)
|
|
19
|
+
// rather than picking one, so the client can fall back across them when one
|
|
20
|
+
// scheme is rejected (e.g. an account password that is not a PAT). When
|
|
21
|
+
// neither env nor any git remote provides a baseUrl, the server is
|
|
22
|
+
// intentionally skipped: a single global install should stay dormant outside
|
|
23
|
+
// of git projects.
|
|
20
24
|
const discovered = await discoverConfig().catch((err) => {
|
|
21
25
|
console.error("Fatal error:", err);
|
|
22
26
|
process.exit(1);
|
|
@@ -25,7 +29,7 @@ else {
|
|
|
25
29
|
console.error(`gitea-mcp: no git remote found in ${process.cwd()} and GITEA_BASE_URL is not set; skipping server start.`);
|
|
26
30
|
process.exit(0);
|
|
27
31
|
}
|
|
28
|
-
runServer(discovered.baseUrl, discovered.
|
|
32
|
+
runServer(discovered.baseUrl, discovered.candidates, discovered.defaultOwner, discovered.defaultRepo).catch((err) => {
|
|
29
33
|
console.error("Fatal error:", err);
|
|
30
34
|
process.exit(1);
|
|
31
35
|
});
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,sEAAsE;IACtE,sDAAsD;IACtD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;IACvD,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACnD,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,CAAC;IACN,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,sEAAsE;IACtE,sDAAsD;IACtD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;IACvD,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACnD,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,CAAC;IACN,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,wEAAwE;IACxE,mEAAmE;IACnE,6EAA6E;IAC7E,mBAAmB;IACnB,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC/D,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CACX,qCAAqC,OAAO,CAAC,GAAG,EAAE,wDAAwD,CAC3G,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,SAAS,CACP,UAAU,CAAC,OAAO,EAClB,UAAU,CAAC,UAAU,EACrB,UAAU,CAAC,YAAY,EACvB,UAAU,CAAC,WAAW,CACvB,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACvB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential candidate model and state machine for fault-tolerant Gitea auth.
|
|
3
|
+
*
|
|
4
|
+
* A discovered credential is treated as an opaque candidate: the `secret`
|
|
5
|
+
* field might be a Gitea Personal Access Token, an account login password,
|
|
6
|
+
* or an OAuth token — the type cannot be determined statically from any
|
|
7
|
+
* source (`~/.git-credentials` stores whatever the user typed into git's
|
|
8
|
+
* password prompt, and `[gitea] token` / `GITEA_TOKEN` are by convention but
|
|
9
|
+
* not guarantee a PAT). The runtime therefore tries each candidate under
|
|
10
|
+
* one or more HTTP auth schemes, advancing on 401/403 until something works
|
|
11
|
+
* or every candidate × scheme is exhausted.
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no I/O, no fetch) so the state machine is unit-tested
|
|
14
|
+
* directly. Mutation is contained to per-candidate state fields; callers
|
|
15
|
+
* never replace the candidates array.
|
|
16
|
+
*
|
|
17
|
+
* SECURITY: `secret` is never logged, never interpolated into error
|
|
18
|
+
* messages, and never surfaced by `summarizeCandidates`. Diagnostic output
|
|
19
|
+
* only ever carries `secretPresent: boolean` and a masked `username`.
|
|
20
|
+
*/
|
|
21
|
+
/** Where a candidate came from. Determines scheme ordering and priority. */
|
|
22
|
+
export type CredentialSource = "gitea-config" | "env" | "credential-store";
|
|
23
|
+
/**
|
|
24
|
+
* HTTP auth scheme. Both are sent via the `Authorization` header.
|
|
25
|
+
* - `token` → `Authorization: token <PAT>` (Gitea's PAT/OAuth-token scheme)
|
|
26
|
+
* - `basic` → `Authorization: Basic base64(<username>:<secret>)` (works for
|
|
27
|
+
* both account passwords and PATs when the username matches the owner)
|
|
28
|
+
*/
|
|
29
|
+
export type AuthScheme = "token" | "basic";
|
|
30
|
+
/**
|
|
31
|
+
* A single candidate credential with its runtime state. The state fields
|
|
32
|
+
* (`status`, `nextSchemeIndex`, `lastTriedScheme`, `lastError`,
|
|
33
|
+
* `activeScheme`) are mutated by the GiteaClient as requests succeed or fail.
|
|
34
|
+
*/
|
|
35
|
+
export interface CandidateCredential {
|
|
36
|
+
source: CredentialSource;
|
|
37
|
+
/** Username from a credential-store URL. Undefined for config/env sources. */
|
|
38
|
+
username?: string;
|
|
39
|
+
/** The secret value (PAT, password, or OAuth token). Never logged. */
|
|
40
|
+
secret: string;
|
|
41
|
+
/**
|
|
42
|
+
* Ordered list of auth schemes to try for this candidate. The first scheme
|
|
43
|
+
* that succeeds is locked in via `activeScheme` and reused for subsequent
|
|
44
|
+
* requests without re-iterating.
|
|
45
|
+
*/
|
|
46
|
+
schemes: AuthScheme[];
|
|
47
|
+
/** Runtime state: pending (untried), active (verified working), exhausted. */
|
|
48
|
+
status: "pending" | "active" | "exhausted";
|
|
49
|
+
/** Index into `schemes` for the next untried scheme. */
|
|
50
|
+
nextSchemeIndex: number;
|
|
51
|
+
/** The scheme that last produced an HTTP error (for diagnostics). */
|
|
52
|
+
lastTriedScheme?: AuthScheme;
|
|
53
|
+
/**
|
|
54
|
+
* The scheme that succeeded and is in active use. Set when status becomes
|
|
55
|
+
* "active"; subsequent requests reuse it without re-iterating.
|
|
56
|
+
*/
|
|
57
|
+
activeScheme?: AuthScheme;
|
|
58
|
+
/**
|
|
59
|
+
* Short error reason from the last failed attempt (e.g. "401"). Never
|
|
60
|
+
* contains the secret or the response body.
|
|
61
|
+
*/
|
|
62
|
+
lastError?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Result of credential discovery — feeds straight into GiteaClient. */
|
|
65
|
+
export interface CredentialDiscoveryResult {
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
defaultOwner?: string;
|
|
68
|
+
defaultRepo?: string;
|
|
69
|
+
/** Name of the remote the values were derived from. */
|
|
70
|
+
remote?: string;
|
|
71
|
+
/** Candidates in priority order (highest priority first). */
|
|
72
|
+
candidates: CandidateCredential[];
|
|
73
|
+
}
|
|
74
|
+
/** A picked next attempt — the candidate index plus the scheme to apply. */
|
|
75
|
+
export interface Attempt {
|
|
76
|
+
candidateIndex: number;
|
|
77
|
+
scheme: AuthScheme;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Decide scheme ordering for a credential-store entry based on username
|
|
81
|
+
* heuristic. Config/env candidates always use `["token"]` (per project
|
|
82
|
+
* decision to preserve their "simple token" semantics).
|
|
83
|
+
*
|
|
84
|
+
* - Username `oauth2`, `x-oauth-basic`, or empty → `["token", "basic"]`:
|
|
85
|
+
* these are git OAuth conventions; basic auth with such a username fails
|
|
86
|
+
* for real passwords, so try `token` first.
|
|
87
|
+
* - Any other (real-looking) username → `["basic", "token"]`: basic auth
|
|
88
|
+
* with the correct username works for both PATs and passwords, so it has
|
|
89
|
+
* the widest coverage and goes first.
|
|
90
|
+
*/
|
|
91
|
+
export declare function orderSchemesForCredentialStore(username?: string): AuthScheme[];
|
|
92
|
+
/**
|
|
93
|
+
* Build the `Authorization` header value for one attempt. Mutates nothing.
|
|
94
|
+
*
|
|
95
|
+
* For `basic`, the username falls back to `oauth2` when absent (a missing
|
|
96
|
+
* username only happens for malformed credential-store entries; Gitea
|
|
97
|
+
* rejects basic auth without a username, so the attempt will fail and the
|
|
98
|
+
* state machine will advance).
|
|
99
|
+
*/
|
|
100
|
+
export declare function buildAuthHeader(candidate: CandidateCredential, scheme: AuthScheme): string;
|
|
101
|
+
/**
|
|
102
|
+
* Pick the next (candidate, scheme) to try. Skips exhausted candidates and
|
|
103
|
+
* candidates whose scheme list is fully tried. Does NOT mutate state — the
|
|
104
|
+
* caller records the attempt via `markAttemptFailed` / `markAttemptSucceeded`.
|
|
105
|
+
*
|
|
106
|
+
* Returns null when every candidate × scheme has been tried.
|
|
107
|
+
*/
|
|
108
|
+
export declare function pickNextAttempt(candidates: CandidateCredential[]): Attempt | null;
|
|
109
|
+
/**
|
|
110
|
+
* Record that an attempt failed with `error` (a short reason like "401").
|
|
111
|
+
* Advances the candidate's scheme index; when no schemes remain, marks the
|
|
112
|
+
* candidate as exhausted.
|
|
113
|
+
*/
|
|
114
|
+
export declare function markAttemptFailed(candidates: CandidateCredential[], candidateIndex: number, error: string): void;
|
|
115
|
+
/**
|
|
116
|
+
* Record that an attempt succeeded. Marks the candidate as active, locks in
|
|
117
|
+
* `activeScheme`, and marks all PRIOR candidates as exhausted (they were
|
|
118
|
+
* tried and failed before this one succeeded).
|
|
119
|
+
*/
|
|
120
|
+
export declare function markAttemptSucceeded(candidates: CandidateCredential[], candidateIndex: number, scheme: AuthScheme): void;
|
|
121
|
+
/**
|
|
122
|
+
* Mask a username for diagnostic output: first character plus `***`. Returns
|
|
123
|
+
* null when the candidate has no username (config/env sources, or malformed
|
|
124
|
+
* credential-store entries).
|
|
125
|
+
*/
|
|
126
|
+
export declare function maskUsername(username?: string): string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Build a redacted summary of all candidates for the `gitea_status` tool.
|
|
129
|
+
* The `secret` value is never included — only `secretPresent: true`. The
|
|
130
|
+
* caller passes the active candidate index (or null) so the summary can flag
|
|
131
|
+
* which candidate is currently in use.
|
|
132
|
+
*/
|
|
133
|
+
export interface CandidateSummary {
|
|
134
|
+
source: CredentialSource;
|
|
135
|
+
schemes: AuthScheme[];
|
|
136
|
+
username: string | null;
|
|
137
|
+
secretPresent: boolean;
|
|
138
|
+
status: "pending" | "active" | "exhausted";
|
|
139
|
+
lastTriedScheme: AuthScheme | null;
|
|
140
|
+
activeScheme: AuthScheme | null;
|
|
141
|
+
lastError: string | null;
|
|
142
|
+
}
|
|
143
|
+
export declare function summarizeCandidates(candidates: CandidateCredential[]): CandidateSummary[];
|
|
144
|
+
/** Find the index of the active candidate, or null when none is active. */
|
|
145
|
+
export declare function findActiveCandidateIndex(candidates: CandidateCredential[]): number | null;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential candidate model and state machine for fault-tolerant Gitea auth.
|
|
3
|
+
*
|
|
4
|
+
* A discovered credential is treated as an opaque candidate: the `secret`
|
|
5
|
+
* field might be a Gitea Personal Access Token, an account login password,
|
|
6
|
+
* or an OAuth token — the type cannot be determined statically from any
|
|
7
|
+
* source (`~/.git-credentials` stores whatever the user typed into git's
|
|
8
|
+
* password prompt, and `[gitea] token` / `GITEA_TOKEN` are by convention but
|
|
9
|
+
* not guarantee a PAT). The runtime therefore tries each candidate under
|
|
10
|
+
* one or more HTTP auth schemes, advancing on 401/403 until something works
|
|
11
|
+
* or every candidate × scheme is exhausted.
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no I/O, no fetch) so the state machine is unit-tested
|
|
14
|
+
* directly. Mutation is contained to per-candidate state fields; callers
|
|
15
|
+
* never replace the candidates array.
|
|
16
|
+
*
|
|
17
|
+
* SECURITY: `secret` is never logged, never interpolated into error
|
|
18
|
+
* messages, and never surfaced by `summarizeCandidates`. Diagnostic output
|
|
19
|
+
* only ever carries `secretPresent: boolean` and a masked `username`.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Decide scheme ordering for a credential-store entry based on username
|
|
23
|
+
* heuristic. Config/env candidates always use `["token"]` (per project
|
|
24
|
+
* decision to preserve their "simple token" semantics).
|
|
25
|
+
*
|
|
26
|
+
* - Username `oauth2`, `x-oauth-basic`, or empty → `["token", "basic"]`:
|
|
27
|
+
* these are git OAuth conventions; basic auth with such a username fails
|
|
28
|
+
* for real passwords, so try `token` first.
|
|
29
|
+
* - Any other (real-looking) username → `["basic", "token"]`: basic auth
|
|
30
|
+
* with the correct username works for both PATs and passwords, so it has
|
|
31
|
+
* the widest coverage and goes first.
|
|
32
|
+
*/
|
|
33
|
+
export function orderSchemesForCredentialStore(username) {
|
|
34
|
+
if (username === undefined)
|
|
35
|
+
return ["token", "basic"];
|
|
36
|
+
const u = username.toLowerCase();
|
|
37
|
+
if (u === "oauth2" || u === "x-oauth-basic") {
|
|
38
|
+
return ["token", "basic"];
|
|
39
|
+
}
|
|
40
|
+
return ["basic", "token"];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the `Authorization` header value for one attempt. Mutates nothing.
|
|
44
|
+
*
|
|
45
|
+
* For `basic`, the username falls back to `oauth2` when absent (a missing
|
|
46
|
+
* username only happens for malformed credential-store entries; Gitea
|
|
47
|
+
* rejects basic auth without a username, so the attempt will fail and the
|
|
48
|
+
* state machine will advance).
|
|
49
|
+
*/
|
|
50
|
+
export function buildAuthHeader(candidate, scheme) {
|
|
51
|
+
if (scheme === "basic") {
|
|
52
|
+
const user = candidate.username ?? "oauth2";
|
|
53
|
+
const encoded = Buffer.from(`${user}:${candidate.secret}`).toString("base64");
|
|
54
|
+
return `Basic ${encoded}`;
|
|
55
|
+
}
|
|
56
|
+
return `token ${candidate.secret}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pick the next (candidate, scheme) to try. Skips exhausted candidates and
|
|
60
|
+
* candidates whose scheme list is fully tried. Does NOT mutate state — the
|
|
61
|
+
* caller records the attempt via `markAttemptFailed` / `markAttemptSucceeded`.
|
|
62
|
+
*
|
|
63
|
+
* Returns null when every candidate × scheme has been tried.
|
|
64
|
+
*/
|
|
65
|
+
export function pickNextAttempt(candidates) {
|
|
66
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
67
|
+
const c = candidates[i];
|
|
68
|
+
if (c.status === "exhausted")
|
|
69
|
+
continue;
|
|
70
|
+
// Active candidates are already locked — the caller short-circuits
|
|
71
|
+
// before entering this iteration loop (GiteaClient.request checks
|
|
72
|
+
// findActiveCandidateIndex first). Here we skip them defensively.
|
|
73
|
+
if (c.status === "active")
|
|
74
|
+
continue;
|
|
75
|
+
if (c.nextSchemeIndex >= c.schemes.length)
|
|
76
|
+
continue;
|
|
77
|
+
return { candidateIndex: i, scheme: c.schemes[c.nextSchemeIndex] };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Record that an attempt failed with `error` (a short reason like "401").
|
|
83
|
+
* Advances the candidate's scheme index; when no schemes remain, marks the
|
|
84
|
+
* candidate as exhausted.
|
|
85
|
+
*/
|
|
86
|
+
export function markAttemptFailed(candidates, candidateIndex, error) {
|
|
87
|
+
const c = candidates[candidateIndex];
|
|
88
|
+
c.lastError = error;
|
|
89
|
+
c.lastTriedScheme = c.schemes[c.nextSchemeIndex];
|
|
90
|
+
c.nextSchemeIndex += 1;
|
|
91
|
+
if (c.nextSchemeIndex >= c.schemes.length || c.status === "active") {
|
|
92
|
+
c.status = "exhausted";
|
|
93
|
+
c.activeScheme = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Record that an attempt succeeded. Marks the candidate as active, locks in
|
|
98
|
+
* `activeScheme`, and marks all PRIOR candidates as exhausted (they were
|
|
99
|
+
* tried and failed before this one succeeded).
|
|
100
|
+
*/
|
|
101
|
+
export function markAttemptSucceeded(candidates, candidateIndex, scheme) {
|
|
102
|
+
for (let i = 0; i < candidateIndex; i++) {
|
|
103
|
+
if (candidates[i].status !== "active")
|
|
104
|
+
candidates[i].status = "exhausted";
|
|
105
|
+
}
|
|
106
|
+
const c = candidates[candidateIndex];
|
|
107
|
+
c.status = "active";
|
|
108
|
+
c.activeScheme = scheme;
|
|
109
|
+
c.lastError = undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Mask a username for diagnostic output: first character plus `***`. Returns
|
|
113
|
+
* null when the candidate has no username (config/env sources, or malformed
|
|
114
|
+
* credential-store entries).
|
|
115
|
+
*/
|
|
116
|
+
export function maskUsername(username) {
|
|
117
|
+
if (!username)
|
|
118
|
+
return null;
|
|
119
|
+
return `${username.charAt(0)}***`;
|
|
120
|
+
}
|
|
121
|
+
export function summarizeCandidates(candidates) {
|
|
122
|
+
return candidates.map((c) => ({
|
|
123
|
+
source: c.source,
|
|
124
|
+
schemes: c.schemes,
|
|
125
|
+
username: maskUsername(c.username),
|
|
126
|
+
secretPresent: c.secret.length > 0,
|
|
127
|
+
status: c.status,
|
|
128
|
+
lastTriedScheme: c.lastTriedScheme ?? null,
|
|
129
|
+
activeScheme: c.activeScheme ?? null,
|
|
130
|
+
lastError: c.lastError ?? null,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
/** Find the index of the active candidate, or null when none is active. */
|
|
134
|
+
export function findActiveCandidateIndex(candidates) {
|
|
135
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
136
|
+
if (candidates[i].status === "active")
|
|
137
|
+
return i;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
|
package/dist/git-config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CredentialDiscoveryResult } from "./credentials.js";
|
|
1
2
|
/** A single parsed git remote. `remote` is the remote name (`origin`, `upstream`, ...). */
|
|
2
3
|
export interface ParsedRemote {
|
|
3
4
|
remote: string;
|
|
@@ -18,15 +19,22 @@ export interface DiscoverOptions {
|
|
|
18
19
|
/** Override credential-store paths (defaults to XDG then `~/.git-credentials`). */
|
|
19
20
|
credentialsPaths?: string[];
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A parsed `~/.git-credentials` line. Both `username` and `password` are
|
|
24
|
+
* URL-decoded. Either may be absent:
|
|
25
|
+
* - `https://user:pass@host` → both present
|
|
26
|
+
* - `https://:pass@host` → only password
|
|
27
|
+
* - `https://tok@host` → only username (git stores a token here when the
|
|
28
|
+
* host accepts one as the "username"; the caller treats it as the secret)
|
|
29
|
+
*
|
|
30
|
+
* `path` is the URL pathname with leading/trailing slashes and any `.git`
|
|
31
|
+
* suffix stripped, used to narrow multiple host matches toward the target repo.
|
|
32
|
+
*/
|
|
33
|
+
export interface ParsedCredentialEntry {
|
|
34
|
+
username?: string;
|
|
35
|
+
password?: string;
|
|
36
|
+
host: string;
|
|
37
|
+
path: string;
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* Parse a git remote URL into host/baseUrl/owner/repo. Accepts `ssh://`, the
|
|
@@ -48,11 +56,18 @@ export declare function selectRemote(remotes: ParsedRemote[]): ParsedRemote | nu
|
|
|
48
56
|
*/
|
|
49
57
|
export declare function readTokenFromGitConfig(content: string, baseUrl: string): string | undefined;
|
|
50
58
|
/**
|
|
51
|
-
*
|
|
52
|
-
* the
|
|
53
|
-
* non-matching lines are skipped.
|
|
59
|
+
* Parse every credential-store line whose host matches. Each line is a URL of
|
|
60
|
+
* the form `protocol://[user[:pass]]@host[:port][/path]`; malformed and
|
|
61
|
+
* non-matching lines are skipped. Returns entries in file order; callers
|
|
62
|
+
* narrow and re-sort by repo path specificity.
|
|
63
|
+
*
|
|
64
|
+
* The `password` field — when present — holds whatever the user typed into
|
|
65
|
+
* git's password prompt: a real account password, a Personal Access Token,
|
|
66
|
+
* or an OAuth token. Git itself does not distinguish, and neither does this
|
|
67
|
+
* parser; the GiteaClient runtime tries each entry under multiple auth
|
|
68
|
+
* schemes to discover what works.
|
|
54
69
|
*/
|
|
55
|
-
export declare function parseGitCredentials(content: string, host: string):
|
|
70
|
+
export declare function parseGitCredentials(content: string, host: string): ParsedCredentialEntry[];
|
|
56
71
|
/** Default credential-store paths: `$XDG_CONFIG_HOME/git/credentials` then `~/.git-credentials`. */
|
|
57
72
|
export declare function defaultCredentialsPaths(): string[];
|
|
58
73
|
/**
|
|
@@ -62,12 +77,21 @@ export declare function defaultCredentialsPaths(): string[];
|
|
|
62
77
|
* remote (`upstream` → `origin` → first). Returns null only when neither is
|
|
63
78
|
* available — callers should treat that as "do not start the server".
|
|
64
79
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
80
|
+
* candidates (in priority order):
|
|
81
|
+
* 1. `[gitea "<baseUrl>"] token` / bare `[gitea] token` from `.git/config`
|
|
82
|
+
* (explicit user configuration; `token` scheme only).
|
|
83
|
+
* 2. `GITEA_TOKEN` env var (explicit env; `token` scheme only — preserves
|
|
84
|
+
* the simple-token semantics).
|
|
85
|
+
* 3. Every host-matching entry in a git credential store, narrowed by repo
|
|
86
|
+
* path specificity (most specific first). Each entry may be a PAT,
|
|
87
|
+
* password, or OAuth token; the client tries each under `basic` and/or
|
|
88
|
+
* `token` schemes per the username heuristic.
|
|
69
89
|
*
|
|
70
90
|
* owner/repo: `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO` (env) win; otherwise
|
|
71
|
-
*
|
|
91
|
+
* taken from the selected remote.
|
|
92
|
+
*
|
|
93
|
+
* The result may have an empty `candidates` array (anonymous mode); the server
|
|
94
|
+
* still starts and a Skill guides the user to provide one. Write tools will
|
|
95
|
+
* fail with 401/403 until a working credential is added.
|
|
72
96
|
*/
|
|
73
|
-
export declare function discoverConfig(options?: DiscoverOptions): Promise<
|
|
97
|
+
export declare function discoverConfig(options?: DiscoverOptions): Promise<CredentialDiscoveryResult | null>;
|