@@ -52992,6 +52992,7 @@ function captchaEl(tag, attrs = {}, children = []) {
5299252992 src: attrs.src,
5299352993 innerText: attrs.innerText || '',
5299452994 textContent: attrs.textContent || attrs.innerText || '',
52995+ value: attrs.value || '',
5299552996 hidden: attrs.hidden === true,
5299652997 style: {},
5299752998 classList: { contains: (c) => String(attrs.class || '').split(/\s+/).includes(c) },
@@ -53123,6 +53124,13 @@ async function detectCaptchaOnFakePage(build, nodes) {
5312353124 });
5312453125}
5312553126
53127+ async function detectCaptchaDetailsOnFakePage(build, nodes) {
53128+ return withCaptchaFakePage(build, nodes, async () => {
53129+ const mod = await import(pathToFileURL(path.join(ROOT, `src/${build}/src/agent/captcha-solver.js`)).href);
53130+ return mod.detectCaptcha(1);
53131+ });
53132+ }
53133+
5312653134test('challenge-dialog routing detects supported widgets and diagnoses unsupported Arkose frames', async () => {
5312753135 for (const [build, AgentClass] of [['chrome', AgentCh], ['firefox', AgentFx]]) {
5312853136 const supportedNodes = [
@@ -53574,12 +53582,17 @@ test('challenge dialog with no enabled supported solver stops the batch for manu
5357453582 }
5357553583});
5357653584
53577- test('CAPTCHA gate helper stays byte-identical and cloud traces retain gate diagnostics outside update rollover', () => {
53585+ test('CAPTCHA helpers stay byte-identical and cloud traces retain gate diagnostics outside update rollover', () => {
5357853586 assert.equal(
5357953587 fs.readFileSync(path.join(ROOT, 'src/chrome/src/agent/captcha-gate.js'), 'utf8'),
5358053588 fs.readFileSync(path.join(ROOT, 'src/firefox/src/agent/captcha-gate.js'), 'utf8'),
5358153589 'chrome and firefox CAPTCHA gate helpers must remain byte-identical',
5358253590 );
53591+ assert.equal(
53592+ fs.readFileSync(path.join(ROOT, 'src/chrome/src/agent/captcha-frame-runtime.js'), 'utf8'),
53593+ fs.readFileSync(path.join(ROOT, 'src/firefox/src/agent/captcha-frame-runtime.js'), 'utf8'),
53594+ 'chrome and firefox CAPTCHA frame runtimes must remain byte-identical',
53595+ );
5358353596 const cloudSource = fs.readFileSync(path.join(ROOT, 'src/chrome/src/cloud-runs.js'), 'utf8');
5358453597 assert.match(cloudSource, /if \(type === 'captcha_gate'\)[\s\S]*?run\.captchaDiagnostics = \{ \.\.\.scrubbedData, observedAt: run\.updatedAt \};/);
5358553598 assert.match(cloudSource, /\.\.\.\(run\.captchaDiagnostics \? \{ captchaDiagnostics: run\.captchaDiagnostics \} : \{\}\)/);
@@ -53839,6 +53852,110 @@ test('captcha detection binds the selected widget to its own response field', as
5383953852 }
5384053853});
5384153854
53855+ test('captcha detection reports exact response token state without exposing token values', async () => {
53856+ const token = 'RAW_CAPTCHA_TOKEN_MUST_NOT_ESCAPE_7fc2';
53857+ for (const build of ['chrome', 'firefox']) {
53858+ const solvedField = captchaEl('textarea', {
53859+ id: 'g-recaptcha-response-solved',
53860+ name: 'g-recaptcha-response',
53861+ value: token,
53862+ });
53863+ const pendingField = captchaEl('textarea', {
53864+ id: 'g-recaptcha-response-pending',
53865+ name: 'g-recaptcha-response',
53866+ });
53867+ const solvedHost = captchaEl('div', {
53868+ class: 'g-recaptcha',
53869+ 'data-sitekey': 'KEY_SOLVED_WIDGET',
53870+ hidden: true,
53871+ }, [solvedField]);
53872+ const pendingHost = captchaEl('div', {
53873+ class: 'g-recaptcha',
53874+ 'data-sitekey': 'KEY_PENDING_WIDGET',
53875+ }, [pendingField]);
53876+
53877+ const pendingDetection = await detectCaptchaDetailsOnFakePage(build, [
53878+ solvedHost,
53879+ pendingHost,
53880+ captchaEl('script', { src: 'https://www.google.com/recaptcha/api.js' }),
53881+ ]);
53882+ assert.equal(
53883+ pendingDetection.selected.responseTokenPresent,
53884+ false,
53885+ `${build}: a solved sibling widget contaminated the selected pending widget`,
53886+ );
53887+ assert.equal(
53888+ pendingDetection.candidates.find(candidate =>
53889+ candidate.websiteKey === 'KEY_SOLVED_WIDGET'
53890+ )?.responseTokenPresent,
53891+ true,
53892+ `${build}: non-empty exact response field was not reported as solved`,
53893+ );
53894+ assert.doesNotMatch(
53895+ JSON.stringify(pendingDetection),
53896+ new RegExp(token),
53897+ `${build}: raw response token escaped through detection`,
53898+ );
53899+
53900+ pendingField.value = token;
53901+ const solvedDetection = await detectCaptchaDetailsOnFakePage(build, [
53902+ solvedHost,
53903+ pendingHost,
53904+ captchaEl('script', { src: 'https://www.google.com/recaptcha/api.js' }),
53905+ ]);
53906+ assert.equal(
53907+ solvedDetection.selected.responseTokenPresent,
53908+ true,
53909+ `${build}: selected non-empty response field was not reported as solved`,
53910+ );
53911+ assert.doesNotMatch(
53912+ JSON.stringify(solvedDetection),
53913+ new RegExp(token),
53914+ `${build}: selected raw response token escaped through detection`,
53915+ );
53916+
53917+ const primaryField = captchaEl('textarea', {
53918+ id: 'h-captcha-response-primary',
53919+ name: 'h-captcha-response',
53920+ });
53921+ const compatibilityField = captchaEl('textarea', {
53922+ id: 'g-recaptcha-response-compatibility',
53923+ name: 'g-recaptcha-response',
53924+ value: token,
53925+ });
53926+ const hcaptcha = await detectCaptchaOnFakePage(build, [
53927+ captchaEl('div', {
53928+ class: 'h-captcha',
53929+ 'data-sitekey': 'HKEY_SOLVED_COMPATIBILITY',
53930+ }, [primaryField, compatibilityField]),
53931+ ]);
53932+ assert.equal(
53933+ hcaptcha.responseTokenPresent,
53934+ true,
53935+ `${build}: solved hCaptcha compatibility field was ignored`,
53936+ );
53937+ assert.doesNotMatch(
53938+ JSON.stringify(hcaptcha),
53939+ new RegExp(token),
53940+ `${build}: hCaptcha compatibility token escaped through detection`,
53941+ );
53942+
53943+ Object.defineProperty(pendingField, 'value', {
53944+ configurable: true,
53945+ get() { throw new Error('hostile response field getter'); },
53946+ });
53947+ const unreadable = await detectCaptchaOnFakePage(build, [
53948+ pendingHost,
53949+ captchaEl('script', { src: 'https://www.google.com/recaptcha/api.js' }),
53950+ ]);
53951+ assert.equal(
53952+ unreadable.responseTokenPresent,
53953+ false,
53954+ `${build}: unreadable response field did not fail closed`,
53955+ );
53956+ }
53957+ });
53958+
5384253959test('captcha detection ranks a visible nested v2 Enterprise challenge above background v3', async () => {
5384353960 const topCandidate = {
5384453961 type: 'recaptcha_v3_enterprise',
0 commit comments