From 589b5a6533c0636bae330f2500c02f17b70be973 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 1 Nov 2026 20:46:07 +0800 Subject: [PATCH] Sources: accept login:password Basic credential for Gitea (API + git store) --- src/infra/environment.ts | 4 +++- src/sources/gitea-api.ts | 7 +++++-- tests/gitea.spec.ts | 10 +++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/infra/environment.ts b/src/infra/environment.ts index b3ec06b..3700d22 100644 --- a/src/infra/environment.ts +++ b/src/infra/environment.ts @@ -31,11 +31,13 @@ async function writeGiteaCredentials(file: string, gitea: GiteaAccount, env: Record): Promise { const token = env[gitea.tokenEnv] ?? '' if (!token) throw new SetupError('giteaTokenMissing', { name: gitea.tokenEnv }) + // A Basic-style credential (`login:password`) stores only the password part for git; a token stores itself. + const secret = token.includes(':') ? token.slice(token.indexOf(':') + 1) : token const origin = new URL(gitea.url).origin const scheme = origin.slice(0, origin.indexOf('://')) const host = origin.slice(origin.indexOf('://') + 3) const temporary = `${file}.${process.pid}.tmp` - await writeFile(temporary, `${scheme}://${encodeURIComponent(gitea.login)}:${encodeURIComponent(token)}@${host}\n`, { mode: 0o600 }) + await writeFile(temporary, `${scheme}://${encodeURIComponent(gitea.login)}:${encodeURIComponent(secret)}@${host}\n`, { mode: 0o600 }) await rename(temporary, file) } diff --git a/src/sources/gitea-api.ts b/src/sources/gitea-api.ts index aaa8761..45dd968 100644 --- a/src/sources/gitea-api.ts +++ b/src/sources/gitea-api.ts @@ -11,7 +11,10 @@ export type GiteaFetch = (url: string, init: { method: string; headers: Record + secret.includes(':') ? `Basic ${Buffer.from(secret, 'utf8').toString('base64')}` : `token ${secret}` +/** Reads the credential from `env[tokenEnv]`; only its variable name is stored in Settings, never the value. */ export function giteaClient(url: string, tokenEnv: string, env: Record = process.env, fetchImpl: GiteaFetch = fetch): GiteaApi { if (!url) throw new Error('The Gitea Source names no URL') @@ -27,7 +30,7 @@ export class GiteaApi { async raw(path: string, init: { method?: string; body?: unknown; signal?: AbortSignal } = {}): Promise { return await this.fetchImpl(`${this.target.url}/api/v1/${path.replace(/^\//, '')}`, { method: init.method ?? 'GET', - headers: { authorization: `token ${this.target.token}`, + headers: { authorization: giteaAuthorization(this.target.token), ...(init.body === undefined ? {} : { 'content-type': 'application/json' }) }, ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }), ...(init.signal === undefined ? {} : { signal: init.signal }) }) diff --git a/tests/gitea.spec.ts b/tests/gitea.spec.ts index c536f5f..506bddd 100644 --- a/tests/gitea.spec.ts +++ b/tests/gitea.spec.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Engine, key, type Assessor, type Executor, type OutboundMessage, type Trigger } from '../src/core/index.ts' import { KV } from '../src/kv.ts' import { GiteaConfigSchema, apply, giteaKind, giteaThreadContext, pollRound } from '../src/sources/gitea.ts' -import { giteaClient, GiteaApiError } from '../src/sources/gitea-api.ts' +import { giteaAuthorization, giteaClient, GiteaApiError } from '../src/sources/gitea-api.ts' import { SOURCE, issueTrigger } from '../src/sources/gitea-trigger.ts' import { giteaLink, sendGitea } from '../src/sources/gitea-channel.ts' import { SourceRegistry } from '../src/sources/registry.ts' @@ -70,6 +70,14 @@ const issue = (number: number, over: Record = {}): Record): Trigger => ({ id: 't', source: SOURCE, threads: [{ source: SOURCE, id: '12' }], actor: { source: SOURCE, id: 'alice' }, human: true, at: '2026-09-08T00:00:00Z', text: '', meta }) +describe('giteaAuthorization', () => { + it('sends a value with a colon as HTTP Basic and a token as token auth', () => { + expect(giteaAuthorization('bot:secret:1')).toBe(`Basic ${Buffer.from('bot:secret:1', 'utf8').toString('base64')}`) + expect(giteaAuthorization('0123456789abcdef0123456789abcdef01234567')).toBe('token 0123456789abcdef0123456789abcdef01234567') + expect(giteaAuthorization('glpat-abc_123')).toBe('token glpat-abc_123') + }) +}) + describe('GiteaConfigSchema', () => { it('defaults a bare config to a safe baseline', () => { expect(GiteaConfigSchema.parse({})).toEqual({