feat: add flow-aware kiro device auth workflow
This commit is contained in:
@@ -121,6 +121,7 @@ const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const SIGNUP_METHOD_EMAIL = 'email';
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
@@ -132,6 +133,28 @@ const FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const self = {
|
||||
MultiPageFlowRegistry: {
|
||||
DEFAULT_KIRO_REGION: 'us-east-1',
|
||||
DEFAULT_KIRO_RS_URL: 'https://kiro.leftcode.xyz/admin',
|
||||
normalizeFlowId(value, fallback = 'openai') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'kiro') {
|
||||
return 'kiro';
|
||||
}
|
||||
if (normalized === 'codex' || normalized === 'openai') {
|
||||
return 'openai';
|
||||
}
|
||||
return String(fallback || 'openai').trim().toLowerCase() === 'kiro' ? 'kiro' : 'openai';
|
||||
},
|
||||
normalizeSourceId(flowId, sourceId, fallback = 'kiro-rs') {
|
||||
const normalizedFlowId = this.normalizeFlowId(flowId);
|
||||
if (normalizedFlowId !== 'kiro') {
|
||||
return 'cpa';
|
||||
}
|
||||
const normalizedSourceId = String(sourceId || '').trim().toLowerCase();
|
||||
return normalizedSourceId === 'kiro-rs' ? normalizedSourceId : fallback;
|
||||
},
|
||||
},
|
||||
GoPayUtils: {
|
||||
normalizeGoPayCountryCode(value) {
|
||||
const digits = String(value || '').replace(/\\D/g, '');
|
||||
@@ -252,6 +275,12 @@ return {
|
||||
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
|
||||
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'phone'), 'phone');
|
||||
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'unknown'), 'email');
|
||||
assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'codex'), 'openai');
|
||||
assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'kiro'), 'kiro');
|
||||
assert.equal(api.normalizePersistentSettingValue('kiroSourceId', 'unknown'), 'kiro-rs');
|
||||
assert.equal(api.normalizePersistentSettingValue('kiroRsUrl', ''), 'https://kiro.leftcode.xyz/admin');
|
||||
assert.equal(api.normalizePersistentSettingValue('kiroRegion', ''), 'us-east-1');
|
||||
assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), ' key-1 ');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadKiroDeviceAuthApi() {
|
||||
const source = fs.readFileSync('background/steps/kiro-device-auth.js', 'utf8');
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundKiroDeviceAuth;`)({});
|
||||
}
|
||||
|
||||
function createResponse({ ok = true, status = 200, json = null, text = '' } = {}) {
|
||||
const bodyText = text || (json ? JSON.stringify(json) : '');
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
statusText: bodyText || `HTTP ${status}`,
|
||||
async text() {
|
||||
return bodyText;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mergeUpdates(updatesList = []) {
|
||||
return updatesList.reduce((acc, item) => Object.assign(acc, item), {});
|
||||
}
|
||||
|
||||
test('kiro device auth module exposes a factory', () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
assert.equal(typeof api?.createKiroDeviceAuthExecutor, 'function');
|
||||
assert.equal(typeof api?.startBuilderIdDeviceLogin, 'function');
|
||||
assert.equal(typeof api?.uploadBuilderIdCredential, 'function');
|
||||
});
|
||||
|
||||
test('kiro start device login registers client, opens auth tab, and completes with runtime payload', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const fetchCalls = [];
|
||||
const stateUpdates = [];
|
||||
const registerCalls = [];
|
||||
const reuseCalls = [];
|
||||
const completeCalls = [];
|
||||
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
body: options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
if (url.endsWith('/client/register')) {
|
||||
return createResponse({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: {
|
||||
clientId: 'client-001',
|
||||
clientSecret: 'secret-001',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.endsWith('/device_authorization')) {
|
||||
return createResponse({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: {
|
||||
deviceCode: 'device-code-001',
|
||||
userCode: 'ABCD-1234',
|
||||
verificationUri: 'https://device.example.com/start',
|
||||
verificationUriComplete: 'https://device.example.com/complete',
|
||||
interval: 7,
|
||||
expiresIn: 900,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fetch URL: ${url}`);
|
||||
},
|
||||
getState: async () => ({
|
||||
kiroRegion: 'eu-west-1',
|
||||
}),
|
||||
registerTab: async (source, tabId) => {
|
||||
registerCalls.push({ source, tabId });
|
||||
},
|
||||
reuseOrCreateTab: async (source, url) => {
|
||||
reuseCalls.push({ source, url });
|
||||
return 88;
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroStartDeviceLogin({
|
||||
nodeId: 'kiro-start-device-login',
|
||||
kiroRegion: 'eu-west-1',
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://oidc.eu-west-1.amazonaws.com/client/register');
|
||||
assert.equal(fetchCalls[1].url, 'https://oidc.eu-west-1.amazonaws.com/device_authorization');
|
||||
assert.deepEqual(fetchCalls[1].body, {
|
||||
clientId: 'client-001',
|
||||
clientSecret: 'secret-001',
|
||||
startUrl: 'https://view.awsapps.com/start',
|
||||
});
|
||||
assert.deepEqual(reuseCalls, [{
|
||||
source: 'kiro-device-auth',
|
||||
url: 'https://device.example.com/complete',
|
||||
}]);
|
||||
assert.deepEqual(registerCalls, [{
|
||||
source: 'kiro-device-auth',
|
||||
tabId: 88,
|
||||
}]);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroClientId, 'client-001');
|
||||
assert.equal(finalState.kiroClientSecret, 'secret-001');
|
||||
assert.equal(finalState.kiroDeviceAuthorizationCode, 'device-code-001');
|
||||
assert.equal(finalState.kiroDeviceCode, 'ABCD-1234');
|
||||
assert.equal(finalState.kiroLoginUrl, 'https://device.example.com/complete');
|
||||
assert.equal(finalState.kiroAuthRegion, 'eu-west-1');
|
||||
assert.equal(finalState.kiroAuthIntervalSeconds, 7);
|
||||
assert.equal(finalState.kiroAuthStatus, 'waiting_user');
|
||||
assert.equal(finalState.kiroUploadStatus, 'waiting_login');
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-start-device-login');
|
||||
assert.equal(completeCalls[0].payload.kiroDeviceCode, 'ABCD-1234');
|
||||
assert.equal(completeCalls[0].payload.kiroLoginUrl, 'https://device.example.com/complete');
|
||||
});
|
||||
|
||||
test('kiro await device login polls until refresh token is captured', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const fetchCalls = [];
|
||||
const stateUpdates = [];
|
||||
const sleepCalls = [];
|
||||
const completeCalls = [];
|
||||
|
||||
let pollCount = 0;
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
body: options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
pollCount += 1;
|
||||
if (pollCount === 1) {
|
||||
return createResponse({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: { error: 'authorization_pending' },
|
||||
});
|
||||
}
|
||||
return createResponse({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: {
|
||||
accessToken: 'access-001',
|
||||
refreshToken: 'refresh-001',
|
||||
expiresIn: 3600,
|
||||
},
|
||||
});
|
||||
},
|
||||
getState: async () => ({
|
||||
kiroClientId: 'client-001',
|
||||
kiroClientSecret: 'secret-001',
|
||||
kiroDeviceAuthorizationCode: 'device-code-001',
|
||||
kiroAuthRegion: 'us-east-1',
|
||||
kiroAuthExpiresAt: Date.now() + 60000,
|
||||
kiroAuthIntervalSeconds: 5,
|
||||
}),
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async (ms) => {
|
||||
sleepCalls.push(ms);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroAwaitDeviceLogin({
|
||||
nodeId: 'kiro-await-device-login',
|
||||
kiroClientId: 'client-001',
|
||||
kiroClientSecret: 'secret-001',
|
||||
kiroDeviceAuthorizationCode: 'device-code-001',
|
||||
kiroAuthRegion: 'us-east-1',
|
||||
kiroAuthExpiresAt: Date.now() + 60000,
|
||||
kiroAuthIntervalSeconds: 5,
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://oidc.us-east-1.amazonaws.com/token');
|
||||
assert.deepEqual(fetchCalls[0].body, {
|
||||
clientId: 'client-001',
|
||||
clientSecret: 'secret-001',
|
||||
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||
deviceCode: 'device-code-001',
|
||||
});
|
||||
assert.deepEqual(sleepCalls, [5000]);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroAuthStatus, 'authorized');
|
||||
assert.equal(finalState.kiroRefreshToken, 'refresh-001');
|
||||
assert.equal(finalState.kiroAccessToken, 'access-001');
|
||||
assert.equal(finalState.kiroUploadStatus, 'ready_to_upload');
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-await-device-login');
|
||||
assert.equal(completeCalls[0].payload.kiroRefreshToken, 'refresh-001');
|
||||
});
|
||||
|
||||
test('kiro upload credential checks connection and uploads builder id credential to kiro.rs', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const fetchCalls = [];
|
||||
const stateUpdates = [];
|
||||
const completeCalls = [];
|
||||
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
headers: options.headers || {},
|
||||
body: options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
if (options.method === 'GET') {
|
||||
return createResponse({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: { success: true },
|
||||
});
|
||||
}
|
||||
return createResponse({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: {
|
||||
success: true,
|
||||
message: 'uploaded',
|
||||
credentialId: 321,
|
||||
email: 'aws-user@example.com',
|
||||
},
|
||||
});
|
||||
},
|
||||
getState: async () => ({
|
||||
kiroRefreshToken: 'refresh-001',
|
||||
kiroClientId: 'client-001',
|
||||
kiroClientSecret: 'secret-001',
|
||||
kiroAuthRegion: 'ap-southeast-1',
|
||||
kiroAuthorizedEmail: 'cached@example.com',
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'admin-key-001',
|
||||
ipProxyEnabled: true,
|
||||
ipProxyProtocol: 'socks5',
|
||||
ipProxyHost: '127.0.0.1',
|
||||
ipProxyPort: '1080',
|
||||
ipProxyUsername: 'proxy-user',
|
||||
ipProxyPassword: 'proxy-pass',
|
||||
}),
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroUploadCredential({
|
||||
nodeId: 'kiro-upload-credential',
|
||||
kiroRefreshToken: 'refresh-001',
|
||||
kiroClientId: 'client-001',
|
||||
kiroClientSecret: 'secret-001',
|
||||
kiroAuthRegion: 'ap-southeast-1',
|
||||
kiroAuthorizedEmail: 'cached@example.com',
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'admin-key-001',
|
||||
ipProxyEnabled: true,
|
||||
ipProxyProtocol: 'socks5',
|
||||
ipProxyHost: '127.0.0.1',
|
||||
ipProxyPort: '1080',
|
||||
ipProxyUsername: 'proxy-user',
|
||||
ipProxyPassword: 'proxy-pass',
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://kiro.example.com/api/admin/credentials');
|
||||
assert.equal(fetchCalls[0].method, 'GET');
|
||||
assert.equal(fetchCalls[0].headers['x-api-key'], 'admin-key-001');
|
||||
|
||||
assert.equal(fetchCalls[1].url, 'https://kiro.example.com/api/admin/credentials');
|
||||
assert.equal(fetchCalls[1].method, 'POST');
|
||||
assert.equal(fetchCalls[1].headers['x-api-key'], 'admin-key-001');
|
||||
assert.deepEqual(fetchCalls[1].body, {
|
||||
refreshToken: 'refresh-001',
|
||||
clientId: 'client-001',
|
||||
clientSecret: 'secret-001',
|
||||
region: 'ap-southeast-1',
|
||||
email: 'cached@example.com',
|
||||
priority: 0,
|
||||
authMethod: 'IdC',
|
||||
provider: 'BuilderId',
|
||||
proxyUrl: 'socks5://127.0.0.1:1080',
|
||||
proxyUsername: 'proxy-user',
|
||||
proxyPassword: 'proxy-pass',
|
||||
});
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroLastConnectionMessage, 'kiro.rs connection ok (HTTP 200)');
|
||||
assert.equal(finalState.kiroAuthorizedEmail, 'aws-user@example.com');
|
||||
assert.equal(finalState.kiroCredentialId, 321);
|
||||
assert.equal(finalState.kiroUploadStatus, 'uploaded');
|
||||
assert.equal(typeof finalState.kiroLastUploadAt, 'number');
|
||||
assert.equal(finalState.kiroLastUploadAt > 0, true);
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-upload-credential');
|
||||
assert.equal(completeCalls[0].payload.kiroCredentialId, 321);
|
||||
assert.equal(completeCalls[0].payload.kiroUploadStatus, 'uploaded');
|
||||
});
|
||||
@@ -0,0 +1,407 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
|
||||
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
|
||||
const backgroundSource = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => backgroundSource.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < backgroundSource.length; i += 1) {
|
||||
const ch = backgroundSource[i];
|
||||
if (ch === '(') parenDepth += 1;
|
||||
if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
}
|
||||
if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < backgroundSource.length; end += 1) {
|
||||
const ch = backgroundSource[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return backgroundSource.slice(start, end);
|
||||
}
|
||||
|
||||
function buildHarness(extra = '') {
|
||||
return new Function(`
|
||||
const self = {};
|
||||
${flowRegistrySource}
|
||||
${settingsSchemaSource}
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const DEFAULT_SUB2API_GROUP_NAMES = ['codex', 'openai-plus'];
|
||||
const PERSISTED_SETTING_DEFAULTS = {
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'email',
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
phoneVerificationEnabled: false,
|
||||
mailProvider: '163',
|
||||
ipProxyEnabled: false,
|
||||
ipProxyService: '711proxy',
|
||||
ipProxyMode: 'account',
|
||||
kiroSourceId: 'kiro-rs',
|
||||
kiroRsUrl: 'https://kiro.leftcode.xyz/admin',
|
||||
kiroRsKey: '',
|
||||
kiroRegion: 'us-east-1',
|
||||
stepExecutionRangeByFlow: {},
|
||||
};
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState'];
|
||||
const LEGACY_AUTO_STEP_DELAY_KEYS = [];
|
||||
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = [];
|
||||
function normalizePanelMode(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa';
|
||||
}
|
||||
function normalizeSignupMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
|
||||
}
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
|
||||
}
|
||||
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value : []; }
|
||||
function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value : []; }
|
||||
function normalizeCloudMailDomains(value) { return Array.isArray(value) ? value : []; }
|
||||
function normalizeMailProvider(value = '') { return String(value || '163').trim().toLowerCase() || '163'; }
|
||||
function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
||||
function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; }
|
||||
function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; }
|
||||
function normalizeIpProxyServiceProfiles(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
||||
function buildIpProxyServiceProfileFromState() {
|
||||
return {
|
||||
mode: 'account',
|
||||
apiUrl: '',
|
||||
accountList: '',
|
||||
accountSessionPrefix: '',
|
||||
accountLifeMinutes: '',
|
||||
poolTargetCount: '20',
|
||||
host: '',
|
||||
port: '',
|
||||
protocol: 'http',
|
||||
username: '',
|
||||
password: '',
|
||||
region: '',
|
||||
};
|
||||
}
|
||||
function normalizeIpProxyAccountList(value) { return String(value || ''); }
|
||||
function normalizeIpProxyAccountSessionPrefix(value) { return String(value || ''); }
|
||||
function normalizeIpProxyAccountLifeMinutes(value) { return String(value || ''); }
|
||||
function normalizeIpProxyPoolTargetCount(value) { return String(value || '20'); }
|
||||
function normalizeIpProxyPort(value) { return String(value || '').trim(); }
|
||||
function normalizeIpProxyProtocol(value) { return String(value || 'http').trim() || 'http'; }
|
||||
function resolveSignupMethod(state = {}) {
|
||||
const activeFlowId = String(state?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID;
|
||||
if (activeFlowId === 'kiro') {
|
||||
return 'email';
|
||||
}
|
||||
return String(state?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
|
||||
${extractFunction('normalizePersistentSettingValue')}
|
||||
${extractFunction('getSettingsSchemaApi')}
|
||||
${extractFunction('buildPersistentSettingsPayload')}
|
||||
${extractFunction('getPersistedSettings')}
|
||||
${extractFunction('setPersistentSettings')}
|
||||
${extra}
|
||||
return {
|
||||
buildPersistentSettingsPayload,
|
||||
getPersistedSettings,
|
||||
setPersistentSettings,
|
||||
getRequestedKeys: typeof getRequestedKeys === 'function' ? getRequestedKeys : () => [],
|
||||
getPersistedWrites: typeof getPersistedWrites === 'function' ? getPersistedWrites : () => [],
|
||||
};
|
||||
`)();
|
||||
}
|
||||
|
||||
test('buildPersistentSettingsPayload writes canonical settings schema into persisted payloads when defaults are materialized', () => {
|
||||
const api = buildHarness();
|
||||
|
||||
const payload = api.buildPersistentSettingsPayload({
|
||||
activeFlowId: 'kiro',
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'secret-key',
|
||||
kiroRegion: 'eu-west-1',
|
||||
}, { fillDefaults: true });
|
||||
|
||||
assert.equal(payload.activeFlowId, 'kiro');
|
||||
assert.equal(payload.kiroSourceId, 'kiro-rs');
|
||||
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(payload.kiroRsKey, 'secret-key');
|
||||
assert.equal(payload.kiroRegion, 'eu-west-1');
|
||||
assert.equal(payload.settingsSchemaVersion, 3);
|
||||
assert.equal(payload.settingsState.activeFlowId, 'kiro');
|
||||
assert.equal(payload.settingsState.flows.kiro.source.selected, 'kiro-rs');
|
||||
});
|
||||
|
||||
test('buildPersistentSettingsPayload accepts schema-only input when requireKnownKeys is enabled', () => {
|
||||
const api = buildHarness();
|
||||
|
||||
const payload = api.buildPersistentSettingsPayload({
|
||||
settingsSchemaVersion: 3,
|
||||
settingsState: {
|
||||
activeFlowId: 'kiro',
|
||||
services: {
|
||||
email: { provider: '163' },
|
||||
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
|
||||
},
|
||||
flows: {
|
||||
openai: {
|
||||
source: { selected: 'cpa', entries: {} },
|
||||
account: { customPassword: '' },
|
||||
signup: {
|
||||
signupMethod: 'email',
|
||||
phoneVerificationEnabled: false,
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
},
|
||||
plus: {
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
|
||||
},
|
||||
},
|
||||
kiro: {
|
||||
source: {
|
||||
selected: 'kiro-rs',
|
||||
entries: {
|
||||
'kiro-rs': {
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'schema-only-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
kiroRegion: 'eu-west-1',
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, { requireKnownKeys: true });
|
||||
|
||||
assert.equal(payload.activeFlowId, 'kiro');
|
||||
assert.equal(payload.kiroSourceId, 'kiro-rs');
|
||||
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(payload.kiroRsKey, 'schema-only-key');
|
||||
assert.equal(payload.kiroRegion, 'eu-west-1');
|
||||
assert.equal(payload.settingsSchemaVersion, 3);
|
||||
});
|
||||
|
||||
test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => {
|
||||
const api = buildHarness(`
|
||||
let requestedKeys = [];
|
||||
const chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
async get(keys) {
|
||||
requestedKeys = Array.isArray(keys) ? [...keys] : [];
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
function getRequestedKeys() {
|
||||
return requestedKeys;
|
||||
}
|
||||
`);
|
||||
|
||||
const state = await api.getPersistedSettings();
|
||||
|
||||
assert.ok(api.getRequestedKeys().includes('settingsSchemaVersion'));
|
||||
assert.ok(api.getRequestedKeys().includes('settingsState'));
|
||||
assert.equal(state.settingsSchemaVersion, 3);
|
||||
assert.equal(state.settingsState.activeFlowId, 'openai');
|
||||
});
|
||||
|
||||
test('getPersistedSettings can project schema-only storage back into legacy flat settings', async () => {
|
||||
const api = buildHarness(`
|
||||
const chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
async get() {
|
||||
return {
|
||||
settingsSchemaVersion: 3,
|
||||
settingsState: {
|
||||
activeFlowId: 'kiro',
|
||||
services: {
|
||||
email: { provider: 'hotmail' },
|
||||
proxy: { enabled: true, provider: '711proxy', mode: 'account' },
|
||||
},
|
||||
flows: {
|
||||
openai: {
|
||||
source: {
|
||||
selected: 'sub2api',
|
||||
entries: {},
|
||||
},
|
||||
account: { customPassword: '' },
|
||||
signup: {
|
||||
signupMethod: 'email',
|
||||
phoneVerificationEnabled: false,
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
},
|
||||
plus: {
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
|
||||
},
|
||||
},
|
||||
kiro: {
|
||||
source: {
|
||||
selected: 'kiro-rs',
|
||||
entries: {
|
||||
'kiro-rs': {
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'stored-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
kiroRegion: 'ap-southeast-1',
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
`);
|
||||
|
||||
const state = await api.getPersistedSettings();
|
||||
|
||||
assert.equal(state.activeFlowId, 'kiro');
|
||||
assert.equal(state.panelMode, 'sub2api');
|
||||
assert.equal(state.mailProvider, 'hotmail');
|
||||
assert.equal(state.ipProxyEnabled, true);
|
||||
assert.equal(state.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(state.kiroRsKey, 'stored-key');
|
||||
assert.equal(state.kiroRegion, 'ap-southeast-1');
|
||||
assert.deepEqual(state.stepExecutionRangeByFlow.kiro, {
|
||||
enabled: true,
|
||||
fromStep: 1,
|
||||
toStep: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('setPersistentSettings materializes canonical schema keys for schema-only updates', async () => {
|
||||
const api = buildHarness(`
|
||||
const persistedWrites = [];
|
||||
const chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
async get() {
|
||||
return {};
|
||||
},
|
||||
async set(payload) {
|
||||
persistedWrites.push(JSON.parse(JSON.stringify(payload)));
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
function getPersistedWrites() {
|
||||
return persistedWrites;
|
||||
}
|
||||
`);
|
||||
|
||||
const persisted = await api.setPersistentSettings({
|
||||
settingsSchemaVersion: 3,
|
||||
settingsState: {
|
||||
activeFlowId: 'kiro',
|
||||
services: {
|
||||
email: { provider: '163' },
|
||||
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
|
||||
},
|
||||
flows: {
|
||||
openai: {
|
||||
source: { selected: 'cpa', entries: {} },
|
||||
account: { customPassword: '' },
|
||||
signup: {
|
||||
signupMethod: 'email',
|
||||
phoneVerificationEnabled: false,
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
},
|
||||
plus: {
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
|
||||
},
|
||||
},
|
||||
kiro: {
|
||||
source: {
|
||||
selected: 'kiro-rs',
|
||||
entries: {
|
||||
'kiro-rs': {
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'nested-only-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
kiroRegion: 'us-west-2',
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const write = api.getPersistedWrites().at(-1);
|
||||
|
||||
assert.equal(persisted.activeFlowId, 'kiro');
|
||||
assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(persisted.kiroRsKey, 'nested-only-key');
|
||||
assert.equal(persisted.kiroRegion, 'us-west-2');
|
||||
assert.equal(persisted.settingsSchemaVersion, 3);
|
||||
assert.equal(write.activeFlowId, 'kiro');
|
||||
assert.equal(write.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(write.kiroRsKey, 'nested-only-key');
|
||||
assert.equal(write.kiroRegion, 'us-west-2');
|
||||
assert.equal(write.settingsSchemaVersion, 3);
|
||||
assert.equal(write.settingsState.activeFlowId, 'kiro');
|
||||
});
|
||||
@@ -2,7 +2,7 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports step 1~10 modules', () => {
|
||||
test('background imports workflow step modules including Kiro device auth', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
[
|
||||
@@ -16,6 +16,7 @@ test('background imports step 1~10 modules', () => {
|
||||
'background/steps/fetch-login-code.js',
|
||||
'background/steps/confirm-oauth.js',
|
||||
'background/steps/platform-verify.js',
|
||||
'background/steps/kiro-device-auth.js',
|
||||
].forEach((path) => {
|
||||
assert.match(source, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background node registry preserves node metadata even before an executor is registered', () => {
|
||||
const source = fs.readFileSync('background/steps/registry.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStepRegistry;`)({});
|
||||
const registry = api.createNodeRegistry([
|
||||
{
|
||||
flowId: 'kiro',
|
||||
legacyStepId: 1,
|
||||
nodeId: 'kiro-start-device-login',
|
||||
displayOrder: 10,
|
||||
executeKey: 'kiro-start-device-login',
|
||||
title: 'Start device login',
|
||||
},
|
||||
]);
|
||||
|
||||
const node = registry.getNodeDefinition('kiro-start-device-login');
|
||||
|
||||
assert.equal(node.flowId, 'kiro');
|
||||
assert.equal(node.legacyStepId, 1);
|
||||
assert.equal(node.title, 'Start device login');
|
||||
assert.throws(
|
||||
() => registry.executeNode('kiro-start-device-login', {}),
|
||||
/Missing node executor: kiro-start-device-login/
|
||||
);
|
||||
});
|
||||
|
||||
test('background node registry executes registered nodes in display order', async () => {
|
||||
const source = fs.readFileSync('background/steps/registry.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStepRegistry;`)({});
|
||||
const events = [];
|
||||
const registry = api.createNodeRegistry([
|
||||
{
|
||||
flowId: 'openai',
|
||||
legacyStepId: 2,
|
||||
nodeId: 'submit-signup-email',
|
||||
displayOrder: 20,
|
||||
executeKey: 'submit-signup-email',
|
||||
title: 'Submit signup email',
|
||||
execute: async (state) => {
|
||||
events.push({ type: 'execute', state });
|
||||
},
|
||||
},
|
||||
{
|
||||
flowId: 'openai',
|
||||
legacyStepId: 1,
|
||||
nodeId: 'open-chatgpt',
|
||||
displayOrder: 10,
|
||||
executeKey: 'open-chatgpt',
|
||||
title: 'Open ChatGPT',
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
registry.getOrderedNodes().map((node) => node.nodeId),
|
||||
['open-chatgpt', 'submit-signup-email']
|
||||
);
|
||||
|
||||
await registry.executeNode('submit-signup-email', { activeFlowId: 'openai' });
|
||||
|
||||
assert.deepStrictEqual(events, [{ type: 'execute', state: { activeFlowId: 'openai' } }]);
|
||||
});
|
||||
@@ -8,37 +8,10 @@ test('background imports node registry and shared workflow definitions', () => {
|
||||
assert.match(source, /data\/step-definitions\.js/);
|
||||
assert.match(source, /background\/workflow-engine\.js/);
|
||||
assert.match(source, /MultiPageStepDefinitions\?\.getNodes/);
|
||||
assert.match(source, /getStepRegistryForState\(state\)/);
|
||||
assert.match(source, /buildNodeRegistry\(definitions/);
|
||||
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
|
||||
assert.match(source, /NORMAL_PHONE_STEP_DEFINITIONS/);
|
||||
assert.match(source, /NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_PAYPAL_PHONE_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_GOPAY_PHONE_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_GPC_PHONE_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
|
||||
assert.match(source, /plusPayPalStepRegistry/);
|
||||
assert.match(source, /plusGoPayStepRegistry/);
|
||||
assert.match(source, /normalPhoneStepRegistry/);
|
||||
assert.match(source, /normalPhoneBoundEmailReloginStepRegistry/);
|
||||
assert.match(source, /plusPayPalPhoneStepRegistry/);
|
||||
assert.match(source, /plusPayPalPhoneBoundEmailReloginStepRegistry/);
|
||||
assert.match(source, /plusGoPayPhoneStepRegistry/);
|
||||
assert.match(source, /plusGoPayPhoneBoundEmailReloginStepRegistry/);
|
||||
assert.match(source, /plusGpcPhoneStepRegistry/);
|
||||
assert.match(source, /plusGpcPhoneBoundEmailReloginStepRegistry/);
|
||||
assert.match(source, /const signupMethod = getSignupMethodForStepDefinitions\(state\);/);
|
||||
assert.match(source, /const useBoundEmailRelogin = signupMethod === SIGNUP_METHOD_PHONE/);
|
||||
assert.match(source, /useBoundEmailRelogin \? normalPhoneBoundEmailReloginStepRegistry : normalPhoneStepRegistry/);
|
||||
assert.match(source, /const paymentMethod = normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\);/);
|
||||
assert.match(source, /paymentMethod === PLUS_PAYMENT_METHOD_GOPAY/);
|
||||
assert.match(source, /useBoundEmailRelogin \? plusGoPayPhoneBoundEmailReloginStepRegistry : plusGoPayPhoneStepRegistry/);
|
||||
assert.match(source, /useBoundEmailRelogin \? plusPayPalPhoneBoundEmailReloginStepRegistry : plusPayPalPhoneStepRegistry/);
|
||||
assert.match(source, /useBoundEmailRelogin \? plusGpcPhoneBoundEmailReloginStepRegistry : plusGpcPhoneStepRegistry/);
|
||||
assert.match(source, /activeStepRegistry\.executeNode\(normalizedNodeId,\s*\{/);
|
||||
assert.match(source, /const stepRegistryCache = new Map\(\);/);
|
||||
assert.match(source, /const definitions = getNodeDefinitionsForState\(state\);/);
|
||||
assert.match(source, /stepRegistryCache\.set\(cacheKey, buildStepRegistry\(definitions\)\)/);
|
||||
assert.match(source, /'bind-email': \(state\) => step8Executor\.executeBindEmail\(state\)/);
|
||||
assert.match(source, /'fetch-bind-email-code': \(state\) => step8Executor\.executeFetchBindEmailCode\(state\)/);
|
||||
assert.match(source, /'relogin-bound-email': \(state\) => executeReloginBoundEmail\(state\)/);
|
||||
@@ -51,9 +24,14 @@ test('background imports node registry and shared workflow definitions', () => {
|
||||
assert.match(source, /background\/steps\/paypal-approve\.js/);
|
||||
assert.match(source, /background\/steps\/gopay-approve\.js/);
|
||||
assert.match(source, /background\/steps\/plus-return-confirm\.js/);
|
||||
assert.match(source, /background\/steps\/kiro-device-auth\.js/);
|
||||
assert.match(source, /const kiroDeviceAuthExecutor = self\.MultiPageBackgroundKiroDeviceAuth\?\.createKiroDeviceAuthExecutor\(/);
|
||||
assert.match(source, /'kiro-start-device-login': \(state\) => kiroDeviceAuthExecutor\.executeKiroStartDeviceLogin\(state\)/);
|
||||
assert.match(source, /'kiro-await-device-login': \(state\) => kiroDeviceAuthExecutor\.executeKiroAwaitDeviceLogin\(state\)/);
|
||||
assert.match(source, /'kiro-upload-credential': \(state\) => kiroDeviceAuthExecutor\.executeKiroUploadCredential\(state\)/);
|
||||
assert.match(source, /'kiro-start-device-login',[\s\S]*'kiro-await-device-login',[\s\S]*'kiro-upload-credential'/);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /createGoPayApproveExecutor\(\{[\s\S]*clickWithDebugger[\s\S]*requestGoPayOtpInput[\s\S]*\}\)/);
|
||||
|
||||
@@ -2,11 +2,16 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
|
||||
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
|
||||
const source = fs.readFileSync('shared/flow-capabilities.js', 'utf8');
|
||||
|
||||
function loadApi() {
|
||||
const scope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageFlowCapabilities;`)(scope);
|
||||
return new Function(
|
||||
'self',
|
||||
`${flowRegistrySource}; ${settingsSchemaSource}; ${source}; return self.MultiPageFlowCapabilities;`
|
||||
)(scope);
|
||||
}
|
||||
|
||||
test('flow capability registry keeps OpenAI phone signup available only when runtime locks allow it', () => {
|
||||
@@ -71,6 +76,32 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
|
||||
assert.deepEqual(capabilityState.supportedPanelModes, []);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes Kiro as an independent flow with its own visible groups', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'kiro',
|
||||
kiroSourceId: 'kiro-rs',
|
||||
panelMode: 'sub2api',
|
||||
signupMethod: 'phone',
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(capabilityState.activeFlowId, 'kiro');
|
||||
assert.equal(capabilityState.canShowPhoneSettings, false);
|
||||
assert.equal(capabilityState.canShowPlusSettings, false);
|
||||
assert.equal(capabilityState.effectiveSignupMethod, 'email');
|
||||
assert.equal(capabilityState.effectiveSourceId, 'kiro-rs');
|
||||
assert.deepEqual(
|
||||
capabilityState.visibleGroupIds,
|
||||
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy']
|
||||
);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry({
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
|
||||
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
|
||||
|
||||
function loadApis() {
|
||||
const scope = {};
|
||||
return new Function('self', `${flowRegistrySource}; ${settingsSchemaSource}; return {
|
||||
flowRegistry: self.MultiPageFlowRegistry,
|
||||
settingsSchema: self.MultiPageSettingsSchema,
|
||||
};`)(scope);
|
||||
}
|
||||
|
||||
test('flow registry exposes openai and kiro with canonical source metadata', () => {
|
||||
const { flowRegistry } = loadApis();
|
||||
|
||||
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']);
|
||||
assert.equal(flowRegistry.getFlowLabel('codex'), 'Codex / OpenAI');
|
||||
assert.equal(flowRegistry.normalizeFlowId('codex'), 'openai');
|
||||
assert.equal(flowRegistry.normalizeSourceId('openai', 'sub2api'), 'sub2api');
|
||||
assert.equal(flowRegistry.normalizeSourceId('kiro', 'anything-else'), 'kiro-rs');
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
|
||||
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy']
|
||||
);
|
||||
});
|
||||
|
||||
test('settings schema normalizes flat input into canonical flow and service namespaces', () => {
|
||||
const { settingsSchema } = loadApis();
|
||||
const schema = settingsSchema.createSettingsSchema();
|
||||
|
||||
const normalized = schema.normalizeSettingsState({
|
||||
activeFlowId: 'kiro',
|
||||
panelMode: 'sub2api',
|
||||
mailProvider: 'hotmail',
|
||||
ipProxyEnabled: true,
|
||||
ipProxyService: '711proxy',
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'secret-key',
|
||||
kiroRegion: 'eu-west-1',
|
||||
stepExecutionRangeByFlow: {
|
||||
openai: { enabled: true, fromStep: 2, toStep: 9 },
|
||||
kiro: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(normalized.activeFlowId, 'kiro');
|
||||
assert.equal(normalized.services.email.provider, 'hotmail');
|
||||
assert.equal(normalized.services.proxy.enabled, true);
|
||||
assert.equal(normalized.flows.openai.source.selected, 'sub2api');
|
||||
assert.equal(normalized.flows.kiro.source.selected, 'kiro-rs');
|
||||
assert.equal(normalized.flows.kiro.source.entries['kiro-rs'].kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(normalized.flows.kiro.source.entries['kiro-rs'].kiroRsKey, 'secret-key');
|
||||
assert.equal(normalized.flows.kiro.options.kiroRegion, 'eu-west-1');
|
||||
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
|
||||
enabled: true,
|
||||
fromStep: 1,
|
||||
toStep: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('settings schema can project canonical state back to legacy payload without losing flow selection', () => {
|
||||
const { settingsSchema } = loadApis();
|
||||
const schema = settingsSchema.createSettingsSchema();
|
||||
const payload = schema.buildLegacySettingsPayload(schema.normalizeSettingsState({
|
||||
activeFlowId: 'kiro',
|
||||
kiroSourceId: 'kiro-rs',
|
||||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||||
kiroRsKey: 'key-123',
|
||||
kiroRegion: 'ap-southeast-1',
|
||||
}));
|
||||
|
||||
assert.equal(payload.activeFlowId, 'kiro');
|
||||
assert.equal(payload.panelMode, 'cpa');
|
||||
assert.equal(payload.kiroSourceId, 'kiro-rs');
|
||||
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(payload.kiroRsKey, 'key-123');
|
||||
assert.equal(payload.kiroRegion, 'ap-southeast-1');
|
||||
assert.equal(payload.settingsSchemaVersion, 3);
|
||||
assert.equal(payload.settingsState.activeFlowId, 'kiro');
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const source = fs.readFileSync('sidepanel/contribution-mode.js', 'utf8');
|
||||
|
||||
function createElement() {
|
||||
return {
|
||||
hidden: false,
|
||||
disabled: false,
|
||||
title: '',
|
||||
textContent: '',
|
||||
value: '',
|
||||
classList: {
|
||||
hiddenState: false,
|
||||
toggle(_className, hidden) {
|
||||
this.hiddenState = Boolean(hidden);
|
||||
},
|
||||
},
|
||||
setAttribute() {},
|
||||
addEventListener() {},
|
||||
};
|
||||
}
|
||||
|
||||
test('contribution mode manager does not project openai-only ui state into kiro flow', () => {
|
||||
const context = {
|
||||
window: {},
|
||||
document: { activeElement: null },
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
|
||||
const createContributionModeManager = context.window.SidepanelContributionMode.createContributionModeManager;
|
||||
const rowVpsUrl = createElement();
|
||||
const dom = {
|
||||
btnContributionMode: createElement(),
|
||||
contributionModePanel: createElement(),
|
||||
contributionModeText: createElement(),
|
||||
contributionModeBadge: createElement(),
|
||||
contributionOauthStatus: createElement(),
|
||||
contributionCallbackStatus: createElement(),
|
||||
contributionModeSummary: createElement(),
|
||||
inputContributionNickname: createElement(),
|
||||
inputContributionQq: createElement(),
|
||||
btnStartContribution: createElement(),
|
||||
btnOpenContributionUpload: createElement(),
|
||||
btnExitContributionMode: createElement(),
|
||||
btnOpenAccountRecords: createElement(),
|
||||
selectPanelMode: createElement(),
|
||||
rowVpsUrl,
|
||||
};
|
||||
const manager = createContributionModeManager({
|
||||
state: {
|
||||
getLatestState: () => ({
|
||||
activeFlowId: 'kiro',
|
||||
flowId: 'kiro',
|
||||
contributionMode: true,
|
||||
contributionSource: 'cpa',
|
||||
}),
|
||||
},
|
||||
dom,
|
||||
helpers: {
|
||||
updatePanelModeUI() {},
|
||||
updateAccountRunHistorySettingsUI() {},
|
||||
updateConfigMenuControls() {},
|
||||
closeConfigMenu() {},
|
||||
closeAccountRecordsPanel() {},
|
||||
isModeSwitchBlocked() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async () => ({}),
|
||||
},
|
||||
constants: {},
|
||||
});
|
||||
|
||||
manager.render();
|
||||
|
||||
assert.equal(dom.contributionModePanel.hidden, true);
|
||||
assert.equal(dom.selectPanelMode.disabled, false);
|
||||
assert.equal(dom.btnContributionMode.disabled, true);
|
||||
assert.equal(dom.btnContributionMode.title, '当前 flow 不支持贡献模式');
|
||||
assert.equal(dom.btnStartContribution.disabled, true);
|
||||
assert.equal(dom.btnOpenContributionUpload.disabled, true);
|
||||
assert.equal(rowVpsUrl.classList.hiddenState, false);
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
const sidepanelHtml = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
function extractFunction(source, name) {
|
||||
const asyncStart = source.indexOf(`async function ${name}`);
|
||||
const normalStart = source.indexOf(`function ${name}`);
|
||||
const start = asyncStart !== -1
|
||||
? asyncStart
|
||||
: normalStart;
|
||||
if (start === -1) {
|
||||
throw new Error(`Function ${name} not found`);
|
||||
}
|
||||
const signatureEnd = source.indexOf(')', start);
|
||||
const bodyStart = source.indexOf('{', signatureEnd);
|
||||
let depth = 0;
|
||||
let end = bodyStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const char = source[end];
|
||||
if (char === '{') {
|
||||
depth += 1;
|
||||
} else if (char === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||
[
|
||||
'id="select-flow"',
|
||||
'id="label-source-selector"',
|
||||
'id="row-kiro-rs-url"',
|
||||
'id="row-kiro-rs-key"',
|
||||
'id="row-kiro-region"',
|
||||
'id="row-kiro-device-code"',
|
||||
'id="row-kiro-login-url"',
|
||||
'id="row-kiro-upload-status"',
|
||||
].forEach((snippet) => {
|
||||
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
});
|
||||
});
|
||||
|
||||
test('sidepanel step definitions rerender when active flow changes even if plus/signup settings stay the same', () => {
|
||||
const bundle = [
|
||||
extractFunction(sidepanelSource, 'normalizeSignupMethod'),
|
||||
extractFunction(sidepanelSource, 'normalizePlusPaymentMethod'),
|
||||
extractFunction(sidepanelSource, 'getStepDefinitionsForMode'),
|
||||
extractFunction(sidepanelSource, 'rebuildStepDefinitionState'),
|
||||
extractFunction(sidepanelSource, 'syncStepDefinitionsForMode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const calls = [];
|
||||
const window = {
|
||||
MultiPageStepDefinitions: {
|
||||
getSteps(options) {
|
||||
calls.push({ type: 'getSteps', options });
|
||||
return [{ id: options.activeFlowId === 'kiro' ? 88 : 6, order: 1, key: options.activeFlowId }];
|
||||
},
|
||||
},
|
||||
};
|
||||
let latestState = { activeFlowId: 'openai' };
|
||||
let currentPlusModeEnabled = false;
|
||||
let currentPlusPaymentMethod = 'paypal';
|
||||
let currentSignupMethod = 'email';
|
||||
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
|
||||
let currentStepDefinitionFlowId = 'openai';
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const DEFAULT_SIGNUP_METHOD = 'email';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
|
||||
let stepDefinitions = [{ id: 6, key: 'openai' }];
|
||||
let STEP_IDS = [6];
|
||||
let STEP_DEFAULT_STATUSES = { 6: 'pending' };
|
||||
let SKIPPABLE_STEPS = new Set([6]);
|
||||
function renderStepsList() {
|
||||
calls.push({ type: 'render', stepIds: [...STEP_IDS] });
|
||||
}
|
||||
${bundle}
|
||||
return {
|
||||
calls,
|
||||
syncStepDefinitionsForMode,
|
||||
getStepIds: () => [...STEP_IDS],
|
||||
getCurrentFlowId: () => currentStepDefinitionFlowId,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.syncStepDefinitionsForMode(false, {
|
||||
activeFlowId: 'kiro',
|
||||
plusPaymentMethod: 'paypal',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
});
|
||||
|
||||
assert.equal(api.getCurrentFlowId(), 'kiro');
|
||||
assert.deepEqual(api.getStepIds(), [88]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: {
|
||||
activeFlowId: 'kiro',
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] });
|
||||
});
|
||||
@@ -4,6 +4,8 @@ const fs = require('node:fs');
|
||||
|
||||
test('background imports shared source registry module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /shared\/flow-registry\.js/);
|
||||
assert.match(source, /shared\/settings-schema\.js/);
|
||||
assert.match(source, /shared\/source-registry\.js/);
|
||||
});
|
||||
|
||||
@@ -21,8 +23,9 @@ test('manifest loads shared source registry before content utils in static bundl
|
||||
});
|
||||
|
||||
test('shared source registry exposes canonical source, alias, detection, and ready policies', () => {
|
||||
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
|
||||
const source = fs.readFileSync('shared/source-registry.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageSourceRegistry;`)({});
|
||||
const api = new Function('self', `${flowRegistrySource}; ${source}; return self.MultiPageSourceRegistry;`)({});
|
||||
const registry = api.createSourceRegistry();
|
||||
|
||||
assert.equal(registry.resolveCanonicalSource('signup-page'), 'openai-auth');
|
||||
@@ -50,6 +53,10 @@ test('shared source registry exposes canonical source, alias, detection, and rea
|
||||
}),
|
||||
'unknown-source'
|
||||
);
|
||||
assert.equal(registry.detectSourceFromLocation({
|
||||
url: 'https://view.awsapps.com/start',
|
||||
hostname: 'view.awsapps.com',
|
||||
}), 'kiro-device-auth');
|
||||
assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
|
||||
assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
|
||||
assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
|
||||
@@ -59,4 +66,5 @@ test('shared source registry exposes canonical source, alias, detection, and rea
|
||||
assert.equal(registry.driverAcceptsCommand('openai-auth', 'fetch-bind-email-code'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('content/platform-panel', 'platform-verify'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('openai-auth', 'platform-verify'), false);
|
||||
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-start-device-login'), true);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
});
|
||||
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
|
||||
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
|
||||
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
|
||||
|
||||
assert.equal(Array.isArray(steps), true);
|
||||
assert.equal(steps.length, 11);
|
||||
@@ -158,10 +159,28 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 18);
|
||||
assert.equal(api.hasFlow('openai'), true);
|
||||
assert.equal(api.hasFlow('kiro'), true);
|
||||
assert.equal(api.hasFlow('site-a'), false);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']);
|
||||
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
|
||||
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
|
||||
assert.deepStrictEqual(
|
||||
kiroSteps.map((step) => step.key),
|
||||
[
|
||||
'kiro-start-device-login',
|
||||
'kiro-await-device-login',
|
||||
'kiro-upload-credential',
|
||||
]
|
||||
);
|
||||
assert.equal(kiroSteps.every((step) => step.flowId === 'kiro'), true);
|
||||
assert.equal(kiroSteps[0].driverId, 'background/kiro-device-auth');
|
||||
assert.equal(kiroSteps[2].sourceId, 'kiro-rs-admin');
|
||||
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'kiro' }), [1, 2, 3]);
|
||||
assert.equal(api.getLastStepId({ activeFlowId: 'kiro' }), 3);
|
||||
assert.deepStrictEqual(
|
||||
api.getNodes({ activeFlowId: 'kiro' }).map((node) => node.next),
|
||||
[['kiro-await-device-login'], ['kiro-upload-credential'], []]
|
||||
);
|
||||
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user