diff --git a/extension/background/background.js b/extension/background/background.js new file mode 100644 index 0000000..988647a --- /dev/null +++ b/extension/background/background.js @@ -0,0 +1,265 @@ +/** + * Background script — message router + Discord token capture + * (webRequest) + Pixiv PKCE OAuth. Direct port of GS background.js; + * api.js client points at FC instead of GS. + */ + +let discordToken = null; +let discordTokenCapturedAt = null; + +let pixivRefreshToken = null; +let pixivTokenCapturedAt = null; +let pixivOAuthPending = null; + +const PIXIV_CLIENT_ID = 'MOBrBDS8blbauoSck0ZfDbtuzpyT'; +const PIXIV_CLIENT_SECRET = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj'; +const PIXIV_OAUTH_URL = 'https://app-api.pixiv.net/web/v1/login'; +const PIXIV_TOKEN_URL = 'https://oauth.secure.pixiv.net/auth/token'; +const PIXIV_REDIRECT_URI = 'https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback'; + +let initialized = false; + +async function ensureInitialized() { + if (initialized) return; + await api.init(); + await loadDiscordToken(); + await loadPixivToken(); + initialized = true; +} + +browser.runtime.onInstalled.addListener(() => ensureInitialized()); +browser.runtime.onStartup.addListener(() => ensureInitialized()); +ensureInitialized().catch(e => console.error('init failed:', e)); + +// ---- Discord token capture via webRequest ---- + +browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + const auth = details.requestHeaders?.find(h => h.name.toLowerCase() === 'authorization'); + if (auth?.value && auth.value !== discordToken) { + saveDiscordToken(auth.value); + } + }, + { urls: ['https://discord.com/api/*'] }, + ['requestHeaders'], +); + +async function loadDiscordToken() { + const s = await browser.storage.local.get(['discordToken', 'discordTokenCapturedAt']); + discordToken = s.discordToken || null; + discordTokenCapturedAt = s.discordTokenCapturedAt || null; +} + +async function saveDiscordToken(token) { + discordToken = token; + discordTokenCapturedAt = new Date().toISOString(); + await browser.storage.local.set({ discordToken: token, discordTokenCapturedAt }); +} + +// ---- Pixiv PKCE OAuth ---- + +async function loadPixivToken() { + const s = await browser.storage.local.get(['pixivRefreshToken', 'pixivTokenCapturedAt']); + pixivRefreshToken = s.pixivRefreshToken || null; + pixivTokenCapturedAt = s.pixivTokenCapturedAt || null; +} + +async function savePixivToken(token) { + pixivRefreshToken = token; + pixivTokenCapturedAt = new Date().toISOString(); + await browser.storage.local.set({ pixivRefreshToken: token, pixivTokenCapturedAt }); +} + +function generateCodeVerifier() { + const a = new Uint8Array(32); + crypto.getRandomValues(a); + return base64UrlEncode(a); +} + +async function generateCodeChallenge(verifier) { + const data = new TextEncoder().encode(verifier); + const hash = await crypto.subtle.digest('SHA-256', data); + return base64UrlEncode(new Uint8Array(hash)); +} + +function base64UrlEncode(buf) { + return btoa(String.fromCharCode(...buf)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} + +async function initiatePixivOAuth() { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = await generateCodeChallenge(codeVerifier); + + const params = new URLSearchParams({ + code_challenge: codeChallenge, + code_challenge_method: 'S256', + client: 'pixiv-android', + }); + const tab = await browser.tabs.create({ url: `${PIXIV_OAUTH_URL}?${params}` }); + + return new Promise((resolve, reject) => { + pixivOAuthPending = { codeVerifier, tabId: tab.id, resolve, reject }; + setTimeout(() => { + if (pixivOAuthPending && pixivOAuthPending.tabId === tab.id) { + pixivOAuthPending = null; + reject(new Error('Pixiv OAuth timed out (5 min)')); + } + }, 5 * 60 * 1000); + }); +} + +browser.webRequest.onBeforeRedirect.addListener( + async (details) => { + if (!pixivOAuthPending) return; + if (details.tabId !== pixivOAuthPending.tabId) return; + const url = new URL(details.redirectUrl); + const code = url.searchParams.get('code'); + if (!code) return; + const verifier = pixivOAuthPending.codeVerifier; + const resolve = pixivOAuthPending.resolve; + const reject = pixivOAuthPending.reject; + pixivOAuthPending = null; + try { + const tokenResp = await fetch(PIXIV_TOKEN_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: PIXIV_CLIENT_ID, + client_secret: PIXIV_CLIENT_SECRET, + code, + code_verifier: verifier, + grant_type: 'authorization_code', + include_policy: 'true', + redirect_uri: PIXIV_REDIRECT_URI, + }), + }); + const body = await tokenResp.json(); + if (!body.refresh_token) { + reject(new Error(`Pixiv token exchange failed: ${JSON.stringify(body)}`)); + return; + } + await savePixivToken(body.refresh_token); + try { await browser.tabs.remove(details.tabId); } catch {} + resolve(body.refresh_token); + } catch (e) { + reject(e); + } + }, + { urls: ['https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback*'] }, +); + +// ---- Message router ---- + +browser.runtime.onMessage.addListener(async (msg) => { + await ensureInitialized(); + switch (msg.type) { + case 'GET_CONFIG': + return { apiUrl: api.baseUrl, apiKey: api.apiKey }; + + case 'TEST_CONNECTION': + try { + await api.testConnection(); + return { connected: true }; + } catch (e) { + return { connected: false, error: e.message }; + } + + case 'GET_PLATFORM_STATUS': { + const status = {}; + for (const key of Object.keys(PLATFORMS)) { + if (PLATFORMS[key].authType === 'cookies') { + status[key] = { hasCookies: false, cookieCount: 0 }; + try { + const n = await getCookieCount(key); + status[key] = { hasCookies: n > 0, cookieCount: n }; + } catch (e) { + status[key] = { error: e.message }; + } + } else if (key === 'discord') { + status[key] = { hasToken: !!discordToken, capturedAt: discordTokenCapturedAt }; + } else if (key === 'pixiv') { + status[key] = { hasToken: !!pixivRefreshToken, capturedAt: pixivTokenCapturedAt }; + } else { + status[key] = {}; + } + } + return status; + } + + case 'EXPORT_COOKIES': { + const key = msg.platform; + const platform = PLATFORMS[key]; + if (!platform) return { error: `Unknown platform: ${key}` }; + try { + if (platform.authType === 'cookies') { + const cookies = await extractCookiesForPlatform(key); + if (cookies.length === 0) return { error: 'No cookies found — log in first.' }; + const data = toNetscapeFormat(cookies); + await api.uploadCredentials(key, 'cookies', data); + return { success: true, cookieCount: cookies.length }; + } + if (key === 'discord') { + if (!discordToken) return { error: 'Open discord.com to capture a token first.' }; + await api.uploadCredentials('discord', 'token', discordToken); + return { success: true }; + } + if (key === 'pixiv') { + if (!pixivRefreshToken) { + await initiatePixivOAuth(); + } + await api.uploadCredentials('pixiv', 'token', pixivRefreshToken); + return { success: true }; + } + return { error: 'Unsupported platform.' }; + } catch (e) { + return { error: e.message }; + } + } + + case 'EXPORT_ALL_COOKIES': { + const results = {}; + for (const key of Object.keys(PLATFORMS)) { + if (PLATFORMS[key].authType !== 'cookies') { + results[key] = { skipped: true }; + continue; + } + try { + const cookies = await extractCookiesForPlatform(key); + if (cookies.length === 0) { + results[key] = { skipped: true, reason: 'no cookies' }; + continue; + } + await api.uploadCredentials(key, 'cookies', toNetscapeFormat(cookies)); + results[key] = { success: true, cookieCount: cookies.length }; + } catch (e) { + results[key] = { error: e.message }; + } + } + return results; + } + + case 'LIST_SOURCES': + try { + return { sources: await api.listSources() }; + } catch (e) { + return { error: e.message }; + } + + case 'CHECK_SOURCE': + try { + return await api.triggerSourceCheck(msg.sourceId); + } catch (e) { + return { error: e.message }; + } + + case 'ADD_AS_SOURCE': + try { + return await api.quickAddSource(msg.url); + } catch (e) { + return { error: e.message }; + } + + default: + return { error: `Unknown message type: ${msg.type}` }; + } +});