Add runtime bridge architecture and fix HTML escaping

Major refactoring to unify runtime providers across sandbox and user script contexts:

1. Runtime Bridge & Router
   - Add RuntimeMessageBridge for unified messaging abstraction
   - Rename SandboxMessageRouter → RuntimeMessageRouter
   - Router now handles both iframe and user script messages
   - Guard for non-extension environments

2. Provider Refactoring
   - ArtifactsRuntimeProvider: Add offline mode with snapshot fallback
   - AttachmentsRuntimeProvider: Remove returnDownloadableFile (moved to dedicated provider)
   - ConsoleRuntimeProvider: Add message collection, remove lifecycle logic
   - FileDownloadRuntimeProvider: New provider for file downloads

3. HTML Escaping Fix
   - Escape </script> in JSON.stringify output to prevent premature tag closure
   - Applies when injecting provider data into <script> tags
   - JavaScript engine automatically unescapes, no runtime changes needed

4. Function Renaming
   - listFiles → listAttachments
   - readTextFile → readTextAttachment
   - readBinaryFile → readBinaryAttachment
   - returnFile → returnDownloadableFile

5. Updated Exports
   - Export new RuntimeMessageBridge and RuntimeMessageRouter
   - Export FileDownloadRuntimeProvider
   - Update all cross-references

This sets the foundation for reusing providers in browser-javascript tool.
This commit is contained in:
Mario Zechner 2025-10-09 17:32:45 +02:00
parent d7d79bd533
commit c2793d8017
11 changed files with 722 additions and 385 deletions

View file

@ -1,7 +1,8 @@
import { LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.js";
import { type MessageConsumer, SANDBOX_MESSAGE_ROUTER } from "./sandbox/SandboxMessageRouter.js";
import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.js";
import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.js";
import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.js";
export interface SandboxFile {
@ -65,16 +66,17 @@ export class SandboxIframe extends LitElement {
): void {
// Unregister previous sandbox if exists
try {
SANDBOX_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
} catch {
// Sandbox might not exist, that's ok
}
providers = [new ConsoleRuntimeProvider(), ...providers];
SANDBOX_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers);
// loadContent is always used for HTML artifacts
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, true);
// Remove previous iframe if exists
this.iframe?.remove();
@ -99,7 +101,7 @@ export class SandboxIframe extends LitElement {
this.iframe.src = this.sandboxUrlProvider!();
// Update router with iframe reference BEFORE appending to DOM
SANDBOX_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for sandbox-ready message directly
const readyHandler = (e: MessageEvent) => {
@ -134,7 +136,7 @@ export class SandboxIframe extends LitElement {
this.iframe.srcdoc = completeHtml;
// Update router with iframe reference BEFORE appending to DOM
SANDBOX_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
this.appendChild(this.iframe);
}
@ -154,13 +156,14 @@ export class SandboxIframe extends LitElement {
providers: SandboxRuntimeProvider[] = [],
consumers: MessageConsumer[] = [],
signal?: AbortSignal,
isHtmlArtifact: boolean = false,
): Promise<SandboxResult> {
if (signal?.aborted) {
throw new Error("Execution aborted");
}
providers = [new ConsoleRuntimeProvider(), ...providers];
SANDBOX_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
const logs: Array<{ type: string; text: string }> = [];
const files: SandboxFile[] = [];
@ -198,10 +201,10 @@ export class SandboxIframe extends LitElement {
},
};
SANDBOX_MESSAGE_ROUTER.addConsumer(sandboxId, executionConsumer);
RUNTIME_MESSAGE_ROUTER.addConsumer(sandboxId, executionConsumer);
const cleanup = () => {
SANDBOX_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
signal?.removeEventListener("abort", abortHandler);
clearTimeout(timeoutId);
this.iframe?.remove();
@ -236,7 +239,7 @@ export class SandboxIframe extends LitElement {
}, 30000);
// 4. Prepare HTML and create iframe
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers);
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, isHtmlArtifact);
if (this.sandboxUrlProvider) {
// Browser extension mode: wait for sandbox-ready
@ -246,7 +249,7 @@ export class SandboxIframe extends LitElement {
this.iframe.src = this.sandboxUrlProvider();
// Update router with iframe reference BEFORE appending to DOM
SANDBOX_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for sandbox-ready message directly
const readyHandler = (e: MessageEvent) => {
@ -276,7 +279,7 @@ export class SandboxIframe extends LitElement {
this.iframe.srcdoc = completeHtml;
// Update router with iframe reference BEFORE appending to DOM
SANDBOX_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
this.appendChild(this.iframe);
}
@ -287,14 +290,18 @@ export class SandboxIframe extends LitElement {
* Prepare complete HTML document with runtime + user code
* PUBLIC so HtmlArtifact can use it for download button
*/
public prepareHtmlDocument(sandboxId: string, userCode: string, providers: SandboxRuntimeProvider[] = []): string {
public prepareHtmlDocument(
sandboxId: string,
userCode: string,
providers: SandboxRuntimeProvider[] = [],
isHtmlArtifact: boolean = false,
): string {
// Runtime script that will be injected
const runtime = this.getRuntimeScript(sandboxId, providers);
// Check if user provided full HTML
const hasHtmlTag = /<html[^>]*>/i.test(userCode);
if (hasHtmlTag) {
// Only check for HTML tags if explicitly marked as HTML artifact
// For javascript_repl, userCode is JavaScript that may contain HTML in string literals
if (isHtmlArtifact) {
// HTML Artifact - inject runtime into existing HTML
const headMatch = userCode.match(/<head[^>]*>/i);
if (headMatch) {
@ -347,20 +354,31 @@ export class SandboxIframe extends LitElement {
Object.assign(allData, provider.getData());
}
// Generate bridge code
const bridgeCode = RuntimeMessageBridge.generateBridgeCode({
context: "sandbox-iframe",
sandboxId,
});
// Collect all runtime functions - pass sandboxId as string literal
const runtimeFunctions: string[] = [];
for (const provider of providers) {
runtimeFunctions.push(`(${provider.getRuntime().toString()})(${JSON.stringify(sandboxId)});`);
}
// Build script
// Build script with HTML escaping
// Escape </script> to prevent premature tag closure in HTML parser
const dataInjection = Object.entries(allData)
.map(([key, value]) => `window.${key} = ${JSON.stringify(value)};`)
.map(([key, value]) => {
const jsonStr = JSON.stringify(value).replace(/<\/script/gi, "<\\/script");
return `window.${key} = ${jsonStr};`;
})
.join("\n");
return `<script>
window.sandboxId = ${JSON.stringify(sandboxId)};
${dataInjection}
${bridgeCode}
${runtimeFunctions.join("\n")}
</script>`;
}