refactor kiro auth flow and share account password service

This commit is contained in:
QLHazyCoder
2026-05-18 12:49:49 +08:00
parent d081d47e1a
commit 12314e446a
21 changed files with 2155 additions and 108 deletions
+1 -1
View File
@@ -517,7 +517,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
### 操作间延迟
`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
`操作间延迟` 默认开启且固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth``platform-verify` 的交互节奏。
+125 -23
View File
@@ -4368,29 +4368,32 @@ async function resetState() {
}
/**
* Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
* Generate a shared account password that satisfies the common policy:
* 8 to 64 chars, including uppercase, lowercase, digits, and symbols.
*/
function generatePassword() {
const minLength = 8;
const maxLength = 64;
const targetLength = 14;
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
const lower = 'abcdefghjkmnpqrstuvwxyz';
const digits = '23456789';
const symbols = '!@#$%&*?';
const all = upper + lower + digits + symbols;
const symbols = '!@#$%^&*?_-+=';
const groups = [upper, lower, digits, symbols];
const all = groups.join('');
const length = Math.min(maxLength, Math.max(minLength, targetLength));
const passwordChars = groups.map((group) => group[Math.floor(Math.random() * group.length)]);
// Ensure at least one of each type
let pw = '';
pw += upper[Math.floor(Math.random() * upper.length)];
pw += lower[Math.floor(Math.random() * lower.length)];
pw += digits[Math.floor(Math.random() * digits.length)];
pw += symbols[Math.floor(Math.random() * symbols.length)];
// Fill remaining 10 chars
for (let i = 0; i < 10; i++) {
pw += all[Math.floor(Math.random() * all.length)];
while (passwordChars.length < length) {
passwordChars.push(all[Math.floor(Math.random() * all.length)]);
}
// Shuffle
return pw.split('').sort(() => Math.random() - 0.5).join('');
for (let i = passwordChars.length - 1; i > 0; i -= 1) {
const swapIndex = Math.floor(Math.random() * (i + 1));
[passwordChars[i], passwordChars[swapIndex]] = [passwordChars[swapIndex], passwordChars[i]];
}
return passwordChars.join('');
}
function normalizePayPalAccount(account = {}) {
@@ -9067,6 +9070,7 @@ function getDownstreamStateResets(step, state = {}) {
kiroCredentialId: null,
kiroDeviceAuthorizationCode: '',
kiroDeviceCode: '',
kiroFullName: '',
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroLoginUrl: '',
@@ -9074,17 +9078,74 @@ function getDownstreamStateResets(step, state = {}) {
kiroUploadError: '',
kiroUploadStatus: '',
kiroUserCode: '',
kiroVerificationRequestedAt: 0,
kiroVerificationUri: '',
kiroVerificationUriComplete: '',
};
}
if (stepKey === 'kiro-await-device-login') {
if (stepKey === 'kiro-submit-email') {
return {
kiroAccessToken: '',
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroAuthorizedEmail: '',
kiroCredentialId: null,
kiroFullName: '',
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroRefreshToken: '',
kiroUploadError: '',
kiroUploadStatus: 'waiting_login',
kiroVerificationRequestedAt: 0,
};
}
if (stepKey === 'kiro-submit-name') {
return {
kiroAccessToken: '',
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroCredentialId: null,
kiroFullName: '',
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroRefreshToken: '',
kiroUploadError: '',
kiroUploadStatus: 'waiting_login',
kiroVerificationRequestedAt: 0,
};
}
if (stepKey === 'kiro-submit-verification-code') {
return {
kiroAccessToken: '',
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroCredentialId: null,
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroRefreshToken: '',
kiroUploadError: '',
kiroUploadStatus: 'waiting_login',
};
}
if (stepKey === 'kiro-fill-password') {
return {
kiroAccessToken: '',
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroCredentialId: null,
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroRefreshToken: '',
kiroUploadError: '',
kiroUploadStatus: 'waiting_login',
};
}
if (stepKey === 'kiro-confirm-access') {
return {
kiroAccessToken: '',
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroCredentialId: null,
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroRefreshToken: '',
@@ -9094,7 +9155,6 @@ function getDownstreamStateResets(step, state = {}) {
}
if (stepKey === 'kiro-upload-credential') {
return {
kiroAuthorizedEmail: '',
kiroCredentialId: null,
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
@@ -10283,6 +10343,7 @@ async function handleNodeData(nodeId, payload) {
'kiroCredentialId',
'kiroDeviceAuthorizationCode',
'kiroDeviceCode',
'kiroFullName',
'kiroLastConnectionMessage',
'kiroLastUploadAt',
'kiroLoginUrl',
@@ -10290,6 +10351,7 @@ async function handleNodeData(nodeId, payload) {
'kiroUploadError',
'kiroUploadStatus',
'kiroUserCode',
'kiroVerificationRequestedAt',
'kiroVerificationUri',
'kiroVerificationUriComplete',
];
@@ -10347,7 +10409,11 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'post-bound-email-phone-verification',
'confirm-oauth',
'kiro-start-device-login',
'kiro-await-device-login',
'kiro-submit-email',
'kiro-submit-name',
'kiro-submit-verification-code',
'kiro-fill-password',
'kiro-confirm-access',
'kiro-upload-credential',
]);
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
@@ -11027,7 +11093,9 @@ async function executeNode(nodeId, options = {}) {
state = await getState();
// Set flow start time on first step
const firstNodeIdForFlow = String(getNodeIdsForState(state)?.[0] || '').trim();
const firstNodeIdForFlow = typeof getNodeIdsForState === 'function'
? String(getNodeIdsForState(state)?.[0] || '').trim()
: '';
if (normalizedNodeId === firstNodeIdForFlow && !state.flowStartTime) {
await setState({ flowStartTime: Date.now() });
}
@@ -12395,14 +12463,16 @@ async function runAutoSequenceFromNodeGraph(startNodeId, context = {}) {
setRestartNode(nodeId);
return true;
};
const defaultActiveFlowId = typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
const flowRegistry = typeof self !== 'undefined' ? self.MultiPageFlowRegistry : null;
const initialFlowState = await getState();
const activeFlowId = String(initialFlowState?.activeFlowId || initialFlowState?.flowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID;
const activeFlowId = String(initialFlowState?.activeFlowId || initialFlowState?.flowId || defaultActiveFlowId).trim().toLowerCase() || defaultActiveFlowId;
const activeFlowLabel = String(
self.MultiPageFlowRegistry?.getFlowLabel?.(activeFlowId)
flowRegistry?.getFlowLabel?.(activeFlowId)
|| activeFlowId
).trim() || activeFlowId;
if (activeFlowId !== DEFAULT_ACTIVE_FLOW_ID) {
if (activeFlowId !== defaultActiveFlowId) {
await broadcastAutoRunStatus('running', {
currentRun: targetRun,
totalRuns,
@@ -12835,6 +12905,7 @@ async function resumeAutoRun() {
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/auth-page-recovery.js', 'content/phone-country-utils.js', 'content/phone-auth.js', 'content/signup-page.js'];
const KIRO_DEVICE_AUTH_INJECT_FILES = ['shared/source-registry.js', 'content/utils.js', 'content/kiro-device-auth-page.js'];
const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
chrome,
addLog,
@@ -13228,14 +13299,41 @@ const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.cre
});
const kiroDeviceAuthExecutor = self.MultiPageBackgroundKiroDeviceAuth?.createKiroDeviceAuthExecutor({
addLog,
chrome,
ensureContentScriptReadyOnTab,
completeNodeFromBackground,
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
ensureMail2925MailboxSession,
fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
generatePassword,
generateRandomName,
getMailConfig,
getTabId,
getState,
HOTMAIL_PROVIDER,
isTabAlive,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
YYDS_MAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
pollYydsMailVerificationCode,
registerTab,
resolveSignupEmailForFlow,
reuseOrCreateTab,
sendToContentScriptResilient,
sendToMailContentScriptResilient,
setPasswordState,
setState,
sleepWithStop,
throwIfStopped,
waitForTabStableComplete,
KIRO_DEVICE_AUTH_INJECT_FILES,
});
const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
addLog,
@@ -13315,7 +13413,11 @@ const stepExecutorsByKey = {
'confirm-oauth': (state) => step9Executor.executeStep9(state),
'platform-verify': (state) => executeStep10(state),
'kiro-start-device-login': (state) => kiroDeviceAuthExecutor.executeKiroStartDeviceLogin(state),
'kiro-await-device-login': (state) => kiroDeviceAuthExecutor.executeKiroAwaitDeviceLogin(state),
'kiro-submit-email': (state) => kiroDeviceAuthExecutor.executeKiroSubmitEmail(state),
'kiro-submit-name': (state) => kiroDeviceAuthExecutor.executeKiroSubmitName(state),
'kiro-submit-verification-code': (state) => kiroDeviceAuthExecutor.executeKiroSubmitVerificationCode(state),
'kiro-fill-password': (state) => kiroDeviceAuthExecutor.executeKiroFillPassword(state),
'kiro-confirm-access': (state) => kiroDeviceAuthExecutor.executeKiroConfirmAccess(state),
'kiro-upload-credential': (state) => kiroDeviceAuthExecutor.executeKiroUploadCredential(state),
};
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
+786 -20
View File
@@ -11,6 +11,44 @@
'codewhisperer:taskassist',
]);
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
function cleanString(value = '') {
return String(value ?? '').trim();
}
@@ -298,16 +336,43 @@
function createKiroDeviceAuthExecutor(deps = {}) {
const {
addLog = async () => {},
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground,
ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
generatePassword = null,
generateRandomName = null,
getMailConfig = null,
getState = async () => ({}),
getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
KIRO_DEVICE_AUTH_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
registerTab = async () => {},
resolveSignupEmailForFlow = null,
reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setPasswordState = async () => {},
setState = async () => {},
sleepWithStop = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms));
},
throwIfStopped = () => {},
waitForTabStableComplete = null,
} = deps;
if (typeof completeNodeFromBackground !== 'function') {
@@ -317,8 +382,15 @@
throw new Error('Kiro device auth executor requires fetch support.');
}
async function log(message, level, nodeId) {
await addLog(message, level, { nodeId });
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
}
async function activateTab(tabId) {
if (!Number.isInteger(tabId) || !chrome?.tabs?.update) {
return;
}
await chrome.tabs.update(tabId, { active: true });
}
async function getExecutionState(state = {}) {
@@ -334,19 +406,346 @@
}
}
async function ensureKiroAuthTab(state = {}, options = {}) {
let tabId = Number.isInteger(state?.kiroAuthTabId)
? state.kiroAuthTabId
: await getTabId('kiro-device-auth');
const loginUrl = cleanString(state?.kiroLoginUrl || state?.kiroVerificationUriComplete || state?.kiroVerificationUri);
if (Number.isInteger(tabId) && await isTabAlive('kiro-device-auth')) {
return tabId;
}
if (!loginUrl) {
throw new Error(options.missingUrlMessage || '缺少 Kiro 授权页地址,请先执行步骤 1。');
}
tabId = await reuseOrCreateTab('kiro-device-auth', loginUrl);
if (!Number.isInteger(tabId)) {
throw new Error(options.openFailedMessage || '无法打开 Kiro 授权页,请重试步骤 1。');
}
await registerTab('kiro-device-auth', tabId);
await setState({ kiroAuthTabId: tabId });
return tabId;
}
async function activateKiroAuthTab(state = {}, options = {}) {
const tabId = await ensureKiroAuthTab(state, options);
await activateTab(tabId);
return tabId;
}
async function ensureKiroPageState(tabId, options = {}) {
if (!Number.isInteger(tabId)) {
throw new Error('缺少 Kiro 授权页标签页,无法继续执行。');
}
if (typeof waitForTabStableComplete === 'function') {
await waitForTabStableComplete(tabId, {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: Number(options.stableMs) || 1500,
initialDelayMs: Number(options.initialDelayMs) || 150,
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab('kiro-device-auth', tabId, {
inject: Array.isArray(KIRO_DEVICE_AUTH_INJECT_FILES) ? KIRO_DEVICE_AUTH_INJECT_FILES : null,
injectSource: 'kiro-device-auth',
timeoutMs: 45000,
retryDelayMs: 800,
logMessage: options.injectLogMessage || 'Kiro 授权页内容脚本未就绪,正在等待页面恢复...',
});
}
if (typeof sendToContentScriptResilient !== 'function') {
return {
state: Array.isArray(options.targetStates) ? options.targetStates[0] || '' : '',
url: '',
};
}
const result = await sendToContentScriptResilient('kiro-device-auth', {
type: 'ENSURE_KIRO_PAGE_STATE',
step: options.step || 0,
source: 'background',
payload: {
targetStates: Array.isArray(options.targetStates) ? options.targetStates : [],
timeoutMs: Number(options.pageTimeoutMs) || 30000,
retryDelayMs: Number(options.pageRetryDelayMs) || 250,
timeoutMessage: options.timeoutMessage || '',
},
}, {
timeoutMs: Math.max(30000, Number(options.pageTimeoutMs) || 30000),
retryDelayMs: 700,
logMessage: options.readyLogMessage || '正在等待 Kiro 页面进入下一状态...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || { state: '', url: '' };
}
async function waitForKiroPageChange(tabId, options = {}) {
if (!Number.isInteger(tabId)) {
throw new Error('缺少 Kiro 授权页标签页,无法继续执行。');
}
if (typeof waitForTabStableComplete === 'function') {
await waitForTabStableComplete(tabId, {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: Number(options.stableMs) || 1200,
initialDelayMs: Number(options.initialDelayMs) || 120,
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab('kiro-device-auth', tabId, {
inject: Array.isArray(KIRO_DEVICE_AUTH_INJECT_FILES) ? KIRO_DEVICE_AUTH_INJECT_FILES : null,
injectSource: 'kiro-device-auth',
timeoutMs: 45000,
retryDelayMs: 800,
logMessage: options.injectLogMessage || 'Kiro 授权页切换中,正在等待页面恢复...',
});
}
if (typeof sendToContentScriptResilient !== 'function') {
return { state: '', url: '' };
}
const result = await sendToContentScriptResilient('kiro-device-auth', {
type: 'ENSURE_KIRO_STATE_CHANGE',
step: options.step || 0,
source: 'background',
payload: {
fromStates: Array.isArray(options.fromStates) ? options.fromStates : [],
timeoutMs: Number(options.pageTimeoutMs) || 30000,
retryDelayMs: Number(options.pageRetryDelayMs) || 250,
timeoutMessage: options.timeoutMessage || '',
},
}, {
timeoutMs: Math.max(30000, Number(options.pageTimeoutMs) || 30000),
retryDelayMs: 700,
logMessage: options.readyLogMessage || '正在等待 Kiro 页面完成跳转...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || { state: '', url: '' };
}
function resolveKiroFullName(state = {}) {
const cachedName = cleanString(state?.kiroFullName);
if (cachedName) {
return cachedName;
}
if (typeof generateRandomName !== 'function') {
throw new Error('Kiro 姓名步骤缺少随机姓名能力,无法继续执行。');
}
const generated = generateRandomName();
if (typeof generated === 'string') {
const normalized = cleanString(generated);
if (normalized) {
return normalized;
}
}
const firstName = cleanString(generated?.firstName);
const lastName = cleanString(generated?.lastName);
const fullName = cleanString(`${firstName} ${lastName}`);
if (!fullName) {
throw new Error('Kiro 姓名步骤未生成有效姓名。');
}
return fullName;
}
function resolveKiroPassword(state = {}) {
const existingPassword = String(state?.customPassword || state?.password || '');
if (existingPassword) {
return {
password: existingPassword,
mode: state?.customPassword ? 'custom' : 'reused',
};
}
if (typeof generatePassword !== 'function') {
throw new Error('Kiro 密码步骤缺少公共密码生成能力,无法继续执行。');
}
return {
password: String(generatePassword() || ''),
mode: 'generated',
};
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
function buildKiroVerificationPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const targetEmail = cleanString(state?.kiroAuthorizedEmail || state?.email).toLowerCase();
const targetEmailHints = targetEmail ? [targetEmail] : [];
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 3
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 15000
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
return {
flowId: 'kiro',
step,
targetEmail,
targetEmailHints,
filterAfterTimestamp,
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
mail2925MatchTargetEmail: isMail2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
maxAttempts,
intervalMs,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + 25000);
}
async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
}
const requestedAt = Math.max(0, Number(state?.kiroVerificationRequestedAt) || Date.now());
const filterAfterTimestamp = mail.provider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt;
const pollPayload = buildKiroVerificationPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId);
await ensureIcloudMailSession({
state,
step,
actionLabel: `步骤 ${step}:确认 iCloud 邮箱登录状态`,
});
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Hotmail'} 轮询验证码...`, 'info', nodeId);
return pollHotmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'LuckMail'} 轮询验证码...`, 'info', nodeId);
return pollLuckmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloudflare Temp Email'} 轮询验证码...`, 'info', nodeId);
return pollCloudflareTempEmailVerificationCode(step, state, pollPayload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'Cloud Mail'} 轮询验证码...`, 'info', nodeId);
return pollCloudMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) {
await log(`步骤 ${step}:正在通过 ${mail.label || 'YYDS Mail'} 轮询验证码...`, 'info', nodeId);
return pollYydsMailVerificationCode(step, state, pollPayload);
}
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await log(`步骤 ${step}:正在确认 ${mail.label || '2925 邮箱'} 登录状态...`, 'info', nodeId);
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: `步骤 ${step}:确认 2925 邮箱登录状态`,
});
} else {
await log(`步骤 ${step}:正在打开 ${mail.label || '邮箱'}...`, 'info', nodeId);
await focusOrOpenMailTab(mail);
}
if (typeof sendToMailContentScriptResilient !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。');
}
const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: pollPayload,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step,
logStepKey: 'kiro-submit-verification-code',
}
);
if (result?.error) {
throw new Error(result.error);
}
if (!result?.code) {
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
}
return result;
}
async function executeKiroStartDeviceLogin(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-start-device-login').trim();
try {
const latestState = await getExecutionState(state);
const auth = await startBuilderIdDeviceLogin(
DEFAULT_REGION,
fetchImpl
);
const auth = await startBuilderIdDeviceLogin(DEFAULT_REGION, fetchImpl);
const loginUrl = cleanString(auth.verificationUriComplete || auth.verificationUri);
const tabId = loginUrl ? await reuseOrCreateTab('kiro-device-auth', loginUrl) : null;
if (Number.isInteger(tabId)) {
await registerTab('kiro-device-auth', tabId);
if (!Number.isInteger(tabId)) {
throw new Error('无法打开 Kiro 授权页,请重试步骤 1。');
}
await registerTab('kiro-device-auth', tabId);
const updates = {
kiroAccessToken: '',
@@ -356,11 +755,13 @@
kiroAuthRegion: auth.region,
kiroAuthStatus: 'waiting_user',
kiroAuthTabId: Number.isInteger(tabId) ? tabId : null,
kiroAuthorizedEmail: '',
kiroClientId: auth.clientId,
kiroClientSecret: auth.clientSecret,
kiroCredentialId: null,
kiroDeviceAuthorizationCode: auth.deviceCode,
kiroDeviceCode: auth.userCode,
kiroFullName: '',
kiroLastConnectionMessage: '',
kiroLastUploadAt: 0,
kiroLoginUrl: loginUrl,
@@ -368,12 +769,22 @@
kiroUploadError: '',
kiroUploadStatus: 'waiting_login',
kiroUserCode: auth.userCode,
kiroVerificationRequestedAt: 0,
kiroVerificationUri: auth.verificationUri,
kiroVerificationUriComplete: loginUrl,
};
await setState(updates);
await log(`Kiro 设备登录已启动。请打开 ${loginUrl},并输入授权码 ${auth.userCode} 完成确认。`, 'info', nodeId);
await activateTab(tabId);
await ensureKiroPageState(tabId, {
step: 1,
targetStates: ['email_entry'],
stableMs: 2500,
initialDelayMs: 300,
injectLogMessage: '步骤 1:Kiro 授权页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 1:正在等待 Kiro 授权页邮箱输入框加载完成...',
});
await log(`Kiro 授权页已就绪,请在下一步中获取邮箱并继续。当前授权码:${auth.userCode}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, updates);
} catch (error) {
const message = getErrorMessage(error);
@@ -385,8 +796,319 @@
}
}
async function executeKiroAwaitDeviceLogin(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-await-device-login').trim();
async function executeKiroSubmitEmail(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-submit-email').trim();
try {
const latestState = await getExecutionState(state);
if (typeof resolveSignupEmailForFlow !== 'function') {
throw new Error('Kiro 邮箱步骤缺少公共邮箱解析能力,无法继续执行。');
}
const tabId = await activateKiroAuthTab(latestState, {
missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。',
openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。',
});
await ensureKiroPageState(tabId, {
step: 2,
targetStates: ['email_entry'],
stableMs: 2500,
initialDelayMs: 300,
injectLogMessage: '步骤 2:Kiro 授权页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 2:正在等待 Kiro 授权页邮箱输入框加载完成...',
});
const resolvedEmail = await resolveSignupEmailForFlow(latestState, {
preserveAccountIdentity: true,
});
await log(`步骤 2:已获取邮箱 ${resolvedEmail},正在提交到 Kiro 授权页...`, 'info', nodeId);
await activateTab(tabId);
const submitResult = await sendToContentScriptResilient('kiro-device-auth', {
type: 'EXECUTE_NODE',
nodeId: 'kiro-submit-email',
step: 2,
source: 'background',
payload: {
email: resolvedEmail,
},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: '步骤 2:正在向 Kiro 授权页提交邮箱...',
});
if (submitResult?.error) {
throw new Error(submitResult.error);
}
const landingResult = await ensureKiroPageState(tabId, {
step: 2,
targetStates: ['name_entry'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 2:邮箱提交后页面切换中,正在等待 Kiro 授权页恢复...',
readyLogMessage: '步骤 2:邮箱已提交,正在等待 Kiro 姓名页加载完成...',
timeoutMessage: '邮箱提交后未进入姓名页,请检查当前邮箱是否已注册或页面是否异常。',
});
const updates = {
kiroAuthorizedEmail: resolvedEmail,
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroFullName: '',
kiroUploadError: '',
kiroUploadStatus: 'waiting_login',
kiroVerificationRequestedAt: 0,
};
await setState(updates);
await log(`步骤 2:邮箱 ${resolvedEmail} 已提交,当前已进入姓名页。`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, {
...updates,
email: resolvedEmail,
accountIdentifierType: 'email',
accountIdentifier: resolvedEmail,
kiroNextState: landingResult?.state || '',
kiroNextUrl: landingResult?.url || '',
});
} catch (error) {
const message = getErrorMessage(error);
await persistFailure({
kiroAuthError: message,
});
throw error;
}
}
async function executeKiroSubmitName(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-submit-name').trim();
try {
const latestState = await getExecutionState(state);
if (!cleanString(latestState?.kiroAuthorizedEmail || latestState?.email)) {
throw new Error('缺少 Kiro 授权邮箱,请先完成步骤 2。');
}
const tabId = await activateKiroAuthTab(latestState, {
missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。',
openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。',
});
await ensureKiroPageState(tabId, {
step: 3,
targetStates: ['name_entry'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 3:Kiro 姓名页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 3:正在等待 Kiro 姓名页加载完成...',
});
const fullName = resolveKiroFullName(latestState);
const verificationRequestedAt = Date.now();
await log(`步骤 3:正在填写姓名 ${fullName} 并继续...`, 'info', nodeId);
const submitResult = await sendToContentScriptResilient('kiro-device-auth', {
type: 'EXECUTE_NODE',
nodeId: 'kiro-submit-name',
step: 3,
source: 'background',
payload: {
fullName,
},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: '步骤 3:正在向 Kiro 姓名页提交姓名...',
});
if (submitResult?.error) {
throw new Error(submitResult.error);
}
const landingResult = await ensureKiroPageState(tabId, {
step: 3,
targetStates: ['otp_page'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 3:姓名提交后页面切换中,正在等待 Kiro 授权页恢复...',
readyLogMessage: '步骤 3:姓名已提交,正在等待 Kiro 验证码页加载完成...',
timeoutMessage: '姓名提交后未进入验证码页,请检查当前页面状态。',
});
const updates = {
kiroAuthError: '',
kiroFullName: fullName,
kiroUploadError: '',
kiroVerificationRequestedAt: verificationRequestedAt,
};
await setState(updates);
await log('步骤 3:姓名已提交,当前已进入验证码页。', 'ok', nodeId);
await completeNodeFromBackground(nodeId, {
...updates,
kiroNextState: landingResult?.state || '',
kiroNextUrl: landingResult?.url || '',
});
} catch (error) {
const message = getErrorMessage(error);
await persistFailure({
kiroAuthError: message,
});
throw error;
}
}
async function executeKiroSubmitVerificationCode(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-submit-verification-code').trim();
try {
const latestState = await getExecutionState(state);
if (!cleanString(latestState?.kiroAuthorizedEmail || latestState?.email)) {
throw new Error('缺少 Kiro 授权邮箱,请先完成步骤 2。');
}
const tabId = await activateKiroAuthTab(latestState, {
missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。',
openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。',
});
await ensureKiroPageState(tabId, {
step: 4,
targetStates: ['otp_page'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 4:Kiro 验证码页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 4:正在等待 Kiro 验证码页加载完成...',
});
const codeResult = await pollKiroVerificationCode(4, latestState, nodeId);
const code = cleanString(codeResult?.code);
if (!code) {
throw new Error('未能获取到 Kiro 邮箱验证码。');
}
await log(`步骤 4:已获取验证码 ${code},正在返回 Kiro 授权页提交...`, 'info', nodeId);
await activateTab(tabId);
const submitResult = await sendToContentScriptResilient('kiro-device-auth', {
type: 'EXECUTE_NODE',
nodeId: 'kiro-submit-verification-code',
step: 4,
source: 'background',
payload: {
code,
},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: '步骤 4:正在向 Kiro 验证码页提交验证码...',
});
if (submitResult?.error) {
throw new Error(submitResult.error);
}
const landingResult = await ensureKiroPageState(tabId, {
step: 4,
targetStates: ['password_page'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 4:验证码提交后页面切换中,正在等待 Kiro 授权页恢复...',
readyLogMessage: '步骤 4:验证码已提交,正在等待 Kiro 密码页加载完成...',
timeoutMessage: '验证码提交后未进入密码页,请检查验证码是否失效或页面是否异常。',
});
const updates = {
kiroAuthError: '',
kiroUploadError: '',
};
await setState(updates);
await log('步骤 4:验证码已提交,当前已进入密码页。', 'ok', nodeId);
await completeNodeFromBackground(nodeId, {
...updates,
code,
emailTimestamp: Number(codeResult?.emailTimestamp || 0) || 0,
mailId: String(codeResult?.mailId || ''),
kiroNextState: landingResult?.state || '',
kiroNextUrl: landingResult?.url || '',
});
} catch (error) {
const message = getErrorMessage(error);
await persistFailure({
kiroAuthError: message,
});
throw error;
}
}
async function executeKiroFillPassword(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-fill-password').trim();
try {
const latestState = await getExecutionState(state);
const tabId = await activateKiroAuthTab(latestState, {
missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。',
openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。',
});
await ensureKiroPageState(tabId, {
step: 5,
targetStates: ['password_page'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 5:Kiro 密码页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 5:正在等待 Kiro 密码页加载完成...',
});
const passwordResolution = resolveKiroPassword(latestState);
const password = passwordResolution.password;
if (!password) {
throw new Error('未生成有效的 Kiro 账户密码。');
}
if (typeof setPasswordState === 'function') {
await setPasswordState(password);
} else {
await setState({ password });
}
const passwordModeLabel = passwordResolution.mode === 'custom'
? '自定义密码'
: (passwordResolution.mode === 'reused' ? '复用现有密码' : '自动生成密码');
await log(`步骤 5:正在填写 Kiro 账户密码(${passwordModeLabel}${password.length} 位)...`, 'info', nodeId);
const submitResult = await sendToContentScriptResilient('kiro-device-auth', {
type: 'EXECUTE_NODE',
nodeId: 'kiro-fill-password',
step: 5,
source: 'background',
payload: {
password,
},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: '步骤 5:正在向 Kiro 密码页提交密码...',
});
if (submitResult?.error) {
throw new Error(submitResult.error);
}
const landingResult = await waitForKiroPageChange(tabId, {
step: 5,
fromStates: ['password_page'],
stableMs: 1200,
initialDelayMs: 120,
injectLogMessage: '步骤 5:密码提交后页面切换中,正在等待 Kiro 授权页恢复...',
readyLogMessage: '步骤 5:密码已提交,正在等待 Kiro 授权页完成跳转...',
timeoutMessage: '密码提交后页面未离开密码页,请检查密码规则或当前页面提示。',
});
const updates = {
kiroAuthError: '',
kiroUploadError: '',
};
await setState(updates);
await log(`步骤 5:密码已提交,当前页面状态:${landingResult?.state || 'unknown'}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, {
...updates,
kiroNextState: landingResult?.state || '',
kiroNextUrl: landingResult?.url || '',
});
} catch (error) {
const message = getErrorMessage(error);
await persistFailure({
kiroAuthError: message,
});
throw error;
}
}
async function executeKiroConfirmAccess(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-confirm-access').trim();
try {
const latestState = await getExecutionState(state);
const clientId = cleanString(latestState.kiroClientId);
@@ -401,13 +1123,49 @@
throw new Error('Kiro 设备登录已过期,请重新执行步骤 1。');
}
const tabId = await activateKiroAuthTab(latestState, {
missingUrlMessage: '缺少 Kiro 授权页地址,请先执行步骤 1。',
openFailedMessage: '无法恢复 Kiro 授权页,请重新执行步骤 1。',
});
await setState({
kiroAuthError: '',
kiroAuthStatus: 'waiting_user',
kiroUploadStatus: 'waiting_login',
});
await log('正在等待 Kiro 设备登录授权确认...', 'info', nodeId);
let landingResult = await ensureKiroPageState(tabId, {
step: 6,
targetStates: ['authorization_page', 'success_page'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 6:Kiro 授权确认页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 6:正在等待 Kiro 授权确认页加载完成...',
timeoutMessage: '未进入 Kiro 授权确认页,请检查当前页面状态。',
});
if (landingResult?.state !== 'success_page') {
await log('步骤 6:正在确认访问并完成 Kiro 授权...', 'info', nodeId);
const submitResult = await sendToContentScriptResilient('kiro-device-auth', {
type: 'EXECUTE_NODE',
nodeId: 'kiro-confirm-access',
step: 6,
source: 'background',
payload: {
maxActions: 3,
},
}, {
timeoutMs: 60000,
retryDelayMs: 700,
logMessage: '步骤 6:正在处理 Kiro 授权确认页...',
});
if (submitResult?.error) {
throw new Error(submitResult.error);
}
landingResult = {
state: String(submitResult?.state || ''),
url: String(submitResult?.url || ''),
};
}
await log('步骤 6:授权页已完成,正在同步 Builder ID 凭据...', 'info', nodeId);
let intervalSeconds = normalizePositiveInteger(latestState.kiroAuthIntervalSeconds, 5);
while (Date.now() < expiresAt) {
throwIfStopped();
@@ -427,8 +1185,12 @@
kiroUploadStatus: 'ready_to_upload',
};
await setState(updates);
await log('Kiro 设备登录已确认,已获取 Refresh Token。', 'ok', nodeId);
await completeNodeFromBackground(nodeId, updates);
await log('步骤 6:确认访问已完成,已获取 Refresh Token。', 'ok', nodeId);
await completeNodeFromBackground(nodeId, {
...updates,
kiroNextState: landingResult?.state || '',
kiroNextUrl: landingResult?.url || '',
});
return;
}
@@ -443,7 +1205,7 @@
const message = getErrorMessage(error);
await persistFailure({
kiroAuthError: message,
kiroAuthStatus: /(expired|过期)/i.test(message) ? 'expired' : 'error',
kiroAuthStatus: /(expired|过期)/i.test(message) ? 'expired' : 'error',
});
throw error;
}
@@ -460,7 +1222,7 @@
const kiroRsUrl = String(latestState.kiroRsUrl || '');
const kiroRsKey = String(latestState.kiroRsKey || '');
if (!refreshToken || !clientId || !clientSecret) {
throw new Error('缺少 Kiro Refresh Token,请先完成步骤 2。');
throw new Error('缺少 Kiro Refresh Token,请先完成步骤 6。');
}
if (!cleanString(kiroRsUrl)) {
throw new Error('缺少 kiro.rs 管理后台地址。');
@@ -473,7 +1235,7 @@
kiroUploadError: '',
kiroUploadStatus: 'uploading',
});
await log('正在上传 Builder ID 凭据到 kiro.rs...', 'info', nodeId);
await log('步骤 7正在上传 Builder ID 凭据到 kiro.rs...', 'info', nodeId);
const connection = await checkKiroRsConnection(kiroRsUrl, kiroRsKey, fetchImpl);
await setState({
@@ -509,7 +1271,7 @@
};
await setState(updates);
await log(`kiro.rs 上传完成:${updates.kiroUploadStatus}`, 'ok', nodeId);
await log(`步骤 7kiro.rs 上传完成,状态${updates.kiroUploadStatus}`, 'ok', nodeId);
await completeNodeFromBackground(nodeId, updates);
} catch (error) {
const message = getErrorMessage(error);
@@ -522,8 +1284,12 @@
}
return {
executeKiroAwaitDeviceLogin,
executeKiroConfirmAccess,
executeKiroFillPassword,
executeKiroStartDeviceLogin,
executeKiroSubmitEmail,
executeKiroSubmitName,
executeKiroSubmitVerificationCode,
executeKiroUploadCredential,
};
}
+552
View File
@@ -0,0 +1,552 @@
console.log('[MultiPage:kiro-device-auth] Content script loaded on', location.href);
const KIRO_DEVICE_AUTH_LISTENER_SENTINEL = 'data-multipage-kiro-device-auth-listener';
const KIRO_CONTINUE_TEXT_PATTERN = /continue|继续/i;
const KIRO_CONFIRM_CONTINUE_TEXT_PATTERN = /confirm and continue|确认并继续/i;
const KIRO_ALLOW_ACCESS_TEXT_PATTERN = /allow access|允许访问/i;
const KIRO_SUCCESS_TEXT_PATTERN = /authorization successful|you may now close this window|you are now signed in|授权成功|可以关闭此窗口|已登录/i;
const KIRO_EMAIL_INPUT_SELECTOR = [
'input[placeholder="username@example.com"]',
'input[type="email"]',
'input[name="email"]',
'input[autocomplete="username"]',
'input[placeholder*="example.com" i]',
].join(', ');
const KIRO_NAME_INPUT_SELECTOR = [
'input[placeholder*="Silva" i]',
'input[autocomplete="name"]',
'input[name="name"]',
'input[name="fullName"]',
].join(', ');
const KIRO_OTP_INPUT_SELECTOR = [
'input[autocomplete="one-time-code"]',
'input[inputmode="numeric"]',
'input[placeholder*="6-digit" i]',
'input[placeholder*="6 位" i]',
'input[name*="otp" i]',
'input[name*="code" i]',
].join(', ');
const KIRO_PASSWORD_INPUT_SELECTOR = [
'input[placeholder="Enter password"]',
'input[placeholder*="Create password" i]',
'input[placeholder*="Password" i]',
'input[name="password"]',
'input[id*="password" i]',
'input[type="password"]',
].join(', ');
const KIRO_CONFIRM_PASSWORD_SELECTOR = [
'input[placeholder*="Re-enter password" i]',
'input[placeholder*="Confirm password" i]',
'input[placeholder*="Confirm Password" i]',
'input[name="confirmPassword"]',
'input[id*="confirm" i]',
].join(', ');
function isVisibleKiroElement(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getKiroPageText() {
return String(document.body?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
function collectVisibleElements(selector) {
return Array.from(document.querySelectorAll(selector))
.filter((el) => isVisibleKiroElement(el));
}
function findFirstVisible(selector) {
return collectVisibleElements(selector)[0] || null;
}
function getElementActionText(el) {
return [
el?.textContent,
el?.value,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
el?.getAttribute?.('data-testid'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function findActionButton(options = {}) {
const {
preferredSelectors = [],
textPattern = null,
formOwner = null,
} = options;
for (const selector of preferredSelectors) {
const preferred = findFirstVisible(selector);
if (preferred && !preferred.disabled && preferred.getAttribute('aria-disabled') !== 'true') {
return preferred;
}
}
const candidates = collectVisibleElements('button, [role="button"], input[type="submit"], input[type="button"]')
.filter((el) => !el.disabled && el.getAttribute('aria-disabled') !== 'true');
const prioritized = formOwner
? candidates.filter((el) => (el.form || el.closest?.('form') || null) === formOwner)
: [];
const pool = prioritized.length ? prioritized : candidates;
if (textPattern instanceof RegExp) {
return pool.find((el) => textPattern.test(getElementActionText(el))) || null;
}
return pool[0] || null;
}
function findVisibleEmailInput() {
return findFirstVisible(KIRO_EMAIL_INPUT_SELECTOR);
}
function findVisibleNameInput() {
return findFirstVisible(KIRO_NAME_INPUT_SELECTOR);
}
function findVisibleOtpInput() {
return findFirstVisible(KIRO_OTP_INPUT_SELECTOR);
}
function findVisiblePasswordInputs() {
return collectVisibleElements(KIRO_PASSWORD_INPUT_SELECTOR);
}
function findVisibleConfirmPasswordInput() {
return findFirstVisible(KIRO_CONFIRM_PASSWORD_SELECTOR);
}
function findEmailContinueButton(emailInput = null) {
const form = emailInput?.form || emailInput?.closest?.('form') || null;
return findActionButton({
preferredSelectors: ['button[data-testid="test-primary-button"]'],
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
formOwner: form,
});
}
function findNameContinueButton(nameInput = null) {
const form = nameInput?.form || nameInput?.closest?.('form') || null;
return findActionButton({
preferredSelectors: ['button[data-testid="signup-next-button"]'],
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
formOwner: form,
});
}
function findOtpVerifyButton(otpInput = null) {
const form = otpInput?.form || otpInput?.closest?.('form') || null;
return findActionButton({
preferredSelectors: ['button[data-testid="email-verification-verify-button"]'],
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
formOwner: form,
});
}
function findPasswordContinueButton(passwordInput = null) {
const form = passwordInput?.form || passwordInput?.closest?.('form') || null;
return findActionButton({
preferredSelectors: ['button[data-testid="test-primary-button"]'],
textPattern: KIRO_CONTINUE_TEXT_PATTERN,
formOwner: form,
});
}
function getAuthorizationActionKind(text = '') {
if (KIRO_CONFIRM_CONTINUE_TEXT_PATTERN.test(text)) {
return 'confirm_continue';
}
if (KIRO_ALLOW_ACCESS_TEXT_PATTERN.test(text)) {
return 'allow_access';
}
return 'unknown';
}
function findAuthorizationActionButton() {
return findActionButton({
preferredSelectors: [
'button[data-testid="confirm-button"]',
'button[data-testid="allow-access-button"]',
],
textPattern: /confirm and continue|allow access|确认并继续|允许访问/i,
});
}
function detectKiroPageState() {
const pageText = getKiroPageText();
const currentUrl = location.href;
const passwordInputs = findVisiblePasswordInputs();
const confirmPasswordInput = findVisibleConfirmPasswordInput();
if (passwordInputs.length) {
const passwordInput = passwordInputs[0];
return {
state: 'password_page',
url: currentUrl,
passwordInput,
confirmPasswordInput,
continueButton: findPasswordContinueButton(passwordInput),
};
}
const authorizationButton = findAuthorizationActionButton();
if (authorizationButton) {
const authorizationActionText = getElementActionText(authorizationButton);
return {
state: 'authorization_page',
url: currentUrl,
actionButton: authorizationButton,
authorizationActionKind: getAuthorizationActionKind(authorizationActionText),
authorizationActionText,
};
}
if (
KIRO_SUCCESS_TEXT_PATTERN.test(pageText)
|| /request approved|access your data|请求已批准|访问您的数据/i.test(pageText)
) {
return {
state: 'success_page',
url: currentUrl,
};
}
const otpInput = findVisibleOtpInput();
if (otpInput) {
return {
state: 'otp_page',
url: currentUrl,
otpInput,
verifyButton: findOtpVerifyButton(otpInput),
};
}
const nameInput = findVisibleNameInput();
if (nameInput) {
return {
state: 'name_entry',
url: currentUrl,
nameInput,
continueButton: findNameContinueButton(nameInput),
};
}
const emailInput = findVisibleEmailInput();
if (emailInput) {
return {
state: 'email_entry',
url: currentUrl,
emailInput,
continueButton: findEmailContinueButton(emailInput),
};
}
return {
state: 'loading',
url: currentUrl,
};
}
async function waitForKiroState(predicate, options = {}) {
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 250));
const start = Date.now();
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const detected = detectKiroPageState();
if (predicate(detected)) {
return detected;
}
await sleep(retryDelayMs);
}
const finalState = detectKiroPageState();
throw new Error(options.timeoutMessage || `等待 Kiro 页面状态超时:${finalState.state}`);
}
async function ensureKiroPageState(payload = {}) {
const targetStates = Array.isArray(payload?.targetStates)
? payload.targetStates.map((entry) => String(entry || '').trim()).filter(Boolean)
: [];
if (!targetStates.length) {
throw new Error('缺少 Kiro 目标页面状态。');
}
return waitForKiroState(
(detected) => targetStates.includes(detected.state),
{
timeoutMs: payload?.timeoutMs,
retryDelayMs: payload?.retryDelayMs,
timeoutMessage: payload?.timeoutMessage || `等待 Kiro 页面进入 ${targetStates.join(' / ')} 超时,当前页面:${location.href}`,
}
);
}
async function waitForKiroStateChange(payload = {}) {
const fromStates = Array.isArray(payload?.fromStates)
? payload.fromStates.map((entry) => String(entry || '').trim()).filter(Boolean)
: [];
if (!fromStates.length) {
throw new Error('缺少 Kiro 原始页面状态。');
}
return waitForKiroState(
(detected) => detected.state !== 'loading' && !fromStates.includes(detected.state),
{
timeoutMs: payload?.timeoutMs,
retryDelayMs: payload?.retryDelayMs,
timeoutMessage: payload?.timeoutMessage || `等待 Kiro 页面离开 ${fromStates.join(' / ')} 超时,当前页面:${location.href}`,
}
);
}
async function waitForKiroAuthorizationAdvance(previousState = {}, options = {}) {
return waitForKiroState(
(detected) => {
if (detected.state === 'success_page') {
return true;
}
if (detected.state !== 'authorization_page') {
return false;
}
const previousKind = String(previousState?.authorizationActionKind || '').trim();
const previousText = String(previousState?.authorizationActionText || '').trim();
const previousUrl = String(previousState?.url || '').trim();
const nextKind = String(detected.authorizationActionKind || '').trim();
const nextText = String(detected.authorizationActionText || '').trim();
return nextKind !== previousKind
|| nextText !== previousText
|| String(detected.url || '').trim() !== previousUrl;
},
{
timeoutMs: options.timeoutMs,
retryDelayMs: options.retryDelayMs,
timeoutMessage: options.timeoutMessage || `等待 Kiro 授权页进入下一步超时:${location.href}`,
}
);
}
async function submitKiroEmail(payload = {}) {
const email = String(payload?.email || '').trim();
if (!email) {
throw new Error('缺少 Kiro 授权邮箱,无法继续提交。');
}
const readyState = await ensureKiroPageState({
targetStates: ['email_entry'],
timeoutMs: payload?.timeoutMs || 30000,
retryDelayMs: payload?.retryDelayMs || 250,
});
if (!readyState.emailInput || !readyState.continueButton) {
throw new Error('Kiro 邮箱页未找到可用的输入框或继续按钮。');
}
fillInput(readyState.emailInput, email);
await sleep(200);
simulateClick(readyState.continueButton);
return {
submitted: true,
state: 'email_submitted',
url: location.href,
};
}
async function submitKiroName(payload = {}) {
const fullName = String(payload?.fullName || '').trim();
if (!fullName) {
throw new Error('缺少 Kiro 注册姓名,无法继续提交。');
}
const readyState = await ensureKiroPageState({
targetStates: ['name_entry'],
timeoutMs: payload?.timeoutMs || 30000,
retryDelayMs: payload?.retryDelayMs || 250,
});
if (!readyState.nameInput || !readyState.continueButton) {
throw new Error('Kiro 姓名页未找到可用的输入框或继续按钮。');
}
fillInput(readyState.nameInput, fullName);
await sleep(200);
simulateClick(readyState.continueButton);
return {
submitted: true,
state: 'name_submitted',
url: location.href,
};
}
async function submitKiroVerificationCode(payload = {}) {
const code = String(payload?.code || '').trim();
if (!code) {
throw new Error('缺少 Kiro 邮箱验证码,无法继续提交。');
}
const readyState = await ensureKiroPageState({
targetStates: ['otp_page'],
timeoutMs: payload?.timeoutMs || 30000,
retryDelayMs: payload?.retryDelayMs || 250,
});
if (!readyState.otpInput || !readyState.verifyButton) {
throw new Error('Kiro 验证码页未找到可用的输入框或继续按钮。');
}
fillInput(readyState.otpInput, code);
await sleep(200);
simulateClick(readyState.verifyButton);
return {
submitted: true,
state: 'verification_submitted',
url: location.href,
};
}
async function submitKiroPassword(payload = {}) {
const password = String(payload?.password || '');
if (!password) {
throw new Error('缺少 Kiro 账户密码,无法继续提交。');
}
const readyState = await ensureKiroPageState({
targetStates: ['password_page'],
timeoutMs: payload?.timeoutMs || 30000,
retryDelayMs: payload?.retryDelayMs || 250,
});
if (!readyState.passwordInput || !readyState.continueButton) {
throw new Error('Kiro 密码页未找到可用的密码框或继续按钮。');
}
fillInput(readyState.passwordInput, password);
await sleep(150);
const confirmPasswordInput = readyState.confirmPasswordInput
|| (() => {
const passwordInputs = findVisiblePasswordInputs();
return passwordInputs.length > 1 ? passwordInputs[1] : null;
})();
if (confirmPasswordInput && confirmPasswordInput !== readyState.passwordInput) {
fillInput(confirmPasswordInput, password);
await sleep(150);
}
simulateClick(readyState.continueButton);
return {
submitted: true,
state: 'password_submitted',
url: location.href,
};
}
async function confirmKiroAccess(payload = {}) {
let currentState = await ensureKiroPageState({
targetStates: ['authorization_page', 'success_page'],
timeoutMs: payload?.timeoutMs || 45000,
retryDelayMs: payload?.retryDelayMs || 250,
});
const maxActions = Math.max(1, Math.min(4, Number(payload?.maxActions) || 3));
const actions = [];
while (currentState.state === 'authorization_page' && actions.length < maxActions) {
if (!currentState.actionButton) {
throw new Error('Kiro 授权页未找到可用的授权按钮。');
}
actions.push({
kind: currentState.authorizationActionKind || 'unknown',
text: currentState.authorizationActionText || getElementActionText(currentState.actionButton),
});
simulateClick(currentState.actionButton);
currentState = await waitForKiroAuthorizationAdvance(currentState, {
timeoutMs: payload?.timeoutMs || 45000,
retryDelayMs: payload?.retryDelayMs || 250,
timeoutMessage: 'Kiro 授权按钮点击后页面未继续,请检查当前授权页状态。',
});
}
if (currentState.state !== 'success_page') {
throw new Error('Kiro 授权页未完成确认访问流程。');
}
return {
submitted: true,
state: currentState.state,
url: currentState.url || location.href,
actions,
};
}
async function handleKiroDeviceAuthCommand(message) {
switch (message.type) {
case 'ENSURE_KIRO_PAGE_STATE':
return ensureKiroPageState(message.payload || {});
case 'ENSURE_KIRO_STATE_CHANGE':
return waitForKiroStateChange(message.payload || {});
case 'EXECUTE_NODE': {
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
if (nodeId === 'kiro-submit-email') {
return submitKiroEmail(message.payload || {});
}
if (nodeId === 'kiro-submit-name') {
return submitKiroName(message.payload || {});
}
if (nodeId === 'kiro-submit-verification-code') {
return submitKiroVerificationCode(message.payload || {});
}
if (nodeId === 'kiro-fill-password') {
return submitKiroPassword(message.payload || {});
}
if (nodeId === 'kiro-confirm-access') {
return confirmKiroAccess(message.payload || {});
}
throw new Error(`kiro-device-auth-page.js 不处理节点:${nodeId}`);
}
default:
return null;
}
}
if (document.documentElement.getAttribute(KIRO_DEVICE_AUTH_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(KIRO_DEVICE_AUTH_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message.type === 'ENSURE_KIRO_PAGE_STATE'
|| message.type === 'ENSURE_KIRO_STATE_CHANGE'
|| message.type === 'EXECUTE_NODE'
) {
resetStopState();
handleKiroDeviceAuthCommand(message)
.then((result) => {
sendResponse({ ok: true, ...(result || {}) });
})
.catch((error) => {
if (isStopError(error)) {
sendResponse({ stopped: true, error: error.message });
return;
}
sendResponse({ error: error?.message || String(error || '未知错误') });
});
return true;
}
return false;
});
}
+39 -3
View File
@@ -124,15 +124,51 @@
{
id: 2,
order: 20,
key: 'kiro-await-device-login',
title: '等待设备登录确认',
key: 'kiro-submit-email',
title: '获取邮箱并继续',
sourceId: 'kiro-device-auth',
driverId: 'background/kiro-device-auth',
command: 'kiro-await-device-login',
command: 'kiro-submit-email',
},
{
id: 3,
order: 30,
key: 'kiro-submit-name',
title: '填写姓名并继续',
sourceId: 'kiro-device-auth',
driverId: 'background/kiro-device-auth',
command: 'kiro-submit-name',
},
{
id: 4,
order: 40,
key: 'kiro-submit-verification-code',
title: '获取验证码并继续',
sourceId: 'kiro-device-auth',
driverId: 'background/kiro-device-auth',
command: 'kiro-submit-verification-code',
},
{
id: 5,
order: 50,
key: 'kiro-fill-password',
title: '设置密码并继续',
sourceId: 'kiro-device-auth',
driverId: 'background/kiro-device-auth',
command: 'kiro-fill-password',
},
{
id: 6,
order: 60,
key: 'kiro-confirm-access',
title: '确认访问并授权',
sourceId: 'kiro-device-auth',
driverId: 'background/kiro-device-auth',
command: 'kiro-confirm-access',
},
{
id: 7,
order: 70,
key: 'kiro-upload-credential',
title: '上传凭据到 kiro.rs',
sourceId: 'kiro-rs-admin',
+1 -1
View File
@@ -75,6 +75,6 @@
## 7. 操作间延迟
`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
`操作间延迟` 默认开启且固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth``platform-verify` 的交互节奏。
+24 -11
View File
@@ -7,7 +7,7 @@
const DEFAULT_KIRO_SOURCE_ID = 'kiro-rs';
const DEFAULT_KIRO_RS_URL = 'https://kiro.leftcode.xyz/admin';
const OPENAI_SOURCE_IDS = Object.freeze(['cpa', 'sub2api', 'codex2api']);
const SHARED_SERVICE_IDS = Object.freeze(['email', 'proxy']);
const SHARED_SERVICE_IDS = Object.freeze(['account', 'email', 'proxy']);
const DEFAULT_FLOW_CAPABILITIES = Object.freeze({
supportsEmailSignup: true,
@@ -37,7 +37,7 @@
openai: {
id: 'openai',
label: 'Codex / OpenAI',
services: ['email', 'proxy'],
services: ['account', 'email', 'proxy'],
capabilities: {
...DEFAULT_FLOW_CAPABILITIES,
supportsPhoneSignup: true,
@@ -50,7 +50,6 @@
stepDefinitionMode: 'openai-dynamic',
},
baseGroups: [
'openai-account',
'openai-plus',
'openai-phone',
'openai-oauth',
@@ -203,7 +202,7 @@
kiro: {
id: 'kiro',
label: 'Kiro',
services: ['email', 'proxy'],
services: ['account', 'email', 'proxy'],
capabilities: {
...DEFAULT_FLOW_CAPABILITIES,
stepDefinitionMode: 'kiro-device-auth',
@@ -225,7 +224,7 @@
label: 'Kiro 授权页',
readyPolicy: 'top-frame-only',
family: 'kiro-device-auth-family',
driverId: null,
driverId: 'content/kiro-device-auth-page',
cleanupScopes: [],
},
'kiro-rs-admin': {
@@ -239,11 +238,25 @@
},
},
driverDefinitions: {
'content/kiro-device-auth-page': {
sourceId: 'kiro-device-auth',
commands: [
'kiro-submit-email',
'kiro-submit-name',
'kiro-submit-verification-code',
'kiro-fill-password',
'kiro-confirm-access',
],
},
'background/kiro-device-auth': {
sourceId: 'kiro-device-auth',
commands: [
'kiro-start-device-login',
'kiro-await-device-login',
'kiro-submit-email',
'kiro-submit-name',
'kiro-submit-verification-code',
'kiro-fill-password',
'kiro-confirm-access',
'kiro-upload-credential',
],
},
@@ -252,6 +265,11 @@
});
const SETTINGS_GROUP_DEFINITIONS = freezeDeep({
'service-account': {
id: 'service-account',
label: '账户',
rowIds: ['row-custom-password'],
},
'service-email': {
id: 'service-email',
label: '邮箱服务',
@@ -283,11 +301,6 @@
label: 'Codex2API 来源',
rowIds: ['row-codex2api-url', 'row-codex2api-admin-key'],
},
'openai-account': {
id: 'openai-account',
label: '账户',
rowIds: ['row-custom-password'],
},
'openai-plus': {
id: 'openai-plus',
label: 'Plus',
+13 -8
View File
@@ -57,6 +57,9 @@
schemaVersion: 3,
activeFlowId: defaultFlowId,
services: {
account: {
customPassword: '',
},
email: {
provider: '163',
},
@@ -91,9 +94,6 @@
},
},
},
account: {
customPassword: '',
},
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
@@ -131,7 +131,7 @@
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 3,
toStep: 7,
},
},
},
@@ -202,6 +202,14 @@
?? defaults.services.proxy.mode
).trim() || defaults.services.proxy.mode,
},
account: {
customPassword: String(
input?.customPassword
?? nested?.services?.account?.customPassword
?? nested?.flows?.openai?.account?.customPassword
?? defaults.services.account.customPassword
).trim(),
},
},
flows: {
openai: {
@@ -242,9 +250,6 @@
},
},
},
account: {
customPassword: String(input?.customPassword ?? nested?.flows?.openai?.account?.customPassword ?? '').trim(),
},
signup: {
signupMethod: String(input?.signupMethod ?? nested?.flows?.openai?.signup?.signupMethod ?? defaults.flows.openai.signup.signupMethod).trim().toLowerCase() === 'phone' ? 'phone' : 'email',
phoneVerificationEnabled: Boolean(input?.phoneVerificationEnabled ?? nested?.flows?.openai?.signup?.phoneVerificationEnabled ?? defaults.flows.openai.signup.phoneVerificationEnabled),
@@ -370,7 +375,7 @@
next.sub2apiDefaultProxyName = openaiState.source.entries.sub2api.sub2apiDefaultProxyName;
next.codex2apiUrl = openaiState.source.entries.codex2api.codex2apiUrl;
next.codex2apiAdminKey = openaiState.source.entries.codex2api.codex2apiAdminKey;
next.customPassword = openaiState.account.customPassword;
next.customPassword = normalizedState.services.account.customPassword;
next.signupMethod = openaiState.signup.signupMethod;
next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled;
next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled;
+1 -1
View File
@@ -273,7 +273,7 @@
<span class="data-label">账户密码</span>
<div class="input-with-icon">
<input type="password" id="input-password" class="data-input data-input-with-icon"
placeholder="codex密码,留空则自动生成" />
placeholder="账户密码,留空则自动生成" />
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
</div>
</div>
+10 -9
View File
@@ -13154,15 +13154,16 @@ stepsList?.addEventListener('click', async (event) => {
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
return;
}
if (step === 3) {
if (inputPassword.value !== (latestState?.customPassword || '')) {
await chrome.runtime.sendMessage({
type: 'SAVE_SETTING',
source: 'sidepanel',
payload: { customPassword: inputPassword.value },
});
syncLatestState({ customPassword: inputPassword.value });
}
const shouldPersistSharedPassword = nodeId === 'fill-password' || nodeId === 'kiro-fill-password';
if (shouldPersistSharedPassword && inputPassword.value !== (latestState?.customPassword || '')) {
await chrome.runtime.sendMessage({
type: 'SAVE_SETTING',
source: 'sidepanel',
payload: { customPassword: inputPassword.value },
});
syncLatestState({ customPassword: inputPassword.value });
}
if (nodeId === 'fill-password') {
if (shouldExecuteStep3WithSignupPhoneIdentity(latestState)) {
const response = await sendSidepanelMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', payload: { nodeId } });
if (response?.error) {
+21 -5
View File
@@ -8,7 +8,15 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
const executedNodeIds = [];
const kiroNodeIds = ['kiro-start-device-login', 'kiro-await-device-login', 'kiro-upload-credential'];
const kiroNodeIds = [
'kiro-start-device-login',
'kiro-submit-email',
'kiro-submit-name',
'kiro-submit-verification-code',
'kiro-fill-password',
'kiro-confirm-access',
'kiro-upload-credential',
];
const openAiNodeIds = ['open-chatgpt', 'submit-signup-email', 'fill-password'];
let helperCalls = 0;
let sessionSeed = 700;
@@ -33,12 +41,16 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
signupMethod: 'email',
stepExecutionRangeByFlow: {
openai: { enabled: false, fromStep: 1, toStep: 11 },
kiro: { enabled: false, fromStep: 1, toStep: 3 },
kiro: { enabled: false, fromStep: 1, toStep: 7 },
},
nodeStatuses: {
'open-chatgpt': 'stopped',
'kiro-start-device-login': 'pending',
'kiro-await-device-login': 'pending',
'kiro-submit-email': 'pending',
'kiro-submit-name': 'pending',
'kiro-submit-verification-code': 'pending',
'kiro-fill-password': 'pending',
'kiro-confirm-access': 'pending',
'kiro-upload-credential': 'pending',
},
tabRegistry: {
@@ -172,7 +184,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
signupMethod: 'email',
stepExecutionRangeByFlow: {
openai: { enabled: false, fromStep: 1, toStep: 11 },
kiro: { enabled: false, fromStep: 1, toStep: 3 },
kiro: { enabled: false, fromStep: 1, toStep: 7 },
},
nodeStatuses: {},
tabRegistry: {},
@@ -190,7 +202,11 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
...currentState,
nodeStatuses: {
'kiro-start-device-login': 'completed',
'kiro-await-device-login': 'completed',
'kiro-submit-email': 'completed',
'kiro-submit-name': 'completed',
'kiro-submit-verification-code': 'completed',
'kiro-fill-password': 'completed',
'kiro-confirm-access': 'completed',
'kiro-upload-credential': 'completed',
},
};
@@ -0,0 +1,23 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
const match = source.match(/function generatePassword\(\)\s*\{[\s\S]*?return passwordChars\.join\(''\);\r?\n\}/);
assert.ok(match, 'generatePassword definition should exist in background.js');
const generatePassword = new Function(`${match[0]}; return generatePassword;`)();
test('generatePassword produces shared account passwords within the required policy', () => {
for (let index = 0; index < 100; index += 1) {
const password = String(generatePassword() || '');
assert.ok(password.length >= 8, `password should be at least 8 characters: ${password}`);
assert.ok(password.length <= 64, `password should be at most 64 characters: ${password}`);
assert.match(password, /[A-Z]/, `password should include an uppercase letter: ${password}`);
assert.match(password, /[a-z]/, `password should include a lowercase letter: ${password}`);
assert.match(password, /[0-9]/, `password should include a digit: ${password}`);
assert.match(password, /[^A-Za-z0-9]/, `password should include a symbol: ${password}`);
}
});
@@ -23,6 +23,20 @@ function mergeUpdates(updatesList = []) {
return updatesList.reduce((acc, item) => Object.assign(acc, item), {});
}
function createChromeRecorder() {
const updates = [];
return {
updates,
chrome: {
tabs: {
update: async (tabId, update) => {
updates.push({ tabId, update });
},
},
},
};
}
test('kiro device auth module exposes a factory', () => {
const api = loadKiroDeviceAuthApi();
assert.equal(typeof api?.createKiroDeviceAuthExecutor, 'function');
@@ -30,19 +44,27 @@ test('kiro device auth module exposes a factory', () => {
assert.equal(typeof api?.uploadBuilderIdCredential, 'function');
});
test('kiro start device login registers client, opens auth tab, and completes with runtime payload', async () => {
test('kiro start device login opens the auth tab and waits for the email entry page', async () => {
const api = loadKiroDeviceAuthApi();
const fetchCalls = [];
const stateUpdates = [];
const registerCalls = [];
const reuseCalls = [];
const completeCalls = [];
const contentReadyCalls = [];
const contentMessages = [];
const stableWaitCalls = [];
const { chrome, updates: tabUpdates } = createChromeRecorder();
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
chrome,
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
ensureContentScriptReadyOnTab: async (source, tabId, options = {}) => {
contentReadyCalls.push({ source, tabId, options });
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({
url,
@@ -75,20 +97,35 @@ test('kiro start device login registers client, opens auth tab, and completes wi
}
throw new Error(`Unexpected fetch URL: ${url}`);
},
getState: async () => ({
}),
getState: async () => ({}),
registerTab: async (source, tabId) => {
registerCalls.push({ source, tabId });
},
KIRO_DEVICE_AUTH_INJECT_FILES: [
'shared/source-registry.js',
'content/utils.js',
'content/kiro-device-auth-page.js',
],
reuseOrCreateTab: async (source, url) => {
reuseCalls.push({ source, url });
return 88;
},
sendToContentScriptResilient: async (source, message, options = {}) => {
contentMessages.push({ source, message, options });
return {
ok: true,
state: 'email_entry',
url: 'https://device.example.com/complete',
};
},
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabStableComplete: async (tabId, options = {}) => {
stableWaitCalls.push({ tabId, options });
},
});
await executor.executeKiroStartDeviceLogin({
@@ -111,6 +148,23 @@ test('kiro start device login registers client, opens auth tab, and completes wi
source: 'kiro-device-auth',
tabId: 88,
}]);
assert.deepEqual(tabUpdates, [{
tabId: 88,
update: { active: true },
}]);
assert.deepEqual(stableWaitCalls, [{
tabId: 88,
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 2500,
initialDelayMs: 300,
},
}]);
assert.equal(contentReadyCalls.length, 1);
assert.equal(contentMessages.length, 1);
assert.equal(contentMessages[0].message.type, 'ENSURE_KIRO_PAGE_STATE');
assert.deepEqual(contentMessages[0].message.payload.targetStates, ['email_entry']);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroClientId, 'client-001');
@@ -122,6 +176,8 @@ test('kiro start device login registers client, opens auth tab, and completes wi
assert.equal(finalState.kiroAuthIntervalSeconds, 7);
assert.equal(finalState.kiroAuthStatus, 'waiting_user');
assert.equal(finalState.kiroUploadStatus, 'waiting_login');
assert.equal(finalState.kiroFullName, '');
assert.equal(finalState.kiroVerificationRequestedAt, 0);
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-start-device-login');
@@ -129,19 +185,428 @@ test('kiro start device login registers client, opens auth tab, and completes wi
assert.equal(completeCalls[0].payload.kiroLoginUrl, 'https://device.example.com/complete');
});
test('kiro await device login polls until refresh token is captured', async () => {
test('kiro submit email resolves the signup email, reactivates the auth tab, and waits for the name page', async () => {
const api = loadKiroDeviceAuthApi();
const stateUpdates = [];
const completeCalls = [];
const resolvedEmails = [];
const contentMessages = [];
const stableWaitCalls = [];
const { chrome, updates: tabUpdates } = createChromeRecorder();
let ensureCallIndex = 0;
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
chrome,
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
ensureContentScriptReadyOnTab: async () => {},
getState: async () => ({
kiroAuthTabId: 88,
kiroLoginUrl: 'https://device.example.com/complete',
email: '',
mailProvider: '163',
}),
isTabAlive: async (source) => source === 'kiro-device-auth',
KIRO_DEVICE_AUTH_INJECT_FILES: [
'shared/source-registry.js',
'content/utils.js',
'content/kiro-device-auth-page.js',
],
resolveSignupEmailForFlow: async (state, options = {}) => {
resolvedEmails.push({ state, options });
return 'user@example.com';
},
sendToContentScriptResilient: async (source, message, options = {}) => {
contentMessages.push({ source, message, options });
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
ensureCallIndex += 1;
return {
ok: true,
state: ensureCallIndex === 1 ? 'email_entry' : 'name_entry',
url: ensureCallIndex === 1
? 'https://device.example.com/complete'
: 'https://device.example.com/name',
};
}
if (message.type === 'EXECUTE_NODE') {
return {
ok: true,
submitted: true,
state: 'email_submitted',
url: 'https://device.example.com/complete',
};
}
throw new Error(`Unexpected content message: ${message.type}`);
},
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabStableComplete: async (tabId, options = {}) => {
stableWaitCalls.push({ tabId, options });
},
});
await executor.executeKiroSubmitEmail({
nodeId: 'kiro-submit-email',
kiroAuthTabId: 88,
kiroLoginUrl: 'https://device.example.com/complete',
email: '',
mailProvider: '163',
});
assert.equal(resolvedEmails.length, 1);
assert.equal(resolvedEmails[0].state.nodeId, 'kiro-submit-email');
assert.deepEqual(resolvedEmails[0].options, {
preserveAccountIdentity: true,
});
assert.deepEqual(tabUpdates, [
{ tabId: 88, update: { active: true } },
{ tabId: 88, update: { active: true } },
]);
assert.deepEqual(stableWaitCalls, [
{
tabId: 88,
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 2500,
initialDelayMs: 300,
},
},
{
tabId: 88,
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 1500,
initialDelayMs: 150,
},
},
]);
assert.equal(contentMessages.length, 3);
assert.deepEqual(contentMessages[0].message.payload.targetStates, ['email_entry']);
assert.equal(contentMessages[1].message.nodeId, 'kiro-submit-email');
assert.deepEqual(contentMessages[1].message.payload, { email: 'user@example.com' });
assert.deepEqual(contentMessages[2].message.payload.targetStates, ['name_entry']);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroAuthorizedEmail, 'user@example.com');
assert.equal(finalState.kiroAuthError, '');
assert.equal(finalState.kiroAuthStatus, 'waiting_user');
assert.equal(finalState.kiroUploadError, '');
assert.equal(finalState.kiroUploadStatus, 'waiting_login');
assert.equal(finalState.kiroFullName, '');
assert.equal(finalState.kiroVerificationRequestedAt, 0);
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-submit-email');
assert.equal(completeCalls[0].payload.email, 'user@example.com');
assert.equal(completeCalls[0].payload.accountIdentifierType, 'email');
assert.equal(completeCalls[0].payload.accountIdentifier, 'user@example.com');
assert.equal(completeCalls[0].payload.kiroNextState, 'name_entry');
assert.equal(completeCalls[0].payload.kiroNextUrl, 'https://device.example.com/name');
});
test('kiro submit name generates a full name and waits for the otp page', async () => {
const api = loadKiroDeviceAuthApi();
const stateUpdates = [];
const completeCalls = [];
const contentMessages = [];
const { chrome, updates: tabUpdates } = createChromeRecorder();
let ensureCallIndex = 0;
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
chrome,
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
ensureContentScriptReadyOnTab: async () => {},
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
getState: async () => ({
kiroAuthTabId: 88,
kiroAuthorizedEmail: 'user@example.com',
kiroLoginUrl: 'https://device.example.com/complete',
}),
isTabAlive: async (source) => source === 'kiro-device-auth',
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
ensureCallIndex += 1;
return {
ok: true,
state: ensureCallIndex === 1 ? 'name_entry' : 'otp_page',
url: ensureCallIndex === 1
? 'https://device.example.com/name'
: 'https://device.example.com/verify',
};
}
if (message.type === 'EXECUTE_NODE') {
return {
ok: true,
submitted: true,
state: 'name_submitted',
url: 'https://device.example.com/name',
};
}
throw new Error(`Unexpected content message: ${message.type}`);
},
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabStableComplete: async () => {},
});
await executor.executeKiroSubmitName({
nodeId: 'kiro-submit-name',
kiroAuthTabId: 88,
kiroAuthorizedEmail: 'user@example.com',
kiroLoginUrl: 'https://device.example.com/complete',
});
assert.deepEqual(tabUpdates, [{
tabId: 88,
update: { active: true },
}]);
assert.equal(contentMessages.length, 3);
assert.deepEqual(contentMessages[0].payload.targetStates, ['name_entry']);
assert.equal(contentMessages[1].nodeId, 'kiro-submit-name');
assert.deepEqual(contentMessages[1].payload, { fullName: 'Ada Lovelace' });
assert.deepEqual(contentMessages[2].payload.targetStates, ['otp_page']);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroFullName, 'Ada Lovelace');
assert.equal(finalState.kiroAuthError, '');
assert.equal(finalState.kiroUploadError, '');
assert.equal(typeof finalState.kiroVerificationRequestedAt, 'number');
assert.equal(finalState.kiroVerificationRequestedAt > 0, true);
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-submit-name');
assert.equal(completeCalls[0].payload.kiroFullName, 'Ada Lovelace');
assert.equal(completeCalls[0].payload.kiroNextState, 'otp_page');
});
test('kiro submit verification code polls mail, returns to the auth tab, and waits for the password page', async () => {
const api = loadKiroDeviceAuthApi();
const stateUpdates = [];
const completeCalls = [];
const mailPollCalls = [];
const contentMessages = [];
const mailOpenCalls = [];
const { chrome, updates: tabUpdates } = createChromeRecorder();
let ensureCallIndex = 0;
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
chrome,
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
ensureContentScriptReadyOnTab: async () => {},
getMailConfig: () => ({
source: 'mail-163',
url: 'https://mail.example.com/inbox',
label: '163 邮箱',
}),
getState: async () => ({
kiroAuthTabId: 88,
kiroAuthorizedEmail: 'user@example.com',
kiroLoginUrl: 'https://device.example.com/complete',
kiroVerificationRequestedAt: 1700000000000,
mailProvider: '163',
}),
isTabAlive: async (source) => source === 'kiro-device-auth',
reuseOrCreateTab: async (source, url) => {
mailOpenCalls.push({ source, url });
return 66;
},
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
ensureCallIndex += 1;
return {
ok: true,
state: ensureCallIndex === 1 ? 'otp_page' : 'password_page',
url: ensureCallIndex === 1
? 'https://device.example.com/verify'
: 'https://device.example.com/password',
};
}
if (message.type === 'EXECUTE_NODE') {
return {
ok: true,
submitted: true,
state: 'verification_submitted',
url: 'https://device.example.com/verify',
};
}
throw new Error(`Unexpected content message: ${message.type}`);
},
sendToMailContentScriptResilient: async (mail, message, options = {}) => {
mailPollCalls.push({ mail, message, options });
return {
ok: true,
code: '654321',
emailTimestamp: 1700000005000,
mailId: 'mail-1',
};
},
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabStableComplete: async () => {},
});
await executor.executeKiroSubmitVerificationCode({
nodeId: 'kiro-submit-verification-code',
kiroAuthTabId: 88,
kiroAuthorizedEmail: 'user@example.com',
kiroLoginUrl: 'https://device.example.com/complete',
kiroVerificationRequestedAt: 1700000000000,
mailProvider: '163',
});
assert.deepEqual(mailOpenCalls, [{
source: 'mail-163',
url: 'https://mail.example.com/inbox',
}]);
assert.equal(mailPollCalls.length, 1);
assert.equal(mailPollCalls[0].message.type, 'POLL_EMAIL');
assert.equal(mailPollCalls[0].message.payload.targetEmail, 'user@example.com');
assert.equal(mailPollCalls[0].message.payload.filterAfterTimestamp, 1700000000000);
assert.deepEqual(tabUpdates, [
{ tabId: 88, update: { active: true } },
{ tabId: 88, update: { active: true } },
]);
assert.equal(contentMessages.length, 3);
assert.deepEqual(contentMessages[0].payload.targetStates, ['otp_page']);
assert.equal(contentMessages[1].nodeId, 'kiro-submit-verification-code');
assert.deepEqual(contentMessages[1].payload, { code: '654321' });
assert.deepEqual(contentMessages[2].payload.targetStates, ['password_page']);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroAuthError, '');
assert.equal(finalState.kiroUploadError, '');
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-submit-verification-code');
assert.equal(completeCalls[0].payload.code, '654321');
assert.equal(completeCalls[0].payload.mailId, 'mail-1');
assert.equal(completeCalls[0].payload.kiroNextState, 'password_page');
});
test('kiro fill password reuses the shared password state and waits for the page to leave password state', async () => {
const api = loadKiroDeviceAuthApi();
const stateUpdates = [];
const completeCalls = [];
const contentMessages = [];
const savedPasswords = [];
const { chrome, updates: tabUpdates } = createChromeRecorder();
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
chrome,
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
ensureContentScriptReadyOnTab: async () => {},
getState: async () => ({
kiroAuthTabId: 88,
kiroLoginUrl: 'https://device.example.com/complete',
customPassword: 'SharedPass123!',
}),
isTabAlive: async (source) => source === 'kiro-device-auth',
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
return {
ok: true,
state: 'password_page',
url: 'https://device.example.com/password',
};
}
if (message.type === 'ENSURE_KIRO_STATE_CHANGE') {
return {
ok: true,
state: 'authorization_page',
url: 'https://device.example.com/authorize',
};
}
if (message.type === 'EXECUTE_NODE') {
return {
ok: true,
submitted: true,
state: 'password_submitted',
url: 'https://device.example.com/password',
};
}
throw new Error(`Unexpected content message: ${message.type}`);
},
setPasswordState: async (password) => {
savedPasswords.push(password);
},
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabStableComplete: async () => {},
});
await executor.executeKiroFillPassword({
nodeId: 'kiro-fill-password',
kiroAuthTabId: 88,
kiroLoginUrl: 'https://device.example.com/complete',
customPassword: 'SharedPass123!',
});
assert.deepEqual(savedPasswords, ['SharedPass123!']);
assert.deepEqual(tabUpdates, [{
tabId: 88,
update: { active: true },
}]);
assert.equal(contentMessages.length, 3);
assert.deepEqual(contentMessages[0].payload.targetStates, ['password_page']);
assert.equal(contentMessages[1].nodeId, 'kiro-fill-password');
assert.deepEqual(contentMessages[1].payload, { password: 'SharedPass123!' });
assert.deepEqual(contentMessages[2].payload.fromStates, ['password_page']);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroAuthError, '');
assert.equal(finalState.kiroUploadError, '');
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-fill-password');
assert.equal(completeCalls[0].payload.kiroNextState, 'authorization_page');
assert.equal(completeCalls[0].payload.kiroNextUrl, 'https://device.example.com/authorize');
});
test('kiro confirm access completes the authorization page and then polls until refresh token is captured', async () => {
const api = loadKiroDeviceAuthApi();
const fetchCalls = [];
const stateUpdates = [];
const sleepCalls = [];
const completeCalls = [];
const contentMessages = [];
const { chrome, updates: tabUpdates } = createChromeRecorder();
let pollCount = 0;
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
chrome,
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
ensureContentScriptReadyOnTab: async () => {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({
url,
@@ -167,13 +632,35 @@ test('kiro await device login polls until refresh token is captured', async () =
});
},
getState: async () => ({
kiroAuthTabId: 88,
kiroClientId: 'client-001',
kiroClientSecret: 'secret-001',
kiroDeviceAuthorizationCode: 'device-code-001',
kiroAuthRegion: 'us-east-1',
kiroAuthExpiresAt: Date.now() + 60000,
kiroAuthIntervalSeconds: 5,
kiroLoginUrl: 'https://device.example.com/complete',
}),
isTabAlive: async (source) => source === 'kiro-device-auth',
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
return {
ok: true,
state: 'authorization_page',
url: 'https://device.example.com/authorize',
};
}
if (message.type === 'EXECUTE_NODE') {
return {
ok: true,
submitted: true,
state: 'success_page',
url: 'https://device.example.com/success',
};
}
throw new Error(`Unexpected content message: ${message.type}`);
},
setState: async (updates) => {
stateUpdates.push(updates);
},
@@ -181,18 +668,28 @@ test('kiro await device login polls until refresh token is captured', async () =
sleepCalls.push(ms);
},
throwIfStopped: () => {},
waitForTabStableComplete: async () => {},
});
await executor.executeKiroAwaitDeviceLogin({
nodeId: 'kiro-await-device-login',
await executor.executeKiroConfirmAccess({
nodeId: 'kiro-confirm-access',
kiroAuthTabId: 88,
kiroClientId: 'client-001',
kiroClientSecret: 'secret-001',
kiroDeviceAuthorizationCode: 'device-code-001',
kiroAuthRegion: 'us-east-1',
kiroAuthExpiresAt: Date.now() + 60000,
kiroAuthIntervalSeconds: 5,
kiroLoginUrl: 'https://device.example.com/complete',
});
assert.deepEqual(tabUpdates, [{
tabId: 88,
update: { active: true },
}]);
assert.equal(contentMessages.length, 2);
assert.deepEqual(contentMessages[0].payload.targetStates, ['authorization_page', 'success_page']);
assert.equal(contentMessages[1].nodeId, 'kiro-confirm-access');
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://oidc.us-east-1.amazonaws.com/token');
assert.deepEqual(fetchCalls[0].body, {
@@ -210,8 +707,10 @@ test('kiro await device login polls until refresh token is captured', async () =
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].nodeId, 'kiro-confirm-access');
assert.equal(completeCalls[0].payload.kiroRefreshToken, 'refresh-001');
assert.equal(completeCalls[0].payload.kiroNextState, 'success_page');
assert.equal(completeCalls[0].payload.kiroNextUrl, 'https://device.example.com/success');
});
test('kiro upload credential checks connection and uploads builder id credential to kiro.rs', async () => {
@@ -205,7 +205,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
options: {
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
},
},
},
@@ -293,7 +293,7 @@ const chrome = {
options: {
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
},
},
},
@@ -317,7 +317,7 @@ const chrome = {
assert.deepEqual(state.stepExecutionRangeByFlow.kiro, {
enabled: true,
fromStep: 1,
toStep: 3,
toStep: 7,
});
});
@@ -379,7 +379,7 @@ function getPersistedWrites() {
options: {
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
},
},
},
+6 -2
View File
@@ -27,9 +27,13 @@ test('background imports node registry and shared workflow definitions', () => {
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-submit-email': \(state\) => kiroDeviceAuthExecutor\.executeKiroSubmitEmail\(state\)/);
assert.match(source, /'kiro-submit-name': \(state\) => kiroDeviceAuthExecutor\.executeKiroSubmitName\(state\)/);
assert.match(source, /'kiro-submit-verification-code': \(state\) => kiroDeviceAuthExecutor\.executeKiroSubmitVerificationCode\(state\)/);
assert.match(source, /'kiro-fill-password': \(state\) => kiroDeviceAuthExecutor\.executeKiroFillPassword\(state\)/);
assert.match(source, /'kiro-confirm-access': \(state\) => kiroDeviceAuthExecutor\.executeKiroConfirmAccess\(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'/);
assert.match(source, /'kiro-start-device-login',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-fill-password',[\s\S]*'kiro-confirm-access',[\s\S]*'kiro-upload-credential'/);
});
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
+1 -1
View File
@@ -98,7 +98,7 @@ test('flow capability registry exposes Kiro as an independent flow with its own
assert.equal(capabilityState.effectiveSourceId, 'kiro-rs');
assert.deepEqual(
capabilityState.visibleGroupIds,
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy']
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
});
+5 -3
View File
@@ -23,7 +23,7 @@ test('flow registry exposes openai and kiro with canonical source metadata', ()
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']
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-account', 'service-email', 'service-proxy']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds || [],
@@ -45,17 +45,19 @@ test('settings schema normalizes flat input into canonical flow and service name
mailProvider: 'hotmail',
ipProxyEnabled: true,
ipProxyService: '711proxy',
customPassword: 'SharedSecret123!',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'secret-key',
stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 2, toStep: 9 },
kiro: { enabled: true, fromStep: 1, toStep: 3 },
kiro: { enabled: true, fromStep: 1, toStep: 7 },
},
});
assert.equal(normalized.activeFlowId, 'kiro');
assert.equal(normalized.services.email.provider, 'hotmail');
assert.equal(normalized.services.proxy.enabled, true);
assert.equal(normalized.services.account.customPassword, 'SharedSecret123!');
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');
@@ -63,7 +65,7 @@ test('settings schema normalizes flat input into canonical flow and service name
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
enabled: true,
fromStep: 1,
toStep: 3,
toStep: 7,
});
});
+1 -1
View File
@@ -203,7 +203,7 @@ function updatePhoneVerificationSettingsUI() {
}
function resolveCurrentSidepanelCapabilities() {
return {
visibleGroupIds: ['openai-account', 'openai-plus', 'openai-phone'],
visibleGroupIds: ['service-account', 'openai-plus', 'openai-phone'],
effectivePanelMode: 'cpa',
panelMode: 'cpa',
effectiveSourceId: 'cpa',
@@ -4,6 +4,7 @@ const fs = require('node:fs');
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
test('sidepanel exposes SUB2API account priority below group setting', () => {
assert.match(html, /id="row-sub2api-account-priority"/);
@@ -39,7 +40,8 @@ test('sidepanel persists and locks SUB2API account priority setting', () => {
source,
/inputSub2ApiAccountPriority\.value = String\(normalizeSub2ApiAccountPriorityValue\(state\?\.sub2apiAccountPriority\)\);/
);
assert.match(source, /rowSub2ApiAccountPriority\.style\.display = useSub2Api \? '' : 'none';/);
assert.match(source, /applyFlowSettingsGroupVisibility\(visibleGroupIds\);/);
assert.match(flowRegistrySource, /'openai-source-sub2api': \{[\s\S]*'row-sub2api-account-priority'/);
assert.match(source, /inputSub2ApiAccountPriority\.disabled = locked;/);
assert.match(
source,
+10
View File
@@ -66,5 +66,15 @@ 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('content/kiro-device-auth-page', 'kiro-submit-email'), true);
assert.equal(registry.driverAcceptsCommand('content/kiro-device-auth-page', 'kiro-submit-name'), true);
assert.equal(registry.driverAcceptsCommand('content/kiro-device-auth-page', 'kiro-submit-verification-code'), true);
assert.equal(registry.driverAcceptsCommand('content/kiro-device-auth-page', 'kiro-fill-password'), true);
assert.equal(registry.driverAcceptsCommand('content/kiro-device-auth-page', 'kiro-confirm-access'), true);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-start-device-login'), true);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-submit-email'), true);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-submit-name'), true);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-submit-verification-code'), true);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-fill-password'), true);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-confirm-access'), true);
});
+23 -7
View File
@@ -168,21 +168,37 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
kiroSteps.map((step) => step.key),
[
'kiro-start-device-login',
'kiro-await-device-login',
'kiro-submit-email',
'kiro-submit-name',
'kiro-submit-verification-code',
'kiro-fill-password',
'kiro-confirm-access',
'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.equal(kiroSteps[6].sourceId, 'kiro-rs-admin');
assert.equal(kiroSteps[0].title, '启动设备登录');
assert.equal(kiroSteps[1].title, '等待设备登录确认');
assert.equal(kiroSteps[2].title, '上传凭据到 kiro.rs');
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'kiro' }), [1, 2, 3]);
assert.equal(api.getLastStepId({ activeFlowId: 'kiro' }), 3);
assert.equal(kiroSteps[1].title, '获取邮箱并继续');
assert.equal(kiroSteps[2].title, '填写姓名并继续');
assert.equal(kiroSteps[3].title, '获取验证码并继续');
assert.equal(kiroSteps[4].title, '设置密码并继续');
assert.equal(kiroSteps[5].title, '确认访问并授权');
assert.equal(kiroSteps[6].title, '上传凭据到 kiro.rs');
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'kiro' }), [1, 2, 3, 4, 5, 6, 7]);
assert.equal(api.getLastStepId({ activeFlowId: 'kiro' }), 7);
assert.deepStrictEqual(
api.getNodes({ activeFlowId: 'kiro' }).map((node) => node.next),
[['kiro-await-device-login'], ['kiro-upload-credential'], []]
[
['kiro-submit-email'],
['kiro-submit-name'],
['kiro-submit-verification-code'],
['kiro-fill-password'],
['kiro-confirm-access'],
['kiro-upload-credential'],
[],
]
);
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');