(function attachBackgroundCpaApi(root, factory) { root.MultiPageBackgroundCpaApi = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaApiModule() { function createCpaApi(deps = {}) { const { addLog = async () => {}, fetchImpl = (...args) => fetch(...args), } = deps; function normalizeString(value = '') { return String(value || '').trim(); } function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function firstNonEmpty(...values) { for (const value of values) { const normalized = normalizeString(value); if (normalized) { return normalized; } } return ''; } function normalizeEmailValue(value = '') { const email = normalizeString(value); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : ''; } function extractStateFromAuthUrl(authUrl = '') { try { return new URL(authUrl).searchParams.get('state') || ''; } catch { return ''; } } function deriveCpaManagementOrigin(vpsUrl) { const normalizedUrl = normalizeString(vpsUrl); if (!normalizedUrl) { throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。'); } let parsed; try { parsed = new URL(normalizedUrl); } catch { throw new Error('CPA 地址格式无效,请先在侧边栏检查。'); } return parsed.origin; } function getCpaApiErrorMessage(payload, responseStatus = 500) { const candidates = [ payload?.error, payload?.message, payload?.detail, payload?.reason, ]; const message = candidates.map(normalizeString).find(Boolean); return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`; } async function fetchCpaManagementJson(origin, path, options = {}) { const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000)); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const managementKey = normalizeString(options.managementKey); const headers = { Accept: 'application/json', 'Content-Type': 'application/json', }; if (managementKey) { headers.Authorization = `Bearer ${managementKey}`; headers['X-Management-Key'] = managementKey; } const response = await fetchImpl(`${origin}${path}`, { method: options.method || 'POST', headers, body: options.body === undefined ? undefined : JSON.stringify(options.body), signal: controller.signal, }); let payload = {}; try { payload = await response.json(); } catch { payload = {}; } if (!response.ok) { throw new Error(getCpaApiErrorMessage(payload, response.status)); } return payload; } catch (error) { if (error?.name === 'AbortError') { throw new Error('CPA 管理接口请求超时,请稍后重试。'); } throw error; } finally { clearTimeout(timer); } } function decodeBase64UrlSegment(segment = '') { const normalized = normalizeString(segment) .replace(/-/g, '+') .replace(/_/g, '/'); if (!normalized) { return ''; } const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4); try { if (typeof Buffer !== 'undefined') { return Buffer.from(padded, 'base64').toString('utf8'); } if (typeof atob === 'function') { const binary = atob(padded); const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); if (typeof TextDecoder !== 'undefined') { return new TextDecoder().decode(bytes); } return binary; } } catch { return ''; } return ''; } function encodeBase64UrlJson(value) { const json = JSON.stringify(value); if (typeof Buffer !== 'undefined') { return Buffer.from(json, 'utf8') .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } const bytes = new TextEncoder().encode(json); let binary = ''; bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } function parseJwtPayload(token = '') { const normalized = normalizeString(token); if (!normalized) { return null; } const parts = normalized.split('.'); if (parts.length < 2) { return null; } try { return JSON.parse(decodeBase64UrlSegment(parts[1])); } catch { return null; } } function getOpenAiAuthSection(payload) { if (!isPlainObject(payload)) { return {}; } const auth = payload['https://api.openai.com/auth']; return isPlainObject(auth) ? auth : {}; } function getOpenAiProfileSection(payload) { if (!isPlainObject(payload)) { return {}; } const profile = payload['https://api.openai.com/profile']; return isPlainObject(profile) ? profile : {}; } function normalizeTimestamp(value) { if (value instanceof Date && !Number.isNaN(value.getTime())) { return value.toISOString(); } if (typeof value === 'number' && Number.isFinite(value)) { const milliseconds = value > 1e11 ? value : value * 1000; const date = new Date(milliseconds); return Number.isNaN(date.getTime()) ? '' : date.toISOString(); } if (typeof value !== 'string' || !value.trim()) { return ''; } const date = new Date(value); return Number.isNaN(date.getTime()) ? '' : date.toISOString(); } function timestampFromUnixSeconds(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return ''; } const date = new Date(numeric * 1000); return Number.isNaN(date.getTime()) ? '' : date.toISOString(); } function epochSecondsFromValue(value) { if (value === undefined || value === null || value === '') { return 0; } const numeric = Number(value); if (Number.isFinite(numeric)) { return Math.trunc(numeric > 1e11 ? numeric / 1000 : numeric); } const parsed = Date.parse(String(value)); return Number.isFinite(parsed) ? Math.trunc(parsed / 1000) : 0; } function buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt) { const normalizedAccountId = normalizeString(accountId); if (!normalizedAccountId) { return ''; } const now = Math.trunc(Date.now() / 1000); const expires = epochSecondsFromValue(expiresAt) || now + 90 * 24 * 60 * 60; const authInfo = { chatgpt_account_id: normalizedAccountId }; if (planType) { authInfo.chatgpt_plan_type = normalizeString(planType); } if (userId) { authInfo.chatgpt_user_id = normalizeString(userId); authInfo.user_id = normalizeString(userId); } const payload = { iat: now, exp: expires, 'https://api.openai.com/auth': authInfo, }; if (email) { payload.email = normalizeString(email); } return `${encodeBase64UrlJson({ alg: 'none', typ: 'JWT', cpa_synthetic: true })}.${encodeBase64UrlJson(payload)}.synthetic`; } function normalizePlanTypeForFileName(planType = '') { return normalizeString(planType) .split(/[^a-zA-Z0-9]+/) .map((part) => part.trim().toLowerCase()) .filter(Boolean) .join('-'); } function sanitizeFileSegment(value = '', fallback = 'chatgpt-session') { const normalized = normalizeString(value) .replace(/[\\/:*?"<>|]+/g, '-') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-+|-+$/g, ''); return normalized || fallback; } function buildCpaAuthFileName(metadata = {}) { const email = sanitizeFileSegment(metadata.email || ''); const planType = normalizePlanTypeForFileName(metadata.planType || ''); const accountId = sanitizeFileSegment(metadata.accountId || ''); if (email && planType) { return `codex-${email}-${planType}.json`; } if (email) { return `codex-${email}.json`; } if (accountId && planType) { return `codex-${accountId}-${planType}.json`; } if (accountId) { return `codex-${accountId}.json`; } return `codex-${Date.now()}.json`; } function buildCpaSessionAuthJson(state = {}, options = {}) { const session = isPlainObject(state?.session) ? state.session : {}; const accessToken = normalizeString(state?.accessToken || session?.accessToken); if (!accessToken) { throw new Error('未读取到可导入的 ChatGPT accessToken。'); } const inputIdToken = firstNonEmpty( state?.idToken, state?.id_token, session?.idToken, session?.id_token ); const refreshToken = firstNonEmpty( state?.refreshToken, state?.refresh_token, session?.refreshToken, session?.refresh_token ); const sessionToken = firstNonEmpty( state?.sessionToken, state?.session_token, session?.sessionToken, session?.session_token ); const accessPayload = parseJwtPayload(accessToken); const idPayload = parseJwtPayload(inputIdToken); const accessAuth = getOpenAiAuthSection(accessPayload); const idAuth = getOpenAiAuthSection(idPayload); const profile = getOpenAiProfileSection(accessPayload); const expiresAt = firstNonEmpty( timestampFromUnixSeconds(accessPayload?.exp), normalizeTimestamp(session?.expires), normalizeTimestamp(session?.expiresAt), normalizeTimestamp(session?.expired), normalizeTimestamp(session?.expires_at) ); const accountIdentifierEmail = normalizeString(state?.accountIdentifierType).toLowerCase() === 'email' ? normalizeEmailValue(state?.accountIdentifier) : ''; const email = firstNonEmpty( normalizeEmailValue(session?.user?.email), normalizeEmailValue(session?.email), normalizeEmailValue(state?.email), accountIdentifierEmail, normalizeEmailValue(profile?.email), normalizeEmailValue(idPayload?.email), normalizeEmailValue(accessPayload?.email) ); const accountId = firstNonEmpty( session?.account?.id, session?.account_id, accessAuth?.chatgpt_account_id, idAuth?.chatgpt_account_id ); const userId = firstNonEmpty( session?.user?.id, session?.user_id, accessAuth?.chatgpt_user_id, accessAuth?.user_id, idAuth?.chatgpt_user_id, idAuth?.user_id ); const planType = firstNonEmpty( session?.account?.planType, session?.account?.plan_type, session?.planType, session?.plan_type, accessAuth?.chatgpt_plan_type, idAuth?.chatgpt_plan_type ); const exportedAt = normalizeTimestamp(options.now || new Date()) || new Date().toISOString(); const syntheticIdToken = inputIdToken ? '' : buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt); const idToken = inputIdToken || syntheticIdToken; const authJson = Object.fromEntries( Object.entries({ type: 'codex', account_id: accountId, chatgpt_account_id: accountId, email, name: firstNonEmpty(email, state?.email, 'ChatGPT Account'), plan_type: planType, chatgpt_plan_type: planType, id_token: idToken, id_token_synthetic: syntheticIdToken ? true : undefined, access_token: accessToken, refresh_token: refreshToken || '', session_token: sessionToken, last_refresh: exportedAt, expired: expiresAt, disabled: session?.disabled === true ? true : undefined, }).filter(([, value]) => value !== undefined && value !== null && value !== '') ); return { authJson, accountId, email, expiresAt, fileName: buildCpaAuthFileName({ email, planType, accountId }), hasRefreshToken: Boolean(refreshToken), }; } async function logWithOptions(message, level = 'info', options = {}) { await addLog(message, level, options.logOptions || {}); } async function requestOAuthUrl(state, options = {}) { const managementKey = normalizeString(state?.vpsPassword); if (!managementKey) { throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。'); } const origin = deriveCpaManagementOrigin(state?.vpsUrl); const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', { method: 'GET', managementKey, timeoutMs: options.timeoutMs, }); const oauthUrl = firstNonEmpty( result?.url, result?.auth_url, result?.authUrl, result?.data?.url, result?.data?.auth_url, result?.data?.authUrl ); const oauthState = firstNonEmpty( result?.state, result?.auth_state, result?.authState, result?.data?.state, result?.data?.auth_state, result?.data?.authState, extractStateFromAuthUrl(oauthUrl) ); if (!oauthUrl || !oauthUrl.startsWith('http')) { throw new Error('CPA 管理接口未返回有效的 auth_url。'); } return { oauthUrl, cpaOAuthState: oauthState || null, cpaManagementOrigin: origin, }; } async function submitOAuthCallback(state, callbackUrl, options = {}) { const managementKey = normalizeString(state?.vpsPassword); if (!managementKey) { throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。'); } const origin = normalizeString(state?.cpaManagementOrigin) || deriveCpaManagementOrigin(state?.vpsUrl); const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', { method: 'POST', managementKey, timeoutMs: options.timeoutMs, body: { provider: 'codex', redirect_url: normalizeString(callbackUrl), }, }); return { localhostUrl: normalizeString(callbackUrl), verifiedStatus: firstNonEmpty(result?.message, result?.status_message, 'CPA 已通过接口提交回调'), }; } async function importCurrentChatGptSession(state = {}, options = {}) { const logLabel = normalizeString(options.logLabel) || 'CPA 会话导入'; const managementKey = normalizeString(state?.vpsPassword); if (!managementKey) { throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。'); } const origin = deriveCpaManagementOrigin(state?.vpsUrl); const sessionAuth = buildCpaSessionAuthJson(state, options); await logWithOptions(`${logLabel}:正在通过 CPA 管理接口导入当前 ChatGPT 会话...`, 'info', options); if (!sessionAuth.hasRefreshToken) { await logWithOptions(`${logLabel}:未包含 refresh_token,access_token 过期后无法自动续期。`, 'warn', options); } await fetchCpaManagementJson(origin, `/v0/management/auth-files?name=${encodeURIComponent(sessionAuth.fileName)}`, { method: 'POST', managementKey, timeoutMs: options.importTimeoutMs || options.timeoutMs, body: sessionAuth.authJson, }); const verifiedStatus = sessionAuth.email ? `CPA 会话导入完成:${sessionAuth.email}` : `CPA 会话导入完成:${sessionAuth.fileName}`; await logWithOptions(verifiedStatus, 'ok', options); return { verifiedStatus, cpaImportedFileName: sessionAuth.fileName, cpaImportedEmail: sessionAuth.email || null, }; } return { buildCpaSessionAuthJson, deriveCpaManagementOrigin, fetchCpaManagementJson, importCurrentChatGptSession, requestOAuthUrl, submitOAuthCallback, }; } return { createCpaApi, }; });