feat: add CPA plus session import strategy
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function createJsonResponse(payload, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => payload,
|
||||
};
|
||||
}
|
||||
|
||||
function loadCpaApiModule() {
|
||||
const source = fs.readFileSync('background/cpa-api.js', 'utf8');
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundCpaApi;`)({});
|
||||
}
|
||||
|
||||
function encodeBase64UrlJson(value) {
|
||||
return Buffer.from(JSON.stringify(value)).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function createJwtToken(payload = {}) {
|
||||
return [
|
||||
encodeBase64UrlJson({ alg: 'HS256', typ: 'JWT' }),
|
||||
encodeBase64UrlJson(payload),
|
||||
'signature',
|
||||
].join('.');
|
||||
}
|
||||
|
||||
test('cpa api imports current ChatGPT session through management auth-files endpoint', async () => {
|
||||
const apiModule = loadCpaApiModule();
|
||||
const logs = [];
|
||||
const fetchCalls = [];
|
||||
const expiresAt = '2026-05-20T12:34:56.000Z';
|
||||
const accessToken = createJwtToken({
|
||||
exp: Math.floor(Date.parse(expiresAt) / 1000),
|
||||
email: 'jwt@example.com',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_123',
|
||||
chatgpt_plan_type: 'plus',
|
||||
chatgpt_user_id: 'user_123',
|
||||
},
|
||||
'https://api.openai.com/profile': {
|
||||
email: 'profile@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
const api = apiModule.createCpaApi({
|
||||
addLog: async (message, level = 'info', options = {}) => {
|
||||
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
|
||||
},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
const body = options.body ? JSON.parse(options.body) : null;
|
||||
fetchCalls.push({
|
||||
path: parsed.pathname,
|
||||
search: parsed.search,
|
||||
method: options.method || 'POST',
|
||||
headers: options.headers || {},
|
||||
body,
|
||||
});
|
||||
|
||||
if (parsed.pathname === '/v0/management/auth-files') {
|
||||
return createJsonResponse({});
|
||||
}
|
||||
|
||||
return createJsonResponse({ message: `unexpected path ${parsed.pathname}` }, 404);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await api.importCurrentChatGptSession({
|
||||
vpsUrl: 'https://cpa.example.com/management.html#/oauth',
|
||||
vpsPassword: 'management-key',
|
||||
session: {
|
||||
accessToken,
|
||||
expires: expiresAt,
|
||||
user: {
|
||||
email: 'flow@example.com',
|
||||
},
|
||||
account: {
|
||||
id: 'acct_123',
|
||||
planType: 'plus',
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
email: 'registration@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'identifier@example.com',
|
||||
}, {
|
||||
logLabel: '步骤 10',
|
||||
logOptions: { step: 10, stepKey: 'cpa-session-import' },
|
||||
});
|
||||
|
||||
const importCall = fetchCalls.find((call) => call.path === '/v0/management/auth-files');
|
||||
assert.ok(importCall, 'expected CPA auth-files import call');
|
||||
assert.equal(importCall.method, 'POST');
|
||||
assert.equal(importCall.headers.Authorization, 'Bearer management-key');
|
||||
assert.equal(importCall.headers['X-Management-Key'], 'management-key');
|
||||
assert.equal(
|
||||
decodeURIComponent(new URLSearchParams(importCall.search).get('name')),
|
||||
'codex-flow@example.com-plus.json'
|
||||
);
|
||||
assert.equal(importCall.body.type, 'codex');
|
||||
assert.equal(importCall.body.account_id, 'acct_123');
|
||||
assert.equal(importCall.body.chatgpt_account_id, 'acct_123');
|
||||
assert.equal(importCall.body.email, 'flow@example.com');
|
||||
assert.equal(importCall.body.plan_type, 'plus');
|
||||
assert.equal(importCall.body.chatgpt_plan_type, 'plus');
|
||||
assert.equal(importCall.body.access_token, accessToken);
|
||||
assert.equal(importCall.body.id_token_synthetic, true);
|
||||
assert.match(String(importCall.body.id_token || ''), /\.synthetic$/);
|
||||
assert.equal(importCall.body.expired, expiresAt);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(importCall.body, 'refresh_token'), false);
|
||||
assert.equal(result.verifiedStatus, 'CPA 会话导入完成:flow@example.com');
|
||||
assert.equal(
|
||||
logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('cpa api falls back to registration email when session has no readable email', () => {
|
||||
const apiModule = loadCpaApiModule();
|
||||
const accessToken = createJwtToken({
|
||||
exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_456',
|
||||
chatgpt_plan_type: 'plus',
|
||||
chatgpt_user_id: 'user_456',
|
||||
},
|
||||
});
|
||||
|
||||
const api = apiModule.createCpaApi();
|
||||
const result = api.buildCpaSessionAuthJson({
|
||||
session: {
|
||||
accessToken,
|
||||
user: {
|
||||
name: 'Flow User',
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
email: 'registration@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'identifier@example.com',
|
||||
});
|
||||
|
||||
assert.equal(result.email, 'registration@example.com');
|
||||
assert.equal(result.authJson.email, 'registration@example.com');
|
||||
assert.equal(result.fileName, 'codex-registration@example.com-plus.json');
|
||||
});
|
||||
|
||||
test('cpa api preserves provided id_token and refresh_token when available', () => {
|
||||
const apiModule = loadCpaApiModule();
|
||||
const accessToken = createJwtToken({
|
||||
exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_789',
|
||||
chatgpt_plan_type: 'plus',
|
||||
chatgpt_user_id: 'user_789',
|
||||
},
|
||||
});
|
||||
|
||||
const api = apiModule.createCpaApi();
|
||||
const result = api.buildCpaSessionAuthJson({
|
||||
session: {
|
||||
accessToken,
|
||||
user: {
|
||||
email: 'session@example.com',
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
id_token: 'provided-id-token',
|
||||
refresh_token: 'refresh-token-1',
|
||||
session_token: 'session-token-1',
|
||||
});
|
||||
|
||||
assert.equal(result.authJson.id_token, 'provided-id-token');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(result.authJson, 'id_token_synthetic'), false);
|
||||
assert.equal(result.authJson.refresh_token, 'refresh-token-1');
|
||||
assert.equal(result.authJson.session_token, 'session-token-1');
|
||||
assert.equal(result.hasRefreshToken, true);
|
||||
});
|
||||
Reference in New Issue
Block a user