diff --git a/README.md b/README.md index f90a833..7b948d3 100644 --- a/README.md +++ b/README.md @@ -517,7 +517,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 ### 操作间延迟 -`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 +`操作间延迟` 默认开启且固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。 diff --git a/background.js b/background.js index 7ca8aeb..58377a9 100644 --- a/background.js +++ b/background.js @@ -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({ diff --git a/background/steps/kiro-device-auth.js b/background/steps/kiro-device-auth.js index 2827bfc..26d7fb3 100644 --- a/background/steps/kiro-device-auth.js +++ b/background/steps/kiro-device-auth.js @@ -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(`步骤 7:kiro.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, }; } diff --git a/content/kiro-device-auth-page.js b/content/kiro-device-auth-page.js new file mode 100644 index 0000000..1b4de7b --- /dev/null +++ b/content/kiro-device-auth-page.js @@ -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; + }); +} diff --git a/data/step-definitions.js b/data/step-definitions.js index 5ed39b3..5548bb5 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -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', diff --git a/docs/使用教程/使用教程.md b/docs/使用教程/使用教程.md index dcd4e07..79fb6f5 100644 --- a/docs/使用教程/使用教程.md +++ b/docs/使用教程/使用教程.md @@ -75,6 +75,6 @@ ## 7. 操作间延迟 -`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 +`操作间延迟` 默认开启且固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。 diff --git a/shared/flow-registry.js b/shared/flow-registry.js index c1a378a..88753f6 100644 --- a/shared/flow-registry.js +++ b/shared/flow-registry.js @@ -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', diff --git a/shared/settings-schema.js b/shared/settings-schema.js index 04363da..6cc441c 100644 --- a/shared/settings-schema.js +++ b/shared/settings-schema.js @@ -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; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index f86febf..924ef5f 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -273,7 +273,7 @@ 账户密码
+ placeholder="账户密码,留空则自动生成" />
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index bcb2002..5c747e9 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -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) { diff --git a/tests/auto-run-kiro-flow-selection.test.js b/tests/auto-run-kiro-flow-selection.test.js index dddc8a7..211eeac 100644 --- a/tests/auto-run-kiro-flow-selection.test.js +++ b/tests/auto-run-kiro-flow-selection.test.js @@ -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', }, }; diff --git a/tests/background-generate-password-policy.test.js b/tests/background-generate-password-policy.test.js new file mode 100644 index 0000000..16a14bc --- /dev/null +++ b/tests/background-generate-password-policy.test.js @@ -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}`); + } +}); diff --git a/tests/background-kiro-device-auth-module.test.js b/tests/background-kiro-device-auth-module.test.js index 5ce43ff..73bd981 100644 --- a/tests/background-kiro-device-auth-module.test.js +++ b/tests/background-kiro-device-auth-module.test.js @@ -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 () => { diff --git a/tests/background-settings-schema-persistence.test.js b/tests/background-settings-schema-persistence.test.js index d84fafb..5495fc0 100644 --- a/tests/background-settings-schema-persistence.test.js +++ b/tests/background-settings-schema-persistence.test.js @@ -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 }, }, }, }, diff --git a/tests/background-step-registry.test.js b/tests/background-step-registry.test.js index 47fa503..c06798a 100644 --- a/tests/background-step-registry.test.js +++ b/tests/background-step-registry.test.js @@ -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', () => { diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js index 14ac97a..a87b759 100644 --- a/tests/flow-capabilities-module.test.js +++ b/tests/flow-capabilities-module.test.js @@ -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'] ); }); diff --git a/tests/flow-registry-settings-schema.test.js b/tests/flow-registry-settings-schema.test.js index 15e9c7c..bd4079c 100644 --- a/tests/flow-registry-settings-schema.test.js +++ b/tests/flow-registry-settings-schema.test.js @@ -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, }); }); diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js index 5c0b325..84aa851 100644 --- a/tests/sidepanel-flow-source-registry.test.js +++ b/tests/sidepanel-flow-source-registry.test.js @@ -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', diff --git a/tests/sidepanel-sub2api-priority-settings.test.js b/tests/sidepanel-sub2api-priority-settings.test.js index d1cfbb1..f665a7e 100644 --- a/tests/sidepanel-sub2api-priority-settings.test.js +++ b/tests/sidepanel-sub2api-priority-settings.test.js @@ -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, diff --git a/tests/source-registry-module.test.js b/tests/source-registry-module.test.js index 6e8048a..2d651ef 100644 --- a/tests/source-registry-module.test.js +++ b/tests/source-registry-module.test.js @@ -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); }); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index a0161ec..a74cb20 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -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 登录与授权');