@amonstack/gitea-mcp 0.2.2 → 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.
@@ -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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"}
@@ -0,0 +1,97 @@
1
+ import { type CredentialDiscoveryResult } from "./credentials.js";
2
+ /** A single parsed git remote. `remote` is the remote name (`origin`, `upstream`, ...). */
3
+ export interface ParsedRemote {
4
+ remote: string;
5
+ url: string;
6
+ host: string;
7
+ baseUrl: string;
8
+ owner: string;
9
+ repo: string;
10
+ }
11
+ /** A raw `[remote "<name>"]` url entry extracted from a git config file. */
12
+ export interface RawRemote {
13
+ name: string;
14
+ url: string;
15
+ }
16
+ export interface DiscoverOptions {
17
+ cwd?: string;
18
+ env?: NodeJS.ProcessEnv;
19
+ /** Override credential-store paths (defaults to XDG then `~/.git-credentials`). */
20
+ credentialsPaths?: string[];
21
+ }
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;
38
+ }
39
+ /**
40
+ * Parse a git remote URL into host/baseUrl/owner/repo. Accepts `ssh://`, the
41
+ * scp-like `user@host:owner/repo` form, and `http(s)://`. SSH URLs derive an
42
+ * `https://` baseUrl because the Gitea API is served over HTTP(S); a non-standard
43
+ * web port cannot be inferred from an SSH URL — use an HTTPS remote or GITEA_BASE_URL.
44
+ */
45
+ export declare function parseGitRemoteUrl(url: string, remote?: string): ParsedRemote | null;
46
+ /** Extract every `[remote "<name>"]` url entry from a git config file's contents. */
47
+ export declare function readGitRemotes(content: string): RawRemote[];
48
+ /** Parse all remotes in a git config file's contents, dropping unparseable urls. */
49
+ export declare function parseRemotes(content: string): ParsedRemote[];
50
+ /** Pick the remote to derive values from: `upstream` first, then `origin`, then the first. */
51
+ export declare function selectRemote(remotes: ParsedRemote[]): ParsedRemote | null;
52
+ /**
53
+ * Read a Gitea token from a git config. A scoped `[gitea "<baseUrl>"] token = ...`
54
+ * section wins; a bare `[gitea] token = ...` is the fallback. Returns undefined
55
+ * when neither matches.
56
+ */
57
+ export declare function readTokenFromGitConfig(content: string, baseUrl: string): string | undefined;
58
+ /**
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.
69
+ */
70
+ export declare function parseGitCredentials(content: string, host: string): ParsedCredentialEntry[];
71
+ /** Default credential-store paths: `$XDG_CONFIG_HOME/git/credentials` then `~/.git-credentials`. */
72
+ export declare function defaultCredentialsPaths(): string[];
73
+ /**
74
+ * Discover the Gitea connection config from env + the local git context.
75
+ *
76
+ * baseUrl: `GITEA_BASE_URL` (env) wins; otherwise derived from the selected
77
+ * remote (`upstream` → `origin` → first). Returns null only when neither is
78
+ * available — callers should treat that as "do not start the server".
79
+ *
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.
89
+ *
90
+ * owner/repo: `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO` (env) win; otherwise
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.
96
+ */
97
+ export declare function discoverConfig(options?: DiscoverOptions): Promise<CredentialDiscoveryResult | null>;
@@ -0,0 +1,295 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { orderSchemesForCredentialStore, } from "./credentials.js";
5
+ /**
6
+ * Parse a git remote URL into host/baseUrl/owner/repo. Accepts `ssh://`, the
7
+ * scp-like `user@host:owner/repo` form, and `http(s)://`. SSH URLs derive an
8
+ * `https://` baseUrl because the Gitea API is served over HTTP(S); a non-standard
9
+ * web port cannot be inferred from an SSH URL — use an HTTPS remote or GITEA_BASE_URL.
10
+ */
11
+ export function parseGitRemoteUrl(url, remote = "origin") {
12
+ const u = url.trim();
13
+ let m = u.match(/^ssh:\/\/(?:[^@/\s]+@)?([^:/\s]+)(?::\d+)?\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
14
+ if (m) {
15
+ const [, host, owner, repo] = m;
16
+ return { remote, url: u, host, baseUrl: `https://${host}`, owner, repo };
17
+ }
18
+ m = u.match(/^(https?:)\/\/(?:[^@/\s]+@)?([^:/\s]+)(?::(\d+))?\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
19
+ if (m) {
20
+ const [, scheme, host, port, owner, repo] = m;
21
+ const baseUrl = port ? `${scheme}//${host}:${port}` : `${scheme}//${host}`;
22
+ return { remote, url: u, host: port ? `${host}:${port}` : host, baseUrl, owner, repo };
23
+ }
24
+ m = u.match(/^(?:[^@/\s]+@)?([^@:/\s]+):([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
25
+ if (m) {
26
+ const [, host, owner, repo] = m;
27
+ return { remote, url: u, host, baseUrl: `https://${host}`, owner, repo };
28
+ }
29
+ return null;
30
+ }
31
+ /** Extract every `[remote "<name>"]` url entry from a git config file's contents. */
32
+ export function readGitRemotes(content) {
33
+ const remotes = [];
34
+ let currentName = null;
35
+ for (const rawLine of content.split(/\r?\n/)) {
36
+ const section = rawLine.match(/^\s*\[remote\s+"([^"]+)"\]/);
37
+ if (section) {
38
+ currentName = section[1];
39
+ continue;
40
+ }
41
+ if (/^\s*\[[^\]]+\]/.test(rawLine)) {
42
+ currentName = null;
43
+ continue;
44
+ }
45
+ if (currentName !== null) {
46
+ const urlMatch = rawLine.match(/^\s*url\s*=\s*(.+?)\s*$/);
47
+ if (urlMatch) {
48
+ remotes.push({ name: currentName, url: urlMatch[1] });
49
+ currentName = null;
50
+ }
51
+ }
52
+ }
53
+ return remotes;
54
+ }
55
+ /** Parse all remotes in a git config file's contents, dropping unparseable urls. */
56
+ export function parseRemotes(content) {
57
+ return readGitRemotes(content)
58
+ .map((r) => parseGitRemoteUrl(r.url, r.name))
59
+ .filter((r) => r !== null);
60
+ }
61
+ /** Pick the remote to derive values from: `upstream` first, then `origin`, then the first. */
62
+ export function selectRemote(remotes) {
63
+ if (remotes.length === 0)
64
+ return null;
65
+ return (remotes.find((r) => r.remote === "upstream") ??
66
+ remotes.find((r) => r.remote === "origin") ??
67
+ remotes[0]);
68
+ }
69
+ /**
70
+ * Read a Gitea token from a git config. A scoped `[gitea "<baseUrl>"] token = ...`
71
+ * section wins; a bare `[gitea] token = ...` is the fallback. Returns undefined
72
+ * when neither matches.
73
+ */
74
+ export function readTokenFromGitConfig(content, baseUrl) {
75
+ const escaped = baseUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
76
+ const scoped = content.match(new RegExp(`\\[gitea\\s+"${escaped}"\\]([\\s\\S]*?)(?=\\n\\s*\\[|$)`));
77
+ if (scoped) {
78
+ const t = scoped[1].match(/^\s*token\s*=\s*(.+?)\s*$/m);
79
+ if (t)
80
+ return t[1];
81
+ }
82
+ const globalSection = content.match(/(?:^|\n)\[gitea\][\t ]*([\s\S]*?)(?=\n\s*\[|$)/);
83
+ if (globalSection) {
84
+ const t = globalSection[1].match(/^\s*token\s*=\s*(.+?)\s*$/m);
85
+ if (t)
86
+ return t[1];
87
+ }
88
+ return undefined;
89
+ }
90
+ /**
91
+ * Parse every credential-store line whose host matches. Each line is a URL of
92
+ * the form `protocol://[user[:pass]]@host[:port][/path]`; malformed and
93
+ * non-matching lines are skipped. Returns entries in file order; callers
94
+ * narrow and re-sort by repo path specificity.
95
+ *
96
+ * The `password` field — when present — holds whatever the user typed into
97
+ * git's password prompt: a real account password, a Personal Access Token,
98
+ * or an OAuth token. Git itself does not distinguish, and neither does this
99
+ * parser; the GiteaClient runtime tries each entry under multiple auth
100
+ * schemes to discover what works.
101
+ */
102
+ export function parseGitCredentials(content, host) {
103
+ const entries = [];
104
+ for (const rawLine of content.split(/\r?\n/)) {
105
+ const line = rawLine.trim();
106
+ if (!line || line.startsWith("#"))
107
+ continue;
108
+ try {
109
+ const credUrl = new URL(line);
110
+ if (credUrl.host !== host)
111
+ continue;
112
+ entries.push({
113
+ username: credUrl.username ? decodeURIComponent(credUrl.username) : undefined,
114
+ password: credUrl.password ? decodeURIComponent(credUrl.password) : undefined,
115
+ host: credUrl.host,
116
+ path: credUrl.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, ""),
117
+ });
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ }
123
+ return entries;
124
+ }
125
+ /**
126
+ * Score a credential entry's path specificity against the target repo path
127
+ * (`owner/repo`). Higher = more specific. Used to narrow multiple host
128
+ * matches toward the most relevant entry first.
129
+ *
130
+ * - Path is empty (host-only entry) → 0
131
+ * - Path is a prefix of the repo path → length of the matching path
132
+ * - Path does not match → -1 (deprioritized but still tried as a fallback)
133
+ */
134
+ function scoreEntryPath(entryPath, repoPath) {
135
+ if (!entryPath)
136
+ return 0;
137
+ if (!repoPath)
138
+ return 0;
139
+ if (repoPath === entryPath || repoPath.startsWith(`${entryPath}/`)) {
140
+ return entryPath.length;
141
+ }
142
+ return -1;
143
+ }
144
+ /** Default credential-store paths: `$XDG_CONFIG_HOME/git/credentials` then `~/.git-credentials`. */
145
+ export function defaultCredentialsPaths() {
146
+ const paths = [];
147
+ const xdg = process.env.XDG_CONFIG_HOME;
148
+ if (xdg)
149
+ paths.push(join(xdg, "git", "credentials"));
150
+ paths.push(join(homedir(), ".git-credentials"));
151
+ return paths;
152
+ }
153
+ async function readOptionalFile(path) {
154
+ try {
155
+ return await readFile(path, "utf-8");
156
+ }
157
+ catch (err) {
158
+ if (err.code !== "ENOENT")
159
+ throw err;
160
+ return "";
161
+ }
162
+ }
163
+ /**
164
+ * Build a `CandidateCredential` from a parsed credential-store entry.
165
+ *
166
+ * - `https://:pass@host` or `https://user:pass@host` → secret = pass,
167
+ * username preserved (basic auth needs it).
168
+ * - `https://tok@host` (username but no password) → secret = tok, no
169
+ * username: this is git's "store the token as the username" convention.
170
+ * Try `token` first (most common), fall back to `basic`.
171
+ */
172
+ function candidateFromEntry(entry) {
173
+ if (entry.password) {
174
+ return {
175
+ source: "credential-store",
176
+ username: entry.username,
177
+ secret: entry.password,
178
+ schemes: orderSchemesForCredentialStore(entry.username),
179
+ status: "pending",
180
+ nextSchemeIndex: 0,
181
+ };
182
+ }
183
+ if (entry.username) {
184
+ return {
185
+ source: "credential-store",
186
+ secret: entry.username,
187
+ schemes: ["token", "basic"],
188
+ status: "pending",
189
+ nextSchemeIndex: 0,
190
+ };
191
+ }
192
+ return null;
193
+ }
194
+ /**
195
+ * Discover the Gitea connection config from env + the local git context.
196
+ *
197
+ * baseUrl: `GITEA_BASE_URL` (env) wins; otherwise derived from the selected
198
+ * remote (`upstream` → `origin` → first). Returns null only when neither is
199
+ * available — callers should treat that as "do not start the server".
200
+ *
201
+ * candidates (in priority order):
202
+ * 1. `[gitea "<baseUrl>"] token` / bare `[gitea] token` from `.git/config`
203
+ * (explicit user configuration; `token` scheme only).
204
+ * 2. `GITEA_TOKEN` env var (explicit env; `token` scheme only — preserves
205
+ * the simple-token semantics).
206
+ * 3. Every host-matching entry in a git credential store, narrowed by repo
207
+ * path specificity (most specific first). Each entry may be a PAT,
208
+ * password, or OAuth token; the client tries each under `basic` and/or
209
+ * `token` schemes per the username heuristic.
210
+ *
211
+ * owner/repo: `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO` (env) win; otherwise
212
+ * taken from the selected remote.
213
+ *
214
+ * The result may have an empty `candidates` array (anonymous mode); the server
215
+ * still starts and a Skill guides the user to provide one. Write tools will
216
+ * fail with 401/403 until a working credential is added.
217
+ */
218
+ export async function discoverConfig(options = {}) {
219
+ const cwd = options.cwd ?? process.cwd();
220
+ const env = options.env ?? process.env;
221
+ const envBaseUrl = env.GITEA_BASE_URL;
222
+ const gitConfigContent = await readOptionalFile(join(cwd, ".git", "config"));
223
+ const parsedRemotes = parseRemotes(gitConfigContent);
224
+ const selected = selectRemote(parsedRemotes);
225
+ const baseUrl = envBaseUrl ?? selected?.baseUrl;
226
+ if (!baseUrl)
227
+ return null;
228
+ let host;
229
+ if (envBaseUrl) {
230
+ try {
231
+ host = new URL(envBaseUrl).host;
232
+ }
233
+ catch {
234
+ host = undefined;
235
+ }
236
+ }
237
+ else {
238
+ host = selected?.host;
239
+ }
240
+ const candidates = [];
241
+ // Source 1: .git/config [gitea "<baseUrl>"] token / bare [gitea] token.
242
+ if (host) {
243
+ const configToken = readTokenFromGitConfig(gitConfigContent, baseUrl);
244
+ if (configToken) {
245
+ candidates.push({
246
+ source: "gitea-config",
247
+ secret: configToken,
248
+ schemes: ["token"],
249
+ status: "pending",
250
+ nextSchemeIndex: 0,
251
+ });
252
+ }
253
+ }
254
+ // Source 2: GITEA_TOKEN env (simple-token semantics — no scheme probing).
255
+ const envToken = env.GITEA_TOKEN;
256
+ if (envToken) {
257
+ candidates.push({
258
+ source: "env",
259
+ secret: envToken,
260
+ schemes: ["token"],
261
+ status: "pending",
262
+ nextSchemeIndex: 0,
263
+ });
264
+ }
265
+ // Source 3..N: credential-store entries, host-matched and path-narrowed.
266
+ if (host) {
267
+ const paths = options.credentialsPaths ?? defaultCredentialsPaths();
268
+ const repoPath = selected ? `${selected.owner}/${selected.repo}` : "";
269
+ const scored = [];
270
+ let order = 0;
271
+ for (const path of paths) {
272
+ const cred = await readOptionalFile(path);
273
+ if (!cred)
274
+ continue;
275
+ for (const entry of parseGitCredentials(cred, host)) {
276
+ scored.push({ entry, score: scoreEntryPath(entry.path, repoPath), order: order++ });
277
+ }
278
+ }
279
+ // Sort by score desc; stable within same score (preserve file/discovery order).
280
+ scored.sort((a, b) => b.score - a.score || a.order - b.order);
281
+ for (const { entry } of scored) {
282
+ const candidate = candidateFromEntry(entry);
283
+ if (candidate)
284
+ candidates.push(candidate);
285
+ }
286
+ }
287
+ return {
288
+ baseUrl,
289
+ defaultOwner: env.GITEA_DEFAULT_OWNER ?? selected?.owner,
290
+ defaultRepo: env.GITEA_DEFAULT_REPO ?? selected?.repo,
291
+ remote: selected?.remote,
292
+ candidates,
293
+ };
294
+ }
295
+ //# sourceMappingURL=git-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-config.js","sourceRoot":"","sources":["../src/git-config.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,29 @@
1
+ import { type CandidateCredential, type CandidateSummary } from "./credentials.js";
1
2
  export interface GiteaConfig {
2
3
  baseUrl: string;
3
- token: string;
4
+ /**
5
+ * Legacy single-token mode. When `candidates` is omitted, this is wrapped
6
+ * as a one-element candidate list with the `token` scheme (preserving the
7
+ * pre-multi-credential behavior exactly).
8
+ */
9
+ token?: string;
10
+ /**
11
+ * Credential candidates in priority order. When provided, enables the
12
+ * fault-tolerant auth state machine: each candidate × scheme is tried in
13
+ * order until one succeeds, with 401/403 advancing to the next attempt.
14
+ */
15
+ candidates?: CandidateCredential[];
16
+ }
17
+ /**
18
+ * HTTP error from the Gitea API. Carries `status` as a structured field so
19
+ * callers (the retry loop, tests) can branch on it without parsing the
20
+ * message string (AGENTS.md §2.3 forbids substring-based control flow).
21
+ */
22
+ export declare class GiteaApiError extends Error {
23
+ readonly status: number;
24
+ readonly statusText: string;
25
+ readonly body: string;
26
+ constructor(status: number, statusText: string, body: string);
4
27
  }
5
28
  export interface Issue {
6
29
  id: number;
@@ -140,8 +163,38 @@ export interface UpdateMilestoneParams {
140
163
  }
141
164
  export declare class GiteaClient {
142
165
  private baseUrl;
143
- private token;
166
+ private candidates;
144
167
  constructor(config: GiteaConfig);
168
+ /**
169
+ * Snapshot of the credential state machine — for the `gitea_status` tool.
170
+ * Secrets are never included; only `secretPresent: boolean` and a masked
171
+ * `username`. See `summarizeCandidates` in `credentials.ts`.
172
+ */
173
+ getCredentialStatus(): {
174
+ candidates: CandidateSummary[];
175
+ activeIndex: number | null;
176
+ totalCandidates: number;
177
+ };
178
+ /**
179
+ * Single HTTP call. Throws `GiteaApiError` on non-2xx so the retry loop can
180
+ * branch on `status` (never on the message string). The `authHeader` is
181
+ * pre-built by the caller from the active candidate + scheme.
182
+ */
183
+ private doRequest;
184
+ /**
185
+ * Auth-aware request entry point. Three modes:
186
+ *
187
+ * 1. Active candidate exists (a prior attempt succeeded): reuse its locked
188
+ * scheme directly, no iteration.
189
+ * 2. No candidates at all: anonymous request (no Authorization header).
190
+ * 3. Otherwise: iterate (candidate, scheme) pairs in priority order, trying
191
+ * each until one succeeds. On 401/403 the current attempt is marked
192
+ * failed and the next is tried; non-auth errors propagate immediately
193
+ * (we do NOT mask 5xx / network errors as auth failures). When every
194
+ * candidate × scheme is exhausted, the most recent `GiteaApiError` is
195
+ * re-thrown so the caller sees the underlying status/body; the
196
+ * `gitea_status` tool surfaces the full attempt history.
197
+ */
145
198
  private request;
146
199
  listIssues(params: ListIssuesParams): Promise<Issue[]>;
147
200
  getIssue(owner: string, repo: string, index: number): Promise<Issue>;