1337 lines
50 KiB
JavaScript
1337 lines
50 KiB
JavaScript
(function attachBackgroundKiroDesktopAuthorizeRunner(root, factory) {
|
||
root.MultiPageBackgroundKiroDesktopAuthorizeRunner = factory(root);
|
||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundKiroDesktopAuthorizeRunnerModule(root) {
|
||
const kiroStateApi = root.MultiPageBackgroundKiroState || null;
|
||
const desktopClientApi = root.MultiPageBackgroundKiroDesktopClient || null;
|
||
const kiroTimeoutApi = root.MultiPageKiroTimeouts || null;
|
||
const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || desktopClientApi?.DEFAULT_REGION || 'us-east-1';
|
||
const DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS = kiroTimeoutApi?.DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS || (3 * 60 * 1000);
|
||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||
const KIRO_REGISTER_PAGE_SOURCE_ID = 'kiro-register-page';
|
||
const KIRO_DESKTOP_SOURCE_ID = 'kiro-desktop-authorize';
|
||
const KIRO_WEB_ACCOUNT_URL = 'https://app.kiro.dev/settings/account';
|
||
const KIRO_WEB_TAB_URL_PATTERNS = Object.freeze([
|
||
'https://app.kiro.dev/*',
|
||
'https://kiro.dev/*',
|
||
]);
|
||
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 isPlainObject(value) {
|
||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||
}
|
||
|
||
function cloneValue(value) {
|
||
if (Array.isArray(value)) {
|
||
return value.map((entry) => cloneValue(entry));
|
||
}
|
||
if (isPlainObject(value)) {
|
||
return Object.fromEntries(
|
||
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
|
||
);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function deepMerge(baseValue, patchValue) {
|
||
if (Array.isArray(patchValue)) {
|
||
return patchValue.map((entry) => cloneValue(entry));
|
||
}
|
||
if (!isPlainObject(patchValue)) {
|
||
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
|
||
}
|
||
|
||
const baseObject = isPlainObject(baseValue) ? baseValue : {};
|
||
const next = {
|
||
...cloneValue(baseObject),
|
||
};
|
||
Object.entries(patchValue).forEach(([key, value]) => {
|
||
next[key] = deepMerge(baseObject[key], value);
|
||
});
|
||
return next;
|
||
}
|
||
|
||
function cleanString(value = '') {
|
||
return String(value ?? '').trim();
|
||
}
|
||
|
||
function readKiroRuntime(state = {}) {
|
||
if (typeof kiroStateApi?.ensureRuntimeState === 'function') {
|
||
return kiroStateApi.ensureRuntimeState(state);
|
||
}
|
||
return deepMerge(
|
||
typeof kiroStateApi?.buildDefaultRuntimeState === 'function'
|
||
? kiroStateApi.buildDefaultRuntimeState()
|
||
: {},
|
||
isPlainObject(state?.runtimeState?.flowState?.kiro)
|
||
? state.runtimeState.flowState.kiro
|
||
: (isPlainObject(state?.flowState?.kiro) ? state.flowState.kiro : {})
|
||
);
|
||
}
|
||
|
||
function buildCanonicalRuntimePatch(currentState = {}, nextRuntimeState = {}) {
|
||
if (typeof kiroStateApi?.buildRuntimeStatePatch === 'function') {
|
||
return kiroStateApi.buildRuntimeStatePatch(currentState, nextRuntimeState);
|
||
}
|
||
const baseRuntimeState = isPlainObject(currentState?.runtimeState)
|
||
? cloneValue(currentState.runtimeState)
|
||
: {};
|
||
const baseFlowState = isPlainObject(baseRuntimeState.flowState)
|
||
? cloneValue(baseRuntimeState.flowState)
|
||
: {};
|
||
return {
|
||
runtimeState: {
|
||
...baseRuntimeState,
|
||
flowState: {
|
||
...baseFlowState,
|
||
kiro: deepMerge(readKiroRuntime(currentState), nextRuntimeState),
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function mergeRuntimePatch(currentState = {}, patch = {}) {
|
||
return buildCanonicalRuntimePatch(
|
||
currentState,
|
||
deepMerge(readKiroRuntime(currentState), patch)
|
||
);
|
||
}
|
||
|
||
function normalizePositiveInteger(value, fallback) {
|
||
const numeric = Math.floor(Number(value));
|
||
if (Number.isInteger(numeric) && numeric > 0) {
|
||
return numeric;
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
function normalizeKiroPageLoadTimeoutMs(value, fallback = DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS) {
|
||
if (typeof kiroTimeoutApi?.normalizeKiroPageLoadTimeoutMs === 'function') {
|
||
return kiroTimeoutApi.normalizeKiroPageLoadTimeoutMs(value, fallback);
|
||
}
|
||
return normalizePositiveInteger(value, normalizePositiveInteger(fallback, DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS));
|
||
}
|
||
|
||
function createTimeoutBudget(timeoutMs = DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS) {
|
||
const totalTimeoutMs = normalizeKiroPageLoadTimeoutMs(timeoutMs, DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS);
|
||
const startedAt = Date.now();
|
||
return {
|
||
totalTimeoutMs,
|
||
getRemainingMs(minimumMs = 1) {
|
||
const normalizedMinimumMs = normalizePositiveInteger(minimumMs, 1);
|
||
return Math.max(normalizedMinimumMs, totalTimeoutMs - (Date.now() - startedAt));
|
||
},
|
||
};
|
||
}
|
||
|
||
function resolveTimeoutBudget(options = {}, fallbackTimeoutMs = DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS) {
|
||
if (options?.timeoutBudget && typeof options.timeoutBudget.getRemainingMs === 'function') {
|
||
return options.timeoutBudget;
|
||
}
|
||
return createTimeoutBudget(
|
||
options?.pageTimeoutMs
|
||
?? options?.timeoutMs
|
||
?? fallbackTimeoutMs
|
||
);
|
||
}
|
||
|
||
function getErrorMessage(error) {
|
||
return error instanceof Error ? error.message : String(error ?? '未知错误');
|
||
}
|
||
|
||
function isKiroWebUrl(rawUrl = '') {
|
||
const normalizedUrl = cleanString(rawUrl);
|
||
if (!normalizedUrl) {
|
||
return false;
|
||
}
|
||
try {
|
||
const parsed = new URL(normalizedUrl);
|
||
const hostname = parsed.hostname.toLowerCase();
|
||
return hostname === 'app.kiro.dev' || hostname === 'kiro.dev';
|
||
} catch (_error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function parseDesktopCallbackUrl(rawUrl, expectedState = '', expectedPort = 0) {
|
||
const normalizedUrl = cleanString(rawUrl);
|
||
if (!normalizedUrl) {
|
||
return null;
|
||
}
|
||
let parsed = null;
|
||
try {
|
||
parsed = new URL(normalizedUrl);
|
||
} catch (_error) {
|
||
return null;
|
||
}
|
||
if (!/^https?:$/.test(parsed.protocol)) {
|
||
return null;
|
||
}
|
||
if (!['127.0.0.1', 'localhost'].includes(parsed.hostname)) {
|
||
return null;
|
||
}
|
||
if (expectedPort && Number(parsed.port || 0) !== Number(expectedPort)) {
|
||
return null;
|
||
}
|
||
if (parsed.pathname !== '/oauth/callback') {
|
||
return null;
|
||
}
|
||
const stateValue = cleanString(parsed.searchParams.get('state'));
|
||
if (expectedState && stateValue && stateValue !== cleanString(expectedState)) {
|
||
return {
|
||
url: normalizedUrl,
|
||
state: stateValue,
|
||
error: `回调 state 不匹配:expected=${cleanString(expectedState)} actual=${stateValue}`,
|
||
};
|
||
}
|
||
const error = cleanString(parsed.searchParams.get('error_description') || parsed.searchParams.get('error'));
|
||
const code = cleanString(parsed.searchParams.get('code'));
|
||
if (error) {
|
||
return {
|
||
url: normalizedUrl,
|
||
state: stateValue,
|
||
error,
|
||
};
|
||
}
|
||
if (!code) {
|
||
return null;
|
||
}
|
||
return {
|
||
url: normalizedUrl,
|
||
state: stateValue,
|
||
code,
|
||
};
|
||
}
|
||
|
||
function createDesktopCallbackTracker(chromeApi) {
|
||
const pendingSessions = new Map();
|
||
const resolvedSessions = new Map();
|
||
let listenersInstalled = false;
|
||
|
||
function installListeners() {
|
||
if (listenersInstalled || !chromeApi) {
|
||
return;
|
||
}
|
||
listenersInstalled = true;
|
||
|
||
const handleNavigation = (details = {}) => {
|
||
const url = cleanString(details.url);
|
||
if (!url) {
|
||
return;
|
||
}
|
||
for (const [stateKey, session] of pendingSessions.entries()) {
|
||
const parsed = parseDesktopCallbackUrl(url, session.expectedState, session.redirectPort);
|
||
if (!parsed) {
|
||
continue;
|
||
}
|
||
const result = {
|
||
...parsed,
|
||
tabId: Number.isInteger(details.tabId) ? details.tabId : (Number.isInteger(session.tabId) ? session.tabId : null),
|
||
};
|
||
resolvedSessions.set(stateKey, result);
|
||
const waiters = Array.isArray(session.waiters) ? session.waiters.splice(0, session.waiters.length) : [];
|
||
pendingSessions.set(stateKey, {
|
||
...session,
|
||
resolved: result,
|
||
waiters: [],
|
||
});
|
||
waiters.forEach(({ resolve }) => resolve(result));
|
||
const targetTabId = Number.isInteger(result.tabId) ? result.tabId : (Number.isInteger(session.tabId) ? session.tabId : null);
|
||
if (Number.isInteger(targetTabId) && chromeApi.tabs?.remove) {
|
||
chromeApi.tabs.remove(targetTabId).catch(() => {});
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
chromeApi.webNavigation?.onBeforeNavigate?.addListener?.(handleNavigation);
|
||
chromeApi.webNavigation?.onCommitted?.addListener?.(handleNavigation);
|
||
chromeApi.webRequest?.onBeforeRequest?.addListener?.(
|
||
handleNavigation,
|
||
{ urls: ['http://127.0.0.1/*', 'http://localhost/*'] }
|
||
);
|
||
}
|
||
|
||
function registerPending(params = {}) {
|
||
installListeners();
|
||
const expectedState = cleanString(params.expectedState);
|
||
if (!expectedState) {
|
||
throw new Error('缺少桌面授权 state,无法注册回调监听。');
|
||
}
|
||
const existingResolved = resolvedSessions.get(expectedState);
|
||
const existingPending = pendingSessions.get(expectedState);
|
||
pendingSessions.set(expectedState, {
|
||
expectedState,
|
||
redirectPort: Number(params.redirectPort || 0) || 0,
|
||
tabId: Number.isInteger(params.tabId) ? params.tabId : (existingPending?.tabId ?? null),
|
||
waiters: existingPending?.waiters || [],
|
||
resolved: existingResolved || existingPending?.resolved || null,
|
||
});
|
||
return existingResolved || null;
|
||
}
|
||
|
||
function consumeResolved(expectedState = '') {
|
||
const stateKey = cleanString(expectedState);
|
||
if (!stateKey || !resolvedSessions.has(stateKey)) {
|
||
return null;
|
||
}
|
||
const result = resolvedSessions.get(stateKey) || null;
|
||
resolvedSessions.delete(stateKey);
|
||
pendingSessions.delete(stateKey);
|
||
return result;
|
||
}
|
||
|
||
function waitForResolved(expectedState = '', timeoutMs = DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS) {
|
||
const stateKey = cleanString(expectedState);
|
||
const immediate = consumeResolved(stateKey);
|
||
if (immediate) {
|
||
return Promise.resolve(immediate);
|
||
}
|
||
const session = pendingSessions.get(stateKey);
|
||
if (!session) {
|
||
return Promise.reject(new Error(`未注册桌面授权回调监听:${stateKey}`));
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
const timer = setTimeout(() => {
|
||
const nextSession = pendingSessions.get(stateKey);
|
||
if (nextSession) {
|
||
nextSession.waiters = (nextSession.waiters || []).filter((entry) => entry.reject !== reject);
|
||
pendingSessions.set(stateKey, nextSession);
|
||
}
|
||
reject(new Error('等待桌面授权回调超时。'));
|
||
}, Math.max(1000, Math.floor(Number(timeoutMs) || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS)));
|
||
session.waiters.push({
|
||
resolve: (result) => {
|
||
clearTimeout(timer);
|
||
resolve(result);
|
||
},
|
||
reject: (error) => {
|
||
clearTimeout(timer);
|
||
reject(error);
|
||
},
|
||
});
|
||
pendingSessions.set(stateKey, session);
|
||
});
|
||
}
|
||
|
||
function clear(expectedState = '') {
|
||
const stateKey = cleanString(expectedState);
|
||
if (!stateKey) {
|
||
return;
|
||
}
|
||
const session = pendingSessions.get(stateKey);
|
||
if (session && Array.isArray(session.waiters)) {
|
||
session.waiters.forEach(({ reject }) => reject(new Error('桌面授权回调监听已清理。')));
|
||
}
|
||
pendingSessions.delete(stateKey);
|
||
resolvedSessions.delete(stateKey);
|
||
}
|
||
|
||
return {
|
||
clear,
|
||
consumeResolved,
|
||
registerPending,
|
||
waitForResolved,
|
||
};
|
||
}
|
||
|
||
function createKiroDesktopAuthorizeRunner(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,
|
||
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,
|
||
maybeSubmitFlowContribution = async () => null,
|
||
KIRO_REGISTER_INJECT_FILES = null,
|
||
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
|
||
pollCloudflareTempEmailVerificationCode = null,
|
||
pollCloudMailVerificationCode = null,
|
||
pollHotmailVerificationCode = null,
|
||
pollLuckmailVerificationCode = null,
|
||
pollYydsMailVerificationCode = null,
|
||
registerTab = async () => {},
|
||
reuseOrCreateTab = async () => null,
|
||
sendToContentScriptResilient = null,
|
||
sendToMailContentScriptResilient = null,
|
||
setState = async () => {},
|
||
sleepWithStop = async (ms) => {
|
||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||
},
|
||
throwIfStopped = () => {},
|
||
waitForTabStableComplete = null,
|
||
} = deps;
|
||
|
||
if (typeof completeNodeFromBackground !== 'function') {
|
||
throw new Error('Kiro desktop authorize runner requires completeNodeFromBackground.');
|
||
}
|
||
if (!desktopClientApi) {
|
||
throw new Error('Kiro desktop authorize runner requires desktop client module.');
|
||
}
|
||
if (typeof fetchImpl !== 'function') {
|
||
throw new Error('Kiro desktop authorize runner requires fetch support.');
|
||
}
|
||
|
||
const callbackTracker = createDesktopCallbackTracker(chrome);
|
||
|
||
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 = {}) {
|
||
if (state && typeof state === 'object' && !Array.isArray(state) && Object.keys(state).length) {
|
||
return state;
|
||
}
|
||
return getState();
|
||
}
|
||
|
||
async function applyRuntimeState(currentState = {}, patch = {}, extraState = {}) {
|
||
const runtimePatch = mergeRuntimePatch(currentState, patch);
|
||
const nextPatch = {
|
||
...runtimePatch,
|
||
...extraState,
|
||
};
|
||
await setState(nextPatch);
|
||
return nextPatch;
|
||
}
|
||
|
||
async function persistFailure(currentState = {}, message = '') {
|
||
await setState(mergeRuntimePatch(currentState, {
|
||
session: {
|
||
lastError: message,
|
||
},
|
||
desktopAuth: {
|
||
status: 'error',
|
||
},
|
||
}));
|
||
}
|
||
|
||
function isMissingTabError(error) {
|
||
return /No tab with id/i.test(getErrorMessage(error));
|
||
}
|
||
|
||
async function finalizeDesktopAuthorizeCallback(currentState = {}, runtimeState = {}, resolvedCallback = {}, nodeId = '') {
|
||
if (resolvedCallback?.error) {
|
||
throw new Error(`桌面授权回调失败:${resolvedCallback.error}`);
|
||
}
|
||
|
||
const authorizationCode = cleanString(resolvedCallback?.code);
|
||
if (!authorizationCode) {
|
||
throw new Error('桌面授权回调缺少 authorization code。');
|
||
}
|
||
|
||
const tokenResult = await desktopClientApi.exchangeDesktopAuthorizationCode({
|
||
region: runtimeState.desktopAuth?.region || DEFAULT_REGION,
|
||
clientId: runtimeState.desktopAuth?.clientId,
|
||
clientSecret: runtimeState.desktopAuth?.clientSecret,
|
||
redirectUri: runtimeState.desktopAuth?.redirectUri,
|
||
code: authorizationCode,
|
||
codeVerifier: runtimeState.desktopAuth?.codeVerifier,
|
||
}, fetchImpl);
|
||
const payload = await applyRuntimeState(currentState, {
|
||
session: {
|
||
currentStage: 'upload',
|
||
pageState: 'callback_captured',
|
||
pageUrl: resolvedCallback.url,
|
||
lastError: '',
|
||
},
|
||
desktopAuth: {
|
||
authorizationCode,
|
||
accessToken: tokenResult.accessToken,
|
||
refreshToken: tokenResult.refreshToken,
|
||
status: 'authorized',
|
||
authorizedAt: Date.now(),
|
||
},
|
||
upload: {
|
||
status: 'ready_to_upload',
|
||
error: '',
|
||
},
|
||
});
|
||
await log('步骤 8:桌面授权回调已捕获,Token 换取成功。', 'ok', nodeId);
|
||
await completeNodeFromBackground(nodeId, payload);
|
||
return payload;
|
||
}
|
||
|
||
async function ensureDesktopAuthorizeTab(state = {}, options = {}) {
|
||
const runtimeState = readKiroRuntime(state);
|
||
let tabId = Number.isInteger(runtimeState.session?.desktopTabId)
|
||
? runtimeState.session.desktopTabId
|
||
: await getTabId(KIRO_DESKTOP_SOURCE_ID);
|
||
const authorizeUrl = cleanString(runtimeState.desktopAuth?.authorizeUrl);
|
||
|
||
if (Number.isInteger(tabId) && await isTabAlive(KIRO_DESKTOP_SOURCE_ID)) {
|
||
return tabId;
|
||
}
|
||
if (!authorizeUrl) {
|
||
throw new Error(options.missingUrlMessage || '缺少桌面授权地址,请先执行步骤 7。');
|
||
}
|
||
tabId = await reuseOrCreateTab(KIRO_DESKTOP_SOURCE_ID, authorizeUrl);
|
||
if (!Number.isInteger(tabId)) {
|
||
throw new Error(options.openFailedMessage || '无法打开桌面授权页,请重试步骤 7。');
|
||
}
|
||
await registerTab(KIRO_DESKTOP_SOURCE_ID, tabId);
|
||
await setState(mergeRuntimePatch(state, {
|
||
session: {
|
||
desktopTabId: tabId,
|
||
},
|
||
}));
|
||
return tabId;
|
||
}
|
||
|
||
async function activateDesktopAuthorizeTab(state = {}, options = {}) {
|
||
const tabId = await ensureDesktopAuthorizeTab(state, options);
|
||
await activateTab(tabId);
|
||
return tabId;
|
||
}
|
||
|
||
async function reattachDesktopAuthorizePage(tabId, options = {}) {
|
||
if (!Number.isInteger(tabId)) {
|
||
throw new Error('缺少 Kiro 桌面授权页标签页,无法重新连接内容脚本。');
|
||
}
|
||
const timeoutBudget = resolveTimeoutBudget(options);
|
||
if (typeof waitForTabStableComplete === 'function') {
|
||
await waitForTabStableComplete(tabId, {
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 300,
|
||
stableMs: Number(options.stableMs) || 1200,
|
||
initialDelayMs: Number(options.initialDelayMs) || 120,
|
||
});
|
||
}
|
||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||
await ensureContentScriptReadyOnTab(KIRO_DESKTOP_SOURCE_ID, tabId, {
|
||
inject: Array.isArray(KIRO_DESKTOP_AUTHORIZE_INJECT_FILES) ? KIRO_DESKTOP_AUTHORIZE_INJECT_FILES : null,
|
||
injectSource: KIRO_DESKTOP_SOURCE_ID,
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 800,
|
||
logMessage: options.injectLogMessage || 'Kiro 桌面授权页已跳转,正在重新连接内容脚本...',
|
||
});
|
||
}
|
||
}
|
||
|
||
function buildDesktopRetryRecovery(tabId, options = {}) {
|
||
return async (_error, context = {}) => {
|
||
const remainingTimeoutMs = normalizeKiroPageLoadTimeoutMs(
|
||
options?.timeoutBudget?.getRemainingMs?.(1000)
|
||
?? context?.remainingTimeoutMs,
|
||
DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS
|
||
);
|
||
await reattachDesktopAuthorizePage(tabId, {
|
||
timeoutMs: remainingTimeoutMs,
|
||
timeoutBudget: createTimeoutBudget(remainingTimeoutMs),
|
||
stableMs: Number(options.recoveryStableMs) || Number(options.stableMs) || 1200,
|
||
initialDelayMs: Number(options.recoveryInitialDelayMs) || 120,
|
||
injectLogMessage: options.recoveryInjectLogMessage || options.injectLogMessage || 'Kiro 桌面授权页已跳转,正在重新连接内容脚本...',
|
||
});
|
||
};
|
||
}
|
||
|
||
async function getDesktopAuthorizePageState(tabId, options = {}) {
|
||
if (!Number.isInteger(tabId)) {
|
||
throw new Error('缺少 Kiro 桌面授权页标签页,无法继续执行。');
|
||
}
|
||
const pageLoadTimeoutMs = normalizeKiroPageLoadTimeoutMs(
|
||
options.pageTimeoutMs,
|
||
DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS
|
||
);
|
||
const timeoutBudget = resolveTimeoutBudget(options, pageLoadTimeoutMs);
|
||
if (typeof waitForTabStableComplete === 'function') {
|
||
await waitForTabStableComplete(tabId, {
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 300,
|
||
stableMs: Number(options.stableMs) || 1200,
|
||
initialDelayMs: Number(options.initialDelayMs) || 120,
|
||
});
|
||
}
|
||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||
await ensureContentScriptReadyOnTab(KIRO_DESKTOP_SOURCE_ID, tabId, {
|
||
inject: Array.isArray(KIRO_DESKTOP_AUTHORIZE_INJECT_FILES) ? KIRO_DESKTOP_AUTHORIZE_INJECT_FILES : null,
|
||
injectSource: KIRO_DESKTOP_SOURCE_ID,
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 800,
|
||
logMessage: options.injectLogMessage || 'Kiro 桌面授权页内容脚本未就绪,正在等待页面恢复...',
|
||
});
|
||
}
|
||
const stateWaitTimeoutMs = timeoutBudget.getRemainingMs(1000);
|
||
const result = await sendToContentScriptResilient(KIRO_DESKTOP_SOURCE_ID, {
|
||
type: 'GET_KIRO_DESKTOP_AUTHORIZE_STATE',
|
||
step: options.step || 0,
|
||
source: 'background',
|
||
}, {
|
||
timeoutMs: stateWaitTimeoutMs,
|
||
retryDelayMs: 700,
|
||
onRetryableError: buildDesktopRetryRecovery(tabId, {
|
||
...options,
|
||
timeoutBudget,
|
||
}),
|
||
logMessage: options.readyLogMessage || '正在读取 Kiro 桌面授权页状态...',
|
||
});
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
return result || { state: '', url: '' };
|
||
}
|
||
|
||
async function executeDesktopAction(tabId, action, payload = {}, options = {}) {
|
||
const timeoutBudget = resolveTimeoutBudget(options);
|
||
const result = await sendToContentScriptResilient(KIRO_DESKTOP_SOURCE_ID, {
|
||
type: 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION',
|
||
step: options.step || 0,
|
||
source: 'background',
|
||
payload: {
|
||
action,
|
||
...payload,
|
||
},
|
||
}, {
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 700,
|
||
onRetryableError: buildDesktopRetryRecovery(tabId, {
|
||
...options,
|
||
timeoutBudget,
|
||
}),
|
||
logMessage: options.logMessage || '正在执行 Kiro 桌面授权动作...',
|
||
});
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
return result || { state: '', url: '' };
|
||
}
|
||
|
||
function resolveDesktopLoginPassword(state = {}) {
|
||
const password = String(state?.customPassword || state?.password || '');
|
||
if (!password) {
|
||
throw new Error('缺少已注册账号密码,无法完成桌面授权重登。');
|
||
}
|
||
return password;
|
||
}
|
||
|
||
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,
|
||
});
|
||
}
|
||
|
||
async function collectKiroWebSessionTabs(currentState = {}) {
|
||
const runtimeState = readKiroRuntime(currentState);
|
||
const candidates = [];
|
||
const seen = new Set();
|
||
const addTab = (tab) => {
|
||
const tabId = Number(tab?.id);
|
||
if (!Number.isInteger(tabId) || seen.has(tabId) || !isKiroWebUrl(tab?.url)) {
|
||
return;
|
||
}
|
||
seen.add(tabId);
|
||
candidates.push(tab);
|
||
};
|
||
|
||
const registeredTabId = runtimeState.session?.registerTabId;
|
||
if (Number.isInteger(registeredTabId) && chrome?.tabs?.get) {
|
||
const tab = await chrome.tabs.get(registeredTabId).catch(() => null);
|
||
addTab(tab);
|
||
}
|
||
|
||
if (chrome?.tabs?.query) {
|
||
const queryKiroTabs = async (queryInfo) => {
|
||
const tabs = await chrome.tabs.query(queryInfo).catch(() => []);
|
||
for (const tab of tabs || []) {
|
||
addTab(tab);
|
||
}
|
||
};
|
||
|
||
await queryKiroTabs({ url: KIRO_WEB_TAB_URL_PATTERNS });
|
||
await queryKiroTabs({ active: true, currentWindow: true });
|
||
}
|
||
|
||
return candidates;
|
||
}
|
||
|
||
async function openKiroWebAccountSessionTab() {
|
||
let tabId = null;
|
||
let tabUrl = KIRO_WEB_ACCOUNT_URL;
|
||
if (chrome?.tabs?.create) {
|
||
const tab = await chrome.tabs.create({
|
||
url: KIRO_WEB_ACCOUNT_URL,
|
||
active: true,
|
||
});
|
||
tabId = Number(tab?.id);
|
||
tabUrl = cleanString(tab?.url || KIRO_WEB_ACCOUNT_URL);
|
||
} else {
|
||
tabId = await reuseOrCreateTab(KIRO_REGISTER_PAGE_SOURCE_ID, KIRO_WEB_ACCOUNT_URL, {
|
||
inject: Array.isArray(KIRO_REGISTER_INJECT_FILES) ? KIRO_REGISTER_INJECT_FILES : null,
|
||
injectSource: KIRO_REGISTER_PAGE_SOURCE_ID,
|
||
});
|
||
}
|
||
if (!Number.isInteger(tabId)) {
|
||
throw new Error('无法打开 Kiro 账号页,请手动打开 app.kiro.dev/settings/account 后重试步骤 7。');
|
||
}
|
||
await registerTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId);
|
||
return {
|
||
id: tabId,
|
||
url: tabUrl || KIRO_WEB_ACCOUNT_URL,
|
||
};
|
||
}
|
||
|
||
async function readKiroWebSessionStateFromTab(tabId, options = {}) {
|
||
const timeoutBudget = resolveTimeoutBudget(options);
|
||
if (typeof waitForTabStableComplete === 'function') {
|
||
await waitForTabStableComplete(tabId, {
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 300,
|
||
stableMs: Number(options.stableMs) || 1500,
|
||
initialDelayMs: Number(options.initialDelayMs) || 150,
|
||
});
|
||
}
|
||
if (typeof ensureContentScriptReadyOnTab === 'function') {
|
||
await ensureContentScriptReadyOnTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId, {
|
||
inject: Array.isArray(KIRO_REGISTER_INJECT_FILES) ? KIRO_REGISTER_INJECT_FILES : null,
|
||
injectSource: KIRO_REGISTER_PAGE_SOURCE_ID,
|
||
timeoutMs: timeoutBudget.getRemainingMs(1000),
|
||
retryDelayMs: 800,
|
||
logMessage: options.injectLogMessage || '步骤 7:正在连接已登录的 Kiro Web 页面...',
|
||
});
|
||
}
|
||
if (typeof sendToContentScriptResilient !== 'function') {
|
||
return null;
|
||
}
|
||
const stateWaitTimeoutMs = timeoutBudget.getRemainingMs(1000);
|
||
const result = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, {
|
||
type: 'GET_KIRO_REGISTER_PAGE_STATE',
|
||
step: 7,
|
||
source: 'background',
|
||
}, {
|
||
timeoutMs: stateWaitTimeoutMs,
|
||
retryDelayMs: 700,
|
||
responseTimeoutMs: Math.min(stateWaitTimeoutMs, 10000),
|
||
logMessage: '步骤 7:正在读取 Kiro Web 登录态...',
|
||
});
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
return result || null;
|
||
}
|
||
|
||
async function restoreKiroWebSessionFromOpenTabs(currentState = {}, nodeId = '') {
|
||
const runtimeState = readKiroRuntime(currentState);
|
||
const existingEmail = cleanString(runtimeState.register?.email || currentState?.email);
|
||
const registerCompleted = cleanString(runtimeState.register?.status) === 'completed';
|
||
const webSignedIn = cleanString(runtimeState.webAuth?.status) === 'signed_in';
|
||
if (existingEmail && registerCompleted && webSignedIn) {
|
||
return {
|
||
currentState,
|
||
runtimeState,
|
||
restored: false,
|
||
};
|
||
}
|
||
|
||
const attemptedTabIds = new Set();
|
||
let detectedSignedInWithoutEmail = false;
|
||
let lastRecoveryError = '';
|
||
const tryRestoreFromTab = async (tab) => {
|
||
const tabId = Number(tab?.id);
|
||
if (!Number.isInteger(tabId) || attemptedTabIds.has(tabId)) {
|
||
return null;
|
||
}
|
||
attemptedTabIds.add(tabId);
|
||
try {
|
||
const pageState = await readKiroWebSessionStateFromTab(tabId, {
|
||
timeoutMs: DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
|
||
injectLogMessage: '步骤 7:Kiro Web 页面内容脚本未就绪,正在等待页面恢复...',
|
||
});
|
||
if (pageState?.state !== 'kiro_web_signed_in') {
|
||
return null;
|
||
}
|
||
const detectedEmail = cleanString(pageState.accountEmail || pageState.email || existingEmail);
|
||
if (!detectedEmail) {
|
||
detectedSignedInWithoutEmail = true;
|
||
return null;
|
||
}
|
||
|
||
const restoredAt = Date.now();
|
||
const payload = await applyRuntimeState(currentState, {
|
||
session: {
|
||
currentStage: 'desktop-authorize',
|
||
registerTabId: tab.id,
|
||
pageState: pageState.state || '',
|
||
pageUrl: pageState.url || tab.url || '',
|
||
lastError: '',
|
||
},
|
||
register: {
|
||
email: detectedEmail,
|
||
status: 'completed',
|
||
completedAt: restoredAt,
|
||
},
|
||
webAuth: {
|
||
status: 'signed_in',
|
||
completedAt: restoredAt,
|
||
},
|
||
upload: {
|
||
status: 'waiting_desktop_authorize',
|
||
error: '',
|
||
},
|
||
}, {
|
||
email: detectedEmail,
|
||
accountIdentifierType: 'email',
|
||
accountIdentifier: detectedEmail,
|
||
});
|
||
const nextState = {
|
||
...currentState,
|
||
...payload,
|
||
};
|
||
await log(`步骤 7:检测到已有 Kiro Web 登录态,已恢复账号 ${detectedEmail},继续启动桌面授权。`, 'ok', nodeId);
|
||
return {
|
||
currentState: nextState,
|
||
runtimeState: readKiroRuntime(nextState),
|
||
restored: true,
|
||
};
|
||
} catch (error) {
|
||
lastRecoveryError = getErrorMessage(error);
|
||
console.warn('[MultiPage:kiro-desktop-authorize] restore web session failed', {
|
||
tabId,
|
||
url: tab?.url,
|
||
message: lastRecoveryError,
|
||
});
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const tabs = await collectKiroWebSessionTabs(currentState);
|
||
for (const tab of tabs) {
|
||
const restoredSession = await tryRestoreFromTab(tab);
|
||
if (restoredSession) {
|
||
return restoredSession;
|
||
}
|
||
}
|
||
|
||
await log('步骤 7:未能从已打开页面确认 Kiro Web 登录态,正在打开 Kiro 账号页重新确认...', 'info', nodeId);
|
||
const accountTab = await openKiroWebAccountSessionTab();
|
||
const restoredSession = await tryRestoreFromTab(accountTab);
|
||
if (restoredSession) {
|
||
return restoredSession;
|
||
}
|
||
|
||
if (detectedSignedInWithoutEmail) {
|
||
throw new Error('已检测到 Kiro Web 登录态,但未能识别账号邮箱。请打开 Kiro 账号设置页后重试步骤 7。');
|
||
}
|
||
const detail = lastRecoveryError ? `最后一次检测错误:${lastRecoveryError}` : '';
|
||
throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`);
|
||
}
|
||
|
||
function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
|
||
const runtimeState = readKiroRuntime(state);
|
||
const targetEmail = cleanString(runtimeState.register?.email || 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 pollDesktopOtpCode(step, state = {}, nodeId = '') {
|
||
if (typeof getMailConfig !== 'function') {
|
||
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。');
|
||
}
|
||
const mail = getMailConfig(state);
|
||
if (mail?.error) {
|
||
throw new Error(mail.error);
|
||
}
|
||
|
||
const runtimeState = readKiroRuntime(state);
|
||
const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now());
|
||
const filterAfterTimestamp = mail.provider === '2925'
|
||
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||
: requestedAt;
|
||
const pollPayload = buildDesktopOtpPollPayload(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 邮箱登录状态`,
|
||
});
|
||
}
|
||
|
||
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-complete-desktop-authorize',
|
||
}
|
||
);
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
if (!result?.code) {
|
||
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
async function executeKiroStartDesktopAuthorize(state = {}) {
|
||
const nodeId = String(state?.nodeId || 'kiro-start-desktop-authorize').trim();
|
||
let currentState = await getExecutionState(state);
|
||
try {
|
||
const sessionState = await restoreKiroWebSessionFromOpenTabs(currentState, nodeId);
|
||
currentState = sessionState.currentState;
|
||
const runtimeState = sessionState.runtimeState;
|
||
|
||
const client = await desktopClientApi.registerDesktopClient({
|
||
region: DEFAULT_REGION,
|
||
clientName: 'Kiro IDE',
|
||
}, fetchImpl);
|
||
const pkce = await desktopClientApi.generatePkcePair();
|
||
const stateToken = cleanString(globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
||
const redirectPort = desktopClientApi.chooseRedirectPort();
|
||
const redirectUri = desktopClientApi.buildRedirectUri(redirectPort);
|
||
const authorizeUrl = desktopClientApi.buildAuthorizeUrl({
|
||
region: client.region,
|
||
clientId: client.clientId,
|
||
redirectUri,
|
||
state: stateToken,
|
||
codeChallenge: pkce.codeChallenge,
|
||
});
|
||
|
||
callbackTracker.registerPending({
|
||
expectedState: stateToken,
|
||
redirectPort,
|
||
});
|
||
|
||
const tabId = await reuseOrCreateTab(KIRO_DESKTOP_SOURCE_ID, authorizeUrl);
|
||
if (!Number.isInteger(tabId)) {
|
||
throw new Error('无法打开 Kiro 桌面授权页,请重试步骤 7。');
|
||
}
|
||
await registerTab(KIRO_DESKTOP_SOURCE_ID, tabId);
|
||
callbackTracker.registerPending({
|
||
expectedState: stateToken,
|
||
redirectPort,
|
||
tabId,
|
||
});
|
||
|
||
const payload = await applyRuntimeState(currentState, {
|
||
session: {
|
||
currentStage: 'desktop-authorize',
|
||
desktopTabId: tabId,
|
||
pageState: '',
|
||
pageUrl: authorizeUrl,
|
||
lastError: '',
|
||
lastWarning: '',
|
||
},
|
||
desktopAuth: {
|
||
region: client.region,
|
||
clientId: client.clientId,
|
||
clientSecret: client.clientSecret,
|
||
clientIdHash: client.clientIdHash,
|
||
state: stateToken,
|
||
codeVerifier: pkce.codeVerifier,
|
||
codeChallenge: pkce.codeChallenge,
|
||
redirectUri,
|
||
redirectPort,
|
||
authorizeUrl,
|
||
authorizationCode: '',
|
||
accessToken: '',
|
||
refreshToken: '',
|
||
status: 'waiting_callback',
|
||
authorizedAt: 0,
|
||
otpRequestedAt: 0,
|
||
tokenSource: 'desktop_authorization_code_pkce',
|
||
},
|
||
upload: {
|
||
status: 'waiting_desktop_authorize',
|
||
error: '',
|
||
credentialId: null,
|
||
lastMessage: '',
|
||
lastUploadedAt: 0,
|
||
},
|
||
});
|
||
await activateTab(tabId);
|
||
await log('步骤 7:Kiro 桌面授权页已打开,下一步将继续完成授权并抓取回调。', 'ok', nodeId);
|
||
await completeNodeFromBackground(nodeId, payload);
|
||
} catch (error) {
|
||
const message = getErrorMessage(error);
|
||
await persistFailure(currentState, message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function executeKiroCompleteDesktopAuthorize(state = {}) {
|
||
const nodeId = String(state?.nodeId || 'kiro-complete-desktop-authorize').trim();
|
||
let currentState = await getExecutionState(state);
|
||
let runtimeState = readKiroRuntime(currentState);
|
||
const desktopState = cleanString(runtimeState.desktopAuth?.state);
|
||
try {
|
||
if (!desktopState) {
|
||
throw new Error('缺少桌面授权 state,请先执行步骤 7。');
|
||
}
|
||
if (!cleanString(runtimeState.desktopAuth?.clientId) || !cleanString(runtimeState.desktopAuth?.clientSecret)) {
|
||
throw new Error('缺少桌面授权客户端凭据,请先执行步骤 7。');
|
||
}
|
||
if (!cleanString(runtimeState.desktopAuth?.redirectUri) || !runtimeState.desktopAuth?.redirectPort) {
|
||
throw new Error('缺少桌面授权回调地址,请先执行步骤 7。');
|
||
}
|
||
if (!cleanString(runtimeState.desktopAuth?.codeVerifier)) {
|
||
throw new Error('缺少桌面授权 PKCE verifier,请先执行步骤 7。');
|
||
}
|
||
|
||
callbackTracker.registerPending({
|
||
expectedState: desktopState,
|
||
redirectPort: runtimeState.desktopAuth.redirectPort,
|
||
tabId: runtimeState.session?.desktopTabId,
|
||
});
|
||
|
||
const timeoutBudget = createTimeoutBudget(DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS);
|
||
const deadline = Date.now() + timeoutBudget.totalTimeoutMs;
|
||
let awaitingCallbackAfterConsent = false;
|
||
const updateLoopState = async (patch = {}) => {
|
||
const runtimePatch = mergeRuntimePatch(currentState, patch);
|
||
await setState(runtimePatch);
|
||
currentState = {
|
||
...currentState,
|
||
...runtimePatch,
|
||
};
|
||
runtimeState = readKiroRuntime(currentState);
|
||
return runtimePatch;
|
||
};
|
||
|
||
while (Date.now() < deadline) {
|
||
throwIfStopped();
|
||
|
||
const resolvedCallback = callbackTracker.consumeResolved(desktopState);
|
||
if (resolvedCallback) {
|
||
await finalizeDesktopAuthorizeCallback(currentState, runtimeState, resolvedCallback, nodeId);
|
||
return;
|
||
}
|
||
|
||
if (awaitingCallbackAfterConsent) {
|
||
const waitedCallback = await callbackTracker.waitForResolved(
|
||
desktopState,
|
||
Math.min(timeoutBudget.getRemainingMs(1000), 1500)
|
||
).catch(() => null);
|
||
if (waitedCallback) {
|
||
await finalizeDesktopAuthorizeCallback(currentState, runtimeState, waitedCallback, nodeId);
|
||
return;
|
||
}
|
||
}
|
||
|
||
let tabId = null;
|
||
if (awaitingCallbackAfterConsent) {
|
||
tabId = await getTabId(KIRO_DESKTOP_SOURCE_ID).catch(() => null);
|
||
if (!Number.isInteger(tabId)) {
|
||
await sleepWithStop(1000);
|
||
continue;
|
||
}
|
||
|
||
const trackedTab = await chrome?.tabs?.get?.(tabId).catch(() => null);
|
||
if (!trackedTab) {
|
||
await sleepWithStop(1000);
|
||
continue;
|
||
}
|
||
|
||
const trackedCallback = parseDesktopCallbackUrl(
|
||
trackedTab.url,
|
||
desktopState,
|
||
runtimeState.desktopAuth?.redirectPort
|
||
);
|
||
if (trackedCallback) {
|
||
await finalizeDesktopAuthorizeCallback(currentState, runtimeState, trackedCallback, nodeId);
|
||
return;
|
||
}
|
||
|
||
if (String(trackedTab.status || '') !== 'complete') {
|
||
await sleepWithStop(1000);
|
||
continue;
|
||
}
|
||
} else {
|
||
tabId = await activateDesktopAuthorizeTab(currentState, {
|
||
missingUrlMessage: '缺少桌面授权地址,请先执行步骤 7。',
|
||
openFailedMessage: '无法恢复桌面授权页,请重新执行步骤 7。',
|
||
});
|
||
}
|
||
|
||
let pageState = null;
|
||
try {
|
||
pageState = await getDesktopAuthorizePageState(tabId, {
|
||
step: 8,
|
||
timeoutBudget,
|
||
injectLogMessage: '步骤 8:Kiro 桌面授权页内容脚本未就绪,正在等待页面恢复...',
|
||
readyLogMessage: '步骤 8:正在读取 Kiro 桌面授权页当前状态...',
|
||
});
|
||
} catch (error) {
|
||
if (awaitingCallbackAfterConsent && isMissingTabError(error)) {
|
||
await sleepWithStop(1000);
|
||
continue;
|
||
}
|
||
throw error;
|
||
}
|
||
|
||
await updateLoopState({
|
||
session: {
|
||
pageState: pageState?.state || '',
|
||
pageUrl: pageState?.url || '',
|
||
lastError: '',
|
||
},
|
||
});
|
||
|
||
if (pageState.state === 'relogin_email') {
|
||
const email = cleanString(runtimeState.register?.email || currentState?.email);
|
||
await log(`步骤 8:桌面授权页要求重新输入邮箱,正在填写 ${email}...`, 'info', nodeId);
|
||
await executeDesktopAction(tabId, 'submit-email', { email }, {
|
||
step: 8,
|
||
timeoutBudget,
|
||
logMessage: '步骤 8:正在向桌面授权页提交邮箱...',
|
||
});
|
||
await sleepWithStop(1200);
|
||
continue;
|
||
}
|
||
|
||
if (pageState.state === 'relogin_password') {
|
||
const password = resolveDesktopLoginPassword(currentState);
|
||
await log('步骤 8:桌面授权页要求重新输入密码,正在填写密码...', 'info', nodeId);
|
||
await executeDesktopAction(tabId, 'submit-password', { password }, {
|
||
step: 8,
|
||
timeoutBudget,
|
||
logMessage: '步骤 8:正在向桌面授权页提交密码...',
|
||
});
|
||
await sleepWithStop(1200);
|
||
continue;
|
||
}
|
||
|
||
if (pageState.state === 'otp_page') {
|
||
if (!runtimeState.desktopAuth?.otpRequestedAt) {
|
||
await updateLoopState({
|
||
desktopAuth: {
|
||
otpRequestedAt: Date.now(),
|
||
status: 'waiting_otp',
|
||
},
|
||
});
|
||
}
|
||
const codeResult = await pollDesktopOtpCode(8, currentState, nodeId);
|
||
const code = cleanString(codeResult?.code);
|
||
if (!code) {
|
||
throw new Error('未获取到桌面授权验证码。');
|
||
}
|
||
await log(`步骤 8:已获取桌面授权验证码 ${code},正在提交...`, 'info', nodeId);
|
||
await executeDesktopAction(tabId, 'submit-otp', { code }, {
|
||
step: 8,
|
||
timeoutBudget,
|
||
logMessage: '步骤 8:正在向桌面授权页提交验证码...',
|
||
});
|
||
await sleepWithStop(1200);
|
||
continue;
|
||
}
|
||
|
||
if (pageState.state === 'consent_page') {
|
||
await log('步骤 8:正在确认 Kiro 桌面授权访问...', 'info', nodeId);
|
||
await executeDesktopAction(tabId, 'confirm-consent', {}, {
|
||
step: 8,
|
||
timeoutBudget,
|
||
logMessage: '步骤 8:正在确认桌面授权访问...',
|
||
});
|
||
awaitingCallbackAfterConsent = true;
|
||
await sleepWithStop(1200);
|
||
continue;
|
||
}
|
||
|
||
if (pageState.state === 'callback_page') {
|
||
const parsedCallback = parseDesktopCallbackUrl(pageState.url, desktopState, runtimeState.desktopAuth?.redirectPort);
|
||
if (parsedCallback) {
|
||
await finalizeDesktopAuthorizeCallback(currentState, runtimeState, parsedCallback, nodeId);
|
||
return;
|
||
}
|
||
}
|
||
|
||
await sleepWithStop(1000);
|
||
}
|
||
|
||
const lastResult = await callbackTracker.waitForResolved(
|
||
desktopState,
|
||
Math.min(timeoutBudget.getRemainingMs(1000), 2000)
|
||
).catch(() => null);
|
||
if (lastResult) {
|
||
await finalizeDesktopAuthorizeCallback(currentState, runtimeState, lastResult, nodeId);
|
||
return;
|
||
}
|
||
|
||
throw new Error('等待桌面授权回调超时。');
|
||
} catch (error) {
|
||
callbackTracker.clear(desktopState);
|
||
const message = getErrorMessage(error);
|
||
await persistFailure(currentState, message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
return {
|
||
executeKiroCompleteDesktopAuthorize,
|
||
executeKiroStartDesktopAuthorize,
|
||
parseDesktopCallbackUrl,
|
||
};
|
||
}
|
||
|
||
return {
|
||
createDesktopCallbackTracker,
|
||
createKiroDesktopAuthorizeRunner,
|
||
parseDesktopCallbackUrl,
|
||
};
|
||
});
|