From e10f38b125f16a69e2e401e4b0001c0df73cf39f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 20 Nov 2025 12:10:31 +0000
Subject: [PATCH 1/4] Initial plan
From 33b5df43d4bf0b0ee35005ea9a412b25316245d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 20 Nov 2025 12:16:06 +0000
Subject: [PATCH 2/4] Add CAPTCHA compatibility fixes for MessagePort handling
and preload resources
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
public/captcha-patch.js | 95 +++++++++++++++++++
public/sw.js | 6 +-
src/utils/captcha-handler.ts | 171 ++++++++++++++++++++++++++++++++++-
src/utils/proxy.ts | 1 +
4 files changed, 270 insertions(+), 3 deletions(-)
create mode 100644 public/captcha-patch.js
diff --git a/public/captcha-patch.js b/public/captcha-patch.js
new file mode 100644
index 0000000..55a0a2f
--- /dev/null
+++ b/public/captcha-patch.js
@@ -0,0 +1,95 @@
+/**
+ * CAPTCHA Compatibility Patch
+ * This script must be loaded BEFORE UV handler to fix MessagePort handling in postMessage
+ *
+ * Fixes the error: "DataCloneError: Failed to execute 'postMessage' on 'Window':
+ * A MessagePort could not be cloned because it was not transferred."
+ */
+
+(function() {
+ 'use strict';
+
+ // Only run once
+ if (window.__captchaPatchApplied) return;
+ window.__captchaPatchApplied = true;
+
+ // Store the original postMessage method
+ const originalWindowPostMessage = Window.prototype.postMessage;
+
+ /**
+ * Enhanced postMessage that properly handles MessagePort transfers
+ * This is critical for CAPTCHA systems (reCAPTCHA, hCaptcha, Cloudflare Turnstile, Yandex)
+ */
+ Window.prototype.postMessage = function(message, targetOrigin, transfer) {
+ try {
+ // If transfer is already provided, use it directly
+ if (transfer !== undefined) {
+ return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
+ }
+
+ // Extract MessagePorts from the message to transfer them properly
+ const ports = [];
+
+ if (message && typeof message === 'object') {
+ // Recursively find MessagePorts in the message
+ const findPorts = (obj, visited) => {
+ if (!obj || typeof obj !== 'object') return;
+
+ // Avoid circular references
+ visited = visited || new WeakSet();
+ if (visited.has(obj)) return;
+ visited.add(obj);
+
+ // Check if this is a MessagePort
+ if (obj instanceof MessagePort) {
+ ports.push(obj);
+ return;
+ }
+
+ // Check arrays
+ if (Array.isArray(obj)) {
+ for (let i = 0; i < obj.length; i++) {
+ findPorts(obj[i], visited);
+ }
+ return;
+ }
+
+ // Check object properties
+ for (const key in obj) {
+ try {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ findPorts(obj[key], visited);
+ }
+ } catch (e) {
+ // Ignore errors accessing properties (e.g., cross-origin)
+ }
+ }
+ };
+
+ findPorts(message);
+ }
+
+ // If we found MessagePorts, transfer them
+ if (ports.length > 0) {
+ return originalWindowPostMessage.call(this, message, targetOrigin, ports);
+ }
+
+ // Otherwise, use the original call
+ return originalWindowPostMessage.call(this, message, targetOrigin);
+ } catch (error) {
+ // If our enhanced version fails, try the original
+ console.warn('[CAPTCHA Patch] Enhanced postMessage failed, using fallback:', error);
+ try {
+ return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
+ } catch (fallbackError) {
+ console.error('[CAPTCHA Patch] Original postMessage also failed:', fallbackError);
+ throw fallbackError;
+ }
+ }
+ };
+
+ // Mark the patched method to prevent UV from breaking it
+ Window.prototype.postMessage.__captchaPatched = true;
+
+ console.log('[CAPTCHA Patch] MessagePort handling enabled for CAPTCHA compatibility');
+})();
diff --git a/public/sw.js b/public/sw.js
index efad95a..d9d86fb 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -18,7 +18,11 @@ const CAPTCHA_DOMAINS = [
"newassets.hcaptcha.com",
"challenges.cloudflare.com",
"cloudflare.com/cdn-cgi/challenge",
- "turnstile.cloudflare.com"
+ "turnstile.cloudflare.com",
+ "yandex.com/captcha",
+ "yandex.ru/captcha",
+ "yandex.net/captcha",
+ "captcha-delivery.com"
];
// Helper function to check if URL is CAPTCHA-related
diff --git a/src/utils/captcha-handler.ts b/src/utils/captcha-handler.ts
index 47a74e5..9730436 100644
--- a/src/utils/captcha-handler.ts
+++ b/src/utils/captcha-handler.ts
@@ -13,7 +13,11 @@ const CAPTCHA_DOMAINS = [
"gstatic.com",
"hcaptcha.com",
"cloudflare.com",
- "challenges.cloudflare.com"
+ "challenges.cloudflare.com",
+ "yandex.com",
+ "yandex.ru",
+ "yandex.net",
+ "captcha-delivery.com"
];
/**
@@ -23,6 +27,9 @@ const CAPTCHA_DOMAINS = [
export function initializeCaptchaHandlers() {
if (typeof window === "undefined") return;
+ // Fix postMessage to properly handle MessagePorts (critical for CAPTCHA functionality)
+ patchPostMessage();
+
// Ensure global CAPTCHA callbacks are accessible
if (!window.___grecaptcha_cfg) {
window.___grecaptcha_cfg = { clients: {} };
@@ -32,6 +39,7 @@ export function initializeCaptchaHandlers() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
+ // Handle iframe elements
if (node instanceof HTMLIFrameElement) {
const src = node.src || "";
// Check if this is a CAPTCHA iframe
@@ -39,7 +47,9 @@ export function initializeCaptchaHandlers() {
src.includes("recaptcha") ||
src.includes("hcaptcha") ||
src.includes("challenges.cloudflare.com") ||
- src.includes("turnstile")
+ src.includes("turnstile") ||
+ src.includes("yandex") ||
+ src.includes("captcha-delivery")
) {
// Ensure the iframe has proper sandbox permissions
if (node.sandbox && node.sandbox.length > 0) {
@@ -54,6 +64,41 @@ export function initializeCaptchaHandlers() {
}
}
}
+
+ // Handle link preload elements for CAPTCHA resources
+ if (node instanceof HTMLLinkElement && node.rel === "preload") {
+ const href = node.href || "";
+ // Check if this is a CAPTCHA-related preload
+ if (
+ href.includes("recaptcha") ||
+ href.includes("hcaptcha") ||
+ href.includes("gstatic.com") ||
+ href.includes("cloudflare.com") ||
+ href.includes("yandex") ||
+ href.includes("captcha-delivery")
+ ) {
+ // Add crossorigin attribute to avoid credential mode mismatch
+ if (!node.hasAttribute("crossorigin")) {
+ node.setAttribute("crossorigin", "anonymous");
+ }
+
+ // Ensure proper 'as' attribute
+ if (!node.hasAttribute("as")) {
+ // Determine 'as' value based on URL
+ if (href.endsWith(".js") || href.includes(".js?")) {
+ node.setAttribute("as", "script");
+ } else if (href.endsWith(".css") || href.includes(".css?")) {
+ node.setAttribute("as", "style");
+ } else if (
+ href.endsWith(".woff2") ||
+ href.endsWith(".woff") ||
+ href.endsWith(".ttf")
+ ) {
+ node.setAttribute("as", "font");
+ }
+ }
+ }
+ }
});
});
});
@@ -162,6 +207,128 @@ function enhanceNetworkRequests() {
Object.setPrototypeOf(window.XMLHttpRequest.prototype, OriginalXHR.prototype);
}
+/**
+ * Patch postMessage to properly handle MessagePort transfers
+ * This is critical for CAPTCHA systems that use MessagePorts for communication
+ */
+function patchPostMessage() {
+ // Store the original postMessage method
+ const originalPostMessage = window.postMessage;
+ const originalWindowPostMessage = Window.prototype.postMessage;
+
+ /**
+ * Enhanced postMessage that properly handles MessagePort transfers
+ */
+ const enhancedPostMessage = function (this: Window, message: any, targetOrigin: string, transfer?: any[]) {
+ try {
+ // Extract MessagePorts from the message
+ const ports: MessagePort[] = [];
+
+ // Check if transfer array is provided
+ if (transfer && Array.isArray(transfer)) {
+ // Transfer array already contains ports, use it directly
+ return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
+ }
+
+ // Check if the message contains MessagePort objects
+ if (message && typeof message === 'object') {
+ // Recursively find MessagePorts in the message
+ const findPorts = (obj: any, visited = new WeakSet()): void => {
+ if (!obj || typeof obj !== 'object') return;
+ if (visited.has(obj)) return;
+ visited.add(obj);
+
+ if (obj instanceof MessagePort) {
+ ports.push(obj);
+ return;
+ }
+
+ // Check arrays
+ if (Array.isArray(obj)) {
+ for (const item of obj) {
+ findPorts(item, visited);
+ }
+ return;
+ }
+
+ // Check object properties
+ for (const key in obj) {
+ try {
+ if (obj.hasOwnProperty(key)) {
+ findPorts(obj[key], visited);
+ }
+ } catch (e) {
+ // Ignore errors accessing properties
+ }
+ }
+ };
+
+ findPorts(message);
+ }
+
+ // If we found MessagePorts, transfer them
+ if (ports.length > 0) {
+ return originalWindowPostMessage.call(this, message, targetOrigin, ports);
+ }
+
+ // Otherwise, use the original call
+ return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
+ } catch (error) {
+ // If our enhanced version fails, fall back to original
+ console.warn('Enhanced postMessage failed, using original:', error);
+ return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
+ }
+ };
+
+ // Override Window.prototype.postMessage
+ try {
+ Object.defineProperty(Window.prototype, 'postMessage', {
+ value: enhancedPostMessage,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ } catch (e) {
+ console.warn('Failed to override Window.prototype.postMessage:', e);
+ }
+
+ // Also patch the window.postMessage directly
+ try {
+ Object.defineProperty(window, 'postMessage', {
+ value: enhancedPostMessage.bind(window),
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ } catch (e) {
+ console.warn('Failed to override window.postMessage:', e);
+ }
+
+ // Patch HTMLIFrameElement.contentWindow.postMessage
+ const originalIFrameContentWindowGetter = Object.getOwnPropertyDescriptor(
+ HTMLIFrameElement.prototype,
+ 'contentWindow'
+ );
+
+ if (originalIFrameContentWindowGetter) {
+ Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
+ get: function () {
+ const contentWindow = originalIFrameContentWindowGetter.get!.call(this);
+ if (contentWindow && contentWindow.postMessage) {
+ try {
+ contentWindow.postMessage = enhancedPostMessage.bind(contentWindow);
+ } catch (e) {
+ // Ignore cross-origin access errors
+ }
+ }
+ return contentWindow;
+ },
+ enumerable: true,
+ configurable: true
+ });
+ }
+}
+
/**
* Global declaration for reCAPTCHA config
*/
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index b65988e..80d80c6 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -151,6 +151,7 @@ class SW {
});
});
};
+ createScript("/captcha-patch.js", false); // Load CAPTCHA patch first
createScript("/vu/uv.bundle.js", true);
createScript("/vu/uv.config.js", true);
createScript("/marcs/scramjet.all.js", true);
From bc46fd473c824ed2049ae3b75d124395f2444b5a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 20 Nov 2025 12:16:43 +0000
Subject: [PATCH 3/4] Apply code formatting with biome
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
public/captcha-patch.js | 36 +++++++++++++++----------------
src/utils/captcha-handler.ts | 41 ++++++++++++++++++++----------------
2 files changed, 41 insertions(+), 36 deletions(-)
diff --git a/public/captcha-patch.js b/public/captcha-patch.js
index 55a0a2f..8cf1f5a 100644
--- a/public/captcha-patch.js
+++ b/public/captcha-patch.js
@@ -1,13 +1,13 @@
/**
* CAPTCHA Compatibility Patch
* This script must be loaded BEFORE UV handler to fix MessagePort handling in postMessage
- *
- * Fixes the error: "DataCloneError: Failed to execute 'postMessage' on 'Window':
+ *
+ * Fixes the error: "DataCloneError: Failed to execute 'postMessage' on 'Window':
* A MessagePort could not be cloned because it was not transferred."
*/
-(function() {
- 'use strict';
+(function () {
+ "use strict";
// Only run once
if (window.__captchaPatchApplied) return;
@@ -20,7 +20,7 @@
* Enhanced postMessage that properly handles MessagePort transfers
* This is critical for CAPTCHA systems (reCAPTCHA, hCaptcha, Cloudflare Turnstile, Yandex)
*/
- Window.prototype.postMessage = function(message, targetOrigin, transfer) {
+ Window.prototype.postMessage = function (message, targetOrigin, transfer) {
try {
// If transfer is already provided, use it directly
if (transfer !== undefined) {
@@ -29,23 +29,23 @@
// Extract MessagePorts from the message to transfer them properly
const ports = [];
-
- if (message && typeof message === 'object') {
+
+ if (message && typeof message === "object") {
// Recursively find MessagePorts in the message
const findPorts = (obj, visited) => {
- if (!obj || typeof obj !== 'object') return;
-
+ if (!obj || typeof obj !== "object") return;
+
// Avoid circular references
visited = visited || new WeakSet();
if (visited.has(obj)) return;
visited.add(obj);
-
+
// Check if this is a MessagePort
if (obj instanceof MessagePort) {
ports.push(obj);
return;
}
-
+
// Check arrays
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
@@ -53,7 +53,7 @@
}
return;
}
-
+
// Check object properties
for (const key in obj) {
try {
@@ -65,24 +65,24 @@
}
}
};
-
+
findPorts(message);
}
-
+
// If we found MessagePorts, transfer them
if (ports.length > 0) {
return originalWindowPostMessage.call(this, message, targetOrigin, ports);
}
-
+
// Otherwise, use the original call
return originalWindowPostMessage.call(this, message, targetOrigin);
} catch (error) {
// If our enhanced version fails, try the original
- console.warn('[CAPTCHA Patch] Enhanced postMessage failed, using fallback:', error);
+ console.warn("[CAPTCHA Patch] Enhanced postMessage failed, using fallback:", error);
try {
return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
} catch (fallbackError) {
- console.error('[CAPTCHA Patch] Original postMessage also failed:', fallbackError);
+ console.error("[CAPTCHA Patch] Original postMessage also failed:", fallbackError);
throw fallbackError;
}
}
@@ -91,5 +91,5 @@
// Mark the patched method to prevent UV from breaking it
Window.prototype.postMessage.__captchaPatched = true;
- console.log('[CAPTCHA Patch] MessagePort handling enabled for CAPTCHA compatibility');
+ console.log("[CAPTCHA Patch] MessagePort handling enabled for CAPTCHA compatibility");
})();
diff --git a/src/utils/captcha-handler.ts b/src/utils/captcha-handler.ts
index 9730436..3b6e7ac 100644
--- a/src/utils/captcha-handler.ts
+++ b/src/utils/captcha-handler.ts
@@ -219,30 +219,35 @@ function patchPostMessage() {
/**
* Enhanced postMessage that properly handles MessagePort transfers
*/
- const enhancedPostMessage = function (this: Window, message: any, targetOrigin: string, transfer?: any[]) {
+ const enhancedPostMessage = function (
+ this: Window,
+ message: any,
+ targetOrigin: string,
+ transfer?: any[]
+ ) {
try {
// Extract MessagePorts from the message
const ports: MessagePort[] = [];
-
+
// Check if transfer array is provided
if (transfer && Array.isArray(transfer)) {
// Transfer array already contains ports, use it directly
return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
}
-
+
// Check if the message contains MessagePort objects
- if (message && typeof message === 'object') {
+ if (message && typeof message === "object") {
// Recursively find MessagePorts in the message
const findPorts = (obj: any, visited = new WeakSet()): void => {
- if (!obj || typeof obj !== 'object') return;
+ if (!obj || typeof obj !== "object") return;
if (visited.has(obj)) return;
visited.add(obj);
-
+
if (obj instanceof MessagePort) {
ports.push(obj);
return;
}
-
+
// Check arrays
if (Array.isArray(obj)) {
for (const item of obj) {
@@ -250,7 +255,7 @@ function patchPostMessage() {
}
return;
}
-
+
// Check object properties
for (const key in obj) {
try {
@@ -262,56 +267,56 @@ function patchPostMessage() {
}
}
};
-
+
findPorts(message);
}
-
+
// If we found MessagePorts, transfer them
if (ports.length > 0) {
return originalWindowPostMessage.call(this, message, targetOrigin, ports);
}
-
+
// Otherwise, use the original call
return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
} catch (error) {
// If our enhanced version fails, fall back to original
- console.warn('Enhanced postMessage failed, using original:', error);
+ console.warn("Enhanced postMessage failed, using original:", error);
return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
}
};
// Override Window.prototype.postMessage
try {
- Object.defineProperty(Window.prototype, 'postMessage', {
+ Object.defineProperty(Window.prototype, "postMessage", {
value: enhancedPostMessage,
writable: true,
enumerable: true,
configurable: true
});
} catch (e) {
- console.warn('Failed to override Window.prototype.postMessage:', e);
+ console.warn("Failed to override Window.prototype.postMessage:", e);
}
// Also patch the window.postMessage directly
try {
- Object.defineProperty(window, 'postMessage', {
+ Object.defineProperty(window, "postMessage", {
value: enhancedPostMessage.bind(window),
writable: true,
enumerable: true,
configurable: true
});
} catch (e) {
- console.warn('Failed to override window.postMessage:', e);
+ console.warn("Failed to override window.postMessage:", e);
}
// Patch HTMLIFrameElement.contentWindow.postMessage
const originalIFrameContentWindowGetter = Object.getOwnPropertyDescriptor(
HTMLIFrameElement.prototype,
- 'contentWindow'
+ "contentWindow"
);
if (originalIFrameContentWindowGetter) {
- Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
+ Object.defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
get: function () {
const contentWindow = originalIFrameContentWindowGetter.get!.call(this);
if (contentWindow && contentWindow.postMessage) {
From 16229ff3c84589ecbb674ba16e04fe3e53f0d54e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 20 Nov 2025 12:23:03 +0000
Subject: [PATCH 4/4] Add comprehensive documentation for CAPTCHA compatibility
fixes
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
docs/CAPTCHA_FIX.md | 149 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 149 insertions(+)
create mode 100644 docs/CAPTCHA_FIX.md
diff --git a/docs/CAPTCHA_FIX.md b/docs/CAPTCHA_FIX.md
new file mode 100644
index 0000000..d02730b
--- /dev/null
+++ b/docs/CAPTCHA_FIX.md
@@ -0,0 +1,149 @@
+# CAPTCHA Compatibility Fix
+
+## Problem Statement
+
+When attempting to load CAPTCHA verification systems (reCAPTCHA, hCaptcha, Cloudflare Turnstile, Yandex Cloud, etc.) through the Ultraviolet proxy, the following errors occurred:
+
+```
+Uncaught DataCloneError: Failed to execute 'postMessage' on 'Window':
+A MessagePort could not be cloned because it was not transferred.
+```
+
+Additionally, preload resource warnings appeared:
+```
+A preload for '...' is found, but is not used because the request credentials mode does not match.
+Consider taking a look at crossorigin attribute.
+```
+
+## Root Cause
+
+The issue stems from how Ultraviolet intercepts `postMessage` calls. CAPTCHA systems extensively use `postMessage` with `MessagePort` objects for secure cross-origin communication between iframes. According to the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), `MessagePort` objects cannot be cloned - they must be explicitly transferred via the `transfer` parameter.
+
+When UV intercepts `postMessage`, it doesn't properly handle the transfer of `MessagePort` objects, causing the `DataCloneError`.
+
+## Solution
+
+### 1. CAPTCHA Patch Script (`/public/captcha-patch.js`)
+
+A standalone script that must be loaded **before** the UV handler. It patches `Window.prototype.postMessage` to:
+
+- Recursively scan the message object for `MessagePort` instances
+- Automatically add found ports to the `transfer` array
+- Properly invoke the native `postMessage` with transfers
+
+**Key Features:**
+- Avoids circular references with `WeakSet` tracking
+- Fallback to original implementation on error
+- Marks itself to prevent UV from overriding the fix
+
+### 2. Enhanced CAPTCHA Handler (`src/utils/captcha-handler.ts`)
+
+Extended to provide comprehensive CAPTCHA support:
+
+**`patchPostMessage()` Function:**
+- Secondary patch layer for iframe contentWindow
+- Handles edge cases UV might miss
+- Ensures proper MessagePort transfer in all contexts
+
+**Preload Resource Monitor:**
+- Watches for `` elements for CAPTCHA resources
+- Automatically adds `crossorigin="anonymous"` attribute
+- Sets appropriate `as` attribute (script, style, font) based on file type
+
+**Supported CAPTCHA Providers:**
+- Google reCAPTCHA v2/v3
+- hCaptcha
+- Cloudflare Turnstile
+- Yandex Cloud CAPTCHA
+- Other providers using similar patterns
+
+### 3. Service Worker Updates (`public/sw.js`)
+
+Added CAPTCHA domain detection for proper request handling:
+- Preserves credentials for CAPTCHA cookies
+- Ensures proper headers for CAPTCHA requests
+- Handles special routing for verification domains
+
+### 4. Proxy Initialization (`src/utils/proxy.ts`)
+
+Modified to load the CAPTCHA patch **before** UV scripts:
+```javascript
+createScript("/captcha-patch.js", false); // Load first
+createScript("/vu/uv.bundle.js", true);
+createScript("/vu/uv.config.js", true);
+```
+
+## Technical Details
+
+### MessagePort Transfer
+
+The fix implements the proper way to handle MessagePorts in `postMessage`:
+
+```javascript
+// ❌ WRONG - Causes DataCloneError
+window.postMessage(messageWithPort, "*");
+
+// ✅ CORRECT - Transfers the port
+window.postMessage(messageWithPort, "*", [messagePort]);
+```
+
+Our patch automatically detects ports in the message and constructs the proper transfer array.
+
+### Crossorigin Attribute
+
+CAPTCHA resources often load from different origins (e.g., `gstatic.com` for reCAPTCHA). Preload hints must match the credential mode:
+
+```html
+
+
+
+
+
+```
+
+## Testing
+
+To test CAPTCHA functionality:
+
+1. Build the project: `npm run build`
+2. Start the server: `npm start`
+3. Navigate through the proxy to a site with CAPTCHA:
+ - reCAPTCHA: https://www.google.com/recaptcha/api2/demo
+ - hCaptcha: https://www.hcaptcha.com/
+ - Cloudflare Turnstile: Sites with Cloudflare bot protection
+
+The CAPTCHA should load and function properly without console errors.
+
+## Browser Compatibility
+
+The fix is compatible with all modern browsers that support:
+- `Window.prototype.postMessage`
+- `MessagePort` API
+- `MutationObserver`
+- `WeakSet` (for circular reference detection)
+
+This includes:
+- Chrome/Edge 60+
+- Firefox 55+
+- Safari 11+
+
+## Security Considerations
+
+- The patch does not modify the security model of `postMessage`
+- MessagePorts are still transferred (not cloned), maintaining their single-owner semantics
+- CAPTCHA verification still occurs server-side; this only fixes client-side communication
+- No sensitive data is exposed or logged
+
+## Future Improvements
+
+Potential enhancements:
+- Upstream fix to Ultraviolet to natively handle MessagePort transfers
+- Performance optimization for large object trees
+- Support for additional transferable objects (e.g., ArrayBuffer)
+
+## References
+
+- [MDN: Window.postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
+- [MDN: MessagePort](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort)
+- [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
+- [HTML Spec: MessagePort transfer](https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports)