refactor kiro flow to web signin authorization

This commit is contained in:
QLHazyCoder
2026-05-19 12:16:59 +08:00
parent 98e776348e
commit ef700be3e2
22 changed files with 521 additions and 1391 deletions
@@ -785,6 +785,10 @@
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 client = await desktopClientApi.registerDesktopClient({
region: DEFAULT_REGION,
+117 -124
View File
@@ -6,16 +6,11 @@
const DEFAULT_REGION = kiroStateApi?.DEFAULT_REGION || 'us-east-1';
const DEFAULT_TARGET_ID = kiroStateApi?.DEFAULT_TARGET_ID || 'kiro-rs';
const DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS = kiroTimeoutApi?.DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS || (3 * 60 * 1000);
const DEVICE_LOGIN_START_URL = 'https://view.awsapps.com/start';
const DEFAULT_SCOPES = Object.freeze([
'codewhisperer:completions',
'codewhisperer:analysis',
'codewhisperer:conversations',
'codewhisperer:transformations',
'codewhisperer:taskassist',
]);
const KIRO_SIGNIN_URL = 'https://app.kiro.dev/signin';
const KIRO_REGISTER_PAGE_SOURCE_ID = 'kiro-register-page';
const KIRO_STEP1_COOKIE_CLEAR_DOMAINS = Object.freeze([
'kiro.dev',
'app.kiro.dev',
'awsapps.com',
'view.awsapps.com',
'login.awsapps.com',
@@ -26,6 +21,8 @@
'profile.aws.amazon.com',
]);
const KIRO_STEP1_COOKIE_CLEAR_ORIGINS = Object.freeze([
'https://app.kiro.dev',
'https://kiro.dev',
'https://view.awsapps.com',
'https://login.awsapps.com',
'https://oidc.us-east-1.amazonaws.com',
@@ -110,10 +107,6 @@
return String(value ?? '').trim();
}
function normalizeRegion(value = '', fallback = DEFAULT_REGION) {
return cleanString(value) || fallback;
}
function normalizePositiveInteger(value, fallback) {
const numeric = Math.floor(Number(value));
if (Number.isInteger(numeric) && numeric > 0) {
@@ -152,10 +145,6 @@
);
}
function buildOidcBaseUrl(region = DEFAULT_REGION) {
return `https://oidc.${normalizeRegion(region)}.amazonaws.com`;
}
function readKiroRuntime(state = {}) {
if (typeof kiroStateApi?.ensureRuntimeState === 'function') {
return kiroStateApi.ensureRuntimeState(state);
@@ -178,17 +167,6 @@
return error instanceof Error ? error.message : String(error ?? '未知错误');
}
async function readResponse(response) {
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_error) {
json = null;
}
return { text, json };
}
function normalizeKiroCookieDomain(domain = '') {
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
}
@@ -260,6 +238,58 @@
return cookies;
}
function shouldReadKiroWebCookie(cookie) {
const domain = normalizeKiroCookieDomain(cookie?.domain);
return domain === 'kiro.dev'
|| domain === 'app.kiro.dev'
|| domain.endsWith('.app.kiro.dev');
}
async function collectKiroWebCookies(chromeApi) {
if (!chromeApi?.cookies?.getAll) {
return [];
}
const stores = chromeApi.cookies.getAllCookieStores
? await chromeApi.cookies.getAllCookieStores()
: [{ id: undefined }];
const cookies = [];
const seen = new Set();
for (const store of stores) {
const storeId = store?.id;
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
for (const cookie of batch || []) {
if (!shouldReadKiroWebCookie(cookie)) {
continue;
}
const key = [
cookie.storeId || storeId || '',
cookie.domain || '',
cookie.path || '',
cookie.name || '',
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
].join('|');
if (seen.has(key)) {
continue;
}
seen.add(key);
cookies.push(cookie);
}
}
return cookies;
}
async function captureKiroWebAuthSummary() {
const cookies = await collectKiroWebCookies(chrome);
const names = new Set(cookies.map((cookie) => cleanString(cookie?.name).toLowerCase()).filter(Boolean));
return {
hasAccessToken: names.has('accesstoken') || names.has('access_token'),
hasSessionToken: names.has('sessiontoken') || names.has('session_token'),
};
}
async function removeKiroStep1Cookie(chromeApi, cookie) {
const details = {
url: buildKiroStep1CookieRemovalUrl(cookie),
@@ -285,75 +315,6 @@
}
}
async function startBuilderIdDeviceLogin(region, fetchImpl) {
const normalizedRegion = normalizeRegion(region);
const oidcBaseUrl = buildOidcBaseUrl(normalizedRegion);
const registerResponse = await fetchImpl(`${oidcBaseUrl}/client/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientName: 'Codex Registration Extension',
clientType: 'public',
scopes: DEFAULT_SCOPES,
grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'],
issuerUrl: DEVICE_LOGIN_START_URL,
}),
});
const registerBody = await readResponse(registerResponse);
if (!registerResponse.ok) {
throw new Error(`Builder ID 客户端注册失败:${cleanString(registerBody.text || registerResponse.statusText) || registerResponse.status}`);
}
const clientId = cleanString(registerBody.json?.clientId);
const clientSecret = String(registerBody.json?.clientSecret || '');
if (!clientId || !clientSecret) {
throw new Error('Builder ID 客户端注册响应缺少凭据。');
}
const authorizationResponse = await fetchImpl(`${oidcBaseUrl}/device_authorization`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientId,
clientSecret,
startUrl: DEVICE_LOGIN_START_URL,
}),
});
const authorizationBody = await readResponse(authorizationResponse);
if (!authorizationResponse.ok) {
throw new Error(`Builder ID 设备授权失败:${cleanString(authorizationBody.text || authorizationResponse.statusText) || authorizationResponse.status}`);
}
const deviceCode = String(authorizationBody.json?.deviceCode || '');
const userCode = cleanString(authorizationBody.json?.userCode);
const verificationUri = cleanString(authorizationBody.json?.verificationUri);
const verificationUriComplete = cleanString(
authorizationBody.json?.verificationUriComplete || verificationUri
);
const interval = normalizePositiveInteger(authorizationBody.json?.interval, 5);
const expiresIn = normalizePositiveInteger(authorizationBody.json?.expiresIn, 600);
if (!deviceCode || !userCode || !verificationUriComplete) {
throw new Error('Builder ID 设备授权响应缺少必要字段。');
}
return {
clientId,
clientSecret,
deviceCode,
expiresAt: Date.now() + expiresIn * 1000,
expiresIn,
interval,
region: normalizedRegion,
userCode,
verificationUri,
verificationUriComplete,
};
}
function createKiroRegisterRunner(deps = {}) {
const {
addLog = async () => {},
@@ -362,7 +323,6 @@
ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
generatePassword = null,
generateRandomName = null,
getMailConfig = null,
@@ -399,9 +359,6 @@
if (typeof completeNodeFromBackground !== 'function') {
throw new Error('Kiro register runner requires completeNodeFromBackground.');
}
if (typeof fetchImpl !== 'function') {
throw new Error('Kiro register runner requires fetch support.');
}
async function log(message, level = 'info', nodeId = '') {
await addLog(message, level, nodeId ? { nodeId } : {});
@@ -883,24 +840,51 @@
const currentState = await getExecutionState(state);
try {
await clearKiroCookiesBeforeStep1();
const auth = await startBuilderIdDeviceLogin(DEFAULT_REGION, fetchImpl);
const loginUrl = cleanString(auth.verificationUriComplete || auth.verificationUri);
const tabId = loginUrl ? await reuseOrCreateTab(KIRO_REGISTER_PAGE_SOURCE_ID, loginUrl) : null;
const loginUrl = KIRO_SIGNIN_URL;
const tabId = await reuseOrCreateTab(KIRO_REGISTER_PAGE_SOURCE_ID, loginUrl);
if (!Number.isInteger(tabId)) {
throw new Error('无法打开 Kiro 注册页,请重试步骤 1。');
}
await registerTab(KIRO_REGISTER_PAGE_SOURCE_ID, tabId);
await activateTab(tabId);
const landingResult = await ensureKiroPageState(tabId, {
let landingResult = await ensureKiroPageState(tabId, {
step: 1,
targetStates: ['email_entry'],
targetStates: ['kiro_signin_page', 'email_entry'],
stableMs: 2500,
initialDelayMs: 300,
injectLogMessage: '步骤 1:Kiro 注册页内容脚本未就绪,正在等待页面恢复...',
readyLogMessage: '步骤 1:正在等待 Kiro 注册页邮箱输入框加载完成...',
readyLogMessage: '步骤 1:正在等待 Kiro 官方登录页加载完成...',
});
if (landingResult?.state === 'kiro_signin_page') {
await log('步骤 1:正在选择 AWS Builder ID 登录方式...', 'info', nodeId);
const selectResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, {
type: 'EXECUTE_NODE',
nodeId: 'kiro-open-register-page',
step: 1,
source: 'background',
payload: {},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
onRetryableError: buildKiroRetryRecovery(tabId, {}),
logMessage: '步骤 1:正在点击 Kiro 官方登录页的 Builder ID...',
});
if (selectResult?.error) {
throw new Error(selectResult.error);
}
landingResult = await ensureKiroPageState(tabId, {
step: 1,
targetStates: ['email_entry'],
stableMs: 2500,
initialDelayMs: 300,
injectLogMessage: '步骤 1:选择 Builder ID 后页面跳转中,正在等待 Kiro 注册页恢复...',
readyLogMessage: '步骤 1:正在等待 Builder ID 邮箱输入框加载完成...',
timeoutMessage: '选择 Builder ID 后未进入邮箱页,请检查当前 Kiro 登录页或代理状态。',
});
}
const nextPatch = {
session: {
currentStage: 'register',
@@ -915,18 +899,16 @@
email: '',
fullName: '',
verificationRequestedAt: 0,
entryClientId: auth.clientId,
entryClientSecret: auth.clientSecret,
entryDeviceCode: auth.deviceCode,
userCode: auth.userCode,
loginUrl,
verificationUri: auth.verificationUri,
verificationUriComplete: loginUrl,
entryExpiresAt: auth.expiresAt,
entryIntervalSeconds: auth.interval,
status: 'waiting_email',
completedAt: 0,
},
webAuth: {
status: 'signin_started',
completedAt: 0,
hasAccessToken: false,
hasSessionToken: false,
},
desktopAuth: {
region: DEFAULT_REGION,
clientId: '',
@@ -957,7 +939,7 @@
};
const payload = await applyRuntimeState(currentState, nextPatch);
await log(`Kiro 注册页已就绪,请在下一步中获取邮箱并继续。当前授权码:${auth.userCode}`, 'ok', nodeId);
await log('Kiro 注册页已就绪,已进入 Builder ID 邮箱页,请在下一步中获取邮箱并继续。', 'ok', nodeId);
await completeNodeFromBackground(nodeId, payload);
} catch (error) {
const message = getErrorMessage(error);
@@ -1315,7 +1297,7 @@
});
let landingResult = await ensureKiroPageState(tabId, {
step: 6,
targetStates: ['authorization_page', 'success_page'],
targetStates: ['authorization_page', 'kiro_web_signed_in'],
stableMs: 1500,
initialDelayMs: 150,
injectLogMessage: '步骤 6:Kiro 授权确认页内容脚本未就绪,正在等待页面恢复...',
@@ -1323,7 +1305,7 @@
timeoutMessage: '未进入 Kiro 授权确认页,请检查当前页面状态。',
});
if (landingResult?.state !== 'success_page') {
if (landingResult?.state !== 'kiro_web_signed_in') {
await log('步骤 6:正在确认访问并完成 Kiro 注册授权...', 'info', nodeId);
const submitResult = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, {
type: 'EXECUTE_NODE',
@@ -1342,12 +1324,18 @@
if (submitResult?.error) {
throw new Error(submitResult.error);
}
landingResult = {
state: String(submitResult?.state || ''),
url: String(submitResult?.url || ''),
};
landingResult = await ensureKiroPageState(tabId, {
step: 6,
targetStates: ['kiro_web_signed_in'],
stableMs: 2000,
initialDelayMs: 300,
injectLogMessage: '步骤 6:授权确认后页面跳转中,正在等待 Kiro Web 登录态恢复...',
readyLogMessage: '步骤 6:授权确认已提交,正在等待回到 Kiro Web...',
timeoutMessage: '授权确认后未回到 Kiro Web 登录完成页,请检查当前页面或代理状态。',
});
}
const webAuthSummary = await captureKiroWebAuthSummary();
const payload = await applyRuntimeState(currentState, {
session: {
currentStage: 'desktop-authorize',
@@ -1359,12 +1347,18 @@
status: 'completed',
completedAt: Date.now(),
},
webAuth: {
status: 'signed_in',
completedAt: Date.now(),
hasAccessToken: Boolean(webAuthSummary.hasAccessToken),
hasSessionToken: Boolean(webAuthSummary.hasSessionToken),
},
upload: {
status: 'waiting_desktop_authorize',
error: '',
},
});
await log('步骤 6:注册页授权已完成,Builder ID 浏览器会话已建立。', 'ok', nodeId);
await log('步骤 6:注册页授权已完成,Kiro Web 登录态已建立。', 'ok', nodeId);
await completeNodeFromBackground(nodeId, payload);
} catch (error) {
const message = getErrorMessage(error);
@@ -1380,12 +1374,11 @@
executeKiroSubmitName,
executeKiroSubmitPassword,
executeKiroSubmitVerificationCode,
startBuilderIdDeviceLogin,
};
}
return {
createKiroRegisterRunner,
startBuilderIdDeviceLogin,
KIRO_SIGNIN_URL,
};
});
+19 -16
View File
@@ -58,6 +58,13 @@
return Number.isInteger(numeric) ? numeric : fallback;
}
function normalizeBoolean(value, fallback = false) {
if (value === true || value === false) {
return value;
}
return fallback;
}
function buildDefaultRuntimeState() {
return {
session: {
@@ -74,18 +81,16 @@
email: '',
fullName: '',
verificationRequestedAt: 0,
entryClientId: '',
entryClientSecret: '',
entryDeviceCode: '',
userCode: '',
loginUrl: '',
verificationUri: '',
verificationUriComplete: '',
entryExpiresAt: 0,
entryIntervalSeconds: 0,
status: '',
completedAt: 0,
},
webAuth: {
status: '',
completedAt: 0,
hasAccessToken: false,
hasSessionToken: false,
},
desktopAuth: {
region: DEFAULT_REGION,
clientId: '',
@@ -133,18 +138,16 @@
email: normalizeString(merged.register?.email),
fullName: normalizeString(merged.register?.fullName),
verificationRequestedAt: Math.max(0, normalizeInteger(merged.register?.verificationRequestedAt)),
entryClientId: normalizeString(merged.register?.entryClientId),
entryClientSecret: normalizeString(merged.register?.entryClientSecret),
entryDeviceCode: normalizeString(merged.register?.entryDeviceCode),
userCode: normalizeString(merged.register?.userCode),
loginUrl: normalizeString(merged.register?.loginUrl),
verificationUri: normalizeString(merged.register?.verificationUri),
verificationUriComplete: normalizeString(merged.register?.verificationUriComplete),
entryExpiresAt: Math.max(0, normalizeInteger(merged.register?.entryExpiresAt)),
entryIntervalSeconds: Math.max(0, normalizeInteger(merged.register?.entryIntervalSeconds)),
status: normalizeString(merged.register?.status),
completedAt: Math.max(0, normalizeInteger(merged.register?.completedAt)),
},
webAuth: {
status: normalizeString(merged.webAuth?.status),
completedAt: Math.max(0, normalizeInteger(merged.webAuth?.completedAt)),
hasAccessToken: normalizeBoolean(merged.webAuth?.hasAccessToken),
hasSessionToken: normalizeBoolean(merged.webAuth?.hasSessionToken),
},
desktopAuth: {
region: normalizeString(merged.desktopAuth?.region, DEFAULT_REGION),
clientId: normalizeString(merged.desktopAuth?.clientId),