refactor kiro auth flow and share account password service
This commit is contained in:
@@ -517,7 +517,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
|
||||
### 操作间延迟
|
||||
|
||||
`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
||||
`操作间延迟` 默认开启且固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
||||
|
||||
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。
|
||||
|
||||
|
||||
+125
-23
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -124,15 +124,51 @@
|
||||
{
|
||||
id: 2,
|
||||
order: 20,
|
||||
key: 'kiro-await-device-login',
|
||||
title: '等待设备登录确认',
|
||||
key: 'kiro-submit-email',
|
||||
title: '获取邮箱并继续',
|
||||
sourceId: 'kiro-device-auth',
|
||||
driverId: 'background/kiro-device-auth',
|
||||
command: 'kiro-await-device-login',
|
||||
command: 'kiro-submit-email',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: 30,
|
||||
key: 'kiro-submit-name',
|
||||
title: '填写姓名并继续',
|
||||
sourceId: 'kiro-device-auth',
|
||||
driverId: 'background/kiro-device-auth',
|
||||
command: 'kiro-submit-name',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order: 40,
|
||||
key: 'kiro-submit-verification-code',
|
||||
title: '获取验证码并继续',
|
||||
sourceId: 'kiro-device-auth',
|
||||
driverId: 'background/kiro-device-auth',
|
||||
command: 'kiro-submit-verification-code',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
order: 50,
|
||||
key: 'kiro-fill-password',
|
||||
title: '设置密码并继续',
|
||||
sourceId: 'kiro-device-auth',
|
||||
driverId: 'background/kiro-device-auth',
|
||||
command: 'kiro-fill-password',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
order: 60,
|
||||
key: 'kiro-confirm-access',
|
||||
title: '确认访问并授权',
|
||||
sourceId: 'kiro-device-auth',
|
||||
driverId: 'background/kiro-device-auth',
|
||||
command: 'kiro-confirm-access',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
order: 70,
|
||||
key: 'kiro-upload-credential',
|
||||
title: '上传凭据到 kiro.rs',
|
||||
sourceId: 'kiro-rs-admin',
|
||||
|
||||
+1
-1
@@ -75,6 +75,6 @@
|
||||
|
||||
## 7. 操作间延迟
|
||||
|
||||
`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
||||
`操作间延迟` 默认开启且固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
||||
|
||||
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。
|
||||
|
||||
+24
-11
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
<span class="data-label">账户密码</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
||||
placeholder="codex密码,留空则自动生成" />
|
||||
placeholder="账户密码,留空则自动生成" />
|
||||
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-9
@@ -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) {
|
||||
|
||||
@@ -8,7 +8,15 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
|
||||
|
||||
const executedNodeIds = [];
|
||||
const kiroNodeIds = ['kiro-start-device-login', 'kiro-await-device-login', 'kiro-upload-credential'];
|
||||
const kiroNodeIds = [
|
||||
'kiro-start-device-login',
|
||||
'kiro-submit-email',
|
||||
'kiro-submit-name',
|
||||
'kiro-submit-verification-code',
|
||||
'kiro-fill-password',
|
||||
'kiro-confirm-access',
|
||||
'kiro-upload-credential',
|
||||
];
|
||||
const openAiNodeIds = ['open-chatgpt', 'submit-signup-email', 'fill-password'];
|
||||
let helperCalls = 0;
|
||||
let sessionSeed = 700;
|
||||
@@ -33,12 +41,16 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
|
||||
signupMethod: 'email',
|
||||
stepExecutionRangeByFlow: {
|
||||
openai: { enabled: false, fromStep: 1, toStep: 11 },
|
||||
kiro: { enabled: false, fromStep: 1, toStep: 3 },
|
||||
kiro: { enabled: false, fromStep: 1, toStep: 7 },
|
||||
},
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'stopped',
|
||||
'kiro-start-device-login': 'pending',
|
||||
'kiro-await-device-login': 'pending',
|
||||
'kiro-submit-email': 'pending',
|
||||
'kiro-submit-name': 'pending',
|
||||
'kiro-submit-verification-code': 'pending',
|
||||
'kiro-fill-password': 'pending',
|
||||
'kiro-confirm-access': 'pending',
|
||||
'kiro-upload-credential': 'pending',
|
||||
},
|
||||
tabRegistry: {
|
||||
@@ -172,7 +184,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
|
||||
signupMethod: 'email',
|
||||
stepExecutionRangeByFlow: {
|
||||
openai: { enabled: false, fromStep: 1, toStep: 11 },
|
||||
kiro: { enabled: false, fromStep: 1, toStep: 3 },
|
||||
kiro: { enabled: false, fromStep: 1, toStep: 7 },
|
||||
},
|
||||
nodeStatuses: {},
|
||||
tabRegistry: {},
|
||||
@@ -190,7 +202,11 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
|
||||
...currentState,
|
||||
nodeStatuses: {
|
||||
'kiro-start-device-login': 'completed',
|
||||
'kiro-await-device-login': 'completed',
|
||||
'kiro-submit-email': 'completed',
|
||||
'kiro-submit-name': 'completed',
|
||||
'kiro-submit-verification-code': 'completed',
|
||||
'kiro-fill-password': 'completed',
|
||||
'kiro-confirm-access': 'completed',
|
||||
'kiro-upload-credential': 'completed',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const match = source.match(/function generatePassword\(\)\s*\{[\s\S]*?return passwordChars\.join\(''\);\r?\n\}/);
|
||||
|
||||
assert.ok(match, 'generatePassword definition should exist in background.js');
|
||||
|
||||
const generatePassword = new Function(`${match[0]}; return generatePassword;`)();
|
||||
|
||||
test('generatePassword produces shared account passwords within the required policy', () => {
|
||||
for (let index = 0; index < 100; index += 1) {
|
||||
const password = String(generatePassword() || '');
|
||||
|
||||
assert.ok(password.length >= 8, `password should be at least 8 characters: ${password}`);
|
||||
assert.ok(password.length <= 64, `password should be at most 64 characters: ${password}`);
|
||||
assert.match(password, /[A-Z]/, `password should include an uppercase letter: ${password}`);
|
||||
assert.match(password, /[a-z]/, `password should include a lowercase letter: ${password}`);
|
||||
assert.match(password, /[0-9]/, `password should include a digit: ${password}`);
|
||||
assert.match(password, /[^A-Za-z0-9]/, `password should include a symbol: ${password}`);
|
||||
}
|
||||
});
|
||||
@@ -23,6 +23,20 @@ function mergeUpdates(updatesList = []) {
|
||||
return updatesList.reduce((acc, item) => Object.assign(acc, item), {});
|
||||
}
|
||||
|
||||
function createChromeRecorder() {
|
||||
const updates = [];
|
||||
return {
|
||||
updates,
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async (tabId, update) => {
|
||||
updates.push({ tabId, update });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('kiro device auth module exposes a factory', () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
assert.equal(typeof api?.createKiroDeviceAuthExecutor, 'function');
|
||||
@@ -30,19 +44,27 @@ test('kiro device auth module exposes a factory', () => {
|
||||
assert.equal(typeof api?.uploadBuilderIdCredential, 'function');
|
||||
});
|
||||
|
||||
test('kiro start device login registers client, opens auth tab, and completes with runtime payload', async () => {
|
||||
test('kiro start device login opens the auth tab and waits for the email entry page', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const fetchCalls = [];
|
||||
const stateUpdates = [];
|
||||
const registerCalls = [];
|
||||
const reuseCalls = [];
|
||||
const completeCalls = [];
|
||||
const contentReadyCalls = [];
|
||||
const contentMessages = [];
|
||||
const stableWaitCalls = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async (source, tabId, options = {}) => {
|
||||
contentReadyCalls.push({ source, tabId, options });
|
||||
},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({
|
||||
url,
|
||||
@@ -75,20 +97,35 @@ test('kiro start device login registers client, opens auth tab, and completes wi
|
||||
}
|
||||
throw new Error(`Unexpected fetch URL: ${url}`);
|
||||
},
|
||||
getState: async () => ({
|
||||
}),
|
||||
getState: async () => ({}),
|
||||
registerTab: async (source, tabId) => {
|
||||
registerCalls.push({ source, tabId });
|
||||
},
|
||||
KIRO_DEVICE_AUTH_INJECT_FILES: [
|
||||
'shared/source-registry.js',
|
||||
'content/utils.js',
|
||||
'content/kiro-device-auth-page.js',
|
||||
],
|
||||
reuseOrCreateTab: async (source, url) => {
|
||||
reuseCalls.push({ source, url });
|
||||
return 88;
|
||||
},
|
||||
sendToContentScriptResilient: async (source, message, options = {}) => {
|
||||
contentMessages.push({ source, message, options });
|
||||
return {
|
||||
ok: true,
|
||||
state: 'email_entry',
|
||||
url: 'https://device.example.com/complete',
|
||||
};
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabStableComplete: async (tabId, options = {}) => {
|
||||
stableWaitCalls.push({ tabId, options });
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executeKiroStartDeviceLogin({
|
||||
@@ -111,6 +148,23 @@ test('kiro start device login registers client, opens auth tab, and completes wi
|
||||
source: 'kiro-device-auth',
|
||||
tabId: 88,
|
||||
}]);
|
||||
assert.deepEqual(tabUpdates, [{
|
||||
tabId: 88,
|
||||
update: { active: true },
|
||||
}]);
|
||||
assert.deepEqual(stableWaitCalls, [{
|
||||
tabId: 88,
|
||||
options: {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 2500,
|
||||
initialDelayMs: 300,
|
||||
},
|
||||
}]);
|
||||
assert.equal(contentReadyCalls.length, 1);
|
||||
assert.equal(contentMessages.length, 1);
|
||||
assert.equal(contentMessages[0].message.type, 'ENSURE_KIRO_PAGE_STATE');
|
||||
assert.deepEqual(contentMessages[0].message.payload.targetStates, ['email_entry']);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroClientId, 'client-001');
|
||||
@@ -122,6 +176,8 @@ test('kiro start device login registers client, opens auth tab, and completes wi
|
||||
assert.equal(finalState.kiroAuthIntervalSeconds, 7);
|
||||
assert.equal(finalState.kiroAuthStatus, 'waiting_user');
|
||||
assert.equal(finalState.kiroUploadStatus, 'waiting_login');
|
||||
assert.equal(finalState.kiroFullName, '');
|
||||
assert.equal(finalState.kiroVerificationRequestedAt, 0);
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-start-device-login');
|
||||
@@ -129,19 +185,428 @@ test('kiro start device login registers client, opens auth tab, and completes wi
|
||||
assert.equal(completeCalls[0].payload.kiroLoginUrl, 'https://device.example.com/complete');
|
||||
});
|
||||
|
||||
test('kiro await device login polls until refresh token is captured', async () => {
|
||||
test('kiro submit email resolves the signup email, reactivates the auth tab, and waits for the name page', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const stateUpdates = [];
|
||||
const completeCalls = [];
|
||||
const resolvedEmails = [];
|
||||
const contentMessages = [];
|
||||
const stableWaitCalls = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
|
||||
let ensureCallIndex = 0;
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getState: async () => ({
|
||||
kiroAuthTabId: 88,
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
email: '',
|
||||
mailProvider: '163',
|
||||
}),
|
||||
isTabAlive: async (source) => source === 'kiro-device-auth',
|
||||
KIRO_DEVICE_AUTH_INJECT_FILES: [
|
||||
'shared/source-registry.js',
|
||||
'content/utils.js',
|
||||
'content/kiro-device-auth-page.js',
|
||||
],
|
||||
resolveSignupEmailForFlow: async (state, options = {}) => {
|
||||
resolvedEmails.push({ state, options });
|
||||
return 'user@example.com';
|
||||
},
|
||||
sendToContentScriptResilient: async (source, message, options = {}) => {
|
||||
contentMessages.push({ source, message, options });
|
||||
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
|
||||
ensureCallIndex += 1;
|
||||
return {
|
||||
ok: true,
|
||||
state: ensureCallIndex === 1 ? 'email_entry' : 'name_entry',
|
||||
url: ensureCallIndex === 1
|
||||
? 'https://device.example.com/complete'
|
||||
: 'https://device.example.com/name',
|
||||
};
|
||||
}
|
||||
if (message.type === 'EXECUTE_NODE') {
|
||||
return {
|
||||
ok: true,
|
||||
submitted: true,
|
||||
state: 'email_submitted',
|
||||
url: 'https://device.example.com/complete',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabStableComplete: async (tabId, options = {}) => {
|
||||
stableWaitCalls.push({ tabId, options });
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executeKiroSubmitEmail({
|
||||
nodeId: 'kiro-submit-email',
|
||||
kiroAuthTabId: 88,
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
email: '',
|
||||
mailProvider: '163',
|
||||
});
|
||||
|
||||
assert.equal(resolvedEmails.length, 1);
|
||||
assert.equal(resolvedEmails[0].state.nodeId, 'kiro-submit-email');
|
||||
assert.deepEqual(resolvedEmails[0].options, {
|
||||
preserveAccountIdentity: true,
|
||||
});
|
||||
assert.deepEqual(tabUpdates, [
|
||||
{ tabId: 88, update: { active: true } },
|
||||
{ tabId: 88, update: { active: true } },
|
||||
]);
|
||||
assert.deepEqual(stableWaitCalls, [
|
||||
{
|
||||
tabId: 88,
|
||||
options: {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 2500,
|
||||
initialDelayMs: 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
tabId: 88,
|
||||
options: {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 1500,
|
||||
initialDelayMs: 150,
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.equal(contentMessages.length, 3);
|
||||
assert.deepEqual(contentMessages[0].message.payload.targetStates, ['email_entry']);
|
||||
assert.equal(contentMessages[1].message.nodeId, 'kiro-submit-email');
|
||||
assert.deepEqual(contentMessages[1].message.payload, { email: 'user@example.com' });
|
||||
assert.deepEqual(contentMessages[2].message.payload.targetStates, ['name_entry']);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroAuthorizedEmail, 'user@example.com');
|
||||
assert.equal(finalState.kiroAuthError, '');
|
||||
assert.equal(finalState.kiroAuthStatus, 'waiting_user');
|
||||
assert.equal(finalState.kiroUploadError, '');
|
||||
assert.equal(finalState.kiroUploadStatus, 'waiting_login');
|
||||
assert.equal(finalState.kiroFullName, '');
|
||||
assert.equal(finalState.kiroVerificationRequestedAt, 0);
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-submit-email');
|
||||
assert.equal(completeCalls[0].payload.email, 'user@example.com');
|
||||
assert.equal(completeCalls[0].payload.accountIdentifierType, 'email');
|
||||
assert.equal(completeCalls[0].payload.accountIdentifier, 'user@example.com');
|
||||
assert.equal(completeCalls[0].payload.kiroNextState, 'name_entry');
|
||||
assert.equal(completeCalls[0].payload.kiroNextUrl, 'https://device.example.com/name');
|
||||
});
|
||||
|
||||
test('kiro submit name generates a full name and waits for the otp page', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const stateUpdates = [];
|
||||
const completeCalls = [];
|
||||
const contentMessages = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
|
||||
let ensureCallIndex = 0;
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
|
||||
getState: async () => ({
|
||||
kiroAuthTabId: 88,
|
||||
kiroAuthorizedEmail: 'user@example.com',
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
}),
|
||||
isTabAlive: async (source) => source === 'kiro-device-auth',
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
contentMessages.push(message);
|
||||
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
|
||||
ensureCallIndex += 1;
|
||||
return {
|
||||
ok: true,
|
||||
state: ensureCallIndex === 1 ? 'name_entry' : 'otp_page',
|
||||
url: ensureCallIndex === 1
|
||||
? 'https://device.example.com/name'
|
||||
: 'https://device.example.com/verify',
|
||||
};
|
||||
}
|
||||
if (message.type === 'EXECUTE_NODE') {
|
||||
return {
|
||||
ok: true,
|
||||
submitted: true,
|
||||
state: 'name_submitted',
|
||||
url: 'https://device.example.com/name',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroSubmitName({
|
||||
nodeId: 'kiro-submit-name',
|
||||
kiroAuthTabId: 88,
|
||||
kiroAuthorizedEmail: 'user@example.com',
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
});
|
||||
|
||||
assert.deepEqual(tabUpdates, [{
|
||||
tabId: 88,
|
||||
update: { active: true },
|
||||
}]);
|
||||
assert.equal(contentMessages.length, 3);
|
||||
assert.deepEqual(contentMessages[0].payload.targetStates, ['name_entry']);
|
||||
assert.equal(contentMessages[1].nodeId, 'kiro-submit-name');
|
||||
assert.deepEqual(contentMessages[1].payload, { fullName: 'Ada Lovelace' });
|
||||
assert.deepEqual(contentMessages[2].payload.targetStates, ['otp_page']);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroFullName, 'Ada Lovelace');
|
||||
assert.equal(finalState.kiroAuthError, '');
|
||||
assert.equal(finalState.kiroUploadError, '');
|
||||
assert.equal(typeof finalState.kiroVerificationRequestedAt, 'number');
|
||||
assert.equal(finalState.kiroVerificationRequestedAt > 0, true);
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-submit-name');
|
||||
assert.equal(completeCalls[0].payload.kiroFullName, 'Ada Lovelace');
|
||||
assert.equal(completeCalls[0].payload.kiroNextState, 'otp_page');
|
||||
});
|
||||
|
||||
test('kiro submit verification code polls mail, returns to the auth tab, and waits for the password page', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const stateUpdates = [];
|
||||
const completeCalls = [];
|
||||
const mailPollCalls = [];
|
||||
const contentMessages = [];
|
||||
const mailOpenCalls = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
|
||||
let ensureCallIndex = 0;
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getMailConfig: () => ({
|
||||
source: 'mail-163',
|
||||
url: 'https://mail.example.com/inbox',
|
||||
label: '163 邮箱',
|
||||
}),
|
||||
getState: async () => ({
|
||||
kiroAuthTabId: 88,
|
||||
kiroAuthorizedEmail: 'user@example.com',
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
kiroVerificationRequestedAt: 1700000000000,
|
||||
mailProvider: '163',
|
||||
}),
|
||||
isTabAlive: async (source) => source === 'kiro-device-auth',
|
||||
reuseOrCreateTab: async (source, url) => {
|
||||
mailOpenCalls.push({ source, url });
|
||||
return 66;
|
||||
},
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
contentMessages.push(message);
|
||||
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
|
||||
ensureCallIndex += 1;
|
||||
return {
|
||||
ok: true,
|
||||
state: ensureCallIndex === 1 ? 'otp_page' : 'password_page',
|
||||
url: ensureCallIndex === 1
|
||||
? 'https://device.example.com/verify'
|
||||
: 'https://device.example.com/password',
|
||||
};
|
||||
}
|
||||
if (message.type === 'EXECUTE_NODE') {
|
||||
return {
|
||||
ok: true,
|
||||
submitted: true,
|
||||
state: 'verification_submitted',
|
||||
url: 'https://device.example.com/verify',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content message: ${message.type}`);
|
||||
},
|
||||
sendToMailContentScriptResilient: async (mail, message, options = {}) => {
|
||||
mailPollCalls.push({ mail, message, options });
|
||||
return {
|
||||
ok: true,
|
||||
code: '654321',
|
||||
emailTimestamp: 1700000005000,
|
||||
mailId: 'mail-1',
|
||||
};
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroSubmitVerificationCode({
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
kiroAuthTabId: 88,
|
||||
kiroAuthorizedEmail: 'user@example.com',
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
kiroVerificationRequestedAt: 1700000000000,
|
||||
mailProvider: '163',
|
||||
});
|
||||
|
||||
assert.deepEqual(mailOpenCalls, [{
|
||||
source: 'mail-163',
|
||||
url: 'https://mail.example.com/inbox',
|
||||
}]);
|
||||
assert.equal(mailPollCalls.length, 1);
|
||||
assert.equal(mailPollCalls[0].message.type, 'POLL_EMAIL');
|
||||
assert.equal(mailPollCalls[0].message.payload.targetEmail, 'user@example.com');
|
||||
assert.equal(mailPollCalls[0].message.payload.filterAfterTimestamp, 1700000000000);
|
||||
assert.deepEqual(tabUpdates, [
|
||||
{ tabId: 88, update: { active: true } },
|
||||
{ tabId: 88, update: { active: true } },
|
||||
]);
|
||||
assert.equal(contentMessages.length, 3);
|
||||
assert.deepEqual(contentMessages[0].payload.targetStates, ['otp_page']);
|
||||
assert.equal(contentMessages[1].nodeId, 'kiro-submit-verification-code');
|
||||
assert.deepEqual(contentMessages[1].payload, { code: '654321' });
|
||||
assert.deepEqual(contentMessages[2].payload.targetStates, ['password_page']);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroAuthError, '');
|
||||
assert.equal(finalState.kiroUploadError, '');
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(completeCalls[0].payload.code, '654321');
|
||||
assert.equal(completeCalls[0].payload.mailId, 'mail-1');
|
||||
assert.equal(completeCalls[0].payload.kiroNextState, 'password_page');
|
||||
});
|
||||
|
||||
test('kiro fill password reuses the shared password state and waits for the page to leave password state', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const stateUpdates = [];
|
||||
const completeCalls = [];
|
||||
const contentMessages = [];
|
||||
const savedPasswords = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getState: async () => ({
|
||||
kiroAuthTabId: 88,
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
customPassword: 'SharedPass123!',
|
||||
}),
|
||||
isTabAlive: async (source) => source === 'kiro-device-auth',
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
contentMessages.push(message);
|
||||
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
|
||||
return {
|
||||
ok: true,
|
||||
state: 'password_page',
|
||||
url: 'https://device.example.com/password',
|
||||
};
|
||||
}
|
||||
if (message.type === 'ENSURE_KIRO_STATE_CHANGE') {
|
||||
return {
|
||||
ok: true,
|
||||
state: 'authorization_page',
|
||||
url: 'https://device.example.com/authorize',
|
||||
};
|
||||
}
|
||||
if (message.type === 'EXECUTE_NODE') {
|
||||
return {
|
||||
ok: true,
|
||||
submitted: true,
|
||||
state: 'password_submitted',
|
||||
url: 'https://device.example.com/password',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content message: ${message.type}`);
|
||||
},
|
||||
setPasswordState: async (password) => {
|
||||
savedPasswords.push(password);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroFillPassword({
|
||||
nodeId: 'kiro-fill-password',
|
||||
kiroAuthTabId: 88,
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
customPassword: 'SharedPass123!',
|
||||
});
|
||||
|
||||
assert.deepEqual(savedPasswords, ['SharedPass123!']);
|
||||
assert.deepEqual(tabUpdates, [{
|
||||
tabId: 88,
|
||||
update: { active: true },
|
||||
}]);
|
||||
assert.equal(contentMessages.length, 3);
|
||||
assert.deepEqual(contentMessages[0].payload.targetStates, ['password_page']);
|
||||
assert.equal(contentMessages[1].nodeId, 'kiro-fill-password');
|
||||
assert.deepEqual(contentMessages[1].payload, { password: 'SharedPass123!' });
|
||||
assert.deepEqual(contentMessages[2].payload.fromStates, ['password_page']);
|
||||
|
||||
const finalState = mergeUpdates(stateUpdates);
|
||||
assert.equal(finalState.kiroAuthError, '');
|
||||
assert.equal(finalState.kiroUploadError, '');
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-fill-password');
|
||||
assert.equal(completeCalls[0].payload.kiroNextState, 'authorization_page');
|
||||
assert.equal(completeCalls[0].payload.kiroNextUrl, 'https://device.example.com/authorize');
|
||||
});
|
||||
|
||||
test('kiro confirm access completes the authorization page and then polls until refresh token is captured', async () => {
|
||||
const api = loadKiroDeviceAuthApi();
|
||||
const fetchCalls = [];
|
||||
const stateUpdates = [];
|
||||
const sleepCalls = [];
|
||||
const completeCalls = [];
|
||||
const contentMessages = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
|
||||
let pollCount = 0;
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
chrome,
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completeCalls.push({ nodeId, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({
|
||||
url,
|
||||
@@ -167,13 +632,35 @@ test('kiro await device login polls until refresh token is captured', async () =
|
||||
});
|
||||
},
|
||||
getState: async () => ({
|
||||
kiroAuthTabId: 88,
|
||||
kiroClientId: 'client-001',
|
||||
kiroClientSecret: 'secret-001',
|
||||
kiroDeviceAuthorizationCode: 'device-code-001',
|
||||
kiroAuthRegion: 'us-east-1',
|
||||
kiroAuthExpiresAt: Date.now() + 60000,
|
||||
kiroAuthIntervalSeconds: 5,
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
}),
|
||||
isTabAlive: async (source) => source === 'kiro-device-auth',
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
contentMessages.push(message);
|
||||
if (message.type === 'ENSURE_KIRO_PAGE_STATE') {
|
||||
return {
|
||||
ok: true,
|
||||
state: 'authorization_page',
|
||||
url: 'https://device.example.com/authorize',
|
||||
};
|
||||
}
|
||||
if (message.type === 'EXECUTE_NODE') {
|
||||
return {
|
||||
ok: true,
|
||||
submitted: true,
|
||||
state: 'success_page',
|
||||
url: 'https://device.example.com/success',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
},
|
||||
@@ -181,18 +668,28 @@ test('kiro await device login polls until refresh token is captured', async () =
|
||||
sleepCalls.push(ms);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeKiroAwaitDeviceLogin({
|
||||
nodeId: 'kiro-await-device-login',
|
||||
await executor.executeKiroConfirmAccess({
|
||||
nodeId: 'kiro-confirm-access',
|
||||
kiroAuthTabId: 88,
|
||||
kiroClientId: 'client-001',
|
||||
kiroClientSecret: 'secret-001',
|
||||
kiroDeviceAuthorizationCode: 'device-code-001',
|
||||
kiroAuthRegion: 'us-east-1',
|
||||
kiroAuthExpiresAt: Date.now() + 60000,
|
||||
kiroAuthIntervalSeconds: 5,
|
||||
kiroLoginUrl: 'https://device.example.com/complete',
|
||||
});
|
||||
|
||||
assert.deepEqual(tabUpdates, [{
|
||||
tabId: 88,
|
||||
update: { active: true },
|
||||
}]);
|
||||
assert.equal(contentMessages.length, 2);
|
||||
assert.deepEqual(contentMessages[0].payload.targetStates, ['authorization_page', 'success_page']);
|
||||
assert.equal(contentMessages[1].nodeId, 'kiro-confirm-access');
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://oidc.us-east-1.amazonaws.com/token');
|
||||
assert.deepEqual(fetchCalls[0].body, {
|
||||
@@ -210,8 +707,10 @@ test('kiro await device login polls until refresh token is captured', async () =
|
||||
assert.equal(finalState.kiroUploadStatus, 'ready_to_upload');
|
||||
|
||||
assert.equal(completeCalls.length, 1);
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-await-device-login');
|
||||
assert.equal(completeCalls[0].nodeId, 'kiro-confirm-access');
|
||||
assert.equal(completeCalls[0].payload.kiroRefreshToken, 'refresh-001');
|
||||
assert.equal(completeCalls[0].payload.kiroNextState, 'success_page');
|
||||
assert.equal(completeCalls[0].payload.kiroNextUrl, 'https://device.example.com/success');
|
||||
});
|
||||
|
||||
test('kiro upload credential checks connection and uploads builder id credential to kiro.rs', async () => {
|
||||
|
||||
@@ -205,7 +205,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
|
||||
options: {
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,7 +293,7 @@ const chrome = {
|
||||
options: {
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -317,7 +317,7 @@ const chrome = {
|
||||
assert.deepEqual(state.stepExecutionRangeByFlow.kiro, {
|
||||
enabled: true,
|
||||
fromStep: 1,
|
||||
toStep: 3,
|
||||
toStep: 7,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -379,7 +379,7 @@ function getPersistedWrites() {
|
||||
options: {
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
|
||||
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 7 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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']
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ function updatePhoneVerificationSettingsUI() {
|
||||
}
|
||||
function resolveCurrentSidepanelCapabilities() {
|
||||
return {
|
||||
visibleGroupIds: ['openai-account', 'openai-plus', 'openai-phone'],
|
||||
visibleGroupIds: ['service-account', 'openai-plus', 'openai-phone'],
|
||||
effectivePanelMode: 'cpa',
|
||||
panelMode: 'cpa',
|
||||
effectiveSourceId: 'cpa',
|
||||
|
||||
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
||||
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
|
||||
|
||||
test('sidepanel exposes SUB2API account priority below group setting', () => {
|
||||
assert.match(html, /id="row-sub2api-account-priority"/);
|
||||
@@ -39,7 +40,8 @@ test('sidepanel persists and locks SUB2API account priority setting', () => {
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.value = String\(normalizeSub2ApiAccountPriorityValue\(state\?\.sub2apiAccountPriority\)\);/
|
||||
);
|
||||
assert.match(source, /rowSub2ApiAccountPriority\.style\.display = useSub2Api \? '' : 'none';/);
|
||||
assert.match(source, /applyFlowSettingsGroupVisibility\(visibleGroupIds\);/);
|
||||
assert.match(flowRegistrySource, /'openai-source-sub2api': \{[\s\S]*'row-sub2api-account-priority'/);
|
||||
assert.match(source, /inputSub2ApiAccountPriority\.disabled = locked;/);
|
||||
assert.match(
|
||||
source,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 登录与授权');
|
||||
|
||||
Reference in New Issue
Block a user