Fix Kiro password field detection

This commit is contained in:
QLHazyCoder
2026-05-20 00:28:06 +08:00
parent 4687d07e2d
commit f165b80597
2 changed files with 311 additions and 26 deletions
+169 -24
View File
@@ -41,22 +41,10 @@ const KIRO_OTP_INPUT_SELECTOR = [
'input[name*="code" i]',
].join(', ');
const KIRO_PASSWORD_INPUT_SELECTOR = [
'input[placeholder="Enter password"]',
'input[placeholder*="Create password" i]',
'input[placeholder*="Password" i]',
'input[name="password"]',
'input[id*="password" i]',
'input[type="password"]',
].join(', ');
const KIRO_CONFIRM_PASSWORD_SELECTOR = [
'input[placeholder*="Re-enter password" i]',
'input[placeholder*="Confirm password" i]',
'input[placeholder*="Confirm Password" i]',
'input[name="confirmPassword"]',
'input[id*="confirm" i]',
].join(', ');
const KIRO_PASSWORD_FIELD_SELECTOR = 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="file"])';
const KIRO_PASSWORD_TEXT_PATTERN = /password|\u5bc6\u7801/i;
const KIRO_CONFIRM_PASSWORD_TEXT_PATTERN = /confirm\s*password|re[-\s]*enter\s*password|repeat\s*password|verify\s*password|\u786e\u8ba4\s*\u5bc6\u7801|\u518d\u6b21.*\u5bc6\u7801|\u91cd\u590d.*\u5bc6\u7801/i;
const KIRO_PRIMARY_PASSWORD_HINT_PATTERN = /enter\s*password|create\s*password|new\s*password|\u8f93\u5165.*\u5bc6\u7801|\u521b\u5efa.*\u5bc6\u7801|^\s*\u5bc6\u7801\s*$/i;
function isVisibleKiroElement(element) {
if (!element) return false;
@@ -190,12 +178,156 @@ function findVisibleOtpInput() {
return findFirstVisible(KIRO_OTP_INPUT_SELECTOR);
}
function getKiroElementText(element) {
return String(element?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
function cssEscapeKiroValue(value = '') {
if (globalThis.CSS?.escape) {
return globalThis.CSS.escape(String(value));
}
return String(value).replace(/["\\]/g, '\\$&');
}
function getKiroInputLabelText(input) {
const labels = [];
for (const label of Array.from(input?.labels || [])) {
const text = getKiroElementText(label);
if (text) labels.push(text);
}
const id = String(input?.id || '').trim();
if (id) {
for (const label of Array.from(document.querySelectorAll(`label[for="${cssEscapeKiroValue(id)}"]`))) {
const text = getKiroElementText(label);
if (text) labels.push(text);
}
}
const labelledBy = String(input?.getAttribute?.('aria-labelledby') || '').trim();
if (labelledBy) {
for (const labelId of labelledBy.split(/\s+/).filter(Boolean)) {
const label = document.getElementById?.(labelId);
const text = getKiroElementText(label);
if (text) labels.push(text);
}
}
const parentLabel = input?.closest?.('label');
const parentLabelText = getKiroElementText(parentLabel);
if (parentLabelText) labels.push(parentLabelText);
return Array.from(new Set(labels)).join(' ');
}
function getKiroPasswordInputContext(input) {
const describedBy = String(input?.getAttribute?.('aria-describedby') || '').trim();
const describedByText = describedBy
? describedBy
.split(/\s+/)
.map((id) => getKiroElementText(document.getElementById?.(id)))
.filter(Boolean)
.join(' ')
: '';
return [
input?.getAttribute?.('placeholder'),
input?.getAttribute?.('name'),
input?.id,
input?.getAttribute?.('aria-label'),
input?.getAttribute?.('data-testid'),
input?.getAttribute?.('autocomplete'),
getKiroInputLabelText(input),
describedByText,
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function createKiroPasswordCandidate(input, index) {
const context = getKiroPasswordInputContext(input);
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
const isConfirm = KIRO_CONFIRM_PASSWORD_TEXT_PATTERN.test(context);
const hasPasswordHint = KIRO_PASSWORD_TEXT_PATTERN.test(context) || type === 'password';
if (!hasPasswordHint && !isConfirm) {
return null;
}
let primaryScore = 0;
let confirmScore = 0;
if (type === 'password') primaryScore += 5;
if (KIRO_PASSWORD_TEXT_PATTERN.test(context)) primaryScore += 20;
if (KIRO_PRIMARY_PASSWORD_HINT_PATTERN.test(context)) primaryScore += 40;
if (isConfirm) {
primaryScore -= 100;
confirmScore += 100;
}
if (/^password$/i.test(String(input?.getAttribute?.('name') || ''))) primaryScore += 40;
if (/^password$/i.test(String(input?.id || ''))) primaryScore += 20;
return {
input,
index,
context,
isConfirm,
primaryScore,
confirmScore,
};
}
function findKiroPasswordFields() {
const candidates = collectVisibleElements(KIRO_PASSWORD_FIELD_SELECTOR)
.map((input, index) => createKiroPasswordCandidate(input, index))
.filter(Boolean);
if (!candidates.length) {
return {
passwordInput: null,
confirmPasswordInput: null,
passwordInputs: [],
};
}
const confirmCandidate = candidates
.filter((candidate) => candidate.isConfirm)
.sort((left, right) => right.confirmScore - left.confirmScore || left.index - right.index)[0] || null;
let passwordCandidate = candidates
.filter((candidate) => !candidate.isConfirm)
.sort((left, right) => right.primaryScore - left.primaryScore || left.index - right.index)[0] || null;
if (!passwordCandidate) {
passwordCandidate = candidates
.filter((candidate) => !confirmCandidate || candidate.input !== confirmCandidate.input)
.sort((left, right) => left.index - right.index)[0] || null;
}
let resolvedConfirmCandidate = confirmCandidate;
if (
(!resolvedConfirmCandidate || resolvedConfirmCandidate.input === passwordCandidate?.input)
&& passwordCandidate
) {
resolvedConfirmCandidate = candidates
.filter((candidate) => candidate.input !== passwordCandidate.input)
.sort((left, right) => left.index - right.index)[0] || null;
}
return {
passwordInput: passwordCandidate?.input || null,
confirmPasswordInput: resolvedConfirmCandidate?.input || null,
passwordInputs: candidates.map((candidate) => candidate.input),
};
}
function findVisiblePasswordInputs() {
return collectVisibleElements(KIRO_PASSWORD_INPUT_SELECTOR);
return findKiroPasswordFields().passwordInputs;
}
function findVisibleConfirmPasswordInput() {
return findFirstVisible(KIRO_CONFIRM_PASSWORD_SELECTOR);
return findKiroPasswordFields().confirmPasswordInput;
}
function findEmailContinueButton(emailInput = null) {
@@ -378,15 +510,14 @@ function detectKiroRegisterPageState() {
}
}
const passwordInputs = findVisiblePasswordInputs();
const confirmPasswordInput = findVisibleConfirmPasswordInput();
if (passwordInputs.length) {
const passwordInput = passwordInputs[0];
const passwordFields = findKiroPasswordFields();
if (passwordFields.passwordInput) {
const passwordInput = passwordFields.passwordInput;
return {
state: 'password_page',
url: currentUrl,
passwordInput,
confirmPasswordInput,
confirmPasswordInput: passwordFields.confirmPasswordInput,
continueButton: findPasswordContinueButton(passwordInput),
};
}
@@ -645,15 +776,29 @@ async function submitKiroPassword(payload = {}) {
fillInput(readyState.passwordInput, password);
await sleep(150);
if (String(readyState.passwordInput.value || '') !== password) {
throw new Error('Kiro \u5bc6\u7801\u9875\u4e3b\u5bc6\u7801\u6846\u586b\u5165\u5931\u8d25\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\uff0c\u8bf7\u68c0\u67e5\u9875\u9762\u5b57\u6bb5\u7ed3\u6784\u3002');
}
const confirmPasswordInput = readyState.confirmPasswordInput
|| (() => {
const passwordInputs = findVisiblePasswordInputs();
return passwordInputs.length > 1 ? passwordInputs[1] : null;
return passwordInputs.find((input) => input !== readyState.passwordInput) || null;
})();
if (confirmPasswordInput && confirmPasswordInput !== readyState.passwordInput) {
fillInput(confirmPasswordInput, password);
await sleep(150);
if (String(confirmPasswordInput.value || '') !== password) {
throw new Error('Kiro \u5bc6\u7801\u9875\u786e\u8ba4\u5bc6\u7801\u6846\u586b\u5165\u5931\u8d25\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\uff0c\u8bf7\u68c0\u67e5\u9875\u9762\u5b57\u6bb5\u7ed3\u6784\u3002');
}
}
if (
confirmPasswordInput
&& confirmPasswordInput !== readyState.passwordInput
&& String(confirmPasswordInput.value || '') !== String(readyState.passwordInput.value || '')
) {
throw new Error('Kiro \u5bc6\u7801\u9875\u4e24\u4e2a\u5bc6\u7801\u6846\u5185\u5bb9\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\u3002');
}
simulateClick(readyState.continueButton);
+142 -2
View File
@@ -5,7 +5,86 @@ const vm = require('node:vm');
const source = fs.readFileSync('content/kiro/register-page.js', 'utf8');
function createTextNode(textContent = '') {
return { textContent };
}
function createInputElement({
id = '',
type = 'text',
placeholder = '',
label = '',
value = '',
} = {}) {
const attributes = {
id,
type,
placeholder,
autocomplete: 'off',
};
return {
tagName: 'INPUT',
id,
type,
value,
labels: label ? [createTextNode(label)] : [],
disabled: false,
readOnly: false,
form: null,
getAttribute(name) {
return attributes[name] || '';
},
getBoundingClientRect() {
return { width: 455, height: 32 };
},
closest(selector) {
if (selector === 'label') {
return null;
}
return null;
},
};
}
function createButtonElement({ text = 'Continue', testId = 'test-primary-button' } = {}) {
return {
tagName: 'BUTTON',
textContent: text,
value: '',
disabled: false,
form: null,
getAttribute(name) {
if (name === 'data-testid') return testId;
if (name === 'type') return 'submit';
return '';
},
getBoundingClientRect() {
return { width: 455, height: 32 };
},
closest() {
return null;
},
};
}
function createHarness({ href, hostname, title = '', bodyText = '' }) {
return createDomHarness({ href, hostname, title, bodyText });
}
function createDomHarness({
href,
hostname,
title = '',
bodyText = '',
inputs = [],
buttons = [],
} = {}) {
const elementsById = new Map();
for (const element of [...inputs, ...buttons]) {
if (element.id) {
elementsById.set(element.id, element);
}
}
const context = {
console: { log() {}, warn() {}, error() {}, info() {} },
location: { href, hostname },
@@ -20,10 +99,22 @@ function createHarness({ href, hostname, title = '', bodyText = '' }) {
},
setAttribute() {},
},
getElementById(id) {
return elementsById.get(id) || null;
},
querySelector() {
return null;
},
querySelectorAll() {
querySelectorAll(selector) {
if (String(selector).startsWith('label[for=')) {
return [];
}
if (String(selector).trim().startsWith('input')) {
return inputs;
}
if (String(selector).includes('button') || String(selector).includes('[role="button"]')) {
return buttons;
}
return [];
},
},
@@ -37,14 +128,18 @@ function createHarness({ href, hostname, title = '', bodyText = '' }) {
sleep() {
return Promise.resolve();
},
fillInput() {},
fillInput(element, value) {
element.value = value;
},
MouseEvent: class {},
PointerEvent: class {},
KeyboardEvent: class {},
Event: class {},
};
context.window = context;
context.globalThis = context;
context.window.getComputedStyle = () => ({ display: 'block', visibility: 'visible' });
context.CSS = { escape: (value) => String(value) };
vm.createContext(context);
vm.runInContext(source, context);
@@ -105,3 +200,48 @@ test('kiro register content extracts signed-in account email from Kiro account p
assert.equal(detected.accountEmail, 'scrap-aged-quirk@duck.com');
assert.equal(detected.email, 'scrap-aged-quirk@duck.com');
});
test('kiro register content fills primary and confirm password fields separately', async () => {
const passwordInput = createInputElement({
id: 'formField15-1779204320095-7559',
type: 'text',
placeholder: 'Enter password',
label: '\u5bc6\u7801',
});
const confirmPasswordInput = createInputElement({
id: 'formField16-1779204320096-1309',
type: 'text',
placeholder: 'Re-enter password',
label: '\u786e\u8ba4\u5bc6\u7801',
});
const checkboxInput = createInputElement({
id: '17-1779204320097-4334',
type: 'checkbox',
label: '\u663e\u793a\u5bc6\u7801',
value: 'on',
});
const continueButton = createButtonElement();
const clicks = [];
const harness = createDomHarness({
href: 'https://us-east-1.signin.aws/platform/d-9067642ac7/signup',
hostname: 'us-east-1.signin.aws',
title: 'Amazon Web Services',
bodyText: 'Create your password',
inputs: [passwordInput, confirmPasswordInput, checkboxInput],
buttons: [continueButton],
});
harness.simulateClick = (element) => clicks.push(element);
const detected = harness.detectKiroRegisterPageState();
assert.equal(detected.state, 'password_page');
assert.equal(detected.passwordInput, passwordInput);
assert.equal(detected.confirmPasswordInput, confirmPasswordInput);
const result = await harness.submitKiroPassword({ password: 'mdy8U9_rzqhw6D' });
assert.equal(result.state, 'password_submitted');
assert.equal(passwordInput.value, 'mdy8U9_rzqhw6D');
assert.equal(confirmPasswordInput.value, 'mdy8U9_rzqhw6D');
assert.deepEqual(clicks, [continueButton]);
});