From daf703164a0cfd8803022631ea8c474398ad647f Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 22 May 2026 08:58:34 +0800 Subject: [PATCH] fix displayed email extraction boundaries --- flows/openai/content/openai-auth.js | 116 ++++++++++++++++++++++++++-- tests/step6-login-state.test.js | 81 +++++++++++++++++++ 2 files changed, 189 insertions(+), 8 deletions(-) diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index 2658fd1..da6100f 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -259,6 +259,112 @@ function getActionText(el) { .trim(); } +function hasEmailTokenBoundary(text, start, end) { + const before = start > 0 ? text[start - 1] : ''; + const after = end < text.length ? text[end] : ''; + return (!before || !/[A-Z0-9._%+-]/i.test(before)) + && (!after || !/[A-Z0-9_%+-]/i.test(after)); +} + +function isValidEmailCandidate(value = '') { + const candidate = String(value || '').trim(); + const atIndex = candidate.lastIndexOf('@'); + if (atIndex <= 0 || atIndex !== candidate.indexOf('@')) { + return false; + } + + const local = candidate.slice(0, atIndex); + const domain = candidate.slice(atIndex + 1); + if (!local || !domain || local.startsWith('.') || local.endsWith('.') || local.includes('..')) { + return false; + } + if (!/^[A-Z0-9._%+-]+$/i.test(local)) { + return false; + } + + const labels = domain.split('.'); + if (labels.length < 2) { + return false; + } + for (const label of labels) { + if (!/^[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?$/i.test(label)) { + return false; + } + } + + const tld = labels[labels.length - 1]; + return /^[A-Z]{2,63}$/i.test(tld) && !/[a-z][A-Z]/.test(tld); +} + +function getEmailFromTextFragment(value = '') { + const text = String(value || '').replace(/\s+/g, ' ').trim(); + if (!text) { + return ''; + } + const pattern = /[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,63}/ig; + for (const match of text.matchAll(pattern)) { + const candidate = String(match[0] || ''); + const start = Number(match.index || 0); + const end = start + candidate.length; + if (hasEmailTokenBoundary(text, start, end) && isValidEmailCandidate(candidate)) { + return candidate.toLowerCase(); + } + } + return ''; +} + +function getVisibleTextNodeEmail() { + if (typeof document?.createTreeWalker !== 'function' || !document.body) { + return ''; + } + const showText = typeof NodeFilter !== 'undefined' ? NodeFilter.SHOW_TEXT : 4; + const walker = document.createTreeWalker(document.body, showText); + let node = walker.nextNode(); + while (node) { + const parent = node.parentElement || node.parentNode || null; + const text = String(node.nodeValue || '').trim(); + if (text && (!parent || typeof isVisibleElement !== 'function' || isVisibleElement(parent))) { + const email = getEmailFromTextFragment(text); + if (email) { + return email; + } + } + node = walker.nextNode(); + } + return ''; +} + +function getDisplayedEmailFromDocument() { + const fieldCandidates = Array.from(document.querySelectorAll?.('input, textarea') || []); + for (const field of fieldCandidates) { + const email = getEmailFromTextFragment(field?.value || field?.getAttribute?.('value') || ''); + if (email) { + return email; + } + } + + const attributeCandidates = Array.from(document.querySelectorAll?.('[aria-label], [title]') || []); + for (const el of attributeCandidates) { + if (typeof isVisibleElement === 'function' && !isVisibleElement(el)) { + continue; + } + const email = getEmailFromTextFragment([ + el?.getAttribute?.('aria-label'), + el?.getAttribute?.('title'), + ].filter(Boolean).join(' ')); + if (email) { + return email; + } + } + + const textNodeEmail = getVisibleTextNodeEmail(); + if (textNodeEmail) { + return textNodeEmail; + } + + return ''; +} + function isActionEnabled(el) { return Boolean(el) && !el.disabled @@ -715,11 +821,7 @@ function findSignupEntryTrigger(options = {}) { } function getSignupPasswordDisplayedEmail() { - const text = (document.body?.innerText || document.body?.textContent || '') - .replace(/\s+/g, ' ') - .trim(); - const matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig); - return matches?.[0] ? String(matches[0]).trim().toLowerCase() : ''; + return getDisplayedEmailFromDocument(); } function inspectSignupEntryState() { @@ -3028,9 +3130,7 @@ function getPageTextSnapshot() { } function getLoginVerificationDisplayedEmail() { - const pageText = getPageTextSnapshot(); - const matches = pageText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || []; - return matches[0] ? String(matches[0]).trim().toLowerCase() : ''; + return getDisplayedEmailFromDocument(); } function getPhoneVerificationDisplayedPhone() { diff --git a/tests/step6-login-state.test.js b/tests/step6-login-state.test.js index b1d4630..529d4f5 100644 --- a/tests/step6-login-state.test.js +++ b/tests/step6-login-state.test.js @@ -51,6 +51,12 @@ function extractFunction(name) { } const bundle = [ + extractFunction('hasEmailTokenBoundary'), + extractFunction('isValidEmailCandidate'), + extractFunction('getEmailFromTextFragment'), + extractFunction('getVisibleTextNodeEmail'), + extractFunction('getDisplayedEmailFromDocument'), + extractFunction('getSignupPasswordDisplayedEmail'), extractFunction('getPageTextSnapshot'), extractFunction('getLoginVerificationDisplayedEmail'), extractFunction('getPhoneVerificationDisplayedPhone'), @@ -74,9 +80,39 @@ const document = { innerText: ${JSON.stringify(overrides.pageText || '')}, textContent: ${JSON.stringify(overrides.pageText || '')}, }, + querySelectorAll(selector) { + if (selector === 'input, textarea') { + return ${JSON.stringify(overrides.fields || [])}; + } + if (selector === '[aria-label], [title]') { + return ${JSON.stringify(overrides.attributeElements || [])}; + } + return []; + }, querySelector() { return null; }, + createTreeWalker() { + const textNodeValues = ${JSON.stringify( + Object.prototype.hasOwnProperty.call(overrides, 'textNodes') + ? overrides.textNodes + : [overrides.pageText || ''] + )}; + const nodes = textNodeValues.map((text) => ({ + nodeValue: text, + parentElement: { + getBoundingClientRect() { + return { width: 1, height: 1 }; + }, + }, + })); + let index = 0; + return { + nextNode() { + return nodes[index++] || null; + }, + }; + }, }; function getLoginTimeoutErrorPageState() { @@ -149,6 +185,8 @@ return { inspectLoginAuthState, isPhoneVerificationPageReady, normalizeStep6Snapshot, + getSignupPasswordDisplayedEmail, + getLoginVerificationDisplayedEmail, }; `)(); } @@ -179,6 +217,49 @@ return { assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com'); } +{ + const api = createApi({ + verificationTarget: { id: 'otp' }, + pageText: 'We emailed a code to skater-twine-carve@duck.comChange', + textNodes: ['We emailed a code to ', 'skater-twine-carve@duck.com', 'Change'], + }); + + const snapshot = api.inspectLoginAuthState(); + assert.strictEqual(snapshot.displayedEmail, 'skater-twine-carve@duck.com'); +} + +{ + const api = createApi({ + verificationTarget: { id: 'otp' }, + pageText: 'We emailed a code to skater-twine-carve@duck.comChange', + textNodes: ['We emailed a code to skater-twine-carve@duck.comChange'], + }); + + const snapshot = api.inspectLoginAuthState(); + assert.strictEqual(snapshot.displayedEmail, ''); +} + +{ + const api = createApi({ + pageText: 'Create your account skater-twine-carve@duck.comChange', + textNodes: ['Create your account ', 'skater-twine-carve@duck.com', 'Change'], + }); + + assert.strictEqual(api.getSignupPasswordDisplayedEmail(), 'skater-twine-carve@duck.com'); +} + +{ + const api = createApi({ + verificationTarget: { id: 'otp' }, + pageText: 'We emailed a code to skater-twine-carve@duck.comChange', + textNodes: ['We emailed a code to skater-twine-carve@duck.comChange'], + fields: [{ value: 'skater-twine-carve@duck.com' }], + }); + + const snapshot = api.inspectLoginAuthState(); + assert.strictEqual(snapshot.displayedEmail, 'skater-twine-carve@duck.com'); +} + { const api = createApi({ pathname: '/email-verification',