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,19 +1,30 @@
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
export interface ConsoleLog {
type: "log" | "warn" | "error" | "info";
text: string;
args?: unknown[];
}
/**
* Console Runtime Provider
*
* REQUIRED provider that should always be included first.
* Provides console capture, error handling, and execution lifecycle management.
* Collects console output for retrieval by caller.
*/
export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
private logs: ConsoleLog[] = [];
private completionError: { message: string; stack: string } | null = null;
private completed = false;
getData(): Record<string, any> {
// No data needed
return {};
}
getRuntime(): (sandboxId: string) => void {
return (sandboxId: string) => {
return (_sandboxId: string) => {
// Console capture
const originalConsole = {
log: console.log,
@ -34,16 +45,21 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
})
.join(" ");
window.parent.postMessage(
{
type: "console",
sandboxId,
method,
text,
},
"*",
);
// Send to extension if available (online mode)
if ((window as any).sendRuntimeMessage) {
(window as any)
.sendRuntimeMessage({
type: "console",
method,
text,
args, // Send raw args for provider collection
})
.catch(() => {
// Ignore errors in fire-and-forget console messages
});
}
// Always log locally too
(originalConsole as any)[method].apply(console, args);
};
});
@ -61,15 +77,15 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
stack: e.error?.stack || text,
};
window.parent.postMessage(
{
type: "console",
sandboxId,
method: "error",
text,
},
"*",
);
if ((window as any).sendRuntimeMessage) {
(window as any)
.sendRuntimeMessage({
type: "console",
method: "error",
text,
})
.catch(() => {});
}
});
window.addEventListener("unhandledrejection", (e) => {
@ -80,15 +96,15 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
stack: e.reason?.stack || text,
};
window.parent.postMessage(
{
type: "console",
sandboxId,
method: "error",
text,
},
"*",
);
if ((window as any).sendRuntimeMessage) {
(window as any)
.sendRuntimeMessage({
type: "console",
method: "error",
text,
})
.catch(() => {});
}
});
// Expose complete() method for user code to call
@ -99,23 +115,21 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
const finalError = error || lastError;
if (finalError) {
window.parent.postMessage(
{
type: "execution-error",
sandboxId,
error: finalError,
},
"*",
);
} else {
window.parent.postMessage(
{
type: "execution-complete",
sandboxId,
},
"*",
);
if ((window as any).sendRuntimeMessage) {
if (finalError) {
(window as any)
.sendRuntimeMessage({
type: "execution-error",
error: finalError,
})
.catch(() => {});
} else {
(window as any)
.sendRuntimeMessage({
type: "execution-complete",
})
.catch(() => {});
}
}
};
@ -129,4 +143,66 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
}
};
}
async handleMessage(message: any, _respond: (response: any) => void): Promise<boolean> {
if (message.type === "console") {
// Collect console output
this.logs.push({
type:
message.method === "error"
? "error"
: message.method === "warn"
? "warn"
: message.method === "info"
? "info"
: "log",
text: message.text,
args: message.args,
});
return true;
}
if (message.type === "execution-complete") {
this.completed = true;
return true;
}
if (message.type === "execution-error") {
this.completed = true;
this.completionError = message.error;
return true;
}
return false;
}
/**
* Get collected console logs
*/
getLogs(): ConsoleLog[] {
return this.logs;
}
/**
* Get completion status
*/
isCompleted(): boolean {
return this.completed;
}
/**
* Get completion error if any
*/
getCompletionError(): { message: string; stack: string } | null {
return this.completionError;
}
/**
* Reset state for reuse
*/
reset(): void {
this.logs = [];
this.completionError = null;
this.completed = false;
}
}