Repository: duckduckgo/content-scope-scripts
Pull request #1612: Add Image Captcha Solver
Author: login: brianhall
Created: 2025-04-03T13:18:01Z | Merged: 2025-04-10T11:57:30Z

**Asana Task/Github Issue:** <!-- Link to Asana Task/Github Issue -->

## Description
Asana: https://app.asana.com/0/72649045549333/1209395858706166/f
<!--
  Provide a terse summary of what the change is, detailed descriptions should belong in ship reviews or tech designs
-->

## Testing Steps

- See Asana

## Checklist

<!--
  These questions are a friendly reminder to shipping code, if you're uncertain ask the AoR owners.
  It's also totally appropriate to not check some of these boxes, if they don't apply to your change.
-->
*Please tick all that apply:*

- [x] I have tested this change locally
- [ ] I have tested this change locally in all supported browsers
- [ ] This change will be visible to users
- [x] I have added automated tests that cover this change
- [x] I have ensured the change is gated by config
- [ ] This change was covered by a ship review
- [ ] This change was covered by a tech design
- [x] Any dependent config has been merged




Diff:
diff --git a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js
index ecd47eca67d..9769b75bb5d 100644
--- a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js
+++ b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js
@@ -1,6 +1,11 @@
 import { test as base } from '@playwright/test';
 import { createConfiguredDbpTest } from './fixtures';
-import { createGetRecaptchaInfoAction, createSolveRecaptchaAction } from '../mocks/broker-protection/captcha.js';
+import {
+    createGetRecaptchaInfoAction,
+    createSolveRecaptchaAction,
+    createGetImageCaptchaInfoAction,
+    createSolveImageCaptchaAction,
+} from '../mocks/broker-protection/captcha.js';
 import { BROKER_PROTECTION_CONFIGS } from './tests-config.js';
 
 const test = createConfiguredDbpTest(base);
@@ -99,4 +104,38 @@ test.describe('Broker Protection Captcha', () => {
             dbp.isQueryParamRemoved(sucessResponse);
         });
     });
+
+    test.describe('image captcha', () => {
+        const imageCaptchaTargetPage = 'image-captcha.html';
+        const imageCaptchaResponseSelector = '#svgCaptchaInputId';
+
+        test.describe('getCaptchaInfo', () => {
+            test('returns the expected response for the correct action data', async ({ createConfiguredDbp }) => {
+                const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default);
+                await dbp.navigatesTo(imageCaptchaTargetPage);
+                await dbp.receivesInlineAction(createGetImageCaptchaInfoAction({ selector: '#svg-captcha-rendering svg' }));
+                const sucessResponse = await dbp.getSuccessResponse();
+                dbp.isCaptchaMatch(sucessResponse, { captchaType: 'image', targetPage: imageCaptchaTargetPage });
+            });
+
+            test('returns an error response when the selector is not an svg or image tag', async ({ createConfiguredDbp }) => {
+                const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default);
+                await dbp.navigatesTo(imageCaptchaTargetPage);
+                await dbp.receivesInlineAction(createGetRecaptchaInfoAction({ selector: '#svg-captcha-rendering' }));
+
+                await dbp.isCaptchaError();
+            });
+        });
+
+        test.describe('solveCaptchaInfo', () => {
+            test('solves the captcha for the correct action data', async ({ createConfiguredDbp }) => {
+                const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default);
+                await dbp.navigatesTo(imageCaptchaTargetPage);
+                await dbp.receivesInlineAction(createSolveImageCaptchaAction({ selector: imageCaptchaResponseSelector }));
+                dbp.getSuccessResponse();
+
+                await dbp.isCaptchaTokenFilled(imageCaptchaResponseSelector);
+            });
+        });
+    });
 });
diff --git a/injected/integration-test/mocks/broker-protection/captcha.js b/injected/integration-test/mocks/broker-protection/captcha.js
index b888a3991f2..32ea05e4e86 100644
--- a/injected/integration-test/mocks/broker-protection/captcha.js
+++ b/injected/integration-test/mocks/broker-protection/captcha.js
@@ -34,6 +34,18 @@ export function createGetRecaptchaInfoAction(actionOverrides = {}) {
     });
 }
 
+/**
+ * @param {Partial<PirAction>} [actionOverrides]
+ */
+export function createGetImageCaptchaInfoAction(actionOverrides = {}) {
+    return createGetCaptchaInfoAction({
+        action: {
+            captchaType: 'image',
+            ...actionOverrides,
+        },
+    });
+}
+
 /**
  * @param {object} params
  * @param {Omit<PirAction, 'id' | 'actionType'>} params.action
@@ -66,6 +78,18 @@ export function createSolveRecaptchaAction(actionOverrides = {}) {
     });
 }
 
+/**
+ * @param {Partial<PirAction>} [actionOverrides]
+ */
+export function createSolveImageCaptchaAction(actionOverrides = {}) {
+    return createSolveCaptchaAction({
+        action: {
+            captchaType: 'image',
+            ...actionOverrides,
+        },
+    });
+}
+
 // Captcha responses
 
 /**
diff --git a/injected/integration-test/page-objects/broker-protection.js b/injected/integration-test/page-objects/broker-protection.js
index 69d35a6828d..227da3c3109 100644
--- a/injected/integration-test/page-objects/broker-protection.js
+++ b/injected/integration-test/page-objects/broker-protection.js
@@ -70,8 +70,8 @@ export class BrokerProtectionPage {
      * @return {Promise<void>}
      */
     async isCaptchaTokenFilled(responseElementSelector) {
-        const captchaTextArea = await this.page.$(responseElementSelector);
-        const captchaToken = await captchaTextArea?.evaluate((element) => element.innerHTML);
+        const captchaTarget = await this.page.$(responseElementSelector);
+        const captchaToken = await captchaTarget?.evaluate((element) => ('value' in element ? element.value : element.innerHTML));
         expect(captchaToken).toBe('test_token');
     }
 
