fix: treat gh CLI token as GitHub account

This commit is contained in:
Bohdan Triapitsyn
2026-06-11 19:30:19 +03:00
parent 465732858f
commit f26950fa4e
25 changed files with 196 additions and 65 deletions
+46 -30
View File
@@ -12,6 +12,7 @@ const SETTINGS_FILE = path.join(OPENCHAMBER_DATA_DIR, 'settings.json');
const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lizomPOC3eFYo56r';
const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email';
export const GH_CLI_ACCOUNT_ID = 'gh-cli';
function ensureStorageDir() {
if (!fs.existsSync(STORAGE_DIR)) {
@@ -159,6 +160,34 @@ function writeAuthList(list) {
writeJsonFile(list);
}
function readSettingsFile() {
try {
if (fs.existsSync(SETTINGS_FILE)) {
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {};
}
} catch {
// ignore
}
return {};
}
function writeSettingsFile(settings) {
ensureStorageDir();
const tmpFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
fs.writeFileSync(tmpFile, JSON.stringify(settings, null, 2), 'utf8');
try {
fs.chmodSync(tmpFile, 0o600);
} catch {
// best-effort
}
fs.renameSync(tmpFile, SETTINGS_FILE);
try {
fs.chmodSync(SETTINGS_FILE, 0o600);
} catch {
// best-effort
}
}
export function getGitHubAuth() {
const list = readAuthList();
if (!list.length) {
@@ -237,6 +266,7 @@ export function activateGitHubAuth(accountId) {
if (index === -1) {
return false;
}
setGhCliActive(false);
list.forEach((entry, idx) => {
entry.current = idx === index;
});
@@ -307,39 +337,25 @@ export function getGitHubScopes() {
export const GITHUB_AUTH_FILE = STORAGE_FILE;
export function isGhCliDisabled() {
try {
if (fs.existsSync(SETTINGS_FILE)) {
const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
return Boolean(parsed?.ghCliDisabled);
}
} catch {
// ignore
}
return false;
return Boolean(readSettingsFile()?.ghCliDisabled);
}
export function setGhCliDisabled(disabled) {
ensureStorageDir();
let settings = {};
try {
if (fs.existsSync(SETTINGS_FILE)) {
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {};
}
} catch {
// ignore
}
const settings = readSettingsFile();
settings.ghCliDisabled = Boolean(disabled);
const tmpFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
fs.writeFileSync(tmpFile, JSON.stringify(settings, null, 2), 'utf8');
try {
fs.chmodSync(tmpFile, 0o600);
} catch {
// best-effort
}
fs.renameSync(tmpFile, SETTINGS_FILE);
try {
fs.chmodSync(SETTINGS_FILE, 0o600);
} catch {
// best-effort
if (settings.ghCliDisabled) {
settings.ghCliActive = false;
}
writeSettingsFile(settings);
}
export function isGhCliActive() {
const settings = readSettingsFile();
return !settings?.ghCliDisabled && Boolean(settings?.ghCliActive);
}
export function setGhCliActive(active) {
const settings = readSettingsFile();
settings.ghCliActive = Boolean(active) && !settings.ghCliDisabled;
writeSettingsFile(settings);
}
+3
View File
@@ -6,7 +6,10 @@ export {
clearGitHubAuth,
getGitHubClientId,
getGitHubScopes,
GH_CLI_ACCOUNT_ID,
isGhCliDisabled,
isGhCliActive,
setGhCliActive,
setGhCliDisabled,
GITHUB_AUTH_FILE,
} from './auth.js';
+3 -2
View File
@@ -1,10 +1,11 @@
import { Octokit } from '@octokit/rest';
import { getGitHubAuth, isGhCliDisabled } from './auth.js';
import { getGitHubAuth, isGhCliActive, isGhCliDisabled } from './auth.js';
import { getGhCliToken } from './gh-cli-credential.js';
export function getOctokitOrNull() {
const auth = getGitHubAuth();
const token = auth?.accessToken || (!isGhCliDisabled() ? getGhCliToken() : null);
const ghToken = !isGhCliDisabled() ? getGhCliToken() : null;
const token = isGhCliActive() ? ghToken || auth?.accessToken : auth?.accessToken || ghToken;
if (!token) {
return null;
}
+76 -11
View File
@@ -76,17 +76,18 @@ export function registerGitHubRoutes(app) {
app.get('/api/github/auth/status', async (_req, res) => {
try {
const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, isGhCliDisabled } = await getGitHubLibraries();
const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, GH_CLI_ACCOUNT_ID, isGhCliActive, isGhCliDisabled, setGhCliActive } = await getGitHubLibraries();
const { getGhCliToken } = await import('./gh-cli-credential.js');
const auth = getGitHubAuth();
const accounts = getGitHubAuthAccounts();
let accounts = getGitHubAuthAccounts();
const ghCliDisabled = isGhCliDisabled();
const ghCliActive = isGhCliActive();
const ghToken = getGhCliToken();
const usingOwnToken = Boolean(auth?.accessToken);
let ghCliUser = null;
if (ghToken !== null && !ghCliDisabled && usingOwnToken) {
if (ghToken !== null && !ghCliDisabled) {
try {
const { Octokit } = await import('@octokit/rest');
ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken }));
@@ -94,11 +95,26 @@ export function registerGitHubRoutes(app) {
ghCliUser = null;
}
}
if (ghCliActive && !ghCliUser) {
setGhCliActive(false);
}
const ghCliCurrent = ghToken !== null && !ghCliDisabled && Boolean(ghCliUser) && (ghCliActive || !usingOwnToken);
if (ghCliUser) {
accounts = accounts
.map((account) => ({ ...account, current: ghCliCurrent ? false : Boolean(account.current) }))
.concat({
id: GH_CLI_ACCOUNT_ID,
user: ghCliUser,
current: ghCliCurrent,
source: 'gh-cli',
});
}
const buildGhCli = (activeUser = null) => ({
available: ghToken !== null,
disabled: ghCliDisabled,
active: !usingOwnToken && ghToken !== null && !ghCliDisabled,
active: ghCliCurrent,
...(!ghCliDisabled && (activeUser || ghCliUser) ? { user: activeUser || ghCliUser } : {}),
});
@@ -119,14 +135,12 @@ export function registerGitHubRoutes(app) {
const fallback = usingOwnToken ? auth.user : null;
const mergedUser = user || fallback;
const ghIsActive = !usingOwnToken && ghToken !== null && !ghCliDisabled;
return res.json({
connected: true,
user: mergedUser,
scope: usingOwnToken ? auth.scope : undefined,
scope: ghCliCurrent ? undefined : auth?.scope,
accounts,
ghCli: buildGhCli(ghIsActive ? mergedUser : null),
ghCli: buildGhCli(ghCliCurrent ? mergedUser : null),
});
} catch (error) {
console.error('Failed to get GitHub auth status:', error);
@@ -238,25 +252,70 @@ export function registerGitHubRoutes(app) {
app.post('/api/github/auth/activate', async (req, res) => {
try {
const { activateGitHubAuth, getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts } = await getGitHubLibraries();
const { activateGitHubAuth, getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, GH_CLI_ACCOUNT_ID, isGhCliDisabled, setGhCliActive } = await getGitHubLibraries();
const accountId = typeof req.body?.accountId === 'string' ? req.body.accountId : '';
if (!accountId) {
return res.status(400).json({ error: 'accountId is required' });
}
if (accountId === GH_CLI_ACCOUNT_ID) {
const { getGhCliToken } = await import('./gh-cli-credential.js');
const ghToken = !isGhCliDisabled() ? getGhCliToken() : null;
if (!ghToken) {
return res.status(404).json({ error: 'GitHub CLI account not found' });
}
const { Octokit } = await import('@octokit/rest');
const user = await getGitHubUserSummary(new Octokit({ auth: ghToken }));
setGhCliActive(true);
const accounts = getGitHubAuthAccounts()
.map((account) => ({ ...account, current: false }))
.concat({ id: GH_CLI_ACCOUNT_ID, user, current: true, source: 'gh-cli' });
return res.json({
connected: true,
user,
accounts,
ghCli: {
available: true,
disabled: false,
active: true,
user,
},
});
}
const activated = activateGitHubAuth(accountId);
if (!activated) {
return res.status(404).json({ error: 'GitHub account not found' });
}
const auth = getGitHubAuth();
const accounts = getGitHubAuthAccounts();
let accounts = getGitHubAuthAccounts();
if (!auth?.accessToken) {
return res.json({ connected: false, accounts });
}
const { getGhCliToken } = await import('./gh-cli-credential.js');
const ghCliDisabled = isGhCliDisabled();
const ghToken = !ghCliDisabled ? getGhCliToken() : null;
let ghCliUser = null;
if (ghToken) {
try {
const { Octokit } = await import('@octokit/rest');
ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken }));
accounts = accounts.concat({
id: GH_CLI_ACCOUNT_ID,
user: ghCliUser,
current: false,
source: 'gh-cli',
});
} catch {
ghCliUser = null;
}
}
const octokit = getOctokitOrNull();
if (!octokit) {
return res.json({ connected: false, accounts });
return res.json({ connected: false, accounts, ghCli: { available: ghToken !== null, disabled: ghCliDisabled, active: false, ...(ghCliUser ? { user: ghCliUser } : {}) } });
}
let user = auth.user || null;
@@ -274,6 +333,12 @@ export function registerGitHubRoutes(app) {
user,
scope: auth.scope,
accounts,
ghCli: {
available: ghToken !== null,
disabled: ghCliDisabled,
active: false,
...(ghCliUser ? { user: ghCliUser } : {}),
},
});
} catch (error) {
console.error('Failed to activate GitHub account:', error);