293 lines
8.8 KiB
JavaScript
293 lines
8.8 KiB
JavaScript
(function attachSessionToJsonConverter(root, factory) {
|
|
root.MultiPageSessionToJsonConverter = factory();
|
|
})(typeof self !== 'undefined' ? self : globalThis, function createSessionToJsonConverterModule() {
|
|
function isPlainObject(value) {
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function firstNonEmpty(...values) {
|
|
for (const value of values) {
|
|
if (typeof value === 'string' && value.trim() !== '') {
|
|
return value.trim();
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function decodeBase64Url(value) {
|
|
const normalized = String(value || '').replace(/-/g, '+').replace(/_/g, '/');
|
|
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
|
|
|
|
if (typeof Buffer !== 'undefined') {
|
|
return Buffer.from(padded, 'base64').toString('utf8');
|
|
}
|
|
|
|
const binary = atob(padded);
|
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
return new TextDecoder().decode(bytes);
|
|
}
|
|
|
|
function bytesToBase64Url(bytes) {
|
|
if (typeof Buffer !== 'undefined') {
|
|
return Buffer.from(bytes).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
}
|
|
|
|
let binary = '';
|
|
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
|
}
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
}
|
|
|
|
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, '');
|
|
}
|
|
return bytesToBase64Url(new TextEncoder().encode(json));
|
|
}
|
|
|
|
function parseJwtPayload(token) {
|
|
if (typeof token !== 'string' || token.trim() === '') {
|
|
return undefined;
|
|
}
|
|
|
|
const segments = token.split('.');
|
|
if (segments.length < 2) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(decodeBase64Url(segments[1]));
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
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()) ? undefined : date.toISOString();
|
|
}
|
|
|
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
return undefined;
|
|
}
|
|
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
}
|
|
|
|
function timestampFromUnixSeconds(value) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric)) {
|
|
return undefined;
|
|
}
|
|
const date = new Date(numeric * 1000);
|
|
return Number.isNaN(date.getTime()) ? undefined : 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) {
|
|
if (!accountId) {
|
|
return undefined;
|
|
}
|
|
|
|
const now = Math.trunc(Date.now() / 1000);
|
|
const authInfo = { chatgpt_account_id: accountId };
|
|
const expires = epochSecondsFromValue(expiresAt) || now + 90 * 24 * 60 * 60;
|
|
|
|
if (planType) {
|
|
authInfo.chatgpt_plan_type = planType;
|
|
}
|
|
|
|
if (userId) {
|
|
authInfo.chatgpt_user_id = userId;
|
|
authInfo.user_id = userId;
|
|
}
|
|
|
|
const payload = {
|
|
iat: now,
|
|
exp: expires,
|
|
'https://api.openai.com/auth': authInfo,
|
|
};
|
|
|
|
if (email) {
|
|
payload.email = email;
|
|
}
|
|
|
|
return `${encodeBase64UrlJson({ alg: 'none', typ: 'JWT', cpa_synthetic: true })}.${encodeBase64UrlJson(payload)}.`;
|
|
}
|
|
|
|
function convertSessionJson(record, options = {}) {
|
|
if (!isPlainObject(record)) {
|
|
throw new Error('session 不是 JSON 对象');
|
|
}
|
|
|
|
const accessToken = firstNonEmpty(
|
|
record.accessToken,
|
|
record.access_token,
|
|
record.token?.accessToken,
|
|
record.token?.access_token,
|
|
record.credentials?.accessToken,
|
|
record.credentials?.access_token
|
|
);
|
|
if (!accessToken) {
|
|
throw new Error('缺少 accessToken');
|
|
}
|
|
|
|
const sessionToken = firstNonEmpty(
|
|
record.sessionToken,
|
|
record.session_token,
|
|
record.token?.sessionToken,
|
|
record.token?.session_token,
|
|
record.credentials?.session_token
|
|
);
|
|
const refreshToken = firstNonEmpty(
|
|
record.refreshToken,
|
|
record.refresh_token,
|
|
record.token?.refreshToken,
|
|
record.token?.refresh_token,
|
|
record.credentials?.refresh_token
|
|
);
|
|
const inputIdToken = firstNonEmpty(
|
|
record.idToken,
|
|
record.id_token,
|
|
record.token?.idToken,
|
|
record.token?.id_token,
|
|
record.credentials?.id_token
|
|
);
|
|
|
|
const payload = parseJwtPayload(accessToken);
|
|
const idPayload = parseJwtPayload(inputIdToken);
|
|
const auth = getOpenAIAuthSection(payload);
|
|
const idAuth = getOpenAIAuthSection(idPayload);
|
|
const profile = getOpenAIProfileSection(payload);
|
|
const expiresAt = firstNonEmpty(
|
|
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
|
|
normalizeTimestamp(record.expires),
|
|
normalizeTimestamp(record.expiresAt),
|
|
normalizeTimestamp(record.expired),
|
|
normalizeTimestamp(record.expires_at)
|
|
);
|
|
const email = firstNonEmpty(
|
|
record.user?.email,
|
|
record.email,
|
|
record.credentials?.email,
|
|
record.providerSpecificData?.email,
|
|
profile.email,
|
|
idPayload?.email,
|
|
payload?.email
|
|
);
|
|
const accountId = firstNonEmpty(
|
|
record.account?.id,
|
|
record.account_id,
|
|
record.chatgptAccountId,
|
|
record.providerSpecificData?.chatgptAccountId,
|
|
record.providerSpecificData?.chatgpt_account_id,
|
|
record.credentials?.chatgpt_account_id,
|
|
auth.chatgpt_account_id,
|
|
idAuth.chatgpt_account_id,
|
|
record.provider === 'codex' ? record.id : undefined
|
|
);
|
|
const userId = firstNonEmpty(
|
|
record.user?.id,
|
|
record.user_id,
|
|
record.chatgptUserId,
|
|
record.providerSpecificData?.chatgptUserId,
|
|
record.providerSpecificData?.chatgpt_user_id,
|
|
auth.chatgpt_user_id,
|
|
auth.user_id,
|
|
idAuth.chatgpt_user_id,
|
|
idAuth.user_id
|
|
);
|
|
const planType = firstNonEmpty(
|
|
record.account?.planType,
|
|
record.account?.plan_type,
|
|
record.planType,
|
|
record.plan_type,
|
|
record.providerSpecificData?.chatgptPlanType,
|
|
record.providerSpecificData?.chatgpt_plan_type,
|
|
record.credentials?.plan_type,
|
|
auth.chatgpt_plan_type,
|
|
idAuth.chatgpt_plan_type
|
|
);
|
|
const exportedAt = Object.prototype.hasOwnProperty.call(options, 'lastRefresh')
|
|
? String(options.lastRefresh ?? '')
|
|
: normalizeTimestamp(options.now || new Date());
|
|
const name = firstNonEmpty(email, options.sourceName, 'ChatGPT Account');
|
|
const syntheticIdToken = !inputIdToken
|
|
? buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt)
|
|
: undefined;
|
|
const idToken = firstNonEmpty(inputIdToken, syntheticIdToken);
|
|
|
|
const output = Object.fromEntries(
|
|
Object.entries({
|
|
type: 'codex',
|
|
account_id: accountId,
|
|
chatgpt_account_id: accountId,
|
|
email,
|
|
name,
|
|
plan_type: planType,
|
|
chatgpt_plan_type: planType,
|
|
id_token: idToken,
|
|
id_token_synthetic: Boolean(syntheticIdToken) || undefined,
|
|
access_token: accessToken,
|
|
refresh_token: refreshToken || '',
|
|
session_token: sessionToken,
|
|
last_refresh: exportedAt,
|
|
expired: expiresAt,
|
|
disabled: Boolean(record.disabled) || undefined,
|
|
}).filter(([, value]) => value !== undefined && value !== null)
|
|
);
|
|
|
|
const warnings = [];
|
|
if (!inputIdToken && syntheticIdToken) {
|
|
warnings.push('Missing real id_token; generated synthetic CPA-compatible id_token.');
|
|
}
|
|
if (!refreshToken) {
|
|
warnings.push('Missing refresh_token; imported account cannot refresh automatically after access token expiry.');
|
|
}
|
|
|
|
return { output, warnings };
|
|
}
|
|
|
|
return {
|
|
convertSessionJson,
|
|
};
|
|
});
|