@@ -99,7 +99,17 @@ export class BrokerProtectionPage {
      */
     isCaptchaMatch(response, { captchaType, targetPage }) {
         const expectedResponse = createCaptchaResponse({ captchaType, targetPage });
-        expect(response).toStrictEqual(expectedResponse);
+
+        switch (captchaType) {
+            case 'image':
+                // Validate that the correct keys are present in the response
+                expect(Object.keys(response).sort()).toStrictEqual(Object.keys(expectedResponse).sort());
+                // Validate that the siteKey looks like a base64 encoded image
+                expect(response.siteKey).toMatch(/^data:image\/jpeg;base64,/);
+                break;
+            default:
+                expect(response).toStrictEqual(expectedResponse);
+        }
     }
 
     async isCaptchaError() {
diff --git a/injected/integration-test/test-pages/broker-protection/pages/image-captcha.html b/injected/integration-test/test-pages/broker-protection/pages/image-captcha.html
new file mode 100644
index 00000000000..bddd0e5fe63
--- /dev/null
+++ b/injected/integration-test/test-pages/broker-protection/pages/image-captcha.html
@@ -0,0 +1,66 @@
+<html data-beasties-container=""><head>
+    <meta charset="utf-8">
+    <title>Privatereports</title>
+    <base href="/">
+
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <link rel="apple-touch-icon" sizes="180x180" href="/assets/private/apple-touch-icon.png">
+    <link rel="icon" type="image/png" sizes="32x32" href="/assets/private/favicon-32x32.png">
+    <link rel="icon" type="image/png" sizes="16x16" href="/assets/private/favicon-16x16.png">
+    <link type="text/css" rel="stylesheet" href="/api/helper/tracking/rec_null?type=preBoot&amp;accept=text%2Fcss" media="screen,print">
+
+    <script type="text/javascript" async="" src="https://www.googletagmanager.com/gtag/destination?id=G-VXWSJFT2NE&amp;l=dataLayer&amp;cx=c&amp;gtm=45He5411v835073010za200&amp;tag_exp=102788824~102803279~102813109~102887799~102926062~102975949~103016951~103021830"></script><script type="text/javascript" async="" src="https://www.googletagmanager.com/gtag/js?id=G-VXWSJFT2NE&amp;l=dataLayer&amp;cx=c&amp;gtm=45He5411v835073010za200&amp;tag_exp=102788824~102803279~102813109~102887799~102926062~102975949~103016951~103021830"></script><script async="" src="https://www.googletagmanager.com/gtm.js?id=GTM-K3QTWBS"></script><script> function f5e99a00(a, b) { try { if (!b) { b = {}; } b.type = a; b.location = location.pathname; } catch (e) { b = {error: e, data: b, location:location.pathname}; } var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/helper/tracking/rec?type=' + a); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify({param: b})); } window.onerror = function (message, source, lineno, colno, error) { f5e99a00("onerror", {message: message, source: source, lineno: lineno, colno: colno, error: error}); }; </script>
+
+  <style>:root{--ol-background-color:white;--ol-accent-background-color:#F5F5F5;--ol-subtle-background-color:rgba(128, 128, 128, .25);--ol-partial-background-color:rgba(255, 255, 255, .75);--ol-foreground-color:#333333;--ol-subtle-foreground-color:#666666;--ol-brand-color:#00AAFF}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}@media print{*,*:before,*:after{background:transparent!important;color:#000!important;-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}@-webkit-keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}@-ms-viewport{width:device-width}*:focus{outline:none}html{height:100%;font-family:sans-serif}html{height:100%;font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0;min-height:100%;background-color:#fff;font-size:14px;line-height:20px;color:#333}body{background-color:#f8fafe}@-webkit-keyframes glyphicons-spin-r{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-webkit-keyframes glyphicons-spin-l{0%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0)}}body{margin:0}body:has(.loader__modal){overflow:hidden}body{font-family:Roboto,Helvetica Neue,Arial,sans-serif;color:#404040;background:#f9f9f9}</style><link rel="stylesheet" href="styles.css" media="all" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.css"></noscript><style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style><style>.toast-wrapper[_ngcontent-ng-c3792183681]{position:fixed;top:16px;right:16px;width:200px;height:100vh;z-index:1000}.toast-wrapper[_ngcontent-ng-c3792183681]   .toastr[_ngcontent-ng-c3792183681]{border-radius:6px;padding:8px 16px;box-shadow:4px 4px 4px #00000040;color:#fff;margin-bottom:16px;width:200px;transition:all .2s;cursor:pointer}.toast-wrapper[_ngcontent-ng-c3792183681]   .toastr.success[_ngcontent-ng-c3792183681]{background-color:#81b34c}.toast-wrapper[_ngcontent-ng-c3792183681]   .toastr.error[_ngcontent-ng-c3792183681]{background-color:#ff4b55}.toast-wrapper[_ngcontent-ng-c3792183681]   .toastr__title[_ngcontent-ng-c3792183681]{margin:0 0 .5em;font-size:18px}.toast-wrapper[_ngcontent-ng-c3792183681]   .toastr__content[_ngcontent-ng-c3792183681]{margin:0;font-size:16px}.toast-wrapper[_ngcontent-ng-c3792183681]   .toastr[_ngcontent-ng-c3792183681]:hover{box-shadow:6px 6px 6px #00000040}</style><style>
+        .grecaptcha-badge {visibility: hidden;}
+        .grecaptcha-badge.recaptcha-v3-during {
+          visibility: visible;
+          position: fixed;
+          top: 33%;
+          left: 50%;
+          margin-top: -30px;
+          margin-left: -128px;
+          z-index: 9999;
+          box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px !important;
+          -moz-box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px !important;
+          -webkit-box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px !important;
+        }
+        #recaptcha-v2-rendering {
+          > div {
+            margin: 0 auto;
+          }
+        }
+        #turnstile-v0-rendering {
+          > iframe {
+            box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+            -moz-box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+            -webkit-box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+          }
+        }
+      </style><style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style><style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style><style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style><style>[_nghost-ng-c3766333856]     .privacy-modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:#00000080;z-index:999}[_nghost-ng-c3766333856]     .privacy-modal{position:fixed;top:56px;left:50%;max-height:calc(100% - 112px);overflow-y:auto;transform:translate(-50%);width:90%;max-width:704px;background:#fff;border-radius:8px;box-shadow:0 4px 10px #0003;z-index:1000}[_nghost-ng-c3766333856]     .privacy-modal-content{text-align:left}[_nghost-ng-c3766333856]     .privacy-modal-content hr{margin:0 12px;border:none!important;border-top:1px solid #CCCCCC!important}@media (min-width: 834px){[_nghost-ng-c3766333856]     .privacy-modal-content hr{margin:0 32px}}[_nghost-ng-c3766333856]     .privacy-modal-content section{padding:6px 12px;font-size:12px;line-height:14.58px;color:#404040}@media (min-width: 834px){[_nghost-ng-c3766333856]     .privacy-modal-content section{padding:8px 32px;font-size:14px;line-height:17.01px}}[_nghost-ng-c3766333856]     .privacy-modal-content section p{margin:0;font-size:12px;line-height:14.58px}@media (min-width: 834px){[_nghost-ng-c3766333856]     .privacy-modal-content section p{font-size:14px;line-height:17.01px}}[_nghost-ng-c3766333856]     .privacy-modal-content section.title{padding:8px 12px;font-size:20px;line-height:23.44px;font-weight:900}@media (min-width: 834px){[_nghost-ng-c3766333856]     .privacy-modal-content section.title{padding:16px 32px}}[_nghost-ng-c3766333856]     .privacy-modal-content section.title .exclamation-circle{width:20px;height:20px;top:3px;position:relative}[_nghost-ng-c3766333856]     .privacy-modal-content section.leading{font-size:14px;font-weight:700;padding-top:16px}[_nghost-ng-c3766333856]     .privacy-modal-content section.button-wrap{padding-bottom:16px}@media (min-width: 834px){[_nghost-ng-c3766333856]     .privacy-modal-content section.button-wrap{padding-bottom:21px}}[_nghost-ng-c3766333856]     button.agree-btn{width:100%;height:48px;background-color:#fff;border:1px solid #E89F10;border-radius:5px;font-size:16px;font-weight:700;line-height:18.75px;text-align:center;color:#404040}@media (min-width: 834px){[_nghost-ng-c3766333856]     button.agree-btn{width:304px;transform:translate(-50%);left:50%;position:relative}}[_nghost-ng-c3766333856]     .checkbox-wrapper{box-sizing:border-box;--background-color: #fff;--checkbox-height: 25px;display:inline-block;position:absolute;left:24px;transform:scale(.8) translateY(-5px)}@media (min-width: 834px){[_nghost-ng-c3766333856]     .checkbox-wrapper{left:12px}}@-moz-keyframes bottomcheck{0%{height:0}to{height:calc(var(--checkbox-height) / 2)}}@-webkit-keyframes _ngcontent-ng-c3766333856_bottomcheck{0%{height:0}to{height:calc(var(--checkbox-height) / 2)}}@keyframes _ngcontent-ng-c3766333856_bottomcheck{0%{height:0}to{height:calc(var(--checkbox-height) / 2)}}@keyframes _ngcontent-ng-c3766333856_topcheck{0%{height:0}50%{height:0}to{height:calc(var(--checkbox-height) * 1.2)}}@-webkit-keyframes _ngcontent-ng-c3766333856_topcheck{0%{height:0}50%{height:0}to{height:calc(var(--checkbox-height) * 1.2)}}@-moz-keyframes topcheck{0%{height:0}50%{height:0}to{height:calc(var(--checkbox-height) * 1.2)}}[_nghost-ng-c3766333856]     .checkbox-wrapper input[type=checkbox]{display:none}[_nghost-ng-c3766333856]     .checkbox-wrapper .check-box{height:var(--checkbox-height);width:var(--checkbox-height);background-color:transparent;border:calc(var(--checkbox-height) * .1) solid #B6B7BC;border-radius:5px;position:relative;display:inline-block;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-transition:border-color ease .2s;-o-transition:border-color ease .2s;-webkit-transition:border-color ease .2s;transition:border-color ease .2s;cursor:pointer}[_nghost-ng-c3766333856]     .checkbox-wrapper .check-box:before, [_nghost-ng-c3766333856]     .checkbox-wrapper .check-box:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;position:absolute;height:0;width:calc(var(--checkbox-height) * .2);background-color:#525252;display:inline-block;-moz-transform-origin:left top;-ms-transform-origin:left top;-o-transform-origin:left top;-webkit-transform-origin:left top;transform-origin:left top;border-radius:5px;content:" ";-webkit-transition:opacity ease .5;-moz-transition:opacity ease .5;transition:opacity ease .5}[_nghost-ng-c3766333856]     .checkbox-wrapper .check-box:before{top:calc(var(--checkbox-height) * .72);left:calc(var(--checkbox-height) * .41);box-shadow:0 0 0 calc(var(--checkbox-height) * .05) var(--background-color);-moz-transform:rotate(-135deg);-ms-transform:rotate(-135deg);-o-transform:rotate(-135deg);-webkit-transform:rotate(-135deg);transform:rotate(-135deg)}[_nghost-ng-c3766333856]     .checkbox-wrapper .check-box:after{top:calc(var(--checkbox-height) * .37);left:calc(var(--checkbox-height) * .05);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}[_nghost-ng-c3766333856]     .checkbox-wrapper input[type=checkbox]:checked+.check-box, [_nghost-ng-c3766333856]     .checkbox-wrapper .check-box.checked{border-color:#525252}[_nghost-ng-c3766333856]     .checkbox-wrapper input[type=checkbox]:checked+.check-box:after, [_nghost-ng-c3766333856]     .checkbox-wrapper .check-box.checked:after{height:calc(var(--checkbox-height) / 2);-moz-animation:bottomcheck .2s ease 0s forwards;-o-animation:bottomcheck .2s ease 0s forwards;-webkit-animation:tomcheck .2s ease 0s forwards;animation:_ngcontent-ng-c3766333856_bottomcheck .2s ease 0s forwards}[_nghost-ng-c3766333856]     .checkbox-wrapper input[type=checkbox]:checked+.check-box:before, [_nghost-ng-c3766333856]     .checkbox-wrapper .check-box.checked:before{height:calc(var(--checkbox-height) * 1.2);-moz-animation:topcheck .4s ease 0s forwards;-o-animation:topcheck .4s ease 0s forwards;-webkit-animation:_ngcontent-ng-c3766333856_topcheck .4s ease 0s forwards;animation:_ngcontent-ng-c3766333856_topcheck .4s ease 0s forwards}[_nghost-ng-c3766333856]     span.red{color:red}[_nghost-ng-c3766333856]     a.link{color:#4851c0!important;text-decoration:underline;font-size:12px!important;line-height:14.58px;margin:0}[_nghost-ng-c3766333856]     .ux1 .privacy-modal-content section.title{color:#404040}[_nghost-ng-c3766333856]     .ux1 .privacy-modal-content section.title .exclamation-circle{fill:#404040}[_nghost-ng-c3766333856]     .ux2 .privacy-modal-content hr{display:none}[_nghost-ng-c3766333856]     .ux2 .privacy-modal-content section.title{color:#fff;background-color:#276fcf}[_nghost-ng-c3766333856]     .ux2 .privacy-modal-content section.title .exclamation-circle{fill:#fff}[_nghost-ng-c3766333856]     .ux3 a.link{color:#36c974!important}[_nghost-ng-c3766333856]     .ux3 .privacy-modal-content hr{display:none}[_nghost-ng-c3766333856]     .ux3 .privacy-modal-content section.title{color:#fa6341;background-color:#f8f8f8}[_nghost-ng-c3766333856]     .ux3 .privacy-modal-content section.title .exclamation-circle{fill:#fa6341}[_nghost-ng-c3766333856]     .ux3 button.agree-btn{border-color:#2342c0}[_nghost-ng-c3766333856]     .ux5 a.link{color:#4444cf!important}[_nghost-ng-c3766333856]     .ux5 .privacy-modal-content hr{display:none}[_nghost-ng-c3766333856]     .ux5 .privacy-modal-content section.title{color:#fff;background-color:#4444cf}[_nghost-ng-c3766333856]     .ux5 .privacy-modal-content section.title .exclamation-circle{fill:#fff}[_nghost-ng-c3766333856]     .ux5 button.agree-btn{border-color:#6bb91e}</style><style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style><style>.content-box[_ngcontent-ng-c1435701707]{min-height:400px}.use[_ngcontent-ng-c1435701707]{position:relative;min-height:40px;padding-bottom:10px}</style><style>.footer[_ngcontent-ng-c2144489660]{opacity:.5;padding-top:5em;padding-bottom:7em;transition:all .5s ease;border-top:1px solid #e1e1e1;height:auto!important;background-color:transparent!important;box-shadow:none!important}.footer[_ngcontent-ng-c2144489660]   ul[_ngcontent-ng-c2144489660]{list-style-type:none;padding:0}.footer[_ngcontent-ng-c2144489660]   ul[_ngcontent-ng-c2144489660]   li[_ngcontent-ng-c2144489660]{margin-bottom:10px}.footer[_ngcontent-ng-c2144489660]   .secure-logos[_ngcontent-ng-c2144489660]{margin:20px 0}.footer[_ngcontent-ng-c2144489660]   .secure-logos[_ngcontent-ng-c2144489660]   img[_ngcontent-ng-c2144489660]{max-width:80px;margin-right:20px}@media print{footer[_ngcontent-ng-c2144489660], .footer[_ngcontent-ng-c2144489660]{display:none!important}}@media (max-width: 991px){.footer[_ngcontent-ng-c2144489660]{text-align:center}.footer[_ngcontent-ng-c2144489660]   li[_ngcontent-ng-c2144489660]{font-weight:700}.search-text-befre[_ngcontent-ng-c2144489660]{font-weight:400}}@media only screen and (max-width: 425px){.footer[_ngcontent-ng-c2144489660]{font-size:11px;font-weight:500;line-height:16px;opacity:.5;padding-top:4em;padding-bottom:4em;transition:all .5s ease;border-top:1px solid #e1e1e1;height:auto!important;background-color:transparent!important;box-shadow:none!important}}</style><style>@media only screen and (max-width: 425px){.disclaimer[_ngcontent-ng-c3471226971]{font-size:11px;font-weight:400;line-height:20px}}</style><style>@media screen and (max-width: 479px){img[_ngcontent-ng-c3241807575]{width:75%!important}}.bar-container[_ngcontent-ng-c3241807575]{width:28px;height:24px;display:flex;flex-direction:column;justify-content:space-between}.bar-container[_ngcontent-ng-c3241807575]   .bar[_ngcontent-ng-c3241807575]{height:4px;width:100%;background-color:#ffb400}</style></head>
+  <body onunload="f5e99a00('bodyUnload');">
+
+
+    <app-root _nghost-ng-c3792183681="" ng-version="19.1.1"><link _ngcontent-ng-c3792183681="" type="text/css" rel="stylesheet" href="/assets/common/roboto/fonts.css" media="screen,print"><link _ngcontent-ng-c3792183681="" type="text/css" rel="stylesheet" href="/assets/common/fontawesome-free-5.3.1-web/css/all.min.css" media="screen,print"><link _ngcontent-ng-c3792183681="" type="text/css" rel="stylesheet" href="/assets/common/glyphicons/css/glyphicons.css" media="screen,print"><!----><router-outlet _ngcontent-ng-c3792183681=""></router-outlet><ng-component _nghost-ng-c1435701707="" class="ng-star-inserted"><div _ngcontent-ng-c1435701707="" class="name-search-opt-out sales theme-legacy theme-ux_3_0" id="loader"><app-sales-header _ngcontent-ng-c1435701707=""><ux-3-0-sales-header _nghost-ng-c3241807575="" class="ng-star-inserted"><div _ngcontent-ng-c3241807575="" class="theme-ux_3_0 ng-star-inserted"><div _ngcontent-ng-c3241807575="" data-collapse="small" data-animation="default" data-duration="400" role="banner" class="header w-nav"><div _ngcontent-ng-c3241807575="" class="nav-container"><a _ngcontent-ng-c3241807575="" class="brand w-nav-brand" href="/"><img _ngcontent-ng-c3241807575="" alt="logo" style="width: 300px;" src="/assets/privatereports/img/logo-light-bg.svg" class="ng-star-inserted"><!----></a><nav _ngcontent-ng-c3241807575="" role="navigation" class="w-nav-menu"><!----></nav><div _ngcontent-ng-c3241807575="" class="menu-button w-nav-button ng-star-inserted"><div _ngcontent-ng-c3241807575="" class="bar-container"><div _ngcontent-ng-c3241807575="" class="bar"></div><div _ngcontent-ng-c3241807575="" class="bar"></div><div _ngcontent-ng-c3241807575="" class="bar"></div></div></div><!----></div></div></div><!----><!----></ux-3-0-sales-header><!----><app-privacy-policy-agreement-modal _nghost-ng-c3766333856="" class="ng-tns-c3766333856-2 ng-star-inserted"><!----></app-privacy-policy-agreement-modal></app-sales-header><div _ngcontent-ng-c1435701707="" class="container-fluid content-wrapper ng-star-inserted"><div _ngcontent-ng-c1435701707="" class="container"><div _ngcontent-ng-c1435701707="" class="row text-center"><div _ngcontent-ng-c1435701707="" class="col-sm-12 loader-header"><h1 _ngcontent-ng-c1435701707=""><uxtemplate _ngcontent-ng-c1435701707="" class="ng-star-inserted"><div id="ux-38" data-key="comp.sales.name-search-opt-out.loader.headline"></div></uxtemplate><!----><!----></h1><p _ngcontent-ng-c1435701707=""><uxtemplate _ngcontent-ng-c1435701707=""><div id="ux-35" data-key="comp.sales.name-search-opt-out.loader.h2.text"></div></uxtemplate></p></div></div><div _ngcontent-ng-c1435701707="" class="row"><div _ngcontent-ng-c1435701707="" class="col-sm-10 col-sm-offset-1 content-box text-center"><div _ngcontent-ng-c1435701707="" class="progress"><div _ngcontent-ng-c1435701707="" role="progressbar" aria-valuenow="20" aria-valuemin="10" aria-valuemax="10" class="progress-bar progress-bar-striped active" style="width: 5%;"> 5% </div></div><div _ngcontent-ng-c1435701707="" class="ng-star-inserted"><uxtemplate _ngcontent-ng-c1435701707=""><div id="ux-39" data-key="comp.sales.name-search-opt-out.loader.result.before"></div></uxtemplate></div><!----><div _ngcontent-ng-c1435701707="" class="use ng-star-inserted"><!----><!----><!----><!----></div><!----><!----><!----><!----><!----><!----><!----></div></div></div></div><div _ngcontent-ng-c1435701707="" class="ng-star-inserted"><uxtemplate _ngcontent-ng-c1435701707=""><div id="ux-36" data-key="comp.name-search.copy.featured-on" data-id="5a86d57ae47ba677bcac138c"><row>  <style>    #landing .container-fluid.uses { display: block; }    .container-fluid.uses { display: none; }  </style>  <div class="container-fluid uses">    <div class="container">      <div class="col-md-5 col-md-offset-1 who-we-are">        <h2>About Us</h2>        <p>PrivateReports is a top, trusted company started in 2020 by data scientists, engineers and customer service professionals with more than 10 years experience delivering public record information via the Internet.  Our goal is total customer satisfaction. We search over 12 billion public records - from thousands of sources - to create an accurate, comprehensive report on almost anyone in the USA. We specialize in running background checks, searching criminal records and digging up hidden information on the dark web -- all so you know the deep truth about the many people in your life. You can look up your new romances, old class mates, annoying neighbors, strange coworkers or even familiar friends and relatives you've known for years. They will never know you're looking them up because our searches are 100% anonymous, guaranteed. This helps you have peace-of-mind, assured you have the best available information to make the decisions that keep you and your loved one safe and secure. </p>        <p class="text-center">         <a class="btn btn-primary" data-target="#sample-report" data-toggle="modal" onclick="return window.pubFunc.app.click(event)" id="who-we-are-link-sample-report">View Sample Report</a> </p>      </div>      <p class="visible-xs" style="height:20px;"> </p>      <div class="col-md-5 col-md-offset-1 what-are-public-records">        <h2>What are public records?</h2>        <p> Public records can be found at the local, state, or federal level which are open to the public through the Freedom of Information Act. In some cases, you can write a letter to the government to receive public record data. In other cases, you may be required to visit the local county office in person. In most cases, you will need to visit hundreds of different public and private websites to get the single, easy-to-read report that PrivateReports provides. Go ahead and give it a try! Just enter someone's name and location, and we'll search our massive database to give you instant results. While it's free to search our site as many times as you want, we charge a nominal fee to receive our 100% comprehensive reports, in order to pay for the time and money required to collect this information, store it, and present it on our website for your easy consumption. It's just $1 for your first report, and then you pay a nominal monthly fee if you want to view more reports, as many as you want! Our goal is total customer satisfaction, and we are confident you will find great value in our data quality and our exceptional customer service.        </p>        <p class="visible-xs text-center">          <a class="btn btn-primary" href="#ux-1" onclick="return window.pubFunc.app.click(event)" id="what-are-public-records-anchor-search">Search</a>        </p>      </div>      <p class="visible-xs" style="height:20px;">      </p>      <div class="col-xs-12 what-comes-in-report visible-xs">        <h2>What comes in my report?</h2>        <p>          A PrivateReports background report can include:        </p>        <ul>            <li>Public Records</li>            <li>Sex Offender status</li>            <li>Court and Arrest Records</li>            <li>Marriage/Divorce Status</li>            <li>Social and</li>            <li>Dating Profiles</li>            <li>Census Data</li>            <li>Address History</li>            <li>Relatives</li>            <li>Contact information</li></ul>                <p>          Our service also includes an extended data search which accesses additional data sources and may reveal details like weapon permits, foreclosures, business associates, asset information, additional contact information and more. Keep in mind, a subscription with PrivateReports allows you to obtain unlimited comprehensive reports during your subscription period.        </p>        <p class="text-center">          <a class="btn btn-primary" data-target="#sample-report" data-toggle="modal" onclick="return window.pubFunc.app.click(event)" id="what-comes-in-report-link-sample-report">View Sample Report</a>        </p>      </div>    </div>  </div>  <!-- featured on  <div class="container-fluid featured-on">    <div class="container text-center">      <div class="row">        <div class="col-md-12">          <h2>Record checks have been featured on</h2>        </div>      </div>      <div class="row">        <div class="col-xs-12 col-sm-2"><img src="/assets/common/pr1/img/cnn-logo.svg" alt=""></div>        <div class="col-xs-12 col-sm-2"><img src="/assets/common/pr1/img/new-york-times-logo.svg" alt=""></div>        <div class="col-xs-12 col-sm-2"><img src="/assets/common/pr1/img/wapo-logo.svg" alt=""></div>        <div class="col-xs-12 col-sm-2"><img src="/assets/common/pr1/img/fox-news-logo.svg" alt=""></div>        <div class="col-xs-12 col-sm-2"><img src="/assets/common/pr1/img/nbc-news-logo.svg" alt=""></div>        <div class="col-xs-12 col-sm-2"><img src="/assets/common/pr1/img/npr-logo.svg" alt=""></div>      </div>    </div>  </div>  --></row></div></uxtemplate></div><div _ngcontent-ng-c1435701707="" class="ng-star-inserted"><uxtemplate _ngcontent-ng-c1435701707=""><div id="ux-37" data-key="comp.name-search.copy.testimonials" data-id="5a86d5844603c677c6c81633"><div class="container-fluid customers-saying">  <div class="container">    <div class="row">      <div class="col-md-12 text-center">        <!-- <h2>What our customers are saying...</h2> -->      </div>    </div>    <!--    <div class="row">      <div class="col-md-4">        <blockquote class="blockquote">          <p><img src="/assets/common/pr1/img/two-quotes.svg">This is one way to really know a potential date.</p>          <footer class="blockquote-footer">Charmaine R.</footer>        </blockquote>      </div>      <div class="col-md-4">        <blockquote class="blockquote">          <p><img src="/assets/common/pr1/img/two-quotes.svg">Another customer quote would go here like this example here.</p>          <footer class="blockquote-footer">Samuel L.</footer>        </blockquote>      </div>      <div class="col-md-4">        <blockquote class="blockquote">          <p><img src="/assets/common/pr1/img/two-quotes.svg">This is one way to really know a potential date.</p>          <footer class="blockquote-footer">Samuel L.</footer>        </blockquote>      </div>    </div>    <div class="row">      <div class="col-md-4">        <blockquote class="blockquote">          <p><img src="/assets/common/pr1/img/two-quotes.svg">Another customer quote would go here like this example here.</p>          <footer class="blockquote-footer">Samuel L.</footer>        </blockquote>      </div>      <div class="col-md-4">        <blockquote class="blockquote">          <p><img src="/assets/common/pr1/img/two-quotes.svg">Another customer quote would go here like this example here.</p>          <footer class="blockquote-footer">Samuel L.</footer>        </blockquote>      </div>      <div class="col-md-4">        <blockquote class="blockquote">          <p><img src="/assets/common/pr1/img/two-quotes.svg">This is one way to really know a potential date.</p>          <footer class="blockquote-footer">Charmaine R.</footer>        </blockquote>      </div>    </div>    -->  </div></div><!-- /customers-saying --></div></uxtemplate></div><app-sales-footer _ngcontent-ng-c1435701707="" _nghost-ng-c2144489660="" class="ng-star-inserted"><!----><!----><!----><div _ngcontent-ng-c2144489660="" class="container-fluid footer ng-star-inserted"><div _ngcontent-ng-c2144489660="" class="container"><div _ngcontent-ng-c2144489660="" class="row"><div _ngcontent-ng-c2144489660="" class="col-md-5"><div _ngcontent-ng-c2144489660="" class="row"><div _ngcontent-ng-c2144489660="" class="col-md-6"><ul _ngcontent-ng-c2144489660=""><li _ngcontent-ng-c2144489660=""><a _ngcontent-ng-c2144489660="" id="link-contact-us" href="/contactUs">Contact Us</a></li><!----><li _ngcontent-ng-c2144489660=""><a _ngcontent-ng-c2144489660="" id="link-opt-out" href="/optOut/name/landingPage">Do Not Sell Or Share My Personal Info</a></li></ul></div><div _ngcontent-ng-c2144489660="" class="col-md-6"><ul _ngcontent-ng-c2144489660=""><li _ngcontent-ng-c2144489660=""><a _ngcontent-ng-c2144489660="" id="link-term" href="/termsConditions">Terms of Use</a></li><li _ngcontent-ng-c2144489660=""><a _ngcontent-ng-c2144489660="" id="link-privacy" href="/privacyPolicy">Privacy Policy</a></li><li _ngcontent-ng-c2144489660="" class="footer__links--item"><a _ngcontent-ng-c2144489660="" id="link-opt-out" href="/optOut/name/landingPage">Limit The Use of My Sensitive Personal Info</a></li></ul></div></div><!----></div><div _ngcontent-ng-c2144489660="" class="col-md-7"><uxtemplate _ngcontent-ng-c2144489660=""><div id="ux-41" data-key="comp.brand.disclosure" data-id="5f3cb91b5048e06363d5bc77">Copyright @ 2024 Infomatics LLC All Rights Reserved<br>By using our content, products &amp; services you agree to our <a href="/terms" onclick="return window.pubFunc.app.click(event)">Terms Of Use</a>, and <a href="/privacy" onclick="return window.pubFunc.app.click(event)">Privacy Policy</a>.<br>Reproduction in whole or in part without express written permission of Infomatics LLC is prohibited.</div></uxtemplate></div></div></div></div><!----><!----><!----><!----><!----><!----><!----><!----><!----></app-sales-footer><app-sales-disclaimer _ngcontent-ng-c1435701707="" _nghost-ng-c3471226971="" class="ng-star-inserted"><div _ngcontent-ng-c3471226971="" class="disclaimer ng-star-inserted"><uxtemplate _ngcontent-ng-c3471226971=""><div id="ux-40" data-key="comp.sales.name-search-opt-out.loader.disclaimer.text"></div></uxtemplate></div><!----><!----></app-sales-disclaimer><!----><!----></div><app-sales-closure _ngcontent-ng-c1435701707=""><div></div></app-sales-closure><dev-message _ngcontent-ng-c1435701707="" class="ng-star-inserted"></dev-message><!----></ng-component><!----><!----><!----></app-root>
+  <link rel="modulepreload" href="chunk-G3C2RHI3.js"><link rel="modulepreload" href="chunk-PIZLXBHJ.js"><link rel="modulepreload" href="chunk-ITTPLPNX.js"><link rel="modulepreload" href="chunk-LUYHZS2P.js"><link rel="modulepreload" href="chunk-F5ZV2UEJ.js"><link rel="modulepreload" href="chunk-4WHAAFDZ.js"><link rel="modulepreload" href="chunk-WCODJ6VO.js"><link rel="modulepreload" href="chunk-5TWCO63E.js"><link rel="modulepreload" href="chunk-5Y2XJLZK.js"><link rel="modulepreload" href="chunk-3FP63RNW.js"><script src="polyfills.js" type="module"></script><script src="scripts.js" defer=""></script><script src="main.js" type="module"></script>
+
+  <iframe id="pixel-0.3277505635532707" class="pixelFrame-comp-postUxc-pixel" height="1" width="1" style="position:fixed;bottom:0;left:0;" frameborder="0" scrolling="NO" sandbox="allow-scripts allow-forms allow-same-origin" src="api/helper/tracking/pixelFrame?type=pixel&amp;_frameKey=comp.postUxc.pixel&amp;"></iframe><iframe id="pixel-0.6060838157906113" class="pixelFrame-comp-sales-name-search-landing-pixel" height="1" width="1" style="position:fixed;bottom:0;left:0;" frameborder="0" scrolling="NO" sandbox="allow-scripts allow-forms allow-same-origin" src="api/helper/tracking/pixelFrame?type=pixel&amp;_frameKey=comp.sales.name-search.landing.pixel&amp;"></iframe><script id="Turnstile_V0_Script" src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=Turnstile_V0_Script_OnLoad"></script><div id="SvgCaptchaElementWrapper">
+          <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;background-color: #0000007d; overflow-x: hidden; overflow-y: auto; z-index: 1072;">
+            <div id="svg-captcha-rendering" style="position: fixed; top: 33%; left: 50%; margin-top: -30px; margin-left: -150px; background-color: #ffffff; padding: 30px; width: 300px; box-sizing: border-box;">
+              <div style="text-align: center;">
+                <svg xmlns="http://www.w3.org/2000/svg" width="150" height="50" viewBox="0,0,150,50"><path d="M17 5 C94 16,62 32,140 44" stroke="#888" fill="none"></path><path fill="#333" d="M56.25 19.53L56.30 19.58L54.87 19.84L54.79 19.75Q52.61 19.93 52.38 21.95L52.25 21.82L52.41 21.98Q52.21 22.58 52.25 23.57L52.27 23.59L52.32 23.64Q52.38 25.61 53.64 26.94L53.54 26.84L53.59 26.89Q54.88 28.26 56.79 28.11L56.82 28.14L56.80 28.12Q58.82 27.93 59.58 27.01L59.59 27.03L59.63 27.06Q60.55 26.01 60.82 23.76L60.95 23.90L60.83 23.77Q61.13 21.56 59.88 20.73L59.85 20.70L59.81 20.67Q59.10 20.03 56.44 19.72ZM62.36 28.84L62.35 28.84L61.04 31.11L61.17 31.24Q58.34 35.90 54.57 39.97L54.71 40.12L54.66 40.07Q53.01 40.21 50.31 41.00L50.31 41.01L50.39 41.09Q55.68 34.77 58.84 29.97L58.70 29.83L58.73 29.86Q57.73 30.54 56.10 30.62L56.03 30.55L56.21 30.73Q53.02 30.70 51.50 28.72L51.67 28.89L51.53 28.75Q50.30 27.14 49.19 22.57L49.29 22.67L49.17 22.55Q49.36 22.28 49.21 21.35L49.09 21.23L49.15 21.29Q48.97 20.33 48.97 19.87L48.88 19.78L48.90 19.80Q48.88 17.99 49.95 17.38L49.95 17.39L50.00 17.43Q51.34 16.80 53.21 16.80L53.11 16.70L53.19 16.78Q56.31 16.70 56.31 16.70L56.39 16.78L56.35 16.75Q58.53 16.79 59.78 16.98L59.68 16.88L59.75 16.95Q61.82 17.15 62.73 17.64L62.69 17.60L62.82 17.73Q64.06 18.36 64.25 20.12L64.39 20.26L64.33 20.20Q64.24 20.34 64.05 23.00L64.18 23.13L64.08 23.03Q63.67 25.13 63.45 25.97L63.49 26.02L63.61 26.13Q63.18 27.50 62.42 28.90ZM64.26 31.24L64.26 31.24L64.32 31.30Q65.27 29.05 65.80 24.75L65.83 24.78L65.88 24.82Q66.04 22.51 66.00 21.64L66.05 21.68L66.05 21.68Q66.03 19.84 64.85 18.92L64.86 18.93L64.60 18.86L64.61 18.88Q64.30 18.07 63.39 17.42L63.32 17.35L63.29 17.33Q61.01 16.34 56.22 16.19L56.23 16.21L56.36 16.33Q55.24 16.16 52.80 16.16L52.78 16.15L52.83 16.20Q51.05 16.16 49.56 16.85L49.70 16.98L49.55 16.83Q48.42 17.65 48.50 19.48L48.64 19.62L48.55 19.52Q48.70 20.40 48.89 22.42L49.01 22.54L49.04 22.57Q49.55 25.09 49.81 25.93L49.86 25.98L49.85 25.96Q50.49 27.90 51.44 29.19L51.30 29.06L51.45 29.20Q51.56 29.38 51.94 29.77L51.95 29.78L52.03 29.86Q52.33 30.35 52.97 31.26L52.99 31.27L52.85 31.13Q54.46 32.63 56.40 32.82L56.38 32.80L56.34 32.76Q54.54 35.53 52.98 37.43L52.94 37.40L49.35 41.80L49.34 41.79Q51.45 40.89 53.08 40.54L53.24 40.70L53.17 40.64Q52.46 41.56 50.90 43.27L50.85 43.23L50.85 43.22Q53.86 42.23 56.98 42.19L56.95 42.16L57.06 42.27Q59.29 39.60 63.63 32.36L63.74 32.48L63.68 32.49L63.70 32.51Q64.05 31.87 64.39 31.37ZM58.22 21.97L58.21 21.95L58.35 22.10Q58.87 21.89 59.33 22.01L59.43 22.11L59.39 22.07Q60.02 22.17 60.40 22.28L60.37 22.25L60.42 22.30Q60.43 22.57 60.50 22.99L60.51 23.00L60.56 23.05Q60.62 23.45 60.50 23.83L60.58 23.90L60.47 23.80Q60.53 25.60 59.35 26.71L59.38 26.74L59.24 26.60Q58.36 27.70 56.72 27.66L56.71 27.65L56.75 27.69Q56.34 27.70 54.82 27.39L54.85 27.43L54.84 27.42Q54.61 26.54 54.61 25.82L54.46 25.66L54.47 25.68Q54.43 25.45 54.43 25.22L54.58 25.36L54.41 25.20Q54.57 23.15 56.86 22.35L56.74 22.24L56.74 22.24Q57.39 21.97 58.23 21.97Z"></path><path fill="#444" d="M90.34 41.23L90.38 41.27L87.40 41.15L87.43 41.18Q84.96 40.99 84.16 40.88L84.08 40.80L84.24 40.96Q82.40 40.60 81.22 39.73L81.08 39.58L81.10 39.60Q79.86 38.25 79.63 34.90L79.72 34.99L79.65 34.92Q79.68 34.50 79.61 33.28L79.50 33.18L79.42 33.09Q79.60 32.85 79.52 31.75L79.34 31.57L79.43 31.66Q79.32 30.75 79.36 30.03L79.48 30.14L79.47 30.14Q79.58 28.12 80.61 27.36L80.63 27.38L80.71 27.46Q82.56 26.22 87.40 25.99L87.49 26.09L87.47 26.07Q89.50 25.97 90.30 25.97L90.18 25.84L90.22 25.89Q91.74 25.93 93.11 26.23L93.18 26.29L93.12 26.24Q92.54 27.48 92.01 29.31L92.02 29.32L91.91 29.22Q91.51 29.39 91.02 29.35L91.07 29.40L91.05 29.38Q90.91 29.31 90.37 29.20L90.31 29.13L89.34 28.89L89.48 29.03Q88.30 28.65 87.69 28.69L87.80 28.80L87.78 28.78Q82.91 28.71 82.30 31.52L82.36 31.58L82.49 31.71Q82.18 32.81 82.26 33.76L82.13 33.63L82.31 33.81Q82.43 36.37 83.66 37.45L83.54 37.32L83.62 37.40Q84.91 38.55 87.50 38.59L87.42 38.50L87.53 38.61Q88.84 38.48 89.57 38.18L89.63 38.24L90.67 37.76L90.61 37.69Q91.22 37.55 91.79 37.47L91.86 37.54L91.83 37.51Q91.98 39.22 92.36 40.51L92.31 40.46L92.48 40.63Q92.07 40.83 90.28 41.18ZM93.64 44.23L93.54 44.13L93.62 44.21Q93.93 44.18 94.77 44.26L94.78 44.26L94.75 44.24Q95.38 44.26 95.73 43.69L95.83 43.80L95.72 43.68Q94.51 41.60 94.01 39.50L94.18 39.66L94.08 39.57Q93.69 39.48 93.23 39.48L93.28 39.53L93.36 39.61Q92.90 39.57 92.52 39.76L92.48 39.72L92.53 39.77Q92.18 38.50 92.02 37.09L91.97 37.04L91.97 37.04Q91.35 36.96 90.14 37.53L90.14 37.53L90.12 37.52Q88.87 38.05 88.38 38.05L88.53 38.20L88.42 38.10Q87.99 38.16 87.38 38.13L87.37 38.12L87.36 38.10Q86.40 38.02 84.42 37.45L84.43 37.45L84.44 37.46Q84.13 36.78 84.17 35.64L84.12 35.59L84.11 35.58Q84.37 31.42 89.32 30.89L89.36 30.93L89.39 30.96Q89.95 30.83 91.32 31.18L91.48 31.34L91.36 31.22Q92.70 31.53 93.43 31.49L93.43 31.49L93.53 31.60Q93.99 29.39 94.68 27.56L94.68 27.57L94.68 27.56Q94.16 27.31 93.78 27.24L93.86 27.32L93.01 27.15L93.01 27.15Q93.17 26.63 93.55 25.76L93.56 25.77L93.73 25.93Q92.04 25.42 90.48 25.42L90.47 25.41L90.58 25.52Q88.15 25.50 83.78 25.88L83.78 25.88L83.87 25.97Q81.78 25.97 80.21 26.96L80.26 27.01L80.23 26.98Q79.11 28.03 79.11 29.93L79.07 29.89L78.98 29.80Q79.02 30.26 79.13 31.48L79.12 31.46L79.11 31.45Q79.17 32.46 79.13 33.07L79.27 33.21L79.14 33.08Q79.26 35.06 79.98 38.79L79.88 38.69L79.94 38.75Q80.43 39.70 80.93 40.20L80.78 40.05L81.03 40.30L81.10 40.33L81.08 40.31Q82.10 42.43 86.02 43.04L86.11 43.14L86.13 43.15Q89.27 43.63 91.94 43.90L91.84 43.80L91.80 43.76Q92.71 43.94 93.62 44.21Z"></path><path d="M10 4 C71 39,90 36,143 49" stroke="#666" fill="none"></path><path d="M13 12 C65 17,63 7,135 17" stroke="#222" fill="none"></path><path fill="#333" d="M28.89 20.10L28.94 20.15L28.94 20.15Q24.48 20.00 20.79 19.01L20.94 19.16L20.94 19.16Q22.33 23.59 22.52 28.43L22.68 28.59L22.54 28.45Q22.88 33.40 21.66 38.00L21.50 37.84L21.59 37.92Q24.41 37.32 27.91 37.17L27.95 37.21L27.97 37.22Q31.87 37.02 33.66 34.58L33.64 34.56L33.60 34.52Q35.19 32.53 35.38 28.42L35.36 28.40L35.32 28.36Q35.41 26.62 34.95 24.91L34.92 24.88L34.84 24.80Q33.64 20.13 28.92 20.13ZM16.98 42.11L16.93 42.06L16.95 42.08Q19.96 36.52 19.96 29.82L19.99 29.86L19.82 29.69Q19.85 21.19 15.55 14.19L15.61 14.25L15.57 14.21Q20.82 16.86 29.08 17.06L29.01 16.98L29.14 17.12Q37.97 17.27 38.32 24.43L38.25 24.37L38.27 24.38Q38.34 27.04 38.30 28.48L38.37 28.56L38.48 28.66Q38.28 33.23 37.45 35.40L37.45 35.40L37.39 35.34Q36.97 36.45 36.21 37.29L36.29 37.36L36.38 37.45Q34.86 38.83 32.23 39.32L32.24 39.33L32.23 39.32Q30.71 39.66 27.70 39.89L27.82 40.01L27.71 39.90Q21.58 40.36 16.98 42.11ZM34.87 41.95L34.88 41.97L34.88 41.96Q37.62 41.63 39.26 40.14L39.27 40.15L39.34 40.22Q40.50 38.42 40.46 35.67L40.38 35.59L40.47 35.68Q40.51 34.35 40.35 30.84L40.33 30.82L40.03 26.33L39.96 26.26Q39.76 23.97 38.81 21.88L38.88 21.95L38.87 21.94Q38.38 21.03 37.70 20.38L37.72 20.41L37.55 20.08L37.57 20.11Q35.84 17.12 31.43 16.74L31.41 16.72L31.54 16.85Q28.72 16.58 21.71 15.86L21.82 15.97L21.81 15.95Q18.44 15.40 14.74 13.38L14.80 13.44L14.73 13.37Q19.65 20.84 19.65 29.60L19.60 29.54L19.75 29.70Q19.70 36.64 16.35 42.81L16.40 42.86L16.35 42.81Q16.92 42.43 18.25 41.93L18.37 42.05L18.37 42.05Q18.09 42.95 17.21 44.44L17.15 44.38L17.20 44.42Q20.23 43.34 23.58 42.85L23.55 42.82L23.55 42.82Q25.59 42.46 29.24 42.35L29.19 42.29L29.24 42.34Q33.04 42.14 34.86 41.95ZM34.93 28.35L35.02 28.44L34.95 28.37Q34.66 36.46 27.96 36.84L27.83 36.71L27.94 36.82Q26.05 36.98 23.99 37.24L23.93 37.18L23.81 37.07Q24.52 33.21 24.52 29.55L24.56 29.59L24.59 29.62Q24.50 25.84 23.77 21.99L23.88 22.10L23.86 22.07Q26.21 22.52 30.89 22.52L30.77 22.40L30.84 22.47Q32.12 22.23 33.75 23.18L33.82 23.24L33.77 23.19Q35.04 25.46 34.89 28.31Z"></path><path fill="#222" d="M113.29 14.07L113.38 14.16L113.27 14.05Q114.25 16.09 115.92 16.43L115.85 16.36L115.87 16.37Q116.44 16.53 117.12 16.33L117.06 16.27L117.13 16.35Q116.95 22.29 116.87 28.16L116.84 28.12L116.75 28.03Q116.86 33.96 117.09 39.90L117.01 39.82L117.05 39.87Q116.77 39.81 116.46 39.81L116.56 39.91L116.54 39.89Q114.83 39.75 113.54 41.57L113.69 41.72L113.54 41.57Q114.30 34.72 114.26 27.87L114.32 27.93L114.29 27.90Q114.16 20.95 113.33 14.10ZM113.24 42.83L113.12 42.71L113.22 42.81Q114.17 40.87 115.23 40.41L115.31 40.49L115.33 40.51Q115.29 41.57 115.29 42.56L115.15 42.42L115.26 42.53Q115.90 42.07 116.70 42.18L116.84 42.32L116.79 42.27Q118.85 42.62 120.07 45.32L120.08 45.34L119.97 45.23Q118.78 37.68 118.78 30.22L118.88 30.32L118.79 30.23Q118.79 22.49 119.62 15.18L119.59 15.15L119.61 15.17Q118.71 17.16 117.57 17.77L117.58 17.79L117.43 17.63Q117.45 16.63 117.53 15.56L117.62 15.65L117.51 15.55Q116.83 16.16 116.07 16.01L116.10 16.04L116.10 16.04Q113.81 15.54 112.82 12.68L112.91 12.77L112.86 12.72Q113.86 20.19 113.93 27.73L113.97 27.77L113.96 27.76Q114.10 35.28 113.22 42.82Z"></path></svg>
+              </div>
+              <div style="margin-top: 5px; color: #404040; font-size: 14px; line-height: 20px;">
+                Please enter the characters exactly as they appear above.
+              </div>
+              <div style="margin-top: 10px; display: flex; justify-content: space-between">
+                <input type="text" id="svgCaptchaInputId" style="font-size: 14px; line-height: 20px; outline: none; width: 160px;" data-ddg-inputtype="unknown">
+                <button id="svgCaptchaButtonId" style="width: 65px;">GO</button>
+              </div>
+              <div id="svgCaptchaMessageId" style="color: red; margin-top: 5px;"></div>
+            </div>
+          </div>
+        </div></body></html>
\ No newline at end of file
diff --git a/injected/src/features/broker-protection/actions/actions.js b/injected/src/features/broker-protection/actions/actions.js
index 8d7ef88301a..3d277e4baa6 100644
--- a/injected/src/features/broker-protection/actions/actions.js
+++ b/injected/src/features/broker-protection/actions/actions.js
@@ -1,6 +1,6 @@
-export { extract } from './extract';
-export { fillForm } from './fill-form';
-export { click } from './click';
-export { expectation } from './expectation';
-export { navigate } from './navigate';
-export { getCaptchaInfo, solveCaptcha } from '../captcha-services/captcha.service';
+export { extract } from './extract.js';
+export { fillForm } from './fill-form.js';
+export { click } from './click.js';
+export { expectation } from './expectation.js';
+export { navigate } from './navigate.js';
+export { getCaptchaInfo, solveCaptcha } from '../captcha-services/captcha.service.js';
diff --git a/injected/src/features/broker-protection/captcha-services/captcha.service.js b/injected/src/features/broker-protection/captcha-services/captcha.service.js
index 9d8114cd033..79f592a01fa 100644
--- a/injected/src/features/broker-protection/captcha-services/captcha.service.js
+++ b/injected/src/features/broker-protection/captcha-services/captcha.service.js
@@ -51,9 +51,9 @@ export function getSupportingCodeToInject(action) {
  *
  * @param {import('../types.js').PirAction} action
  * @param {Document | HTMLElement} root
- * @return {import('../types.js').ActionResponse}
+ * @return {Promise<import('../types.js').ActionResponse>}
  */
-export function getCaptchaInfo(action, root = document) {
+export async function getCaptchaInfo(action, root = document) {
     const { id: actionID, actionType, captchaType, selector } = action;
     if (!captchaType) {
         // ensures backward compatibility with old actions
@@ -72,7 +72,7 @@ export function getCaptchaInfo(action, root = document) {
         return createError(captchaProvider.error.message);
     }
 
-    const captchaIdentifier = captchaProvider.getCaptchaIdentifier(captchaContainer);
+    const captchaIdentifier = await captchaProvider.getCaptchaIdentifier(captchaContainer);
     if (!captchaIdentifier) {
         return createError(`could not extract captcha identifier from the container with selector ${selector}`);
     }
diff --git a/injected/src/features/broker-protection/captcha-services/providers/cloudflare-turnstile.js b/injected/src/features/broker-protection/captcha-services/providers/cloudflare-turnstile.js
index 6aa6145d271..89be7c82d38 100644
--- a/injected/src/features/broker-protection/captcha-services/providers/cloudflare-turnstile.js
+++ b/injected/src/features/broker-protection/captcha-services/providers/cloudflare-turnstile.js
@@ -22,7 +22,7 @@ export class CloudFlareTurnstileProvider {
      */
     getCaptchaIdentifier(_captchaContainerElement) {
         // TODO: Implement
-        return null;
+        return Promise.resolve(null);
     }
 
     getSupportingCodeToInject() {
diff --git a/injected/src/features/broker-protection/captcha-services/providers/hcaptcha.js b/injected/src/features/broker-protection/captcha-services/providers/hcaptcha.js
index f4fd87753a7..5cf7f529983 100644
--- a/injected/src/features/broker-protection/captcha-services/providers/hcaptcha.js
+++ b/injected/src/features/broker-protection/captcha-services/providers/hcaptcha.js
@@ -22,7 +22,7 @@ export class HCaptchaProvider {
      */
     getCaptchaIdentifier(_captchaContainerElement) {
         // TODO: Implement
-        return null;
+        return Promise.resolve(null);
     }
 
     getSupportingCodeToInject() {
diff --git a/injected/src/features/broker-protection/captcha-services/providers/image.js b/injected/src/features/broker-protection/captcha-services/providers/image.js
new file mode 100644
index 00000000000..9661f629a3e
--- /dev/null
+++ b/injected/src/features/broker-protection/captcha-services/providers/image.js
@@ -0,0 +1,90 @@
+import { PirError } from '../../types';
+import { svgToBase64Jpg, imageToBase64 } from '../utils/image';
+import { injectTokenIntoElement } from '../utils/token';
+import { isElementType } from '../utils/element';
+import { stringifyFunction } from '../utils/stringify-function';
+
+/**
+ * @import { CaptchaProvider } from './provider.interface';
+ * @implements {CaptchaProvider}
+ */
+export class ImageProvider {
+    getType() {
+        return 'image';
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    isSupportedForElement(captchaImageElement) {
+        if (!captchaImageElement) {
+            return false;
+        }
+
+        return isElementType(captchaImageElement, ['img', 'svg']);
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    async getCaptchaIdentifier(captchaImageElement) {
+        if (isSVGElement(captchaImageElement)) {
+            return await svgToBase64Jpg(captchaImageElement);
+        }
+
+        if (isImgElement(captchaImageElement)) {
+            return imageToBase64(captchaImageElement);
+        }
+
+        return PirError.create(
+            `[ImageProvider.getCaptchaIdentifier] could not extract Base64 from image with tag name: ${captchaImageElement.tagName}`,
+        );
+    }
+
+    getSupportingCodeToInject() {
+        return null;
+    }
+
+    /**
+     * @param {HTMLElement} captchaInputElement - The captcha input element
+     */
+    canSolve(captchaInputElement) {
+        return isElementType(captchaInputElement, ['input', 'textarea']);
+    }
+
+    /**
+     * @param {HTMLInputElement} captchaInputElement - The captcha input element
+     * @param {string} token - The solved captcha token
+     */
+    injectToken(captchaInputElement, token) {
+        return injectTokenIntoElement({ captchaInputElement, token });
+    }
+
+    /**
+     * @param {HTMLElement} _captchaInputElement - The element containing the captcha
+     * @param {string} _token - The solved captcha token
+     */
+    getSolveCallback(_captchaInputElement, _token) {
+        return stringifyFunction({
+            functionBody: function callbackNoop() {},
+            functionName: 'callbackNoop',
+            args: {},
+        });
+    }
+}
+
+/**
+ * @param {HTMLElement} element
+ * @return {element is SVGElement}
+ */
+function isSVGElement(element) {
+    return isElementType(element, 'svg');
+}
+
+/**
+ * @param {HTMLElement} element
+ * @return {element is HTMLImageElement}
+ */
+function isImgElement(element) {
+    return isElementType(element, 'img');
+}
diff --git a/injected/src/features/broker-protection/captcha-services/providers/provider.interface.js b/injected/src/features/broker-protection/captcha-services/providers/provider.interface.js
index 337ed219e54..efc2868cf72 100644
--- a/injected/src/features/broker-protection/captcha-services/providers/provider.interface.js
+++ b/injected/src/features/broker-protection/captcha-services/providers/provider.interface.js
@@ -28,10 +28,10 @@ export class CaptchaProvider {
      * Extracts the site key from the captcha container element
      * @abstract
      * @param {HTMLElement} _captchaContainerElement - The element containing the captcha
-     * @returns {PirError | string | null} The site key or null if not found
+     * @returns {Promise<PirError | string | null>} The site key or null if not found
      */
     getCaptchaIdentifier(_captchaContainerElement) {
-        throw new Error('getCaptchaIdentifier() missing implementation');
+        return Promise.reject(new Error('getCaptchaIdentifier() missing implementation'));
     }
 
     /**
diff --git a/injected/src/features/broker-protection/captcha-services/providers/recaptcha.js b/injected/src/features/broker-protection/captcha-services/providers/recaptcha.js
index db52fbb2d9d..82dc5a6528c 100644
--- a/injected/src/features/broker-protection/captcha-services/providers/recaptcha.js
+++ b/injected/src/features/broker-protection/captcha-services/providers/recaptcha.js
@@ -46,9 +46,11 @@ export class ReCaptchaProvider {
      * @param {HTMLElement} captchaContainerElement
      */
     getCaptchaIdentifier(captchaContainerElement) {
-        return safeCallWithError(
-            () => getSiteKeyFromSearchParam({ captchaElement: this._getCaptchaElement(captchaContainerElement), siteKeyAttrName: 'k' }),
-            { errorMessage: '[ReCaptchaProvider.getCaptchaIdentifier] could not extract site key' },
+        return Promise.resolve(
+            safeCallWithError(
+                () => getSiteKeyFromSearchParam({ captchaElement: this._getCaptchaElement(captchaContainerElement), siteKeyAttrName: 'k' }),
+                { errorMessage: '[ReCaptchaProvider.getCaptchaIdentifier] could not extract site key' },
+            ),
         );
     }
 
diff --git a/injected/src/features/broker-protection/captcha-services/providers/registry.js b/injected/src/features/broker-protection/captcha-services/providers/registry.js
index 9ef8c7a9c11..391c74e800f 100644
--- a/injected/src/features/broker-protection/captcha-services/providers/registry.js
+++ b/injected/src/features/broker-protection/captcha-services/providers/registry.js
@@ -1,5 +1,6 @@
 import { CaptchaFactory } from '../factory';
 import { ReCaptchaProvider } from './recaptcha';
+import { ImageProvider } from './image';
 
 const captchaFactory = new CaptchaFactory();
 
@@ -19,4 +20,6 @@ captchaFactory.registerProvider(
     }),
 );
 
+captchaFactory.registerProvider(new ImageProvider());
+
 export { captchaFactory };
diff --git a/injected/src/features/broker-protection/captcha-services/utils/element.js b/injected/src/features/broker-protection/captcha-services/utils/element.js
new file mode 100644
index 00000000000..99e4b498cf5
--- /dev/null
+++ b/injected/src/features/broker-protection/captcha-services/utils/element.js
@@ -0,0 +1,13 @@
+/**
+ *
+ * @param {HTMLElement} element - The element to check
+ * @param {string | string[]} tag - The tag name(s) to check against
+ * @returns {boolean} - True if the element is of the specified tag name(s), false otherwise
+ */
+export function isElementType(element, tag) {
+    if (Array.isArray(tag)) {
+        return tag.some((t) => isElementType(element, t));
+    }
+
+    return element.tagName.toLowerCase() === tag.toLowerCase();
+}
diff --git a/injected/src/features/broker-protection/captcha-services/utils/image.js b/injected/src/features/broker-protection/captcha-services/utils/image.js
new file mode 100644
index 00000000000..eed0d399894
--- /dev/null
+++ b/injected/src/features/broker-protection/captcha-services/utils/image.js
@@ -0,0 +1,65 @@
+/**
+ * Converts an SVG element to a base64-encoded JPEG string.
+ *
+ * @param {SVGElement} svgElement - The SVG element to convert
+ * @param {string} [backgroundColor='white'] - The background color for the JPEG image
+ * @return {Promise<string>} - A promise that resolves to the base64-encoded JPEG image
+ */
+export function svgToBase64Jpg(svgElement, backgroundColor = 'white') {
+    const svgString = new XMLSerializer().serializeToString(svgElement);
+    const svgDataUrl = 'data:image/svg+xml;base64,' + btoa(svgString);
+
+    return new Promise((resolve, reject) => {
+        const img = new Image();
+        img.onload = () => {
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+
+            if (!ctx) {
+                reject(new Error('Could not get 2D context from canvas'));
+                return;
+            }
+
+            canvas.width = img.width;
+            canvas.height = img.height;
+
+            ctx.fillStyle = backgroundColor;
+            ctx.fillRect(0, 0, canvas.width, canvas.height);
+            ctx.drawImage(img, 0, 0);
+
+            const jpgBase64 = canvas.toDataURL('image/jpeg');
+
+            resolve(jpgBase64);
+        };
+        img.onerror = (error) => {
+            reject(error);
+        };
+
+        img.src = svgDataUrl;
+    });
+}
+
+/**
+ * Converts an image element to a base64-encoded JPEG string
+ *
+ * @param {HTMLImageElement} imageElement - The image element to convert.
+ * @return {string} - The base64-encoded JPEG string.
+ * @throws {Error} - Throws an error if the canvas context cannot be obtained.
+ */
+export function imageToBase64(imageElement) {
+    const canvas = document.createElement('canvas');
+    const ctx = canvas.getContext('2d');
+
+    if (!ctx) {
+        throw Error('[imageToBase64] Could not get 2D context from canvas');
+    }
+
+    canvas.width = imageElement.width;
+    canvas.height = imageElement.height;
+
+    ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height);
+
+    const base64String = canvas.toDataURL('image/jpeg'); // You can change the format if needed
+
+    return base64String;
+}
diff --git a/injected/src/features/broker-protection/captcha-services/utils/token.js b/injected/src/features/broker-protection/captcha-services/utils/token.js
index 3a07ea76f04..4360a08ff48 100644
--- a/injected/src/features/broker-protection/captcha-services/utils/token.js
+++ b/injected/src/features/broker-protection/captcha-services/utils/token.js
@@ -1,27 +1,58 @@
 import { PirError, PirSuccess } from '../../types';
 import { safeCallWithError } from '../../utils/safe-call';
 import { getElementByTagName } from '../../utils/utils';
+import { isElementType } from './element';
 
 /**
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
-            element.innerHTML = token;
-            return PirSuccess.create({ injected: true });
+            if ((isInputElement(element) && ['text', 'hidden'].includes(element.type)) || isTextAreaElement(element)) {
+                element.value = token;
+                return PirSuccess.create({ injected: true });
+            } else {
+                return PirError.create(`[injectTokenIntoElement] element is neither a text input or textarea`);
+            }
         },
-        { errorMessage: `[injectTokenIntoElement] error injecting token into element ${elementName}` },
+        { errorMessage: `[injectTokenIntoElement] error injecting token into element` },
     );
 }
+
+/**
+ * @param {HTMLElement} element
+ * @return {element is HTMLInputElement}
+ */
+function isInputElement(element) {
+    return isElementType(element, 'input');
+}
+
+/**
+ * @param {HTMLElement} element
+ * @return {element is HTMLTextAreaElement}
+ */
+function isTextAreaElement(element) {
+    return isElementType(element, 'textarea');
+}
diff --git a/injected/src/features/broker-protection/execute.js b/injected/src/features/broker-protection/execute.js
index 9588aa7f833..67c16e4d6ac 100644
--- a/injected/src/features/broker-protection/execute.js
+++ b/injected/src/features/broker-protection/execute.js
@@ -21,7 +21,7 @@ export async function execute(action, inputData, root = document) {
             case 'fillForm':
                 return fillForm(action, data(action, inputData, 'extractedProfile'), root);
             case 'getCaptchaInfo':
-                return getCaptchaInfo(action, root);
+                return await getCaptchaInfo(action, root);
             case 'solveCaptcha':
                 return solveCaptcha(action, data(action, inputData, 'token'), root);
             default: {
diff --git a/injected/unit-test/broker-protection.js b/injected/unit-test/broker-protection.js
index 5aae2c9282a..9b7ae75477b 100644
--- a/injected/unit-test/broker-protection.js
+++ b/injected/unit-test/broker-protection.js
@@ -11,6 +11,7 @@ import { generatePhoneNumber, generateZipCode, generateStreetAddress } from '../
 import { CityStateExtractor } from '../src/features/broker-protection/extractors/address.js';
 import { ProfileHashTransformer } from '../src/features/broker-protection/extractors/profile-url.js';
 import { getComparisonFunction } from '../src/features/broker-protection/actions/click.js';
+import { isElementType } from '../src/features/broker-protection/captcha-services/utils/element.js';
 
 describe('Actions', () => {
     describe('extract', () => {
@@ -678,6 +679,32 @@ describe('generators', () => {
     });
 });
 
+describe('captcha-services', () => {
+    describe('utils', () => {
+        describe('isElementType', () => {
+            it('should return true for single valid element type', () => {
+                const type = 'img';
+                const element = /** @type {HTMLElement} */ ({ tagName: type });
+                expect(isElementType(element, type)).toBe(true);
+            });
+
+            it('should return true for multiple valid element types', () => {
+                const validElementTypes = ['input', 'textarea', 'img', 'svg'];
+                const type = 'svg';
+                const element = /** @type {HTMLElement} */ ({ tagName: type });
+                expect(isElementType(element, validElementTypes)).toBe(true);
+            });
+
+            it('should return false for invalid element types', () => {
+                const validElementTypes = ['input', 'textarea', 'img', 'svg'];
+                const type = 'div';
+                const element = /** @type {HTMLElement} */ ({ tagName: type });
+                expect(isElementType(element, validElementTypes)).toBe(false);
+            });
+        });
+    });
+});
+
 describe('utils', () => {
     describe('generateRandomInt', () => {
         it('generates an integers between the min and max values', () => {


Reviews:
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:30:28Z
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T10:58:51Z
user: login: madblex
body: LGTM! No blockers, just some recommendations
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:27:08Z
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:02:23Z
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:10:32Z
user: login: madblex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:41:03Z
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:51:46Z
user: login: madblex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T06:15:56Z
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:27:11Z
user: login: madblex
body: LGTM with one nit
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T11:28:45Z
user: login: brianhall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T11:32:26Z
user: login: madblex
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T11:35:47Z
user: login: jonathanKingston
body: Just for code ownership r+ maybe it's re-required after the file changes.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T11:50:42Z

Review comments:
diff_hunk: @@ -0,0 +1,78 @@
+import { PirError } from '../../types';
+import { svgToBase64Jpg, imageToBase64 } from '../utils/image';
+import { injectTokenIntoElement } from '../utils/token';
+import { isElementType } from '../utils/element';
+import { stringifyFunction } from '../utils/stringify-function';
+
+/**
+ * @import { CaptchaProvider } from './provider.interface';
+ * @implements {CaptchaProvider}
+ */
+export class ImageProvider {
+    getType() {
+        return 'image';
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    isSupportedForElement(captchaImageElement) {
+        if (!captchaImageElement) {
+            return false;
+        }
+
+        return isElementType(captchaImageElement, ['img', 'svg']);
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    async getCaptchaIdentifier(captchaImageElement) {
+        if (captchaImageElement.tagName.toLocaleLowerCase() === 'svg') {
+            // Converting to unknown to avoid TypeScript error when going from HTMLElement to SVGElement
+            const unknownElement = /** @type {unknown} */ (captchaImageElement);
+            const svgElement = /** @type {SVGElement} */ (unknownElement);
+            const base64Image = await svgToBase64Jpg(svgElement);
path: injected/src/features/broker-protection/captcha-services/providers/image.js
user: login: brianhall
body: I chose not to use `safeCallWithError` here because I’d have to rewrite quite a bit of code to work in an asynchronous context and I think it’s throwing/catching errors ok as-is, but let me know if you think that’s a mistake. I could be convinced otherwise.
created_at: 2025-04-03T16:30:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,22 +6,36 @@ import { getElementByTagName } from '../../utils/utils';
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
+            if ('value' in element) {
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: brianhall
body: It occurs to me that we’re injecting these into either inputs or textarea, so I think ‘value’ would be sufficient in either case? I think the innerHTML thing is a carryover from the old code. If this is the case we can simplify here. Let me know if you disagree.
created_at: 2025-04-04T10:58:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { PirError } from '../../types';
+import { svgToBase64Jpg, imageToBase64 } from '../utils/image';
+import { injectTokenIntoElement } from '../utils/token';
+import { isElementType } from '../utils/element';
+import { stringifyFunction } from '../utils/stringify-function';
+
+/**
+ * @import { CaptchaProvider } from './provider.interface';
+ * @implements {CaptchaProvider}
+ */
+export class ImageProvider {
+    getType() {
+        return 'image';
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    isSupportedForElement(captchaImageElement) {
+        if (!captchaImageElement) {
+            return false;
+        }
+
+        return isElementType(captchaImageElement, ['img', 'svg']);
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    async getCaptchaIdentifier(captchaImageElement) {
+        if (captchaImageElement.tagName.toLocaleLowerCase() === 'svg') {
+            // Converting to unknown to avoid TypeScript error when going from HTMLElement to SVGElement
+            const unknownElement = /** @type {unknown} */ (captchaImageElement);
+            const svgElement = /** @type {SVGElement} */ (unknownElement);
+            const base64Image = await svgToBase64Jpg(svgElement);
path: injected/src/features/broker-protection/captcha-services/providers/image.js
user: login: madblex
body: nit: To avoid this you can create a type guard:

```js
/**
 * @param {HTMLElement} element
 * @return {element is SVGElement}
 */
function isSVGElement(element) {
  return element.tagName.toLocaleLowerCase() === 'svg';
}
```

and use as follows:
```suggestion
        if (isSVGElement(captchaImageElement)) {
            const base64Image = await svgToBase64Jpg(captchaImageElement);
```

created_at: 2025-04-08T10:12:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+import { PirError } from '../../types';
+import { svgToBase64Jpg, imageToBase64 } from '../utils/image';
+import { injectTokenIntoElement } from '../utils/token';
+import { isElementType } from '../utils/element';
+import { stringifyFunction } from '../utils/stringify-function';
+
+/**
+ * @import { CaptchaProvider } from './provider.interface';
+ * @implements {CaptchaProvider}
+ */
+export class ImageProvider {
+    getType() {
+        return 'image';
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    isSupportedForElement(captchaImageElement) {
+        if (!captchaImageElement) {
+            return false;
+        }
+
+        return isElementType(captchaImageElement, ['img', 'svg']);
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    async getCaptchaIdentifier(captchaImageElement) {
+        if (captchaImageElement.tagName.toLocaleLowerCase() === 'svg') {
+            // Converting to unknown to avoid TypeScript error when going from HTMLElement to SVGElement
+            const unknownElement = /** @type {unknown} */ (captchaImageElement);
+            const svgElement = /** @type {SVGElement} */ (unknownElement);
+            const base64Image = await svgToBase64Jpg(svgElement);
path: injected/src/features/broker-protection/captcha-services/providers/image.js
user: login: madblex
body: It's ok, once we add a catch-all pixel for actions, we'll have visibility into this
created_at: 2025-04-08T10:14:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+/**
+ * Converts an SVG element to a base64-encoded JPEG string.
+ *
+ * @param {SVGElement} svgElement - The SVG element to convert
+ * @param {string} [backgroundColor='white'] - The background color for the JPEG image
+ * @return {Promise<string>} - A promise that resolves to the base64-encoded JPEG image
+ */
+export function svgToBase64Jpg(svgElement, backgroundColor = 'white') {
+    const svgString = new XMLSerializer().serializeToString(svgElement);
+    const svgDataUrl = 'data:image/svg+xml;base64,' + btoa(svgString);
+
+    return new Promise((resolve, reject) => {
+        // Create an Image object
+        const img = new Image();
+        img.onload = () => {
+            // Create a canvas element
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+
+            if (!ctx) {
+                reject(new Error('Could not get 2D context from canvas'));
+                return;
+            }
+
+            // Set canvas dimensions to match the image
+            canvas.width = img.width;
+            canvas.height = img.height;
+
+            // Set the background color of the canvas
+            ctx.fillStyle = backgroundColor;
+            ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+            // Draw the image onto the canvas
+            ctx.drawImage(img, 0, 0);
+
+            // Convert the canvas content to a JPEG base64 string
+            const jpgBase64 = canvas.toDataURL('image/jpeg');
+
+            // Resolve the promise with the base64-encoded JPEG
+            resolve(jpgBase64);
+        };
+        img.onerror = (error) => {
+            reject(error);
+        };
+
+        // Set the source of the image to the base64-encoded SVG
+        img.src = svgDataUrl;
+    });
+}
+
+/**
+ * Converts an image element to a base64-encoded JPEG string
+ *
+ * @param {HTMLImageElement} imageElement - The image element to convert.
+ * @return {string} - The base64-encoded JPEG string.
+ * @throws {Error} - Throws an error if the canvas context cannot be obtained.
+ */
+export function imageToBase64(imageElement) {
+    const canvas = document.createElement('canvas');
+    const ctx = canvas.getContext('2d');
+
+    if (!ctx) {
+        throw Error('[imageToBase64] Could not get 2D context from canvas');
+    }
+
+    // Set canvas dimensions to match the image
+    canvas.width = imageElement.width;
+    canvas.height = imageElement.height;
+
+    // Draw the image onto the canvas
+    ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height);
+
+    // Convert the canvas content to a JPEG base64 string
+    const base64String = canvas.toDataURL('image/jpeg'); // You can change the format if needed
path: injected/src/features/broker-protection/captcha-services/utils/image.js
user: login: madblex
body: nit: I think the code is self explanatory and does not need comments
created_at: 2025-04-08T10:17:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,22 +6,36 @@ import { getElementByTagName } from '../../utils/utils';
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
+            if ('value' in element) {
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: madblex
body: It's a carryover so I'd ask @shakyShane. It won't hurt to check for the input types since there are other HTML elements supporting the `value` attribute (lists, selects, etc)
created_at: 2025-04-08T10:25:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,22 +6,36 @@ import { getElementByTagName } from '../../utils/utils';
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
+            if ('value' in element) {
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: brianhall
body: Do you mean add an additional check on the type attribute to ensure we’re only inserting tokens into inputs and textareas?
created_at: 2025-04-09T15:02:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { PirError } from '../../types';
+import { svgToBase64Jpg, imageToBase64 } from '../utils/image';
+import { injectTokenIntoElement } from '../utils/token';
+import { isElementType } from '../utils/element';
+import { stringifyFunction } from '../utils/stringify-function';
+
+/**
+ * @import { CaptchaProvider } from './provider.interface';
+ * @implements {CaptchaProvider}
+ */
+export class ImageProvider {
+    getType() {
+        return 'image';
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    isSupportedForElement(captchaImageElement) {
+        if (!captchaImageElement) {
+            return false;
+        }
+
+        return isElementType(captchaImageElement, ['img', 'svg']);
+    }
+
+    /**
+     * @param {HTMLElement} captchaImageElement - The captcha image element
+     */
+    async getCaptchaIdentifier(captchaImageElement) {
+        if (captchaImageElement.tagName.toLocaleLowerCase() === 'svg') {
+            // Converting to unknown to avoid TypeScript error when going from HTMLElement to SVGElement
+            const unknownElement = /** @type {unknown} */ (captchaImageElement);
+            const svgElement = /** @type {SVGElement} */ (unknownElement);
+            const base64Image = await svgToBase64Jpg(svgElement);
path: injected/src/features/broker-protection/captcha-services/providers/image.js
user: login: brianhall
body: Ah, that’s much better. Thank you!
created_at: 2025-04-09T15:10:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,22 +6,36 @@ import { getElementByTagName } from '../../utils/utils';
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
+            if ('value' in element) {
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: madblex
body: Yes, that's what I meant
created_at: 2025-04-09T15:41:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,22 +6,36 @@ import { getElementByTagName } from '../../utils/utils';
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
+            if ('value' in element) {
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: brianhall
body: Ok, thanks. I’ve updated this, please take a look when you get a chance.
created_at: 2025-04-09T16:51:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,59 @@
 import { PirError, PirSuccess } from '../../types';
 import { safeCallWithError } from '../../utils/safe-call';
 import { getElementByTagName } from '../../utils/utils';
+import { isElementType } from './element';
 
 /**
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
-            element.innerHTML = token;
+            if ((isInputElement(element) && ['text', 'hidden'].includes(element.type)) || isTextAreaElement(element)) {
+                element.value = token;
+            } else if (isInputElement(element)) {
+                return PirError.create(`[injectTokenIntoElement] element is not a text input or textarea`);
+            }
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: madblex
body: Is it possible that `element` is neither an input / HTML? I guess it's unlikely, but we'd return success in that case, which is confusing
created_at: 2025-04-10T06:15:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,59 @@
 import { PirError, PirSuccess } from '../../types';
 import { safeCallWithError } from '../../utils/safe-call';
 import { getElementByTagName } from '../../utils/utils';
+import { isElementType } from './element';
 
 /**
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
-            element.innerHTML = token;
+            if ((isInputElement(element) && ['text', 'hidden'].includes(element.type)) || isTextAreaElement(element)) {
+                element.value = token;
+            } else if (isInputElement(element)) {
+                return PirError.create(`[injectTokenIntoElement] element is not a text input or textarea`);
+            }
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: brianhall
body: Oops, that was a typo. Should be fixed now.
created_at: 2025-04-10T08:27:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,59 @@
 import { PirError, PirSuccess } from '../../types';
 import { safeCallWithError } from '../../utils/safe-call';
 import { getElementByTagName } from '../../utils/utils';
+import { isElementType } from './element';
 
 /**
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
-            element.innerHTML = token;
+            if ((isInputElement(element) && ['text', 'hidden'].includes(element.type)) || isTextAreaElement(element)) {
+                element.value = token;
+            } else {
+                return PirError.create(`[injectTokenIntoElement] element is neither a text input or textarea`);
+            }
+
             return PirSuccess.create({ injected: true });
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: madblex
body: nit: Now that the conditions are clarified, would it make sense to group the code for success and error paths? i.e to move the `return PirSuccess.create({ injected: true });` on line 35 after setting the element value
created_at: 2025-04-10T11:27:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,59 @@
 import { PirError, PirSuccess } from '../../types';
 import { safeCallWithError } from '../../utils/safe-call';
 import { getElementByTagName } from '../../utils/utils';
+import { isElementType } from './element';
 
 /**
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
-            element.innerHTML = token;
+            if ((isInputElement(element) && ['text', 'hidden'].includes(element.type)) || isTextAreaElement(element)) {
+                element.value = token;
+            } else if (isInputElement(element)) {
+                return PirError.create(`[injectTokenIntoElement] element is not a text input or textarea`);
+            }
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: madblex
body: Looks good!
created_at: 2025-04-10T11:28:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,59 @@
 import { PirError, PirSuccess } from '../../types';
 import { safeCallWithError } from '../../utils/safe-call';
 import { getElementByTagName } from '../../utils/utils';
+import { isElementType } from './element';
 
 /**
  * Inject a token into a named element
  * @import { PirResponse } from '../../types';
  * @param {object} params
- * @param {HTMLElement} params.captchaContainerElement  - The element containing the captcha
- * @param {string} params.elementName - Name attribute of the element to inject into
+ * @param {HTMLElement} [params.captchaContainerElement]  - The element containing the captcha
+ * @param {HTMLElement} [params.captchaInputElement]  - The element containing the captcha (optional)
+ * @param {string} [params.elementName] - Name attribute of the element to inject into (optional)
  * @param {string} params.token - The token to inject
  * @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
  */
-export function injectTokenIntoElement({ captchaContainerElement, elementName, token }) {
-    const element = getElementByTagName(captchaContainerElement, elementName);
+export function injectTokenIntoElement({ captchaContainerElement, captchaInputElement, elementName, token }) {
+    let element;
+
+    if (captchaInputElement) {
+        element = captchaInputElement;
+    } else if (elementName) {
+        element = getElementByTagName(captchaContainerElement, elementName);
+    } else {
+        return PirError.create(`[injectTokenIntoElement] must pass in either captcha input element or element name`);
+    }
+
     if (!element) {
-        return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
+        return PirError.create(`[injectTokenIntoElement] could not find element to inject token into`);
     }
 
     return safeCallWithError(
         () => {
-            element.innerHTML = token;
+            if ((isInputElement(element) && ['text', 'hidden'].includes(element.type)) || isTextAreaElement(element)) {
+                element.value = token;
+            } else {
+                return PirError.create(`[injectTokenIntoElement] element is neither a text input or textarea`);
+            }
+
             return PirSuccess.create({ injected: true });
path: injected/src/features/broker-protection/captcha-services/utils/token.js
user: login: brianhall
body: Yep, absolutely right. Adopted this suggestion.
created_at: 2025-04-10T11:32:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-04-03T13:18:19Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *content-scope-scripts* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 07f4cda2aa8c989faa19a9ad40c2f86d0f9aed6b |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/content-scope-scripts/deploys/67f7ac4a60545500089c1b45 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1612--content-scope-scripts.netlify.app](https://deploy-preview-1612--content-scope-scripts.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE2MTItLWNvbnRlbnQtc2NvcGUtc2NyaXB0cy5uZXRsaWZ5LmFwcCJ9.Fyq_2v3-yGydqVizhStyEl6LkWKpyYPe_QlKYIW8S3w)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [content-scope-scripts Preview](https://deploy-preview-1612--content-scope-scripts.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/content-scope-scripts/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-03T13:18:31Z
body: ### Temporary Branch Update

The temporary branch has been updated with the latest changes. Below are the details:

  - **Branch Name**: [pr-releases/pr-1612](https://github.com/duckduckgo/content-scope-scripts/tree/pr-releases/pr-1612)
  - **Commit Hash**: [06a3717d25ac757d7b265e5faac7cd38d4dfe02f](https://github.com/duckduckgo/content-scope-scripts/commit/06a3717d25ac757d7b265e5faac7cd38d4dfe02f)
  - **Last Updated**: April 10, 2025 at 11:32:53 AM UTC
  - **Install Command**: `npm i github:duckduckgo/content-scope-scripts#06a3717d25ac757d7b265e5faac7cd38d4dfe02f`

  Please use the above install command to update to the latest version.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T13:18:44Z
body: ### *[Beta]* Generated file diff
*Time updated:* Thu, 10 Apr 2025 11:33:03 GMT

<details open>
<summary>Android</summary>

        - android/brokerProtection.js


File has changed
</details>
<details >
<summary>Integration</summary>

        - integration/contentScope.js


File has changed
</details>
<details open>
<summary>Windows</summary>

        - windows/contentScope.js


File has changed
</details>
<details open>
<summary>Apple</summary>

        - dist/contentScopeIsolated.js


File has changed
</details>
author_association: CONTRIBUTOR
user: login: brianhall
created_at: 2025-04-04T11:23:36Z
body: Not sure why the auto respond PR steps are failing… 
author_association: CONTRIBUTOR
user: login: madblex
created_at: 2025-04-10T06:30:12Z
body: > Not sure why the auto respond PR steps are failing…

@brianhall I pushed a commit to fix it, hope that's ok.
author_association: CONTRIBUTOR
user: login: brianhall
created_at: 2025-04-10T08:20:21Z
body: > > Not sure why the auto respond PR steps are failing…
> 
> @brianhall I pushed a commit to fix it, hope that's ok.

For sure. My editor must have stripped the extensions out, thank you!
author_association: CONTRIBUTOR