import { chromium } from 'playwright';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import chalk from 'chalk';
export class IssuesAuthManager {
private cookieFile: string;
constructor() {
// Store cookies in user's home directory
this.cookieFile = path.join(os.homedir(), '.chromium-issues-cookie');
}
getCookieFilePath(): string {
return this.cookieFile;
}
async authenticate(options: { headless?: boolean } = {}): Promise<string> {
console.log(chalk.cyan('๐ Starting issues.chromium.org authentication...'));
console.log(chalk.gray('This will open Chrome for you to sign in with your Google account.'));
const browser = await chromium.launch({
headless: false, // Always use headed mode for auth
channel: 'chrome', // Use real Chrome
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox'
]
});
try {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1280, height: 800 },
bypassCSP: true
});
const page = await context.newPage();
console.log(chalk.yellow('\n๐ Please sign in to your Google account in the browser window...'));
console.log(chalk.gray('Waiting for you to complete sign-in...'));
// Navigate to issues.chromium.org
await page.goto('https://issues.chromium.org');
// Wait for authentication - poll for cookies
let authCookie: any = null;
let attempts = 0;
const maxAttempts = 120; // 2 minutes timeout
while (attempts < maxAttempts) {
await page.waitForTimeout(1000); // Wait 1 second between checks
// Get all cookies
const cookies = await context.cookies();
// Look for key authentication cookies
const sidCookie = cookies.find(cookie => cookie.name === 'SID');
const psid1Cookie = cookies.find(cookie => cookie.name === '__Secure-1PSID');
const psid3Cookie = cookies.find(cookie => cookie.name === '__Secure-3PSID');
// Check if we have authentication cookies
if (sidCookie && (psid1Cookie || psid3Cookie)) {
authCookie = sidCookie;
console.log(chalk.green('\nโ Authentication detected!'));
break;
}
attempts++;
// Show progress every 10 seconds
if (attempts % 10 === 0) {
console.log(chalk.gray(`Still waiting for sign-in... (${attempts}s)`));
}
}
if (!authCookie) {
// Get all cookies for debugging
const allCookies = await context.cookies();
console.log(chalk.red('\nAvailable cookies:'));
allCookies.forEach(cookie => {
console.log(chalk.gray(` - ${cookie.name}`));
});
throw new Error('Authentication timeout. Could not find required Google session cookies.');
}
// Get cookies from issues.chromium.org domain specifically
// These are the required cookies for authenticated API calls:
// - OSID, __Secure-OSID: Session cookies for issues.chromium.org
// - __Secure-1PAPISID, __Secure-3PAPISID: API session cookies
// Note: .google.com cookies like __Secure-1PSID cause 400 errors
const cookies = await context.cookies('https://issues.chromium.org');
const requiredCookieNames = ['OSID', '__Secure-OSID', '__Secure-1PAPISID', '__Secure-3PAPISID'];
const relevantCookies = cookies.filter(c =>
requiredCookieNames.includes(c.name) &&
(c.domain === 'issues.chromium.org' || c.domain === '.issues.chromium.org')
);
if (relevantCookies.length < 2) {
// Show available cookies for debugging
console.log(chalk.yellow('\nAvailable issues.chromium.org cookies:'));
cookies.filter(c => c.domain.includes('issues.chromium.org')).forEach(c => {
console.log(chalk.gray(` - ${c.name} (${c.domain})`));
});
throw new Error(`Could not find required issues.chromium.org cookies. Found: ${relevantCookies.map(c => c.name).join(', ')}`);
}
const cookieString = relevantCookies.map(c => `${c.name}=${c.value}`).join('; ');
// Save to file
await this.saveCookies(cookieString);
console.log(chalk.green(`\nโ
Authentication successful! Cookies saved to ${this.cookieFile}`));
console.log(chalk.gray(`Saved cookies: ${relevantCookies.map(c => c.name).join(', ')}`));
console.log(chalk.gray('You can now access restricted issues.'));
return cookieString;
} finally {
await browser.close();
}
}
async getCookies(): Promise<string | null> {
try {
const cookies = await fs.readFile(this.cookieFile, 'utf-8');
return cookies.trim();
} catch (error) {
return null;
}
}
async saveCookies(cookies: string): Promise<void> {
await fs.writeFile(this.cookieFile, cookies, { mode: 0o600 }); // Secure permissions
}
async clearCookies(): Promise<void> {
try {
await fs.unlink(this.cookieFile);
console.log(chalk.yellow('๐๏ธ Cleared saved issues authentication'));
} catch (error) {
// File doesn't exist, that's fine
}
}
async checkAuth(): Promise<boolean> {
const cookies = await this.getCookies();
if (!cookies) {
return false;
}
// Test if cookies are still valid by trying to fetch a known restricted endpoint
try {
const response = await fetch('https://issues.chromium.org/action/yes', {
headers: {
'Cookie': cookies,
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
return response.ok;
} catch {
return false;
}
}
async setCookiesFromString(cookieString: string): Promise<void> {
// Extract only the required cookies from issues.chromium.org domain
// Note: .google.com cookies cause 400 errors when sent to issues.chromium.org API
const requiredCookies = ['OSID', '__Secure-OSID', '__Secure-1PAPISID', '__Secure-3PAPISID'];
const cookieParts = cookieString.split(';').map(s => s.trim());
const validCookies = cookieParts.filter(part => {
const name = part.split('=')[0];
return requiredCookies.includes(name);
});
if (validCookies.length < 2) {
console.log(chalk.yellow('โ ๏ธ Warning: Cookie string is missing required authentication cookies.'));
console.log(chalk.gray('Required cookies from issues.chromium.org domain:'));
console.log(chalk.gray(' - OSID, __Secure-OSID (session cookies)'));
console.log(chalk.gray(' - __Secure-1PAPISID, __Secure-3PAPISID (API cookies)'));
console.log(chalk.gray('Saving provided cookies anyway, but authentication may not work.'));
await this.saveCookies(cookieString);
} else {
const filteredCookieString = validCookies.join('; ');
await this.saveCookies(filteredCookieString);
console.log(chalk.green(`โ
Cookies saved to ${this.cookieFile}`));
console.log(chalk.gray(`Saved cookies: ${validCookies.map(c => c.split('=')[0]).join(', ')}`));
}
}
}
// Helper function to get cookies with fallback
export async function getIssuesAuthCookies(providedCookie?: string): Promise<string | null> {
// If cookie is provided via parameter, use it
if (providedCookie) {
return providedCookie;
}
// Otherwise, try to load from saved file
const authManager = new IssuesAuthManager();
const savedCookies = await authManager.getCookies();
if (savedCookies) {
return savedCookies;
}
return null;
}