diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1868c7c..c2fd282 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,4 @@ // Programmatic API — allows using the proxy from other Node.js code export { startProxy } from './proxy.js'; export type { ProxyOptions } from './proxy.js'; -export { injectFiberHook } from './inject.js'; +export { injectFiberHook, buildFiberHookExternalScript } from './inject.js'; diff --git a/packages/cli/src/inject.ts b/packages/cli/src/inject.ts index 5ca12ab..aa58cf6 100644 --- a/packages/cli/src/inject.ts +++ b/packages/cli/src/inject.ts @@ -1,69 +1,98 @@ // -- HTML Injection ---------------------------------------------------------- -// Injects the Originmain fiber hook `; - } - return cachedFiberTag; -} - /** - * Build the bridge config ` + `window.__OM_ISO_BASE__="/__om_isolation__";\n` + + buildProxyFiberHookScript() ); } /** - * Strip inline Content-Security-Policy meta tags from HTML. + * The `; +} + +/** + * Strip inline Content-Security-Policy meta tags from HTML — both the + * blocking variant (`Content-Security-Policy`) and the report-only variant. + * The regex tolerates attribute reordering, optional quoting, and casing. */ function stripMetaCsp(html: string): string { return html.replace( - /]+http-equiv\s*=\s*["']?\s*content-security-policy\s*["']?[^>]*\/?>/gi, + /]+http-equiv\s*=\s*["']?\s*content-security-policy(-report-only)?\s*["']?[^>]*\/?>/gi, '', ); } /** - * Inject the fiber hook + bridge config scripts into an HTML string. + * Inject the fiber hook script tag into an HTML string. + * + * The `indexUrl` argument is no longer needed in the injected HTML (the script + * body that bakes it in is fetched separately at /__om_fiber_hook__.js). + * It is accepted for API compatibility with the proxy's previous call shape. */ -export function injectFiberHook(html: string, indexUrl?: string | null): string { - const injection = getBridgeConfigTag(indexUrl) + getFiberTag(); - const cleaned = stripMetaCsp(html); +export function injectFiberHook(html: string, _indexUrl?: string | null): string { + const tag = getFiberTag(); + const cleaned = stripMetaCsp(html); - // Try after + // ── Primary injection: immediately after ───────────────────────── + let withHead = cleaned; const headMatch = cleaned.match(/]*>/i); if (headMatch?.index !== undefined) { const insertAt = headMatch.index + headMatch[0].length; - return cleaned.slice(0, insertAt) + injection + cleaned.slice(insertAt); + withHead = cleaned.slice(0, insertAt) + tag + cleaned.slice(insertAt); + } else { + // No ? Try after as a fallback. + const htmlMatch = cleaned.match(/]*>/i); + if (htmlMatch?.index !== undefined) { + const insertAt = htmlMatch.index + htmlMatch[0].length; + withHead = cleaned.slice(0, insertAt) + tag + cleaned.slice(insertAt); + } else { + // Last resort: prepend. + withHead = tag + cleaned; + } } - // Try after - const htmlMatch = cleaned.match(/]*>/i); - if (htmlMatch?.index !== undefined) { - const insertAt = htmlMatch.index + htmlMatch[0].length; - return cleaned.slice(0, insertAt) + injection + cleaned.slice(insertAt); + // ── Redundant injection: immediately before ───────────────────── + // Provides a fallback if the head-position tag is interfered with (e.g. + // removed during React 19 hydration of ). The script's IIFE is + // idempotent — a second execution after the first ran is a no-op. + const bodyCloseIdx = withHead.lastIndexOf(''); + if (bodyCloseIdx >= 0) { + return withHead.slice(0, bodyCloseIdx) + tag + withHead.slice(bodyCloseIdx); } - // Final fallback: prepend - return injection + cleaned; + return withHead; } diff --git a/packages/cli/src/proxy.ts b/packages/cli/src/proxy.ts index 5fce83f..4465687 100644 --- a/packages/cli/src/proxy.ts +++ b/packages/cli/src/proxy.ts @@ -14,7 +14,7 @@ import { connect as netConnect } from 'node:net'; import type { IncomingMessage, ServerResponse, RequestOptions, ClientRequest } from 'node:http'; import type { Socket } from 'node:net'; -import { injectFiberHook } from './inject.js'; +import { injectFiberHook, buildFiberHookExternalScript } from './inject.js'; import { handleIsolationRequest } from './isolation-server.js'; import html2canvasSource from 'html2canvas/dist/html2canvas.min.js'; @@ -88,6 +88,33 @@ export function startProxy(opts: ProxyOptions): { close: () => void } { return; } + // ── Serve the fiber hook as an EXTERNAL script (not inline) ─────────── + // We used to inject the hook as an inline tag, but + // inline scripts are blocked by many real-world conditions: + // • CSPs without 'unsafe-inline' (often set by upstream proxies/CDNs) + // • React 19 hydration tearing out unmanaged elements + // • Some browser-extension content filters + // Serving the script same-origin from the proxy bypasses every one of + // these because: + // • script-src 'self' is allowed by virtually every CSP + // • React doesn't reconcile the content of external