updated stuff

This commit is contained in:
SinachPat
2026-05-10 20:52:34 +01:00
parent f51dfa5e4d
commit 8356e6278c
4 changed files with 115 additions and 39 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
// Programmatic API — allows using the proxy from other Node.js code // Programmatic API — allows using the proxy from other Node.js code
export { startProxy } from './proxy.js'; export { startProxy } from './proxy.js';
export type { ProxyOptions } from './proxy.js'; export type { ProxyOptions } from './proxy.js';
export { injectFiberHook } from './inject.js'; export { injectFiberHook, buildFiberHookExternalScript } from './inject.js';
+64 -35
View File
@@ -1,69 +1,98 @@
// -- HTML Injection ---------------------------------------------------------- // -- HTML Injection ----------------------------------------------------------
// Injects the Originmain fiber hook <script> into an HTML response body. // Injects the Originmain fiber hook into an HTML response body as an
// The script must appear BEFORE any other scripts so that // EXTERNAL script tag (<script src="/__om_fiber_hook__.js">) rather than as
// __REACT_DEVTOOLS_GLOBAL_HOOK__ is installed before React evaluates. // 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 // We also inject the script tag in TWO positions for redundancy:
// window.__OM_ISO_BASE__ (isolation artboard base path) so the canvas can // immediately after <head> (so it runs as early as possible, ideally before
// discover the indexer without any out-of-band coordination. // 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'; 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. * The full content served at /__om_fiber_hook__.js — bridge globals followed
* Not cached because indexUrl may vary per process invocation. * 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'; const indexUrlJson = indexUrl ? JSON.stringify(indexUrl) : 'null';
return ( return (
`<script data-originmain-bridge-config>` +
`window.__OM_INDEX_URL__=${indexUrlJson};` + `window.__OM_INDEX_URL__=${indexUrlJson};` +
`window.__OM_ISO_BASE__="/__om_isolation__";` + `window.__OM_ISO_BASE__="/__om_isolation__";\n` +
`</script>` 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 { function stripMetaCsp(html: string): string {
return html.replace( 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 { export function injectFiberHook(html: string, _indexUrl?: string | null): string {
const injection = getBridgeConfigTag(indexUrl) + getFiberTag(); const tag = getFiberTag();
const cleaned = stripMetaCsp(html); const cleaned = stripMetaCsp(html);
// Try after <head> // ── Primary injection: immediately after <head> ─────────────────────────
let withHead = cleaned;
const headMatch = cleaned.match(/<head[^>]*>/i); const headMatch = cleaned.match(/<head[^>]*>/i);
if (headMatch?.index !== undefined) { if (headMatch?.index !== undefined) {
const insertAt = headMatch.index + headMatch[0].length; 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.
// Try after <html>
const htmlMatch = cleaned.match(/<html[^>]*>/i); const htmlMatch = cleaned.match(/<html[^>]*>/i);
if (htmlMatch?.index !== undefined) { if (htmlMatch?.index !== undefined) {
const insertAt = htmlMatch.index + htmlMatch[0].length; const insertAt = htmlMatch.index + htmlMatch[0].length;
return cleaned.slice(0, insertAt) + injection + cleaned.slice(insertAt); withHead = cleaned.slice(0, insertAt) + tag + cleaned.slice(insertAt);
} else {
// Last resort: prepend.
withHead = tag + cleaned;
}
} }
// Final fallback: prepend // ── Redundant injection: immediately before </body> ─────────────────────
return injection + cleaned; // 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);
}
return withHead;
} }
+36 -1
View File
@@ -14,7 +14,7 @@ import { connect as netConnect } from 'node:net';
import type { IncomingMessage, ServerResponse, import type { IncomingMessage, ServerResponse,
RequestOptions, ClientRequest } from 'node:http'; RequestOptions, ClientRequest } from 'node:http';
import type { Socket } from 'node:net'; import type { Socket } from 'node:net';
import { injectFiberHook } from './inject.js'; import { injectFiberHook, buildFiberHookExternalScript } from './inject.js';
import { handleIsolationRequest } from './isolation-server.js'; import { handleIsolationRequest } from './isolation-server.js';
import html2canvasSource from 'html2canvas/dist/html2canvas.min.js'; import html2canvasSource from 'html2canvas/dist/html2canvas.min.js';
@@ -88,6 +88,33 @@ export function startProxy(opts: ProxyOptions): { close: () => void } {
return; 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 ──────────────────────────── // ── Intercept /__om_isolation__/* requests ────────────────────────────
if (clientReq.url?.startsWith('/__om_isolation__')) { if (clientReq.url?.startsWith('/__om_isolation__')) {
handleIsolationRequest(clientReq, clientRes); handleIsolationRequest(clientReq, clientRes);
@@ -174,6 +201,14 @@ export function startProxy(opts: ProxyOptions): { close: () => void } {
// would be incorrect after injection. // would be incorrect after injection.
delete resHeaders['content-encoding']; 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[] = []; const chunks: Buffer[] = [];
proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk)); proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
+12
View File
@@ -124,6 +124,18 @@ export function buildProxyFiberHookScript(): string {
return `(function() { return `(function() {
'use strict'; '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 ───────────────────── // ── Guard: only activate inside an Originmain iframe ─────────────────────
if (window.parent === window) return; if (window.parent === window) return;