fix(gitea-bot): mint PAT via Basic auth, not Sudo header

Gitea's POST /users/{name}/tokens is explicitly Basic-auth only;
neither the admin token nor Sudo header is accepted. Keep the random
password we generate at createUser time and pass it straight into
createAccessTokenFor as Basic auth.

For bots that already exist from a half-failed previous provision
run, reset their password via PATCH /admin/users/{name} so we can
Basic-auth as them and mint a fresh token.

Made-with: Cursor
This commit is contained in:
2026-04-21 10:58:25 -07:00
parent b9511601bc
commit d9d3514647
2 changed files with 64 additions and 10 deletions

View File

@@ -183,22 +183,62 @@ export async function createUser(opts: {
}
/**
* Mint a Personal Access Token on behalf of another user. This uses
* Gitea's `Sudo` header — only works when GITEA_API_TOKEN is a site-admin
* token. Returns the plaintext token string, which must be stored
* encrypted (Gitea never shows it again).
* Mint a Personal Access Token for a user.
*
* Gitea's `POST /users/{username}/tokens` endpoint is explicitly Basic-auth
* only: neither an admin token nor the `Sudo` header work. So we require
* the target user's password (we know it — we just created the user with
* a random one and can discard it right after).
*
* Returns the plaintext token (`sha1` field) — store it encrypted;
* Gitea never shows it again.
*/
export async function createAccessTokenFor(opts: {
username: string;
password: string;
name: string;
scopes?: string[];
}): Promise<{ id: number; name: string; sha1: string; token_last_eight: string }> {
const { username, name, scopes = ['write:repository', 'write:issue', 'write:user'] } = opts;
return giteaFetch(`/users/${username}/tokens`, {
const { username, password, name, scopes = ['write:repository', 'write:issue', 'write:user'] } = opts;
const basic = Buffer.from(`${username}:${password}`).toString('base64');
const url = `${GITEA_API_URL}/api/v1/users/${username}/tokens`;
const res = await fetch(url, {
method: 'POST',
headers: { Sudo: username },
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${basic}`,
},
body: JSON.stringify({ name, scopes }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Gitea token-mint error ${res.status} on /users/${username}/tokens: ${text}`);
}
return res.json();
}
/**
* Admin: edit an existing user. Used during re-provisioning to reset
* the bot's password so we can Basic-auth as it and mint a fresh PAT.
*/
export async function adminEditUser(opts: {
username: string;
password?: string;
fullName?: string;
email?: string;
}): Promise<GiteaUser> {
const body: Record<string, unknown> = {
source_id: 0,
// Editing requires specifying login name explicitly.
login_name: opts.username,
};
if (opts.password) body.password = opts.password;
if (opts.fullName) body.full_name = opts.fullName;
if (opts.email) body.email = opts.email;
return giteaFetch(`/admin/users/${opts.username}`, {
method: 'PATCH',
body: JSON.stringify(body),
});
}
export interface GiteaTeam {