Fix Kiro existing web session desktop auth

This commit is contained in:
QLHazyCoder
2026-05-19 14:49:46 +08:00
parent 56fcbbdc30
commit 6f88486a7d
6 changed files with 452 additions and 17 deletions
+9 -3
View File
@@ -10014,10 +10014,15 @@ async function skipNode(nodeId) {
await setNodeStatus(normalizedNodeId, 'skipped');
await addLog(`节点 ${normalizedNodeId} 已跳过`, 'warn');
if (normalizedNodeId === 'open-chatgpt') {
const linkedSkipNodeIdsByRoot = {
'open-chatgpt': ['submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success'],
'kiro-open-register-page': ['kiro-submit-email', 'kiro-submit-name', 'kiro-submit-verification-code', 'kiro-submit-password', 'kiro-complete-register-consent'],
};
const linkedSkipNodeIds = linkedSkipNodeIdsByRoot[normalizedNodeId] || [];
if (linkedSkipNodeIds.length) {
const latestState = await getState();
const skippedNodes = [];
for (const linkedNodeId of ['submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success']) {
for (const linkedNodeId of linkedSkipNodeIds) {
const linkedStatus = latestState.nodeStatuses?.[linkedNodeId];
const linkedNodeAllowed = typeof isNodeExecutionAllowedForState === 'function'
? isNodeExecutionAllowedForState(linkedNodeId, latestState)
@@ -10028,7 +10033,7 @@ async function skipNode(nodeId) {
}
}
if (skippedNodes.length) {
await addLog(`节点 open-chatgpt 已跳过,节点 ${skippedNodes.join('、')} 也已同时跳过。`, 'warn');
await addLog(`节点 ${normalizedNodeId} 已跳过,节点 ${skippedNodes.join('、')} 也已同时跳过。`, 'warn');
}
}
@@ -13290,6 +13295,7 @@ const kiroRegisterRunner = self.MultiPageBackgroundKiroRegisterRunner?.createKir
getState,
HOTMAIL_PROVIDER,
isTabAlive,
KIRO_REGISTER_INJECT_FILES,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
+179 -9
View File
@@ -7,7 +7,12 @@
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_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})',
@@ -143,6 +148,20 @@
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) {
@@ -345,6 +364,7 @@
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false,
KIRO_REGISTER_INJECT_FILES = null,
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null,
@@ -650,6 +670,161 @@
});
}
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 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 tabs = await collectKiroWebSessionTabs(currentState);
let detectedSignedInWithoutEmail = false;
for (const tab of tabs) {
try {
const pageState = await readKiroWebSessionStateFromTab(tab.id, {
timeoutMs: DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS,
injectLogMessage: '步骤 7:Kiro Web 页面内容脚本未就绪,正在等待页面恢复...',
});
if (pageState?.state !== 'kiro_web_signed_in') {
continue;
}
const detectedEmail = cleanString(pageState.accountEmail || pageState.email || existingEmail);
if (!detectedEmail) {
detectedSignedInWithoutEmail = true;
continue;
}
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) {
console.warn('[MultiPage:kiro-desktop-authorize] restore web session failed', {
tabId: tab?.id,
url: tab?.url,
message: getErrorMessage(error),
});
}
}
if (detectedSignedInWithoutEmail) {
throw new Error('已检测到 Kiro Web 登录态,但未能识别账号邮箱。请打开 Kiro 账号设置页后重试步骤 7。');
}
throw new Error('Kiro Web 登录态尚未建立。请先完成步骤 6,或打开已登录的 Kiro Web 页面后从步骤 7 继续。');
}
function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
@@ -779,16 +954,11 @@
async function executeKiroStartDesktopAuthorize(state = {}) {
const nodeId = String(state?.nodeId || 'kiro-start-desktop-authorize').trim();
const currentState = await getExecutionState(state);
let currentState = await getExecutionState(state);
try {
const runtimeState = readKiroRuntime(currentState);
if (!cleanString(runtimeState.register?.email || currentState?.email)) {
throw new Error('缺少已注册邮箱,请先完成注册页步骤。');
}
if (cleanString(runtimeState.register?.status) !== 'completed'
|| cleanString(runtimeState.webAuth?.status) !== 'signed_in') {
throw new Error('Kiro Web 登录态尚未建立,请先完成步骤 6。');
}
const sessionState = await restoreKiroWebSessionFromOpenTabs(currentState, nodeId);
currentState = sessionState.currentState;
const runtimeState = sessionState.runtimeState;
const client = await desktopClientApi.registerDesktopClient({
region: DEFAULT_REGION,
+76
View File
@@ -9,6 +9,13 @@ 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_CLOUDFRONT_403_TEXT_PATTERN = /403 error|the request could not be satisfied|generated by cloudfront/i;
const KIRO_AWS_REQUEST_ERROR_TEXT_PATTERN = /sorry,\s*there was an error processing your request\.?\s*please try again\.?|抱歉,处理您的请求时出错。请重试。?/i;
const KIRO_EMAIL_TEXT_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
const KIRO_NON_ACCOUNT_EMAIL_DOMAINS = new Set([
'amazon.com',
'aws.amazon.com',
'example.com',
'kiro.dev',
]);
const KIRO_EMAIL_INPUT_SELECTOR = [
'input[placeholder="username@example.com"]',
@@ -67,6 +74,58 @@ function getKiroPageText() {
.trim();
}
function normalizeKiroEmailCandidate(value = '') {
const matched = String(value || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
return matched ? matched[0].trim().toLowerCase() : '';
}
function isLikelyKiroAccountEmail(value = '') {
const normalized = normalizeKiroEmailCandidate(value);
if (!normalized) {
return false;
}
const [, domain = ''] = normalized.split('@');
const normalizedDomain = domain.toLowerCase();
if (KIRO_NON_ACCOUNT_EMAIL_DOMAINS.has(normalizedDomain)) {
return false;
}
return !normalized.startsWith('no-reply@')
&& !normalized.startsWith('noreply@')
&& !normalized.startsWith('support@')
&& !normalizedDomain.endsWith('.amazon.com')
&& !normalizedDomain.endsWith('.amazonaws.com')
&& !normalizedDomain.endsWith('.kiro.dev')
&& !normalizedDomain.endsWith('.signin.aws')
&& !normalizedDomain.endsWith('.profile.aws');
}
function extractFirstKiroAccountEmailFromText(text = '') {
const matches = String(text || '').match(KIRO_EMAIL_TEXT_PATTERN) || [];
return matches.map((entry) => normalizeKiroEmailCandidate(entry)).find(isLikelyKiroAccountEmail) || '';
}
function findKiroSignedInAccountEmail(pageText = '') {
const selector = [
'input[type="email"]',
'input[name*="email" i]',
'[data-testid*="email" i]',
'[aria-label*="email" i]',
'[title*="email" i]',
].join(', ');
for (const element of collectVisibleElements(selector)) {
const candidate = extractFirstKiroAccountEmailFromText([
element?.value,
element?.textContent,
element?.getAttribute?.('aria-label'),
element?.getAttribute?.('title'),
].filter(Boolean).join(' '));
if (candidate) {
return candidate;
}
}
return extractFirstKiroAccountEmailFromText(pageText);
}
function collectVisibleElements(selector) {
return Array.from(document.querySelectorAll(selector))
.filter((element) => isVisibleKiroElement(element));
@@ -309,9 +368,12 @@ function detectKiroRegisterPageState() {
};
}
if (isKiroWebSignedInPage(pageText)) {
const accountEmail = findKiroSignedInAccountEmail(pageText);
return {
state: 'kiro_web_signed_in',
url: currentUrl,
accountEmail,
email: accountEmail,
};
}
}
@@ -642,6 +704,19 @@ async function confirmKiroRegisterConsent(payload = {}) {
async function handleKiroRegisterCommand(message) {
switch (message.type) {
case 'GET_KIRO_REGISTER_PAGE_STATE': {
const detected = detectKiroRegisterPageState();
return {
state: detected?.state || '',
url: detected?.url || location.href,
accountEmail: detected?.accountEmail || '',
email: detected?.email || detected?.accountEmail || '',
fatalMessage: detected?.fatalMessage || '',
authorizationActionKind: detected?.authorizationActionKind || '',
authorizationActionText: detected?.authorizationActionText || '',
actionText: detected?.actionText || '',
};
}
case 'ENSURE_KIRO_PAGE_STATE':
return ensureKiroRegisterPageState(message.payload || {});
case 'ENSURE_KIRO_STATE_CHANGE':
@@ -679,6 +754,7 @@ if (document.documentElement.getAttribute(KIRO_REGISTER_PAGE_LISTENER_SENTINEL)
if (
message.type === 'ENSURE_KIRO_PAGE_STATE'
|| message.type === 'ENSURE_KIRO_STATE_CHANGE'
|| message.type === 'GET_KIRO_REGISTER_PAGE_STATE'
|| message.type === 'EXECUTE_NODE'
) {
resetStopState();
@@ -85,9 +85,125 @@ test('kiro desktop authorize runner uses a shared 3-minute page-load timeout bud
test('kiro desktop authorization is gated by completed Kiro Web sign-in', () => {
const source = fs.readFileSync('background/kiro/desktop-authorize-runner.js', 'utf8');
assert.match(source, /runtimeState\.register\?\.status/);
assert.match(source, /runtimeState\.webAuth\?\.status/);
assert.match(source, /restoreKiroWebSessionFromOpenTabs/);
assert.match(source, /GET_KIRO_REGISTER_PAGE_STATE/);
assert.match(source, /Kiro Web 登录态尚未建立/);
assert.match(source, /已检测到 Kiro Web 登录态/);
});
test('executeKiroStartDesktopAuthorize restores existing Kiro Web session before desktop auth', async () => {
const api = loadDesktopAuthorizeRunnerApi();
let currentState = {
kiroRuntime: {
session: {},
register: {},
webAuth: {},
desktopAuth: {},
upload: {},
},
};
const setStateCalls = [];
const logs = [];
let completedPayload = null;
let openedAuthorizeUrl = '';
let registerReadySource = '';
let desktopRegisteredTabId = null;
const runner = api.createKiroDesktopAuthorizeRunner({
addLog: async (message, level) => {
logs.push({ message, level });
},
chrome: {
tabs: {
get: async (tabId) => ({
id: tabId,
status: 'complete',
url: tabId === 77
? 'https://app.kiro.dev/settings/account'
: 'https://oidc.us-east-1.amazonaws.com/authorize',
}),
query: async (queryInfo) => {
if (Array.isArray(queryInfo.url)) {
return [{
id: 77,
status: 'complete',
url: 'https://app.kiro.dev/settings/account',
}];
}
return [];
},
update: async () => ({}),
},
webNavigation: {
onBeforeNavigate: { addListener: () => {} },
onCommitted: { addListener: () => {} },
},
webRequest: {
onBeforeRequest: { addListener: () => {} },
},
},
completeNodeFromBackground: async (_nodeId, payload) => {
completedPayload = payload;
},
ensureContentScriptReadyOnTab: async (source, tabId, options = {}) => {
assert.equal(tabId, 77);
registerReadySource = source;
assert.equal(options.injectSource, 'kiro-register-page');
},
fetchImpl: async () => ({
ok: true,
text: async () => JSON.stringify({
clientId: 'restored-client-id',
clientSecret: 'restored-client-secret',
}),
}),
getState: async () => currentState,
getTabId: async () => null,
isTabAlive: async () => false,
KIRO_REGISTER_INJECT_FILES: ['content/kiro/register-page.js'],
registerTab: async (source, tabId) => {
if (source === 'kiro-desktop-authorize') {
desktopRegisteredTabId = tabId;
}
},
reuseOrCreateTab: async (_source, url) => {
openedAuthorizeUrl = url;
return 88;
},
sendToContentScriptResilient: async (source, message) => {
assert.equal(source, 'kiro-register-page');
assert.equal(message.type, 'GET_KIRO_REGISTER_PAGE_STATE');
return {
ok: true,
state: 'kiro_web_signed_in',
url: 'https://app.kiro.dev/settings/account',
accountEmail: 'restored@duck.com',
};
},
setState: async (patch) => {
setStateCalls.push(patch);
currentState = { ...currentState, ...patch };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabStableComplete: async () => ({ status: 'complete' }),
});
await runner.executeKiroStartDesktopAuthorize(currentState);
assert.equal(registerReadySource, 'kiro-register-page');
assert.equal(desktopRegisteredTabId, 88);
assert.match(openedAuthorizeUrl, /client_id=restored-client-id/);
assert.equal(currentState.email, 'restored@duck.com');
assert.equal(currentState.kiroRuntime.register.email, 'restored@duck.com');
assert.equal(currentState.kiroRuntime.register.status, 'completed');
assert.equal(currentState.kiroRuntime.webAuth.status, 'signed_in');
assert.equal(completedPayload?.kiroRuntime?.desktopAuth?.clientId, 'restored-client-id');
assert.equal(
logs.some(({ message }) => message.includes('检测到已有 Kiro Web 登录态,已恢复账号 restored@duck.com')),
true
);
assert.equal(setStateCalls.length >= 2, true);
});
test('executeKiroCompleteDesktopAuthorize finishes from callback page without waiting for tracker replay', async () => {
+55 -3
View File
@@ -17,6 +17,18 @@ const OPENAI_NODE_IDS = [
'platform-verify',
];
const KIRO_NODE_IDS = [
'kiro-open-register-page',
'kiro-submit-email',
'kiro-submit-name',
'kiro-submit-verification-code',
'kiro-submit-password',
'kiro-complete-register-consent',
'kiro-start-desktop-authorize',
'kiro-complete-desktop-authorize',
'kiro-upload-credential',
];
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
@@ -61,7 +73,7 @@ function extractFunction(name) {
return source.slice(start, end);
}
function createApi() {
function createApi(nodeIds = OPENAI_NODE_IDS) {
const bundle = [
extractFunction('isStepDoneStatus'),
extractFunction('skipNode'),
@@ -70,7 +82,7 @@ function createApi() {
return new Function(`
const DEFAULT_STATE = { nodeStatuses: {} };
function getNodeIdsForState() {
return ${JSON.stringify(OPENAI_NODE_IDS)};
return ${JSON.stringify(nodeIds)};
}
function normalizeStatusMapForNodes(statuses = {}) {
return { ...statuses };
@@ -80,7 +92,7 @@ function isNodeExecutionAllowedForState() {
return true;
}
function getExecutionAllowedNodeIdsForState() {
return ${JSON.stringify(OPENAI_NODE_IDS)};
return ${JSON.stringify(nodeIds)};
}
${bundle}
return { skipNode };
@@ -174,3 +186,43 @@ test('skipNode from open-chatgpt skips only unfinished linked signup nodes', asy
true
);
});
test('skipNode cascades from kiro open-register through the whole register branch', async () => {
const statuses = Object.fromEntries(KIRO_NODE_IDS.map((nodeId) => [nodeId, 'pending']));
const events = {
setNodeStatusCalls: [],
logs: [],
};
const api = createApi(KIRO_NODE_IDS);
globalThis.ensureManualInteractionAllowed = async () => ({
nodeStatuses: { ...statuses },
});
globalThis.getState = async () => ({
nodeStatuses: { ...statuses },
});
globalThis.setNodeStatus = async (nodeId, status) => {
events.setNodeStatusCalls.push({ nodeId, status });
statuses[nodeId] = status;
};
globalThis.addLog = async (message, level) => {
events.logs.push({ message, level });
};
const result = await api.skipNode('kiro-open-register-page');
assert.deepStrictEqual(result, { ok: true, nodeId: 'kiro-open-register-page', status: 'skipped' });
assert.deepStrictEqual(events.setNodeStatusCalls, [
{ nodeId: 'kiro-open-register-page', status: 'skipped' },
{ nodeId: 'kiro-submit-email', status: 'skipped' },
{ nodeId: 'kiro-submit-name', status: 'skipped' },
{ nodeId: 'kiro-submit-verification-code', status: 'skipped' },
{ nodeId: 'kiro-submit-password', status: 'skipped' },
{ nodeId: 'kiro-complete-register-consent', status: 'skipped' },
]);
assert.equal(events.logs[0]?.message, '节点 kiro-open-register-page 已跳过');
assert.equal(
events.logs[1]?.message,
'节点 kiro-open-register-page 已跳过,节点 kiro-submit-email、kiro-submit-name、kiro-submit-verification-code、kiro-submit-password、kiro-complete-register-consent 也已同时跳过。'
);
});
@@ -90,3 +90,18 @@ test('kiro register content treats Kiro web success callback as signed in', () =
assert.equal(detected.state, 'kiro_web_signed_in');
});
test('kiro register content extracts signed-in account email from Kiro account page text', () => {
const harness = createHarness({
href: 'https://app.kiro.dev/settings/account',
hostname: 'app.kiro.dev',
title: 'Account',
bodyText: 'Account Email scrap-aged-quirk@duck.com support@kiro.dev',
});
const detected = harness.detectKiroRegisterPageState();
assert.equal(detected.state, 'kiro_web_signed_in');
assert.equal(detected.accountEmail, 'scrap-aged-quirk@duck.com');
assert.equal(detected.email, 'scrap-aged-quirk@duck.com');
});