feat(web,ui): built-in codeberg.org gitea host and provider detectUrls

This commit is contained in:
2026-08-17 09:56:59 +00:00
parent a30c7bce0a
commit 697925ee0d
22 changed files with 292 additions and 189 deletions
@@ -11,12 +11,14 @@
## Public exports
- `GIT_PROVIDER_DEFAULTS`: `{ github: 'https://api.github.com', gitlab: 'https://gitlab.com', gitea: null }`. Built-in defaults are **not persisted**; they are applied at read time by getters.
- `GIT_PROVIDER_DEFAULTS`: `{ github: 'https://api.github.com', gitlab: 'https://gitlab.com', gitea: 'https://codeberg.org' }`. Built-in defaults are **not persisted**; they are applied at read time by getters.
- `GIT_PROVIDER_DEFAULT_DETECT_URLS`: `{ github: ['github.com'], gitlab: ['gitlab.com'], gitea: ['codeberg.org'] }`. Built-in detection hostnames; remotes on these hosts classify as the provider with no configuration (mirrors the client-side built-ins in `packages/ui/src/lib/gitProvider.ts`).
- `normalizeBaseUrl(raw)`: normalize an API base URL (add `https://` when the scheme is missing, strip trailing slashes, preserve subpaths like `/gitlab`), `null` for empty/unparseable input.
- `normalizeDetectionHost(raw)`: extract the bare lowercase hostname from any git remote/URL form (`https://`, `ssh://`, scp-like `git@host:path`, IPv6); mirrors `packages/ui/src/lib/gitHost.ts` `parseGitHost`.
- `sanitizeGitProviders(payload)`: validate/normalize the `gitProviders` shape — only `github|gitlab|gitea` keys survive; `apiBaseUrl` via `normalizeBaseUrl`, `detectUrls` deduped bare hostnames; empty/absent values dropped; returns `undefined` when nothing valid remains.
- `readGitProvidersConfig()`: read the `gitProviders` section from `settings.json` (`OPENCHAMBER_DATA_DIR` env override, else `~/.config/openchamber`); never throws, returns `{}` on missing/invalid data.
- `getProviderApiBaseUrl(provider)`: configured value -> `GIT_PROVIDER_DEFAULTS[provider]` -> `null` (gitea).
- `getProviderApiBaseUrl(provider)`: configured value -> `GIT_PROVIDER_DEFAULTS[provider]` -> `null`.
- `getProviderDetectUrls(provider)`: effective detection hostnames — built-in default hosts plus configured `detectUrls`, deduped (the built-ins always apply).
- `githubWebOriginFromApiBase(apiBase)`: GitHub web origin from an API base — `https://api.github.com` -> `https://github.com`; Enterprise `https://host/api[/v3]` -> `https://host` (trailing `/api`/`/api/v3` stripped, subpath prefixes kept); otherwise the URL origin; never throws, falls back to `https://github.com`.
## Settings shape
@@ -14,7 +14,17 @@ const GIT_PROVIDER_KEYS = ['github', 'gitlab', 'gitea'];
export const GIT_PROVIDER_DEFAULTS = {
github: 'https://api.github.com',
gitlab: 'https://gitlab.com',
gitea: null,
gitea: 'https://codeberg.org',
};
// Built-in detection hostnames: remotes on these hosts are recognized as the
// provider even when the user configures nothing. Configured detectUrls extend
// them — the built-ins always apply (mirrors the client-side detection in
// packages/ui/src/lib/gitProvider.ts).
export const GIT_PROVIDER_DEFAULT_DETECT_URLS = {
github: ['github.com'],
gitlab: ['gitlab.com'],
gitea: ['codeberg.org'],
};
/**
@@ -171,12 +181,23 @@ export function readGitProvidersConfig() {
/**
* Effective API base URL for a provider: the configured settings.json value if
* present, else the built-in default (null for gitea, which has none).
* present, else the built-in default.
*/
export function getProviderApiBaseUrl(provider) {
return readGitProvidersConfig()[provider]?.apiBaseUrl || GIT_PROVIDER_DEFAULTS[provider] || null;
}
/**
* Effective detection hostnames for a provider: the built-in default hosts
* plus any user-configured detectUrls, deduped. The built-ins always apply so
* a default host (e.g. github.com) keeps classifying remotes even when custom
* enterprise hosts are configured.
*/
export function getProviderDetectUrls(provider) {
const configured = readGitProvidersConfig()[provider]?.detectUrls ?? [];
return [...new Set([...(GIT_PROVIDER_DEFAULT_DETECT_URLS[provider] ?? []), ...configured])];
}
/**
* Derive the GitHub web origin from an API base URL. The public API host
* (`https://api.github.com`) maps to `https://github.com`; an Enterprise API
@@ -8,11 +8,13 @@ process.env.OPENCHAMBER_DATA_DIR = TEMP_DATA_DIR;
const {
GIT_PROVIDER_DEFAULTS,
GIT_PROVIDER_DEFAULT_DETECT_URLS,
normalizeBaseUrl,
normalizeDetectionHost,
sanitizeGitProviders,
readGitProvidersConfig,
getProviderApiBaseUrl,
getProviderDetectUrls,
githubWebOriginFromApiBase,
} = await import('./config.js');
@@ -126,7 +128,7 @@ describe('readGitProvidersConfig / getProviderApiBaseUrl', () => {
expect(readGitProvidersConfig()).toEqual({});
expect(getProviderApiBaseUrl('github')).toBe('https://api.github.com');
expect(getProviderApiBaseUrl('gitlab')).toBe('https://gitlab.com');
expect(getProviderApiBaseUrl('gitea')).toBeNull();
expect(getProviderApiBaseUrl('gitea')).toBe('https://codeberg.org');
});
test('reads the configured values from settings.json', () => {
@@ -142,7 +144,7 @@ describe('readGitProvidersConfig / getProviderApiBaseUrl', () => {
});
expect(getProviderApiBaseUrl('github')).toBe('https://github.example.com/api/v3');
expect(getProviderApiBaseUrl('gitlab')).toBe('https://gitlab.example.com');
expect(getProviderApiBaseUrl('gitea')).toBeNull();
expect(getProviderApiBaseUrl('gitea')).toBe('https://codeberg.org');
});
test('never throws on a malformed settings file', () => {
@@ -152,6 +154,26 @@ describe('readGitProvidersConfig / getProviderApiBaseUrl', () => {
});
});
describe('getProviderDetectUrls', () => {
test('returns the built-in default hosts when nothing is configured', () => {
expect(getProviderDetectUrls('github')).toEqual(['github.com']);
expect(getProviderDetectUrls('gitlab')).toEqual(['gitlab.com']);
expect(getProviderDetectUrls('gitea')).toEqual(['codeberg.org']);
expect(GIT_PROVIDER_DEFAULT_DETECT_URLS.gitea).toEqual(['codeberg.org']);
});
test('keeps the built-in hosts and appends configured detectUrls', () => {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify({
gitProviders: {
github: { detectUrls: ['github.example.com'] },
gitea: { detectUrls: ['gitea.example.com', 'codeberg.org'] },
},
}));
expect(getProviderDetectUrls('github')).toEqual(['github.com', 'github.example.com']);
expect(getProviderDetectUrls('gitea')).toEqual(['codeberg.org', 'gitea.example.com']);
});
});
describe('githubWebOriginFromApiBase', () => {
test('maps the public api host to github.com', () => {
expect(githubWebOriginFromApiBase('https://api.github.com')).toBe('https://github.com');
@@ -5,7 +5,7 @@
- This module owns Gitea/Forgejo auth (Personal Access Token), raw REST v1 client access, remote-URL repo resolution, and Gitea issue / pull-request (PR) APIs for OpenChamber, including issue create/update and PR create/update/merge writes.
- From a user perspective, this is the layer that lets the app show Gitea issues and pull requests for a local project, including comments and per-file diffs, and create, edit, and merge pull requests.
- Gitea and Forgejo share the same GitHub-style REST v1 API, so this module serves both. Gitea calls remote work **pull requests** (PR), not merge requests. Gitea repos are flat `owner/repo` — there are no multi-segment namespaces.
- The module mirrors `packages/web/server/lib/gitlab/` (PAT auth + raw-fetch client) but uses a **Personal Access Token** against the `Authorization: token <pat>` header and a **user-supplied base URL** (Gitea is self-hosted; there is no default instance).
- The module mirrors `packages/web/server/lib/gitlab/` (PAT auth + raw-fetch client) but uses a **Personal Access Token** against the `Authorization: token <pat>` header and a **user-supplied base URL** (Gitea is self-hosted; codeberg.org is the only built-in default).
## Entrypoints and structure
@@ -29,8 +29,8 @@
- `clearGiteaAuth()`: remove the current account.
- `normalizeBaseUrl(raw)`: add `https://` when a scheme is missing, strip trailing slash, return `null` for invalid input.
- `GITEA_AUTH_FILE`: auth file path.
- `getGiteaDefaultBaseUrl()`: effective default base URL — configured `gitProviders.gitea.apiBaseUrl` from `settings.json`, else `null`. Used to prefill the connect form and as the connect/status default; stored accounts still require an explicit base URL (there is no invented host).
- There is **no built-in default base URL**: Gitea/Forgejo is self-hosted, so the instance URL is always user-provided.
- `getGiteaDefaultBaseUrl()`: effective default base URL — configured `gitProviders.gitea.apiBaseUrl` from `settings.json`, else `https://codeberg.org`. Used to prefill the connect form and as the connect/status default; stored accounts still require an explicit base URL.
- The only built-in default base URL is **codeberg.org** (a well-known public Forgejo instance); any other Gitea/Forgejo instance URL is user-provided.
### Client (`client.js`)
@@ -47,7 +47,7 @@
- Auth storage: `~/.config/openchamber/gitea-auth.json` (override with `OPENCHAMBER_DATA_DIR`).
- Writes are atomic (tmp file + rename) and file mode is `0o600`.
- Base URL resolution: the caller-supplied `baseUrl` (normalized) is the primary source — there is no built-in default instance. A configured `settings.json` `gitProviders.gitea.apiBaseUrl` acts as the connect-form default/fallback. Stored entries without a usable base URL are dropped.
- Base URL resolution: the caller-supplied `baseUrl` (normalized) is the primary source, then the effective default (configured `settings.json` `gitProviders.gitea.apiBaseUrl`, else `https://codeberg.org`). Stored entries without a usable base URL are dropped.
- Account id: `` `${host}:${username}` `` (e.g. `gitea.example.com:alice`), falling back to `token:<first8>` when the username is missing.
- Auth header on every request: `Authorization: token <pat>`.
- Gitea's `GET /user` uses `login`/`full_name`/`html_url`; `setGiteaAuth` accepts both that and the `username`/`web_url` variants.
@@ -92,7 +92,7 @@
| Method | Path | Shape |
|---|---|---|
| GET | `/api/gitea/auth/status` | `{ connected, user?, accounts[], defaultBaseUrl? }` (`defaultBaseUrl` present when connected; the configured `gitProviders.gitea.apiBaseUrl`, else `null`) |
| GET | `/api/gitea/auth/status` | `{ connected, user?, accounts[], defaultBaseUrl? }` (`defaultBaseUrl` present when connected; the effective default — configured `gitProviders.gitea.apiBaseUrl`, else `https://codeberg.org`) |
| POST | `/api/gitea/auth/connect` | body `{ accessToken, baseUrl? }` -> `{ connected, user, accounts }`; `400` for missing/invalid token; `400` when neither a valid `baseUrl` nor a configured default exists |
| POST | `/api/gitea/auth/activate` | body `{ accountId }` -> `{ connected, user, accounts }`; `404` unknown account |
| DELETE | `/api/gitea/auth` | `{ removed }` |
+6 -6
View File
@@ -10,13 +10,13 @@ const OPENCHAMBER_DATA_DIR = process.env.OPENCHAMBER_DATA_DIR
const STORAGE_DIR = OPENCHAMBER_DATA_DIR;
const STORAGE_FILE = path.join(STORAGE_DIR, 'gitea-auth.json');
// Gitea/Forgejo are self-hosted — there is deliberately NO built-in default
// base URL. The instance URL is always user-provided (see `normalizeBaseUrl`);
// auth.js never invents a host for a stored account. A configured
// settings.json gitProviders.gitea.apiBaseUrl can act as the default for the
// connect form, but stored accounts still require an explicit baseUrl.
// Gitea/Forgejo are primarily self-hosted, but codeberg.org (a well-known
// public Forgejo instance) acts as the built-in default base URL. The instance
// URL is always user-provided when connecting to a different host (see
// `normalizeBaseUrl`); auth.js never invents a host for a stored account. A
// configured settings.json gitProviders.gitea.apiBaseUrl overrides the default.
/** Effective default Gitea base URL: configured settings.json value, else null (no built-in default). */
/** Effective default Gitea base URL: configured settings.json value, else codeberg.org. */
export function getGiteaDefaultBaseUrl() {
return getProviderApiBaseUrl('gitea');
}