updated stuff
This commit is contained in:
@@ -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';
|
||||
|
||||
+66
-37
@@ -1,69 +1,98 @@
|
||||
// -- HTML Injection ----------------------------------------------------------
|
||||
// Injects the Originmain fiber hook <script> into an HTML response body.
|
||||
// The script must appear BEFORE any other scripts so that
|
||||
// __REACT_DEVTOOLS_GLOBAL_HOOK__ is installed before React evaluates.
|
||||
// Injects the Originmain fiber hook into an HTML response body as an
|
||||
// EXTERNAL script tag (<script src="/__om_fiber_hook__.js">) rather than as
|
||||
// an inline script. External scripts survive several blocking conditions that
|
||||
// inline scripts hit in the wild:
|
||||
// • CSPs without 'unsafe-inline' (script-src 'self' is widely allowed)
|
||||
// • React 19 hydration removing unmanaged <head> children
|
||||
// • Browser-extension inline-script filters
|
||||
// The script body itself is served at /__om_fiber_hook__.js by the proxy
|
||||
// (see proxy.ts). The bridge-config globals (__OM_INDEX_URL__,
|
||||
// __OM_ISO_BASE__) are baked into the head of that served script, so we don't
|
||||
// need a separate inline configuration tag in the HTML at all.
|
||||
//
|
||||
// Also injects window.__OM_INDEX_URL__ (AST indexer API) and
|
||||
// window.__OM_ISO_BASE__ (isolation artboard base path) so the canvas can
|
||||
// discover the indexer without any out-of-band coordination.
|
||||
// We also inject the script tag in TWO positions for redundancy:
|
||||
// immediately after <head> (so it runs as early as possible, ideally before
|
||||
// any other scripts) AND immediately before </body> (a fallback in case the
|
||||
// first injection is interfered with — e.g. removed during React hydration).
|
||||
// The script's IIFE has internal idempotency guards so a double-execution is
|
||||
// harmless.
|
||||
|
||||
import { buildProxyFiberHookScript } from '@originmain/renderer';
|
||||
|
||||
/** The fiber hook script wrapped in a <script> tag, generated once at startup. */
|
||||
let cachedFiberTag: string | undefined;
|
||||
|
||||
function getFiberTag(): string {
|
||||
if (cachedFiberTag === undefined) {
|
||||
cachedFiberTag = `<script data-originmain-fiber-hook>${buildProxyFiberHookScript()}</script>`;
|
||||
}
|
||||
return cachedFiberTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the bridge config <script> tag.
|
||||
* Not cached because indexUrl may vary per process invocation.
|
||||
* The full content served at /__om_fiber_hook__.js — bridge globals followed
|
||||
* by the fiber-hook IIFE. The proxy writes this directly into the JS response.
|
||||
*/
|
||||
function getBridgeConfigTag(indexUrl: string | null | undefined): string {
|
||||
export function buildFiberHookExternalScript(
|
||||
indexUrl: string | null | undefined,
|
||||
): string {
|
||||
const indexUrlJson = indexUrl ? JSON.stringify(indexUrl) : 'null';
|
||||
return (
|
||||
`<script data-originmain-bridge-config>` +
|
||||
`window.__OM_INDEX_URL__=${indexUrlJson};` +
|
||||
`window.__OM_ISO_BASE__="/__om_isolation__";` +
|
||||
`</script>`
|
||||
`window.__OM_ISO_BASE__="/__om_isolation__";\n` +
|
||||
buildProxyFiberHookScript()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip inline Content-Security-Policy meta tags from HTML.
|
||||
* The <script> tag injected into HTML — references the external endpoint.
|
||||
* Not marked async/defer so it runs in document order before any subsequent
|
||||
* script, exactly like the previous inline tag did.
|
||||
*/
|
||||
function getFiberTag(): string {
|
||||
return `<script src="/__om_fiber_hook__.js" data-originmain-fiber-hook></script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
/<meta[^>]+http-equiv\s*=\s*["']?\s*content-security-policy\s*["']?[^>]*\/?>/gi,
|
||||
/<meta[^>]+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 <head>
|
||||
// ── Primary injection: immediately after <head> ─────────────────────────
|
||||
let withHead = cleaned;
|
||||
const headMatch = cleaned.match(/<head[^>]*>/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 <head>? Try after <html> as a fallback.
|
||||
const htmlMatch = cleaned.match(/<html[^>]*>/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 <html>
|
||||
const htmlMatch = cleaned.match(/<html[^>]*>/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 </body> ─────────────────────
|
||||
// Provides a fallback if the head-position tag is interfered with (e.g.
|
||||
// removed during React 19 hydration of <head>). The script's IIFE is
|
||||
// idempotent — a second execution after the first ran is a no-op.
|
||||
const bodyCloseIdx = withHead.lastIndexOf('</body>');
|
||||
if (bodyCloseIdx >= 0) {
|
||||
return withHead.slice(0, bodyCloseIdx) + tag + withHead.slice(bodyCloseIdx);
|
||||
}
|
||||
|
||||
// Final fallback: prepend
|
||||
return injection + cleaned;
|
||||
return withHead;
|
||||
}
|
||||
|
||||
@@ -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 <script>...</script> 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 <head> 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 <script src="..."> tags
|
||||
// • The script body comes from a separate HTTP request, so HTML caching
|
||||
// of the page doesn't pin a stale script body.
|
||||
if (clientReq.url?.startsWith('/__om_fiber_hook__.js')) {
|
||||
const script = buildFiberHookExternalScript(opts.indexUrl);
|
||||
const buf = Buffer.from(script, 'utf-8');
|
||||
clientRes.writeHead(200, {
|
||||
'content-type': 'application/javascript; charset=utf-8',
|
||||
'content-length': String(buf.byteLength),
|
||||
// Don't cache — the proxy may be restarted with different indexUrl
|
||||
// and we never want a stale bundle pinned in the iframe.
|
||||
'cache-control': 'no-store, no-cache, must-revalidate',
|
||||
...CORS_HEADERS,
|
||||
});
|
||||
clientRes.end(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Intercept /__om_isolation__/* requests ────────────────────────────
|
||||
if (clientReq.url?.startsWith('/__om_isolation__')) {
|
||||
handleIsolationRequest(clientReq, clientRes);
|
||||
@@ -174,6 +201,14 @@ export function startProxy(opts: ProxyOptions): { close: () => void } {
|
||||
// would be incorrect after injection.
|
||||
delete resHeaders['content-encoding'];
|
||||
|
||||
// Force fresh fetches every time. Without this, browsers (and proxies)
|
||||
// can pin an injected HTML response in cache, so the iframe loads an
|
||||
// older version of our injected <script> tag — even after the CLI is
|
||||
// rebuilt and restarted. no-store is the strongest such directive.
|
||||
resHeaders['cache-control'] = 'no-store, no-cache, must-revalidate';
|
||||
delete resHeaders['etag'];
|
||||
delete resHeaders['last-modified'];
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
|
||||
@@ -124,6 +124,18 @@ export function buildProxyFiberHookScript(): string {
|
||||
return `(function() {
|
||||
'use strict';
|
||||
|
||||
// ── Idempotency guard ─────────────────────────────────────────────────────
|
||||
// The proxy may inject this script in two positions (after <head> and before
|
||||
// </body>) for redundancy. If the script already ran in this iframe, the
|
||||
// second invocation must be a no-op. We tag a window-level marker on first
|
||||
// execution and bail on subsequent ones.
|
||||
if (window.__OM_FIBER_HOOK_INSTALLED__) return;
|
||||
try {
|
||||
Object.defineProperty(window, '__OM_FIBER_HOOK_INSTALLED__', {
|
||||
value: true, writable: false, configurable: false,
|
||||
});
|
||||
} catch (e) { window.__OM_FIBER_HOOK_INSTALLED__ = true; }
|
||||
|
||||
// ── Guard: only activate inside an Originmain iframe ─────────────────────
|
||||
if (window.parent === window) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user