| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- // background/cpa-api.js — CPA management API helpers for Codex auth JSON import
- (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 deriveCpaManagementOrigin(vpsUrl) {
- const normalizedUrl = normalizeString(vpsUrl);
- if (!normalizedUrl) {
- throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
- }
- try {
- return new URL(normalizedUrl).origin;
- } catch {
- throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
- }
- }
- 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 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,
- };
- }
- return {
- createCpaApi,
- };
- });
|