updated stuff
This commit is contained in:
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user