diff --git a/AGENTS.md b/AGENTS.md index 872e27b1..c83b996c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,10 @@ Quota provider registry, dispatch, and provider integrations for usage endpoints Git repository operations for the web server runtime. - Module docs: `packages/web/server/lib/git/DOCUMENTATION.md` +##### github +GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing. +- Module docs: `packages/web/server/lib/github/DOCUMENTATION.md` + ## Build / dev commands (verified) All scripts are in `package.json`. - Validate: `bun run type-check`, `bun run lint` diff --git a/packages/web/server/index.js b/packages/web/server/index.js index 95d0f50a..f412becb 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -7216,17 +7216,12 @@ async function main(options = {}) { // ================= GitHub OAuth (Device Flow) ================= - // Note: scopes may be overridden via OPENCHAMBER_GITHUB_SCOPES or settings.json (see github-auth.js). + // Note: scopes may be overridden via OPENCHAMBER_GITHUB_SCOPES or settings.json (see lib/github/auth.js). let githubLibraries = null; const getGitHubLibraries = async () => { if (!githubLibraries) { - const [auth, device, octokit] = await Promise.all([ - import('./lib/github-auth.js'), - import('./lib/github-device-flow.js'), - import('./lib/github-octokit.js'), - ]); - githubLibraries = { ...auth, ...device, ...octokit }; + githubLibraries = await import('./lib/github/index.js'); } return githubLibraries; }; @@ -7480,7 +7475,7 @@ async function main(options = {}) { return res.json({ connected: false }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory, remote); if (!repo) { return res.json({ connected: true, repo: null, branch, pr: null, checks: null, canMerge: false }); @@ -7706,7 +7701,7 @@ async function main(options = {}) { return res.status(401).json({ error: 'GitHub not connected' }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory, remote); if (!repo) { return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' }); @@ -7883,7 +7878,7 @@ async function main(options = {}) { return res.status(401).json({ error: 'GitHub not connected' }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' }); @@ -7958,7 +7953,7 @@ async function main(options = {}) { return res.status(401).json({ error: 'GitHub not connected' }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' }); @@ -8001,7 +7996,7 @@ async function main(options = {}) { return res.status(401).json({ error: 'GitHub not connected' }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' }); @@ -8052,7 +8047,7 @@ async function main(options = {}) { return res.json({ connected: false }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.json({ connected: true, repo: null, issues: [] }); @@ -8108,7 +8103,7 @@ async function main(options = {}) { return res.json({ connected: false }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.json({ connected: true, repo: null, issue: null }); @@ -8169,7 +8164,7 @@ async function main(options = {}) { return res.json({ connected: false }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.json({ connected: true, repo: null, comments: [] }); @@ -8214,7 +8209,7 @@ async function main(options = {}) { return res.json({ connected: false }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.json({ connected: true, repo: null, prs: [] }); @@ -8289,7 +8284,7 @@ async function main(options = {}) { return res.json({ connected: false }); } - const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js'); + const { resolveGitHubRepoFromDirectory } = await import('./lib/github/index.js'); const { repo } = await resolveGitHubRepoFromDirectory(directory); if (!repo) { return res.json({ connected: true, repo: null, pr: null }); diff --git a/packages/web/server/lib/github/DOCUMENTATION.md b/packages/web/server/lib/github/DOCUMENTATION.md new file mode 100644 index 00000000..f98b7eca --- /dev/null +++ b/packages/web/server/lib/github/DOCUMENTATION.md @@ -0,0 +1,47 @@ +# GitHub Module Documentation + +## Purpose +This module provides GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing utilities for the web server runtime. + +## Entrypoints and structure +- `packages/web/server/lib/github/index.js`: public entrypoint imported by `packages/web/server/index.js`. +- `packages/web/server/lib/github/auth.js`: auth storage, multi-account support, and client ID/scope configuration. +- `packages/web/server/lib/github/device-flow.js`: OAuth device code flow implementation for browserless auth. +- `packages/web/server/lib/github/octokit.js`: Octokit client factory backed by current auth. +- `packages/web/server/lib/github/repo/index.js`: GitHub remote URL parser and directory-to-repo resolver. + +## Public exports (from index.js) + +### Auth (`auth.js`) +- `getGitHubAuth()`: Returns current auth entry (accessToken, user, scope, accountId). +- `getGitHubAuthAccounts()`: Returns list of all configured accounts. +- `setGitHubAuth({ accessToken, scope, tokenType, user, accountId })`: Stores or updates auth entry. +- `activateGitHubAuth(accountId)`: Sets specified account as current. +- `clearGitHubAuth()`: Removes current account or deletes storage file if last account. +- `getGitHubClientId()`: Resolves client ID from env var, settings.json, or default. +- `getGitHubScopes()`: Resolves scopes from env var, settings.json, or default. +- `GITHUB_AUTH_FILE`: Storage file path constant. + +### Device flow (`device-flow.js`) +- `startDeviceFlow({ clientId, scope })`: Requests device code from GitHub. +- `exchangeDeviceCode({ clientId, deviceCode })`: Polls for access token. + +### Octokit (`octokit.js`) +- `getOctokitOrNull()`: Returns configured Octokit instance or null if no auth. + +### Repo (`repo/index.js`) +- `parseGitHubRemoteUrl(raw)`: Parses SSH/HTTPS URLs into `{ owner, repo, url }`. +- `resolveGitHubRepoFromDirectory(directory, remoteName)`: Resolves GitHub repo from git remote. + +## Storage and configuration +- Auth storage: `~/.config/openchamber/github-auth.json` (atomic writes, mode 0o600). +- Client ID: `OPENCHAMBER_GITHUB_CLIENT_ID` env var → `settings.json` → default. +- Scopes: `OPENCHAMBER_GITHUB_SCOPES` env var → `settings.json` → default. + +## Account resolution +Account IDs are resolved in priority order: explicit `accountId` → user login → user ID → token prefix. + +## Notes for contributors +- All auth operations use atomic file writes for safe multi-instance sharing. +- Device flow handles GitHub's `authorization_pending` responses at caller level. +- Repo parser supports `git@github.com:`, `ssh://git@github.com/`, and `https://github.com/` URL formats. diff --git a/packages/web/server/lib/github-auth.js b/packages/web/server/lib/github/auth.js similarity index 100% rename from packages/web/server/lib/github-auth.js rename to packages/web/server/lib/github/auth.js diff --git a/packages/web/server/lib/github-device-flow.js b/packages/web/server/lib/github/device-flow.js similarity index 100% rename from packages/web/server/lib/github-device-flow.js rename to packages/web/server/lib/github/device-flow.js diff --git a/packages/web/server/lib/github/index.js b/packages/web/server/lib/github/index.js new file mode 100644 index 00000000..b86c4749 --- /dev/null +++ b/packages/web/server/lib/github/index.js @@ -0,0 +1,24 @@ +export { + getGitHubAuth, + getGitHubAuthAccounts, + setGitHubAuth, + activateGitHubAuth, + clearGitHubAuth, + getGitHubClientId, + getGitHubScopes, + GITHUB_AUTH_FILE, +} from './auth.js'; + +export { + startDeviceFlow, + exchangeDeviceCode, +} from './device-flow.js'; + +export { + getOctokitOrNull, +} from './octokit.js'; + +export { + parseGitHubRemoteUrl, + resolveGitHubRepoFromDirectory, +} from './repo/index.js'; diff --git a/packages/web/server/lib/github-octokit.js b/packages/web/server/lib/github/octokit.js similarity index 80% rename from packages/web/server/lib/github-octokit.js rename to packages/web/server/lib/github/octokit.js index 75c9bdb7..e374cfa5 100644 --- a/packages/web/server/lib/github-octokit.js +++ b/packages/web/server/lib/github/octokit.js @@ -1,5 +1,5 @@ import { Octokit } from '@octokit/rest'; -import { getGitHubAuth } from './github-auth.js'; +import { getGitHubAuth } from './auth.js'; export function getOctokitOrNull() { const auth = getGitHubAuth(); diff --git a/packages/web/server/lib/github-repo.js b/packages/web/server/lib/github/repo/index.js similarity index 97% rename from packages/web/server/lib/github-repo.js rename to packages/web/server/lib/github/repo/index.js index c5a1b603..6d0babd8 100644 --- a/packages/web/server/lib/github-repo.js +++ b/packages/web/server/lib/github/repo/index.js @@ -1,4 +1,4 @@ -import { getRemoteUrl } from './git/index.js'; +import { getRemoteUrl } from '../../git/index.js'; export const parseGitHubRemoteUrl = (raw) => { if (typeof raw !== 'string') {