feat: 添加 Kiro 授权流程的 cookie 清理和重试机制,优化跨页面恢复
This commit is contained in:
@@ -13318,6 +13318,7 @@ const kiroDeviceAuthExecutor = self.MultiPageBackgroundKiroDeviceAuth?.createKir
|
||||
YYDS_MAIL_PROVIDER,
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
||||
isRetryableContentScriptTransportError,
|
||||
pollCloudflareTempEmailVerificationCode,
|
||||
pollCloudMailVerificationCode,
|
||||
pollHotmailVerificationCode,
|
||||
|
||||
@@ -10,6 +10,25 @@
|
||||
'codewhisperer:transformations',
|
||||
'codewhisperer:taskassist',
|
||||
]);
|
||||
const KIRO_STEP1_COOKIE_CLEAR_DOMAINS = Object.freeze([
|
||||
'awsapps.com',
|
||||
'view.awsapps.com',
|
||||
'login.awsapps.com',
|
||||
'amazonaws.com',
|
||||
'signin.aws',
|
||||
'signin.aws.amazon.com',
|
||||
'profile.aws',
|
||||
'profile.aws.amazon.com',
|
||||
]);
|
||||
const KIRO_STEP1_COOKIE_CLEAR_ORIGINS = Object.freeze([
|
||||
'https://view.awsapps.com',
|
||||
'https://login.awsapps.com',
|
||||
'https://oidc.us-east-1.amazonaws.com',
|
||||
'https://signin.aws',
|
||||
'https://signin.aws.amazon.com',
|
||||
'https://profile.aws',
|
||||
'https://profile.aws.amazon.com',
|
||||
]);
|
||||
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
|
||||
@@ -107,6 +126,102 @@
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeKiroCookieDomain(domain = '') {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
}
|
||||
|
||||
function matchesKiroNamedHostFamily(domain = '', family = '') {
|
||||
const normalizedDomain = normalizeKiroCookieDomain(domain);
|
||||
const normalizedFamily = normalizeKiroCookieDomain(family);
|
||||
if (!normalizedDomain || !normalizedFamily) {
|
||||
return false;
|
||||
}
|
||||
return normalizedDomain === normalizedFamily
|
||||
|| normalizedDomain.endsWith(`.${normalizedFamily}`)
|
||||
|| normalizedDomain.startsWith(`${normalizedFamily}.`)
|
||||
|| normalizedDomain.includes(`.${normalizedFamily}.`);
|
||||
}
|
||||
|
||||
function shouldClearKiroStep1Cookie(cookie) {
|
||||
const domain = normalizeKiroCookieDomain(cookie?.domain);
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
return KIRO_STEP1_COOKIE_CLEAR_DOMAINS.some((target) => (
|
||||
domain === target
|
||||
|| domain.endsWith(`.${target}`)
|
||||
|| matchesKiroNamedHostFamily(domain, target)
|
||||
));
|
||||
}
|
||||
|
||||
function buildKiroStep1CookieRemovalUrl(cookie) {
|
||||
const host = normalizeKiroCookieDomain(cookie?.domain);
|
||||
const rawPath = String(cookie?.path || '/');
|
||||
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
||||
return `https://${host}${path}`;
|
||||
}
|
||||
|
||||
async function collectKiroStep1Cookies(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 (!shouldClearKiroStep1Cookie(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 removeKiroStep1Cookie(chromeApi, cookie) {
|
||||
const details = {
|
||||
url: buildKiroStep1CookieRemovalUrl(cookie),
|
||||
name: cookie.name,
|
||||
};
|
||||
if (cookie.storeId) {
|
||||
details.storeId = cookie.storeId;
|
||||
}
|
||||
if (cookie.partitionKey) {
|
||||
details.partitionKey = cookie.partitionKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await chromeApi.cookies.remove(details);
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:kiro-step1] remove cookie failed', {
|
||||
domain: cookie?.domain,
|
||||
name: cookie?.name,
|
||||
message: getErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildCredentialUploadOptions(state = {}) {
|
||||
const next = {
|
||||
priority: Math.max(0, Math.floor(Number(state?.kiroRsPriority) || 0)),
|
||||
@@ -354,6 +469,7 @@
|
||||
YYDS_MAIL_PROVIDER = 'yyds-mail',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
|
||||
isRetryableContentScriptTransportError = () => false,
|
||||
isTabAlive = async () => false,
|
||||
KIRO_DEVICE_AUTH_INJECT_FILES = null,
|
||||
pollCloudflareTempEmailVerificationCode = null,
|
||||
@@ -406,6 +522,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function clearKiroCookiesBeforeStep1() {
|
||||
if (!chrome?.cookies?.getAll || !chrome.cookies?.remove) {
|
||||
await log('步骤 1:当前浏览器不支持 cookies API,跳过打开 Kiro 授权页前 cookie 清理。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
await log('步骤 1:打开 Kiro 授权页前清理 AWS Builder ID 相关 cookies...', 'info');
|
||||
const cookies = await collectKiroStep1Cookies(chrome);
|
||||
let removedCount = 0;
|
||||
for (const cookie of cookies) {
|
||||
if (await removeKiroStep1Cookie(chrome, cookie)) {
|
||||
removedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chrome.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chrome.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: KIRO_STEP1_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await log(`步骤 1:browsingData 补扫 cookies 失败:${getErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await log(`步骤 1:已清理 ${removedCount} 个 AWS Builder ID 相关 cookies。`, 'ok');
|
||||
}
|
||||
|
||||
async function ensureKiroAuthTab(state = {}, options = {}) {
|
||||
let tabId = Number.isInteger(state?.kiroAuthTabId)
|
||||
? state.kiroAuthTabId
|
||||
@@ -435,6 +580,42 @@
|
||||
return tabId;
|
||||
}
|
||||
|
||||
async function reattachKiroContentScript(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 授权页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildKiroRetryRecovery(tabId, options = {}) {
|
||||
return async (error) => {
|
||||
if (!isRetryableContentScriptTransportError(error)) {
|
||||
return;
|
||||
}
|
||||
await reattachKiroContentScript(tabId, {
|
||||
stableMs: Number(options.recoveryStableMs) || Number(options.stableMs) || 1200,
|
||||
initialDelayMs: Number(options.recoveryInitialDelayMs) || 120,
|
||||
injectLogMessage: options.recoveryInjectLogMessage || options.injectLogMessage || 'Kiro 授权页已跳转,正在重新连接内容脚本...',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureKiroPageState(tabId, options = {}) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('缺少 Kiro 授权页标签页,无法继续执行。');
|
||||
@@ -475,6 +656,7 @@
|
||||
}, {
|
||||
timeoutMs: Math.max(30000, Number(options.pageTimeoutMs) || 30000),
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, options),
|
||||
logMessage: options.readyLogMessage || '正在等待 Kiro 页面进入下一状态...',
|
||||
});
|
||||
if (result?.error) {
|
||||
@@ -520,6 +702,7 @@
|
||||
}, {
|
||||
timeoutMs: Math.max(30000, Number(options.pageTimeoutMs) || 30000),
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, options),
|
||||
logMessage: options.readyLogMessage || '正在等待 Kiro 页面完成跳转...',
|
||||
});
|
||||
if (result?.error) {
|
||||
@@ -739,6 +922,7 @@
|
||||
async function executeKiroStartDeviceLogin(state = {}) {
|
||||
const nodeId = String(state?.nodeId || 'kiro-start-device-login').trim();
|
||||
try {
|
||||
await clearKiroCookiesBeforeStep1();
|
||||
const auth = await startBuilderIdDeviceLogin(DEFAULT_REGION, fetchImpl);
|
||||
const loginUrl = cleanString(auth.verificationUriComplete || auth.verificationUri);
|
||||
const tabId = loginUrl ? await reuseOrCreateTab('kiro-device-auth', loginUrl) : null;
|
||||
@@ -834,6 +1018,7 @@
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, {}),
|
||||
logMessage: '步骤 2:正在向 Kiro 授权页提交邮箱...',
|
||||
});
|
||||
if (submitResult?.error) {
|
||||
@@ -913,6 +1098,7 @@
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, {}),
|
||||
logMessage: '步骤 3:正在向 Kiro 姓名页提交姓名...',
|
||||
});
|
||||
if (submitResult?.error) {
|
||||
@@ -990,6 +1176,7 @@
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, {}),
|
||||
logMessage: '步骤 4:正在向 Kiro 验证码页提交验证码...',
|
||||
});
|
||||
if (submitResult?.error) {
|
||||
@@ -1072,6 +1259,7 @@
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, {}),
|
||||
logMessage: '步骤 5:正在向 Kiro 密码页提交密码...',
|
||||
});
|
||||
if (submitResult?.error) {
|
||||
@@ -1155,6 +1343,7 @@
|
||||
}, {
|
||||
timeoutMs: 60000,
|
||||
retryDelayMs: 700,
|
||||
onRetryableError: buildKiroRetryRecovery(tabId, {}),
|
||||
logMessage: '步骤 6:正在处理 Kiro 授权确认页...',
|
||||
});
|
||||
if (submitResult?.error) {
|
||||
|
||||
@@ -616,6 +616,16 @@
|
||||
return Math.max(1, Math.min(requestedTimeoutMs, Math.floor(Number(remainingTimeoutMs))));
|
||||
}
|
||||
|
||||
function buildRetryableTransportTimeoutError(source, error) {
|
||||
const rawMessage = error?.message || String(error || '');
|
||||
if (isRetryableContentScriptTransportError(error)) {
|
||||
return new Error(
|
||||
`${getSourceLabel(source)} 页面刚完成跳转或刷新,内容脚本还没有重新接回;扩展已自动重试,但仍未恢复。请重试当前步骤。`
|
||||
);
|
||||
}
|
||||
return new Error(rawMessage || `${getSourceLabel(source)} 页面通信失败。`);
|
||||
}
|
||||
|
||||
function getMessageDebugLabel(source, message, tabId = null) {
|
||||
const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
|
||||
if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
|
||||
@@ -878,6 +888,7 @@
|
||||
logMessage = '',
|
||||
logStep = null,
|
||||
logStepKey = '',
|
||||
onRetryableError = null,
|
||||
responseTimeoutMs,
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
@@ -916,10 +927,23 @@
|
||||
logged = true;
|
||||
}
|
||||
|
||||
if (typeof onRetryableError === 'function') {
|
||||
await onRetryableError(err, {
|
||||
attempt,
|
||||
elapsedMs: Date.now() - start,
|
||||
remainingTimeoutMs: Math.max(0, timeoutMs - (Date.now() - start)),
|
||||
source,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
await sleepOrStop(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError && isRetryableContentScriptTransportError(lastError)) {
|
||||
throw buildRetryableTransportTimeoutError(source, lastError);
|
||||
}
|
||||
throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,25 @@
|
||||
"content/duck-mail.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://view.awsapps.com/*",
|
||||
"https://login.awsapps.com/*",
|
||||
"https://*.awsapps.com/*",
|
||||
"https://signin.aws/*",
|
||||
"https://signin.aws.amazon.com/*",
|
||||
"https://*.signin.aws/*",
|
||||
"https://profile.aws/*",
|
||||
"https://profile.aws.amazon.com/*",
|
||||
"https://*.profile.aws/*"
|
||||
],
|
||||
"js": [
|
||||
"shared/source-registry.js",
|
||||
"content/utils.js",
|
||||
"content/kiro-device-auth-page.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
'openai-plus',
|
||||
'openai-phone',
|
||||
'openai-oauth',
|
||||
'openai-step6',
|
||||
],
|
||||
sources: {
|
||||
cpa: {
|
||||
@@ -319,6 +320,11 @@
|
||||
label: 'OAuth',
|
||||
rowIds: ['row-oauth-flow-timeout', 'row-oauth-display'],
|
||||
},
|
||||
'openai-step6': {
|
||||
id: 'openai-step6',
|
||||
label: '第六步',
|
||||
rowIds: ['row-step6-cookie-settings'],
|
||||
},
|
||||
'kiro-source-kiro-rs': {
|
||||
id: 'kiro-source-kiro-rs',
|
||||
label: 'kiro.rs 配置',
|
||||
|
||||
@@ -126,6 +126,32 @@
|
||||
'kiro-device-auth',
|
||||
]);
|
||||
|
||||
function normalizeHostname(hostname = '') {
|
||||
return String(hostname || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function matchesNamedHostFamily(hostname = '', family = '') {
|
||||
const normalizedHost = normalizeHostname(hostname);
|
||||
const normalizedFamily = normalizeHostname(family);
|
||||
if (!normalizedHost || !normalizedFamily) {
|
||||
return false;
|
||||
}
|
||||
return normalizedHost === normalizedFamily
|
||||
|| normalizedHost.endsWith(`.${normalizedFamily}`)
|
||||
|| normalizedHost.startsWith(`${normalizedFamily}.`)
|
||||
|| normalizedHost.includes(`.${normalizedFamily}.`);
|
||||
}
|
||||
|
||||
function isKiroAuthHost(hostname = '') {
|
||||
const normalized = normalizeHostname(hostname);
|
||||
return normalized === 'view.awsapps.com'
|
||||
|| normalized === 'login.awsapps.com'
|
||||
|| matchesNamedHostFamily(normalized, 'signin.aws')
|
||||
|| matchesNamedHostFamily(normalized, 'profile.aws')
|
||||
|| normalized === 'amazonaws.com'
|
||||
|| normalized.endsWith('.amazonaws.com');
|
||||
}
|
||||
|
||||
function getRuntimeSourceDefinitions() {
|
||||
return {
|
||||
...(typeof flowRegistryApi.getRuntimeSourceDefinitions === 'function'
|
||||
@@ -227,15 +253,15 @@
|
||||
}
|
||||
|
||||
function isSignupPageHost(hostname = '') {
|
||||
return AUTH_PAGE_HOSTS.has(String(hostname || '').toLowerCase());
|
||||
return AUTH_PAGE_HOSTS.has(normalizeHostname(hostname));
|
||||
}
|
||||
|
||||
function isSignupEntryHost(hostname = '') {
|
||||
return ENTRY_PAGE_HOSTS.has(String(hostname || '').toLowerCase());
|
||||
return ENTRY_PAGE_HOSTS.has(normalizeHostname(hostname));
|
||||
}
|
||||
|
||||
function is163MailHost(hostname = '') {
|
||||
const normalized = String(hostname || '').toLowerCase();
|
||||
const normalized = normalizeHostname(hostname);
|
||||
return normalized === 'mail.163.com'
|
||||
|| normalized.endsWith('.mail.163.com')
|
||||
|| normalized === 'mail.126.com'
|
||||
@@ -300,8 +326,7 @@
|
||||
case 'gopay-flow':
|
||||
return /gopay|gojek/i.test(candidate.hostname);
|
||||
case 'kiro-device-auth':
|
||||
return candidate.hostname === 'view.awsapps.com'
|
||||
|| candidate.hostname.endsWith('.amazonaws.com');
|
||||
return isKiroAuthHost(candidate.hostname);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -324,7 +349,7 @@
|
||||
if (normalizedHostname === 'www.icloud.com' || normalizedHostname === 'www.icloud.com.cn') return 'icloud-mail';
|
||||
if (normalizedUrl.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
|
||||
if (normalizedUrl.includes('2925.com')) return 'mail-2925';
|
||||
if (normalizedHostname === 'view.awsapps.com' || normalizedHostname.endsWith('.amazonaws.com')) return 'kiro-device-auth';
|
||||
if (isKiroAuthHost(normalizedHostname)) return 'kiro-device-auth';
|
||||
if (isSignupEntryHost(normalizedHostname)) return 'chatgpt';
|
||||
return 'unknown-source';
|
||||
}
|
||||
|
||||
@@ -2068,13 +2068,8 @@ header {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
#row-auto-delay-settings .setting-group-primary {
|
||||
flex: 0 0 auto;
|
||||
min-width: 116px;
|
||||
}
|
||||
|
||||
#row-auto-delay-settings .setting-group-secondary {
|
||||
margin-left: 0;
|
||||
margin-left: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -2082,6 +2077,10 @@ header {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#row-step6-cookie-settings .step6-cookie-cleanup-setting {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.step-execution-range-setting {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -592,9 +592,9 @@
|
||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-auto-delay-settings">
|
||||
<div class="data-row" id="row-step6-cookie-settings">
|
||||
<span class="data-label">第六步</span>
|
||||
<div class="data-inline setting-pair auto-delay-setting-pair">
|
||||
<div class="data-inline">
|
||||
<div class="setting-group setting-group-primary step6-cookie-cleanup-setting">
|
||||
<span class="setting-caption setting-caption-left">清 Cookies</span>
|
||||
<label class="toggle-switch" for="input-step6-cookie-cleanup-enabled">
|
||||
@@ -604,6 +604,11 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-auto-delay-settings">
|
||||
<span class="data-label">启动前</span>
|
||||
<div class="data-inline setting-pair auto-delay-setting-pair">
|
||||
<div class="setting-group setting-group-secondary auto-run-delay-setting">
|
||||
<span class="setting-caption">延迟</span>
|
||||
<label class="toggle-switch" for="input-auto-delay-enabled">
|
||||
|
||||
@@ -54,7 +54,28 @@ test('kiro start device login opens the auth tab and waits for the email entry p
|
||||
const contentReadyCalls = [];
|
||||
const contentMessages = [];
|
||||
const stableWaitCalls = [];
|
||||
const removedCookies = [];
|
||||
const browsingDataCalls = [];
|
||||
const { chrome, updates: tabUpdates } = createChromeRecorder();
|
||||
chrome.cookies = {
|
||||
getAllCookieStores: async () => [{ id: 'store-a' }],
|
||||
getAll: async () => [
|
||||
{ domain: '.view.awsapps.com', path: '/start', name: 'awsapps', storeId: 'store-a' },
|
||||
{ domain: '.oidc.us-east-1.amazonaws.com', path: '/', name: 'oidc', storeId: 'store-a' },
|
||||
{ domain: '.signin.aws', path: '/', name: 'signin', storeId: 'store-a' },
|
||||
{ domain: '.profile.aws.amazon.com', path: '/', name: 'profile-amazon', storeId: 'store-a' },
|
||||
{ domain: '.example.com', path: '/', name: 'keep', storeId: 'store-a' },
|
||||
],
|
||||
remove: async (details) => {
|
||||
removedCookies.push(details);
|
||||
return details;
|
||||
},
|
||||
};
|
||||
chrome.browsingData = {
|
||||
removeCookies: async (details) => {
|
||||
browsingDataCalls.push(details);
|
||||
},
|
||||
};
|
||||
|
||||
const executor = api.createKiroDeviceAuthExecutor({
|
||||
addLog: async () => {},
|
||||
@@ -132,6 +153,40 @@ test('kiro start device login opens the auth tab and waits for the email entry p
|
||||
nodeId: 'kiro-start-device-login',
|
||||
});
|
||||
|
||||
assert.deepEqual(removedCookies, [
|
||||
{
|
||||
url: 'https://view.awsapps.com/start',
|
||||
name: 'awsapps',
|
||||
storeId: 'store-a',
|
||||
},
|
||||
{
|
||||
url: 'https://oidc.us-east-1.amazonaws.com/',
|
||||
name: 'oidc',
|
||||
storeId: 'store-a',
|
||||
},
|
||||
{
|
||||
url: 'https://signin.aws/',
|
||||
name: 'signin',
|
||||
storeId: 'store-a',
|
||||
},
|
||||
{
|
||||
url: 'https://profile.aws.amazon.com/',
|
||||
name: 'profile-amazon',
|
||||
storeId: 'store-a',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(browsingDataCalls, [{
|
||||
since: 0,
|
||||
origins: [
|
||||
'https://view.awsapps.com',
|
||||
'https://login.awsapps.com',
|
||||
'https://oidc.us-east-1.amazonaws.com',
|
||||
'https://signin.aws',
|
||||
'https://signin.aws.amazon.com',
|
||||
'https://profile.aws',
|
||||
'https://profile.aws.amazon.com',
|
||||
],
|
||||
}]);
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
assert.equal(fetchCalls[0].url, 'https://oidc.us-east-1.amazonaws.com/client/register');
|
||||
assert.equal(fetchCalls[1].url, 'https://oidc.us-east-1.amazonaws.com/device_authorization');
|
||||
|
||||
@@ -152,6 +152,60 @@ test('tab runtime gives step 5 profile submit enough response time for slow page
|
||||
);
|
||||
});
|
||||
|
||||
test('tab runtime replays retryable transport recovery hook and surfaces a localized timeout error', async () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
|
||||
|
||||
let recoveryCalls = 0;
|
||||
const runtime = api.createTabRuntime({
|
||||
LOG_PREFIX: '[test]',
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async () => ({
|
||||
id: 9,
|
||||
windowId: 1,
|
||||
url: 'https://profile.aws.amazon.com/complete',
|
||||
status: 'complete',
|
||||
}),
|
||||
query: async () => [],
|
||||
sendMessage: async () => {
|
||||
throw new Error('Could not establish connection. Receiving end does not exist.');
|
||||
},
|
||||
},
|
||||
},
|
||||
getSourceLabel: () => 'Kiro 授权页',
|
||||
getState: async () => ({
|
||||
tabRegistry: {
|
||||
'kiro-device-auth': { tabId: 9, ready: true },
|
||||
},
|
||||
sourceLastUrls: {},
|
||||
}),
|
||||
isRetryableContentScriptTransportError: (error) => /Receiving end does not exist/i.test(String(error?.message || error || '')),
|
||||
matchesSourceUrlFamily: () => false,
|
||||
setState: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
runtime.sendToContentScriptResilient('kiro-device-auth', {
|
||||
type: 'ENSURE_KIRO_PAGE_STATE',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 5,
|
||||
retryDelayMs: 0,
|
||||
onRetryableError: async () => {
|
||||
recoveryCalls += 1;
|
||||
},
|
||||
}),
|
||||
/页面刚完成跳转或刷新,内容脚本还没有重新接回/
|
||||
);
|
||||
|
||||
assert.equal(recoveryCalls > 0, true);
|
||||
});
|
||||
|
||||
test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -21,6 +21,10 @@ test('flow registry exposes openai and kiro with canonical source metadata', ()
|
||||
assert.equal(flowRegistry.normalizeFlowId('codex'), 'openai');
|
||||
assert.equal(flowRegistry.normalizeSourceId('openai', 'sub2api'), 'sub2api');
|
||||
assert.equal(flowRegistry.normalizeSourceId('kiro', 'anything-else'), 'kiro-rs');
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
|
||||
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-source-cpa', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
|
||||
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-account', 'service-email', 'service-proxy']
|
||||
@@ -33,6 +37,10 @@ test('flow registry exposes openai and kiro with canonical source metadata', ()
|
||||
flowRegistry.getSettingsGroupDefinition('openai-phone')?.rowIds || [],
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getSettingsGroupDefinition('openai-step6')?.rowIds || [],
|
||||
['row-step6-cookie-settings']
|
||||
);
|
||||
});
|
||||
|
||||
test('settings schema normalizes flat input into canonical flow and service namespaces', () => {
|
||||
|
||||
@@ -37,6 +37,7 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||
[
|
||||
'id="select-flow"',
|
||||
'id="label-source-selector"',
|
||||
'id="row-step6-cookie-settings"',
|
||||
'id="row-kiro-rs-url"',
|
||||
'id="row-kiro-rs-key"',
|
||||
'id="row-kiro-device-code"',
|
||||
|
||||
@@ -42,16 +42,19 @@ test('sidepanel no longer exposes operation delay switch and places step executi
|
||||
assert.doesNotMatch(html, /id="row-operation-delay-settings"/);
|
||||
assert.doesNotMatch(html, /id="input-operation-delay-enabled"/);
|
||||
|
||||
const step6CookieIndex = html.indexOf('id="row-step6-cookie-settings"');
|
||||
const autoDelayIndex = html.indexOf('id="row-auto-delay-settings"');
|
||||
const oauthTimeoutIndex = html.indexOf('id="row-oauth-flow-timeout"');
|
||||
const stepRangeIndex = html.indexOf('id="row-step-execution-range"');
|
||||
const oauthDisplayIndex = html.indexOf('id="row-oauth-display"');
|
||||
|
||||
assert.notEqual(step6CookieIndex, -1);
|
||||
assert.notEqual(autoDelayIndex, -1);
|
||||
assert.notEqual(oauthTimeoutIndex, -1);
|
||||
assert.notEqual(stepRangeIndex, -1);
|
||||
assert.notEqual(oauthDisplayIndex, -1);
|
||||
assert.ok(stepRangeIndex > autoDelayIndex, 'step execution range should still remain below the step6 toggle row');
|
||||
assert.ok(autoDelayIndex > step6CookieIndex, 'startup delay row should render below the openai step6 cookie row');
|
||||
assert.ok(stepRangeIndex > autoDelayIndex, 'step execution range should still remain below the startup delay row');
|
||||
assert.ok(stepRangeIndex > oauthTimeoutIndex, 'step execution range should render below oauth timeout');
|
||||
assert.ok(stepRangeIndex < oauthDisplayIndex, 'step execution range should stay above oauth runtime display');
|
||||
});
|
||||
|
||||
@@ -22,6 +22,25 @@ test('manifest loads shared source registry before content utils in static bundl
|
||||
}
|
||||
});
|
||||
|
||||
test('manifest ships a static Kiro auth bundle for cross-page recovery', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
const kiroEntry = (manifest.content_scripts || []).find((entry) => {
|
||||
const matches = Array.isArray(entry.matches) ? entry.matches : [];
|
||||
return matches.includes('https://view.awsapps.com/*')
|
||||
&& matches.includes('https://signin.aws/*')
|
||||
&& matches.includes('https://signin.aws.amazon.com/*')
|
||||
&& matches.includes('https://profile.aws/*')
|
||||
&& matches.includes('https://profile.aws.amazon.com/*');
|
||||
});
|
||||
|
||||
assert.ok(kiroEntry, 'missing static Kiro auth content script entry');
|
||||
assert.deepEqual(kiroEntry.js, [
|
||||
'shared/source-registry.js',
|
||||
'content/utils.js',
|
||||
'content/kiro-device-auth-page.js',
|
||||
]);
|
||||
});
|
||||
|
||||
test('shared source registry exposes canonical source, alias, detection, and ready policies', () => {
|
||||
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
|
||||
const source = fs.readFileSync('shared/source-registry.js', 'utf8');
|
||||
@@ -57,6 +76,54 @@ test('shared source registry exposes canonical source, alias, detection, and rea
|
||||
url: 'https://view.awsapps.com/start',
|
||||
hostname: 'view.awsapps.com',
|
||||
}), 'kiro-device-auth');
|
||||
assert.equal(registry.detectSourceFromLocation({
|
||||
url: 'https://signin.aws/register',
|
||||
hostname: 'signin.aws',
|
||||
}), 'kiro-device-auth');
|
||||
assert.equal(registry.detectSourceFromLocation({
|
||||
url: 'https://profile.aws/complete',
|
||||
hostname: 'profile.aws',
|
||||
}), 'kiro-device-auth');
|
||||
assert.equal(registry.detectSourceFromLocation({
|
||||
url: 'https://signin.aws.amazon.com/register',
|
||||
hostname: 'signin.aws.amazon.com',
|
||||
}), 'kiro-device-auth');
|
||||
assert.equal(registry.detectSourceFromLocation({
|
||||
url: 'https://profile.aws.amazon.com/complete',
|
||||
hostname: 'profile.aws.amazon.com',
|
||||
}), 'kiro-device-auth');
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'kiro-device-auth',
|
||||
'https://signin.aws/register',
|
||||
'https://view.awsapps.com/start'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'kiro-device-auth',
|
||||
'https://profile.aws/complete',
|
||||
'https://signin.aws/register'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'kiro-device-auth',
|
||||
'https://profile.aws.amazon.com/complete',
|
||||
'https://signin.aws.amazon.com/register'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'kiro-device-auth',
|
||||
'https://oidc.us-east-1.amazonaws.com/authorize',
|
||||
'https://view.awsapps.com/start'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
|
||||
assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
|
||||
assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
|
||||
|
||||
Reference in New Issue
Block a user