// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5) // Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com console.log('[MultiPage:signup-page] Content script loaded on', location.href); // Listen for commands from Background chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK' || message.type === 'CLICK_RESEND_EMAIL' || message.type === 'HANDLE_ABOUT_YOU') { resetStopState(); handleCommand(message).then((result) => { sendResponse({ ok: true, ...(result || {}) }); }).catch(err => { if (isStopError(err)) { log(`Step ${message.step || 8}: Stopped by user.`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } if (message.type === 'STEP8_FIND_AND_CLICK') { log(`Step 8: ${err.message}`, 'error'); sendResponse({ error: err.message }); return; } reportError(message.step, err.message); sendResponse({ error: err.message }); }); return true; } }); async function handleCommand(message) { switch (message.type) { case 'EXECUTE_STEP': switch (message.step) { case 2: return await step2_clickRegister(); case 3: return await step3_fillEmailPassword(message.payload); case 5: return await step5_fillNameBirthday(message.payload); case 6: return await step6_login(message.payload); case 8: return await step8_findAndClick(); default: throw new Error(`signup-page.js does not handle step ${message.step}`); } case 'FILL_CODE': // Step 4 = signup code, Step 7 = login code (same handler) return await fillVerificationCode(message.step, message.payload); case 'CLICK_RESEND_EMAIL': return await clickResendEmail(message.step); case 'STEP8_FIND_AND_CLICK': return await step8_findAndClick(); case 'HANDLE_ABOUT_YOU': return await handleAboutYouPage(message.payload); } } // ============================================================ // Step 2: Click Register // ============================================================ async function step2_clickRegister() { log('Step 2: Looking for Register/Sign up button...'); let registerBtn = null; try { registerBtn = await waitForElementByText( 'a, button, [role="button"], [role="link"]', /sign\s*up|register|create\s*account|注册/i, 10000 ); } catch { // Some pages may have a direct link try { registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000); } catch { throw new Error( 'Could not find Register/Sign up button. ' + 'Check auth page DOM in DevTools. URL: ' + location.href ); } } await humanPause(450, 1200); simulateClick(registerBtn); log('Step 2: Clicked Register button'); // Don't mark step 2 complete until the signup form is actually reachable. // This avoids auto-run racing ahead while a human-verification challenge // or page transition is still in progress. try { await waitForElement( 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]', 30000 ); log('Step 2: Signup form is ready after human verification/page transition'); } catch { // Fallback: some auth pages may delay rendering, but URL/path has already moved on. if (/signup|register|email|continue/i.test(location.href)) { log('Step 2: Signup page transition detected, proceeding', 'warn'); } else { throw new Error('Register clicked, but signup form did not become ready after 30s. Human verification may still be pending. URL: ' + location.href); } } reportComplete(2); } // ============================================================ // Step 3: Fill Email & Password // ============================================================ async function step3_fillEmailPassword(payload) { const { email } = payload; if (!email) throw new Error('No email provided. Paste email in Side Panel first.'); log(`Step 3: Filling email: ${email}`); // Find email input let emailInput = null; try { emailInput = await waitForElement( 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]', 10000 ); } catch { throw new Error('Could not find email input field on signup page. URL: ' + location.href); } await humanPause(500, 1400); fillInput(emailInput, email); log('Step 3: Email filled'); // Check if password field is on the same page let passwordInput = document.querySelector('input[type="password"]'); if (!passwordInput) { // Need to submit email first to get to password page log('Step 3: No password field yet, submitting email first...'); const submitBtn = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null); if (submitBtn) { await humanPause(400, 1100); simulateClick(submitBtn); log('Step 3: Submitted email, waiting for password field...'); await sleep(2000); } try { passwordInput = await waitForElement('input[type="password"]', 10000); } catch { throw new Error('Could not find password input after submitting email. URL: ' + location.href); } } if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.'); await humanPause(600, 1500); fillInput(passwordInput, payload.password); log('Step 3: Password filled'); // Report complete BEFORE submit, because submit causes page navigation // which kills the content script connection reportComplete(3, { email }); // Submit the form (page will navigate away after this) await sleep(500); const submitBtn = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null); if (submitBtn) { await humanPause(500, 1300); simulateClick(submitBtn); log('Step 3: Form submitted'); } } // ============================================================ // Click "重新发送电子邮件" (used before step 4 and step 7 polling) // ============================================================ async function clickResendEmail(step) { log(`Step ${step}: Looking for "重新发送电子邮件" button...`); let resendBtn = null; try { resendBtn = await waitForElementByText( 'a, button, [role="button"], [role="link"], span', /重新发送电子邮件|resend\s*email/i, 10000 ); } catch { log(`Step ${step}: "重新发送电子邮件" button not found, skipping`, 'warn'); return; } // Prevent parent form POST submission (Remix/React Router route without action) const parentForm = resendBtn.closest('form'); const blockSubmit = (e) => e.preventDefault(); if (parentForm) parentForm.addEventListener('submit', blockSubmit, { once: true }); await humanPause(400, 1000); resendBtn.click(); log(`Step ${step}: Clicked "重新发送电子邮件"`, 'ok'); await sleep(2000); if (parentForm) parentForm.removeEventListener('submit', blockSubmit); } // ============================================================ // Fill Verification Code (used by step 4 and step 7) // ============================================================ async function fillVerificationCode(step, payload) { const { code } = payload; if (!code) throw new Error('No verification code provided.'); log(`Step ${step}: Filling verification code: ${code}`); // Find code input — could be a single input or multiple separate inputs let codeInput = null; try { codeInput = await waitForElement( 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]', 10000 ); } catch { // Check for multiple single-digit inputs (common pattern) const singleInputs = document.querySelectorAll('input[maxlength="1"]'); if (singleInputs.length >= 6) { log(`Step ${step}: Found single-digit code inputs, filling individually...`); for (let i = 0; i < 6 && i < singleInputs.length; i++) { fillInput(singleInputs[i], code[i]); await sleep(100); } await sleep(1000); // Verify page navigated away from verification page await verifyCodeAccepted(step); reportComplete(step); return; } throw new Error('Could not find verification code input. URL: ' + location.href); } fillInput(codeInput, code); log(`Step ${step}: Code filled`); // Submit await sleep(500); const submitBtn = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证|继续/i, 5000).catch(() => null); if (submitBtn) { await humanPause(450, 1200); simulateClick(submitBtn); log(`Step ${step}: Verification submitted`); } // Wait and verify the page actually moved past the verification page await verifyCodeAccepted(step); reportComplete(step); } function hasVisibleVerificationInputs() { const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]')) .filter(isElementVisible); if (singleInputs.length >= 6) return true; const selectors = [ 'input[name="code"]', 'input[name="otp"]', 'input[type="text"][maxlength="6"]', 'input[aria-label*="code" i]', 'input[placeholder*="code" i]', 'input[inputmode="numeric"]', ]; return selectors.some(selector => Array.from(document.querySelectorAll(selector)).some(isElementVisible)); } function hasPostVerificationSignals() { const currentPath = location.pathname; if (currentPath.includes('/about-you')) return true; const nextStepSelectors = [ 'input[name="name"]', 'input[autocomplete="name"]', 'input[name="birthday"]', '[role="spinbutton"][data-type="year"]', 'button[data-testid*="continue"]', ]; if (nextStepSelectors.some(selector => Array.from(document.querySelectorAll(selector)).some(isElementVisible))) { return true; } const pageText = (document.body?.innerText || '').slice(0, 4000).toLowerCase(); return ( pageText.includes('使用 chatgpt 登录到 codex') || pageText.includes('continue to codex') || pageText.includes('consent') || pageText.includes('生日') || pageText.includes('full name') ); } async function verifyCodeAccepted(step, timeout = 15000) { const start = Date.now(); const verificationPaths = ['/email-verification', '/verify', '/otp']; while (Date.now() - start < timeout) { throwIfStopped(); const currentPath = location.pathname; const stillOnVerification = verificationPaths.some(p => currentPath.includes(p)); const verificationInputsVisible = hasVisibleVerificationInputs(); const postVerificationSignals = hasPostVerificationSignals(); if (!stillOnVerification || (!verificationInputsVisible && postVerificationSignals)) { log(`Step ${step}: Verification code accepted, page navigated to ${currentPath}`); return; } // Check for error messages on the page (wrong code) const errorEl = document.querySelector('[class*="error"], [class*="Error"], [role="alert"]'); if (errorEl) { const errorText = (errorEl.textContent || '').trim(); if (errorText && errorText.length < 200) { throw new Error(`Verification code rejected: ${errorText}. URL: ${location.href}`); } } await sleep(500); } // Still on verification page after timeout — code was likely wrong throw new Error(`Verification code ${step === 4 ? 'signup' : 'login'} was not accepted (page did not navigate). URL: ${location.href}`); } // ============================================================ // Step 6: Login with registered account (on OAuth auth page) // ============================================================ async function step6_login(payload) { const { email, password } = payload; if (!email) throw new Error('No email provided for login.'); log(`Step 6: Logging in with ${email}...`); // Wait for email input on the auth page let emailInput = null; try { emailInput = await waitForElement( 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]', 15000 ); } catch { throw new Error('Could not find email input on login page. URL: ' + location.href); } await humanPause(500, 1400); fillInput(emailInput, email); log('Step 6: Email filled'); // Submit email await sleep(500); const submitBtn1 = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null); if (submitBtn1) { await humanPause(400, 1100); simulateClick(submitBtn1); log('Step 6: Submitted email'); } const passwordInput = await waitForLoginPasswordField(); if (passwordInput) { log('Step 6: Password field found, filling password...'); await humanPause(550, 1450); fillInput(passwordInput, password); await sleep(500); const submitBtn2 = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null); // Report complete BEFORE submit in case page navigates reportComplete(6, { needsOTP: true }); if (submitBtn2) { await humanPause(450, 1200); simulateClick(submitBtn2); log('Step 6: Submitted password, may need verification code (step 7)'); } return; } // No password field — OTP flow log('Step 6: No password field. OTP flow or auto-redirect.'); reportComplete(6, { needsOTP: true }); } async function waitForLoginPasswordField(timeout = 25000) { const start = Date.now(); while (Date.now() - start < timeout) { throwIfStopped(); const passwordInput = findVisiblePasswordInput(); if (passwordInput) { return passwordInput; } await sleep(250); } log(`Step 6: Password field did not appear within ${Math.round(timeout / 1000)}s.`, 'warn'); return null; } function findVisiblePasswordInput() { const inputs = document.querySelectorAll('input[type="password"]'); for (const input of inputs) { if (isElementVisible(input)) { return input; } } return null; } function isElementVisible(el) { if (!el) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } // ============================================================ // Step 8: Find "继续" on OAuth consent page for debugger click // ============================================================ // After login + verification, page shows: // "使用 ChatGPT 登录到 Codex" with a "继续" submit button. // Background performs the actual click through the debugger Input API. async function step8_findAndClick() { log('Step 8: Looking for OAuth consent "继续" button...'); const continueBtn = await findContinueButton(); await waitForButtonEnabled(continueBtn); await humanPause(350, 900); continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); continueBtn.focus(); await sleep(250); // Click using native .click() — more reliable than synthetic MouseEvent for trusted forms continueBtn.click(); log('Step 8: Clicked "继续" button via .click()', 'ok'); // Fallback: if the button is inside a form, also try submitting the form directly await sleep(2000); const form = continueBtn.closest('form'); if (form && location.href.includes('authorize')) { log('Step 8: Also submitting parent form as fallback...'); form.requestSubmit ? form.requestSubmit(continueBtn) : form.submit(); } return { clicked: true, buttonText: (continueBtn.textContent || '').trim(), url: location.href, }; } async function findContinueButton() { try { return await waitForElement( 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107', 10000 ); } catch { try { return await waitForElementByText('button', /继续|Continue/, 5000); } catch { throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href); } } } async function waitForButtonEnabled(button, timeout = 8000) { const start = Date.now(); while (Date.now() - start < timeout) { throwIfStopped(); if (isButtonEnabled(button)) return; await sleep(150); } throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href); } function isButtonEnabled(button) { return Boolean(button) && !button.disabled && button.getAttribute('aria-disabled') !== 'true'; } function getSerializableRect(el) { const rect = el.getBoundingClientRect(); if (!rect.width || !rect.height) { throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href); } return { left: rect.left, top: rect.top, width: rect.width, height: rect.height, centerX: rect.left + (rect.width / 2), centerY: rect.top + (rect.height / 2), }; } // ============================================================ // Handle /about-you page (appears after login if birthday was missing) // ============================================================ async function handleAboutYouPage(payload) { if (!location.href.includes('/about-you')) { return { handled: false }; } log('Detected /about-you page, filling birthday info...'); const { year, month, day, fullName } = payload || {}; if (!year || !month || !day) { throw new Error('No birthday data available for about-you page.'); } // Wait for the page to fully load await sleep(1000); // Fill name if present and empty const nameInput = document.querySelector('input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]'); if (nameInput && !nameInput.value && fullName) { fillInput(nameInput, fullName); log('About-you: Name filled'); await humanPause(300, 800); } // Fill birthday using the shared helper await fillBirthdayFields(year, month, day, 'About-you'); // Click continue/submit button await sleep(500); const submitBtn = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /continue|继续|完成|done|agree|submit/i, 5000).catch(() => null); if (submitBtn) { await humanPause(500, 1300); simulateClick(submitBtn); log('About-you: Submitted form'); } return { handled: true }; } // ============================================================ // Shared birthday filling helper (used by Step 5 and about-you) // ============================================================ async function fillBirthdayFields(year, month, day, logPrefix) { const prefix = logPrefix || 'Birthday'; // Strategy 1: React Aria DateField spinbuttons const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]'); const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]'); const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]'); if (yearSpinner && monthSpinner && daySpinner) { log(`${prefix}: Filling spinbutton date fields...`); async function setSpinButton(el, value) { el.focus(); await sleep(100); document.execCommand('selectAll', false, null); await sleep(50); const valueStr = String(value); for (const char of valueStr) { el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true })); el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true })); el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true })); el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true })); await sleep(50); } el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true })); el.blur(); await sleep(100); } await humanPause(450, 1100); await setSpinButton(yearSpinner, year); await humanPause(250, 650); await setSpinButton(monthSpinner, String(month).padStart(2, '0')); await humanPause(250, 650); await setSpinButton(daySpinner, String(day).padStart(2, '0')); log(`${prefix}: Spinbutton date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); return true; } // Strategy 2: Select dropdowns (month/day/year) const selects = document.querySelectorAll('select'); if (selects.length >= 2) { let monthSelect = null, daySelect = null, yearSelect = null; for (const sel of selects) { const name = (sel.name || sel.id || sel.getAttribute('aria-label') || '').toLowerCase(); const opts = Array.from(sel.options).map(o => o.value); if (name.includes('month') || name.includes('mm')) { monthSelect = sel; } else if (name.includes('day') || name.includes('dd')) { daySelect = sel; } else if (name.includes('year') || name.includes('yyyy')) { yearSelect = sel; } else { // Heuristic: identify by option count and values const numericOpts = opts.filter(v => /^\d+$/.test(v)); if (!monthSelect && numericOpts.length >= 12 && numericOpts.length <= 13) { monthSelect = sel; } else if (!daySelect && numericOpts.length >= 28 && numericOpts.length <= 32) { daySelect = sel; } else if (!yearSelect && numericOpts.some(v => Number(v) > 1900 && Number(v) < 2100)) { yearSelect = sel; } } } if (monthSelect || daySelect || yearSelect) { log(`${prefix}: Filling select dropdown date fields...`); if (monthSelect) { setSelectValue(monthSelect, String(month)); await humanPause(200, 500); } if (daySelect) { setSelectValue(daySelect, String(day)); await humanPause(200, 500); } if (yearSelect) { setSelectValue(yearSelect, String(year)); await humanPause(200, 500); } log(`${prefix}: Select date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); return true; } } // Strategy 3: input[type="date"] const dateInput = document.querySelector('input[type="date"]'); if (dateInput) { const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; nativeSetter.call(dateInput, dateStr); dateInput.dispatchEvent(new Event('input', { bubbles: true })); dateInput.dispatchEvent(new Event('change', { bubbles: true })); log(`${prefix}: Date input filled: ${dateStr}`); return true; } // Strategy 4: Hidden input[name="birthday"] (fallback) const hiddenBirthday = document.querySelector('input[name="birthday"]'); if (hiddenBirthday) { const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; hiddenBirthday.value = dateStr; hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true })); log(`${prefix}: Hidden birthday input set: ${dateStr}`); return true; } return false; } function setSelectValue(selectEl, value) { // Try exact match first, then try padded value const candidates = [value, value.padStart(2, '0')]; for (const v of candidates) { const option = Array.from(selectEl.options).find(o => o.value === v || o.textContent.trim() === v); if (option) { selectEl.value = option.value; selectEl.dispatchEvent(new Event('change', { bubbles: true })); selectEl.dispatchEvent(new Event('input', { bubbles: true })); return; } } // Last resort: set by index if value is numeric const numVal = Number(value); if (!isNaN(numVal) && numVal > 0 && numVal < selectEl.options.length) { selectEl.selectedIndex = numVal; selectEl.dispatchEvent(new Event('change', { bubbles: true })); selectEl.dispatchEvent(new Event('input', { bubbles: true })); } } // ============================================================ // Step 5: Fill Name & Birthday / Age // ============================================================ async function step5_fillNameBirthday(payload) { const { firstName, lastName, age, year, month, day } = payload; if (!firstName || !lastName) throw new Error('No name data provided.'); const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null); const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value))); if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) { throw new Error('No birthday or age data provided.'); } const fullName = `${firstName} ${lastName}`; log(`Step 5: Filling name: ${fullName}`); // Actual DOM structure: // - Full name: // - Birthday: React Aria DateField or hidden input[name="birthday"] // - Age: // --- Full Name (single field, not first+last) --- let nameInput = null; try { nameInput = await waitForElement( 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]', 10000 ); } catch { throw new Error('Could not find name input. URL: ' + location.href); } await humanPause(500, 1300); fillInput(nameInput, fullName); log(`Step 5: Name filled: ${fullName}`); // Detect birthday/age input type with polling let ageInput = null; let hasBirthdayUI = false; for (let i = 0; i < 100; i++) { ageInput = document.querySelector('input[name="age"]'); // Some pages include a hidden birthday input even though the real UI is "age". // In that case we must prioritize filling age to satisfy required validation. if (ageInput) break; // Check for any supported birthday UI (spinbuttons, selects, date input, hidden) const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]'); const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]'); const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]'); const hiddenBirthday = document.querySelector('input[name="birthday"]'); const dateInput = document.querySelector('input[type="date"]'); const selects = document.querySelectorAll('select'); if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday || dateInput || selects.length >= 2) { hasBirthdayUI = true; break; } await sleep(100); } if (hasBirthdayUI) { if (!hasBirthdayData) { throw new Error('Birthday field detected, but no birthday data provided.'); } // Use shared helper that tries spinbuttons → selects → date input → hidden input const filled = await fillBirthdayFields(year, month, day, 'Step 5'); if (!filled) { log('Step 5: Warning - could not fill any visible birthday control', 'warn'); } } else if (ageInput) { if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) { throw new Error('Age field detected, but no age data provided.'); } await humanPause(500, 1300); fillInput(ageInput, String(resolvedAge)); log(`Step 5: Age filled: ${resolvedAge}`); // Some age-mode pages still submit a hidden birthday field. // Keep it aligned with generated data so backend validation won't reject. const hiddenBirthday = document.querySelector('input[name="birthday"]'); if (hiddenBirthday && hasBirthdayData) { const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; hiddenBirthday.value = dateStr; hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true })); log(`Step 5: Hidden birthday input set (age mode): ${dateStr}`); } } else { throw new Error('Could not find birthday or age input. URL: ' + location.href); } // Click "完成帐户创建" button await sleep(500); const completeBtn = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null); // Report complete BEFORE submit (page navigates to add-phone after this) reportComplete(5); if (completeBtn) { await humanPause(500, 1300); simulateClick(completeBtn); log('Step 5: Clicked "完成帐户创建"'); } }