From 1ce1f34a23fe8ed106a7c6c1c281106f58e1b684 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Thu, 30 Jan 2025 13:03:51 -0800 Subject: [PATCH] chore: initial common-iframe-sandbox, isolating CSP/security features to a single package (#300) chore: initial common-iframe-sandbox, isolating CSP/security features to a single package. --- .github/workflows/ui-tests.yml | 2 +- .../packages/common-iframe-sandbox/README.md | 43 ++++ .../common-iframe-sandbox/package.json | 40 ++++ .../src/common-iframe-sandbox.ts | 140 +++++++++++++ .../common-iframe-sandbox/src/context.ts | 22 +++ .../packages/common-iframe-sandbox/src/csp.ts | 43 ++++ .../common-iframe-sandbox/src/index.ts | 4 + .../src/ipc.ts} | 21 +- .../test/iframe-csp.test.js | 0 .../test/iframe.test.js | 0 .../test/utils.js | 8 +- .../common-iframe-sandbox/tsconfig.json | 16 ++ .../common-iframe-sandbox/vite.config.ts | 14 ++ typescript/packages/common-ui/package.json | 2 +- .../common-ui/src/components/common-iframe.ts | 187 ++---------------- typescript/packages/common-ui/src/index.ts | 1 - .../lookslike-high-level/package.json | 1 + .../packages/lookslike-high-level/src/data.ts | 4 +- typescript/packages/pnpm-lock.yaml | 50 ++++- typescript/packages/pnpm-workspace.yaml | 1 + 20 files changed, 395 insertions(+), 204 deletions(-) create mode 100644 typescript/packages/common-iframe-sandbox/README.md create mode 100644 typescript/packages/common-iframe-sandbox/package.json create mode 100644 typescript/packages/common-iframe-sandbox/src/common-iframe-sandbox.ts create mode 100644 typescript/packages/common-iframe-sandbox/src/context.ts create mode 100644 typescript/packages/common-iframe-sandbox/src/csp.ts create mode 100644 typescript/packages/common-iframe-sandbox/src/index.ts rename typescript/packages/{common-ui/src/iframe-ipc.ts => common-iframe-sandbox/src/ipc.ts} (71%) rename typescript/packages/{common-ui => common-iframe-sandbox}/test/iframe-csp.test.js (100%) rename typescript/packages/{common-ui => common-iframe-sandbox}/test/iframe.test.js (100%) rename typescript/packages/{common-ui => common-iframe-sandbox}/test/utils.js (92%) create mode 100644 typescript/packages/common-iframe-sandbox/tsconfig.json create mode 100644 typescript/packages/common-iframe-sandbox/vite.config.ts diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 4192e3cd8..ea2cd05e8 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -30,5 +30,5 @@ jobs: pnpm install - name: UI Tests run: | - cd typescript/packages/common-ui + cd typescript/packages/common-iframe-sandbox pnpm test diff --git a/typescript/packages/common-iframe-sandbox/README.md b/typescript/packages/common-iframe-sandbox/README.md new file mode 100644 index 000000000..27721bec9 --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/README.md @@ -0,0 +1,43 @@ +# common-iframe-sandbox + +This package contains a custom element ``, which enables a sandboxed iframe to execute arbitrary code. + +> [!CAUTION] +> This is experimental software and no guarantees of security are provided. +> Continue reading for full details of current status and limitations. + +## Usage + +`` takes a `src` string of HTML content and a `context` object. The contents in `src` are loaded +inside a sandboxed iframe (via `srcdoc`). + +```js +const element = document.createElement('common-iframe-sandbox'); +element.src = "

Hello

"; +element.context = {}; +``` + +The element has a `context` property that can be set to read, write, or subscribe to a key/value store from the iframe contents. +See the inter-frame communications in [ipc.ts](/typescript/packages/common-iframe-sandbox/src/ipc.ts). + +To handle these messages, a global singleton handler is used that must be set via `setIframeContextHandler(handler)`. The handler is called with the `context` provided to the iframe, and the handler determines how values are stored and retrieved. +See [context.ts](/typescript/packages/common-iframe-sandbox/src/context.ts). + +## Missing Functionality + +* Support updating the `src` property. +* Flushing subscriptions inbetween frame loads. +* Uniquely identify context handler calls so that they can be mapped to the correct iframe instance when there are multiple active sandboxed iframes. +* Support browsers that do not support `HTMLIFrameElement.prototype.csp` (non-chromium). +* Abort on unsupported browsers. +* Further testing. + +## Incomplete Security Considerations + +* `document.baseURI` is accessible in an iframe, leaking the parent URL +* Currently without CFC, data can be written in the iframe containing other sensitive data, + or newly synthesized fingerprinting via capabilities (accelerometer, webrtc, canvas), + and saved back into the database, where some other vector of exfiltration could occur. +* Exposing iframe status to outer content could be considered leaky, + though all content is inlined, not HTTP URLs. + https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#error_and_load_event_behavior \ No newline at end of file diff --git a/typescript/packages/common-iframe-sandbox/package.json b/typescript/packages/common-iframe-sandbox/package.json new file mode 100644 index 000000000..0837a4d39 --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/package.json @@ -0,0 +1,40 @@ +{ + "name": "@commontools/iframe-sandbox", + "version": "0.1.0", + "description": "ui", + "main": "src/index.js", + "type": "module", + "packageManager": "pnpm@10.0.0", + "engines": { + "npm": "please-use-pnpm", + "yarn": "please-use-pnpm", + "pnpm": ">= 10.0.0", + "node": "20.11.0" + }, + "scripts": { + "test": "npm run build && web-test-runner test/**/*.test.js --node-resolve", + "format": "prettier --write . --ignore-path ../../../.prettierignore", + "lint": "eslint .", + "build": "tsc && vite build", + "watch": "web-test-runner test/**/*.test.js --node-resolve --watch" + }, + "author": "", + "license": "UNLICENSED", + "dependencies": { + "lit": "^3.2.1", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/node": "^22.10.10", + "eslint": "^9.19.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", + "globals": "^15.14.0", + "prettier": "^3.4.2", + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vite-plugin-dts": "^4.5.0", + "@web/test-runner": "^0.19.0" + } +} diff --git a/typescript/packages/common-iframe-sandbox/src/common-iframe-sandbox.ts b/typescript/packages/common-iframe-sandbox/src/common-iframe-sandbox.ts new file mode 100644 index 000000000..d94951ea8 --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/src/common-iframe-sandbox.ts @@ -0,0 +1,140 @@ +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { Ref, createRef, ref } from "lit/directives/ref.js"; +import * as IPC from "./ipc.js"; +import { getIframeContextHandler } from "./context.js"; +import { CSP } from "./csp.js"; + +// @summary A sandboxed iframe to execute arbitrary scripts. +// @tag common-iframe-sandbox +// @prop {string} src - String representation of HTML content to load within an iframe. +// @prop context - Cell context. +// @event {CustomEvent} error - An error from the iframe. +// @event {CustomEvent} load - The iframe was successfully loaded. +@customElement("common-iframe-sandbox") +export class CommonIframeSandboxElement extends LitElement { + @property({ type: String }) src = ""; + @property({ type: Object }) context?: object; + + private iframeRef: Ref = createRef(); + + private subscriptions: Map = new Map(); + + private handleMessage = (event: MessageEvent) => { + if (event.data?.source == "react-devtools-content-script") { + return; + } + + if (event.source !== this.iframeRef.value?.contentWindow) { + return; + } + + const IframeHandler = getIframeContextHandler(); + if (IframeHandler == null) { + console.error("common-iframe-sandbox: No iframe handler defined."); + return; + } + + if (!this.context) { + console.error("common-iframe-sandbox: missing `context`."); + return; + } + + if (!IPC.isGuestMessage(event.data)) { + console.error("common-iframe-sandbox: Malformed message from guest."); + return; + } + + const message: IPC.GuestMessage = event.data; + + switch (message.type) { + case IPC.GuestMessageType.Error: { + const { description, source, lineno, colno, stacktrace } = message.data; + const error = { description, source, lineno, colno, stacktrace }; + this.dispatchEvent(new CustomEvent("error", { + detail: error, + })); + return; + } + + case IPC.GuestMessageType.Read: { + const key = message.data; + const value = IframeHandler.read(this.context, key); + const response: IPC.HostMessage = { + type: IPC.HostMessageType.Update, + data: [key, value], + } + this.iframeRef.value?.contentWindow?.postMessage(response, "*"); + return; + } + + case IPC.GuestMessageType.Write: { + const [key, value] = message.data; + IframeHandler.write(this.context, key, value); + return; + } + + case IPC.GuestMessageType.Subscribe: { + const key = message.data; + + if (this.subscriptions.has(key)) { + console.warn("common-iframe-sandbox: Already subscribed to `${key}`"); + return; + } + let receipt = IframeHandler.subscribe(this.context, key, (key, value) => this.notifySubscribers(key, value)); + this.subscriptions.set(key, receipt); + return; + } + + case IPC.GuestMessageType.Unsubscribe: { + const key = message.data; + let receipt = this.subscriptions.get(key); + if (!receipt) { + return; + } + IframeHandler.unsubscribe(this.context, receipt); + this.subscriptions.delete(key); + return; + } + }; + } + + private notifySubscribers(key: string, value: any) { + const response: IPC.HostMessage = { + type: IPC.HostMessageType.Update, + data: [key, value], + } + this.iframeRef.value?.contentWindow?.postMessage(response, "*"); + } + + private boundHandleMessage = this.handleMessage.bind(this); + + override connectedCallback() { + super.connectedCallback(); + window.addEventListener("message", this.boundHandleMessage); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("message", this.boundHandleMessage); + } + + private handleLoad() { + this.dispatchEvent(new CustomEvent("load")); + } + + override render() { + return html` + + `; + } +} \ No newline at end of file diff --git a/typescript/packages/common-iframe-sandbox/src/context.ts b/typescript/packages/common-iframe-sandbox/src/context.ts new file mode 100644 index 000000000..bc24c0df7 --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/src/context.ts @@ -0,0 +1,22 @@ +// An `IframeContextHandler` is used by consumers to +// register how read/writing values from frames are handled. +export interface IframeContextHandler { + read(context: any, key: string): any, + write(context: any, key: string, value: any): void, + subscribe(context: any, key: string, callback: (key: string, value: any) => void): any, + unsubscribe(context: any, receipt: any): void, +} + +let IframeHandler: IframeContextHandler | null = null; + +// Set the `IframeContextHandler` singleton. Allows indirect cell synchronizing +// so that this sandboxing doesn't need to concern itself with application-level +// synchronizing mechanisms. +export function setIframeContextHandler(handler: IframeContextHandler) { + IframeHandler = handler; +} + +// Get the `IframeContextHandler` singleton. +export function getIframeContextHandler(): IframeContextHandler | null { + return IframeHandler; +} \ No newline at end of file diff --git a/typescript/packages/common-iframe-sandbox/src/csp.ts b/typescript/packages/common-iframe-sandbox/src/csp.ts new file mode 100644 index 000000000..1c46ba1a7 --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/src/csp.ts @@ -0,0 +1,43 @@ +const SCRIPT_CDNS = [ + 'https://unpkg.com', + 'https://cdn.tailwindcss.com' +]; + +// This CSP directive uses 'unsafe-inline' to allow +// origin-less styles and scripts to be used, defeating +// many traditional uses of CSP. +export const CSP = `` + + // Disable all fetch directives. Re-enable + // each specific fetch directive as needed. + `default-src 'none';` + + // Scripts: Allow 1P, inline, and CDNs. + `script-src 'self' 'unsafe-inline' ${SCRIPT_CDNS.join(' ')};` + + // Styles: Allow 1P, inline. + `style-src 'self' 'unsafe-inline';` + + // Images: Allow 1P, inline. + `img-src 'self' 'unsafe-inline';` + + // Disabling until we have a concrete case. + `form-action 'none';` + + // Disable element + `base-uri 'none';` + + // Iframes/Workers: Use default (disabled) + `child-src 'none';` + + // Ping/XHR/Fetch/Sockets: Allow 1P only + `connect-src 'self';` + + // This is a deprecated/Chrome-only CSP directive. + // This blocks `` and + // the Chrome-only ``. + // `default-src` is used correctly as a fallback for + // prefetch + //`prefetch-src 'none';` + + // Fonts: Use default (disabled) + //`font-src 'none';` + + // Media: Use default (disabled) + //`media-src 'none';` + + // Manifest: Use default (disabled) + //`manifest-src 'none';` + + // Object/Embeds: Use default (disabled) + //`object-src 'none';` + + ``; + +export const META_TAG_CSP = ``; \ No newline at end of file diff --git a/typescript/packages/common-iframe-sandbox/src/index.ts b/typescript/packages/common-iframe-sandbox/src/index.ts new file mode 100644 index 000000000..d545bb75a --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/src/index.ts @@ -0,0 +1,4 @@ +export * from "./csp.js"; +export * as IPC from "./ipc.js"; +export * from "./context.js"; +export * from "./common-iframe-sandbox.js"; diff --git a/typescript/packages/common-ui/src/iframe-ipc.ts b/typescript/packages/common-iframe-sandbox/src/ipc.ts similarity index 71% rename from typescript/packages/common-ui/src/iframe-ipc.ts rename to typescript/packages/common-iframe-sandbox/src/ipc.ts index 8d1870eb4..115fac367 100644 --- a/typescript/packages/common-ui/src/iframe-ipc.ts +++ b/typescript/packages/common-iframe-sandbox/src/ipc.ts @@ -1,23 +1,4 @@ -// Types used by the `common-iframe` IPC. - -export interface IframeContextHandler { - read(context: any, key: string): any, - write(context: any, key: string, value: any): void, - subscribe(context: any, key: string, callback: (key: string, value: any) => void): any, - unsubscribe(context: any, receipt: any): void, -} - -let IframeHandler: IframeContextHandler | null = null; -// Set the `IframeContextHandler` singleton. Allows cell synchronizing -// so that `common-ui` doesn't directly depend on `common-runner`. -export function setIframeContextHandler(handler: IframeContextHandler) { - IframeHandler = handler; -} - -// Get the `IframeContextHandler` singleton. -export function getIframeContextHandler(): IframeContextHandler | null { - return IframeHandler; -} +// Types used by the `common-iframe-sandbox` IPC. export interface GuestError { description: string; diff --git a/typescript/packages/common-ui/test/iframe-csp.test.js b/typescript/packages/common-iframe-sandbox/test/iframe-csp.test.js similarity index 100% rename from typescript/packages/common-ui/test/iframe-csp.test.js rename to typescript/packages/common-iframe-sandbox/test/iframe-csp.test.js diff --git a/typescript/packages/common-ui/test/iframe.test.js b/typescript/packages/common-iframe-sandbox/test/iframe.test.js similarity index 100% rename from typescript/packages/common-ui/test/iframe.test.js rename to typescript/packages/common-iframe-sandbox/test/iframe.test.js diff --git a/typescript/packages/common-ui/test/utils.js b/typescript/packages/common-iframe-sandbox/test/utils.js similarity index 92% rename from typescript/packages/common-ui/test/utils.js rename to typescript/packages/common-iframe-sandbox/test/utils.js index 93402c683..872cad3fa 100644 --- a/typescript/packages/common-ui/test/utils.js +++ b/typescript/packages/common-iframe-sandbox/test/utils.js @@ -1,4 +1,4 @@ -import { IframeIPC } from "../lib/src/index.js"; +import { setIframeContextHandler } from "../lib/src/index.js"; export class ContextShim { constructor(object = {}) { @@ -35,7 +35,7 @@ export class ContextShim { } export function setIframeTestHandler() { - IframeIPC.setIframeContextHandler({ + setIframeContextHandler({ read(context, key) { return context.get(key); }, @@ -68,9 +68,9 @@ export function render(src, context = {}) { return new Promise(resolve => { const parent = document.createElement('div'); parent.id = FIXTURE_ID; - const iframe = document.createElement('common-iframe'); + const iframe = document.createElement('common-iframe-sandbox'); iframe.context = context; - iframe.addEventListener('load', e => { + iframe.addEventListener('load', _ => { resolve(iframe); }) parent.appendChild(iframe); diff --git a/typescript/packages/common-iframe-sandbox/tsconfig.json b/typescript/packages/common-iframe-sandbox/tsconfig.json new file mode 100644 index 000000000..78834a37c --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "moduleResolution": "nodenext", + "compilerOptions": { + "lib": ["es2022", "esnext.array", "esnext", "dom"], + "outDir": "./lib", + "declaration": true, + "jsx": "preserve", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment", + "experimentalDecorators": true, + "useDefineForClassFields": false + }, + "include": ["src/**/*.ts", "test/**/*.ts", "src/**/*.tsx", "vite-env.d.ts"], + "exclude": [] +} diff --git a/typescript/packages/common-iframe-sandbox/vite.config.ts b/typescript/packages/common-iframe-sandbox/vite.config.ts new file mode 100644 index 000000000..ff7f58669 --- /dev/null +++ b/typescript/packages/common-iframe-sandbox/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import dts from "vite-plugin-dts"; + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { lib: { + entry: resolve(__dirname, "src/index.ts"), + // We need a name when building as umd/iife, which web-test-runner does + name: "common-ui" + }}, + resolve: { alias: { src: resolve("src/") } }, + plugins: [dts()], +}); diff --git a/typescript/packages/common-ui/package.json b/typescript/packages/common-ui/package.json index 04871cef6..c9cbdd1e3 100644 --- a/typescript/packages/common-ui/package.json +++ b/typescript/packages/common-ui/package.json @@ -22,7 +22,7 @@ "license": "UNLICENSED", "dependencies": { "@cfworker/json-schema": "^4.1.0", - "@commontools/runner": "workspace:*", + "@commontools/iframe-sandbox": "workspace:*", "@shoelace-style/shoelace": "^2.19.1", "lit": "^3.2.1", "merkle-reference": "^1.1.0", diff --git a/typescript/packages/common-ui/src/components/common-iframe.ts b/typescript/packages/common-ui/src/components/common-iframe.ts index d452b5aed..6a13485ac 100644 --- a/typescript/packages/common-ui/src/components/common-iframe.ts +++ b/typescript/packages/common-ui/src/components/common-iframe.ts @@ -1,67 +1,13 @@ import { LitElement, html, css } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { Ref, createRef, ref } from "lit/directives/ref.js"; -import { IframeIPC } from "../index.js"; +import { CommonIframeSandboxElement as _, IPC } from "@commontools/iframe-sandbox"; -// This CSP directive uses 'unsafe-inline' to allow -// origin-less styles and scripts to be used, defeating -// many traditional uses of CSP. -const CSP = "" + - // Disable all fetch directives. Re-enable - // each specific fetch directive as needed. - "default-src 'none';" + - // Scripts: Allow 1P, inline, and CDNs. - "script-src 'self' 'unsafe-inline' unpkg.com cdn.tailwindcss.com;" + - // Styles: Allow 1P, inline. - "style-src 'self' 'unsafe-inline';" + - // Images: Allow 1P, inline. - "img-src 'self' 'unsafe-inline';" + - // Disabling until we have a concrete case. - "form-action 'none';" + - // Disable element - "base-uri 'none';" + - // Iframes/Workers: Use default (disabled) - "child-src 'none';" + - // Ping/XHR/Fetch/Sockets: Allow 1P only - "connect-src 'self';" + - // This is a deprecated/Chrome-only CSP directive. - // This blocks `` and - // the Chrome-only ``. - // `default-src` is used correctly as a fallback for - // prefetch - //"prefetch-src 'none';" + - // Fonts: Use default (disabled) - //"font-src 'none';" + - // Media: Use default (disabled) - //"media-src 'none';" + - // Manifest: Use default (disabled) - //"manifest-src 'none';" + - // Object/Embeds: Use default (disabled) - //"object-src 'none';" + - ""; - -// @summary A sandboxed iframe to execute arbitrary scripts. +// @summary An iframe to execute arbitrary scripts. See `@commontools/iframe-sandbox` +// for security details. // @tag common-iframe // @prop {string} src - String representation of HTML content to load within an iframe. // @prop context - Cell context. -// @event {CustomEvent} error - An error from the iframe. // @event {CustomEvent} load - The iframe was successfully loaded. -// -// ## Missing Functionality -// -// * Support updating the `src` property. -// * Flushing subscriptions inbetween frame loads. -// -// ## Incomplete Security Considerations -// -// * `document.baseURI` is accessible in an iframe, leaking the parent URL -// * Currently without CFC, data can be written in the iframe containing other sensitive data, -// or newly synthesized fingerprinting via capabilities (accelerometer, webrtc, canvas), -// and saved back into the database, where some other vector of exfiltration could occur. -// * Exposing iframe status to outer content could be considered leaky, -// though all content is inlined, not HTTP URLs. -// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#error_and_load_event_behavior -// @customElement("common-iframe") export class CommonIframeElement extends LitElement { @property({ type: String }) src = ""; @@ -70,11 +16,7 @@ export class CommonIframeElement extends LitElement { // we'll add a an extra level of indirection with the "context" property. @property({ type: Object }) context?: object; - @state() private errorDetails: IframeIPC.GuestError | null = null; - - private iframeRef: Ref = createRef(); - - private subscriptions: Map = new Map(); + @state() private errorDetails: IPC.GuestError | null = null; static override styles = css` .error-modal { @@ -109,113 +51,13 @@ export class CommonIframeElement extends LitElement { } `; - private handleMessage = (event: MessageEvent) => { - if (event.data?.source == "react-devtools-content-script") { - return; - } - - if (event.source !== this.iframeRef.value?.contentWindow) { - return; - } - - const IframeHandler = IframeIPC.getIframeContextHandler(); - if (IframeHandler == null) { - console.error("common-iframe: No iframe handler defined."); - return; - } - - if (!this.context) { - console.error("common-iframe: missing `context`."); - return; - } - - if (!IframeIPC.isGuestMessage(event.data)) { - console.error("common-iframe: Malformed message from guest."); - return; - } - - const message: IframeIPC.GuestMessage = event.data; - - switch (message.type) { - case IframeIPC.GuestMessageType.Error: { - const { description, source, lineno, colno, stacktrace } = message.data; - this.errorDetails = { description, source, lineno, colno, stacktrace }; - this.dispatchEvent(new CustomEvent("error", { - detail: this.errorDetails, - })); - return; - } - - case IframeIPC.GuestMessageType.Read: { - const key = message.data; - const value = IframeHandler.read(this.context, key); - // TODO: This might cause infinite loops, since the data can be a graph. - const response: IframeIPC.HostMessage = { - type: IframeIPC.HostMessageType.Update, - data: [key, value], - } - this.iframeRef.value?.contentWindow?.postMessage(response, "*"); - return; - } - - case IframeIPC.GuestMessageType.Write: { - const [key, value] = message.data; - IframeHandler.write(this.context, key, value); - return; - } - - case IframeIPC.GuestMessageType.Subscribe: { - const key = message.data; - - if (this.subscriptions.has(key)) { - console.warn("common-iframe: Already subscribed to `${key}`"); - return; - } - let receipt = IframeHandler.subscribe(this.context, key, (key, value) => this.notifySubscribers(key, value)); - this.subscriptions.set(key, receipt); - return; - } - - case IframeIPC.GuestMessageType.Unsubscribe: { - const key = message.data; - let receipt = this.subscriptions.get(key); - if (!receipt) { - return; - } - IframeHandler.unsubscribe(this.context, receipt); - this.subscriptions.delete(key); - return; - } - }; - } - - private notifySubscribers(key: string, value: any) { - // TODO: This might cause infinite loops, since the data can be a graph. - // /!\ Why is this serialized? - const copy = - value !== undefined ? JSON.parse(JSON.stringify(value)) : undefined; - const response: IframeIPC.HostMessage = { - type: IframeIPC.HostMessageType.Update, - data: [key, copy], - } - this.iframeRef.value?.contentWindow?.postMessage(response, "*"); - } - - private boundHandleMessage = this.handleMessage.bind(this); - - override connectedCallback() { - super.connectedCallback(); - window.addEventListener("message", this.boundHandleMessage); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener("message", this.boundHandleMessage); - } - - private handleLoad() { + private onLoad() { this.dispatchEvent(new CustomEvent("load")); } + + private onError(e: CustomEvent) { + this.errorDetails = e.detail; + } private dismissError() { this.errorDetails = null; @@ -230,15 +72,14 @@ export class CommonIframeElement extends LitElement { override render() { return html` - ${this.errorDetails ? html` diff --git a/typescript/packages/common-ui/src/index.ts b/typescript/packages/common-ui/src/index.ts index e3e47b657..b361853e9 100644 --- a/typescript/packages/common-ui/src/index.ts +++ b/typescript/packages/common-ui/src/index.ts @@ -1,6 +1,5 @@ export * as components from "./components/index.js"; export * as style from "./components/style.js"; -export * as IframeIPC from "./iframe-ipc.js"; import { setupShoelace } from "./components/shoelace/index.js"; setupShoelace(); diff --git a/typescript/packages/lookslike-high-level/package.json b/typescript/packages/lookslike-high-level/package.json index 338499e5d..2f17dcc4c 100644 --- a/typescript/packages/lookslike-high-level/package.json +++ b/typescript/packages/lookslike-high-level/package.json @@ -27,6 +27,7 @@ "@commontools/llm-client": "workspace:*", "@commontools/os-ui": "workspace:*", "@commontools/runner": "workspace:*", + "@commontools/iframe-sandbox": "workspace:*", "@commontools/ui": "workspace:*", "lit": "^3.2.1", "marked": "^15.0.6", diff --git a/typescript/packages/lookslike-high-level/src/data.ts b/typescript/packages/lookslike-high-level/src/data.ts index 4e2989280..0b34228fd 100644 --- a/typescript/packages/lookslike-high-level/src/data.ts +++ b/typescript/packages/lookslike-high-level/src/data.ts @@ -29,7 +29,7 @@ import { import { createStorage } from "./storage.js"; import * as allRecipes from "./recipes/index.js"; import { buildRecipe } from "./localBuild.js"; -import { IframeIPC } from "@commontools/ui"; +import { setIframeContextHandler } from "@commontools/iframe-sandbox"; // Necessary, so that suggestions are indexed. import "./recipes/todo-list-as-task.jsx"; @@ -274,7 +274,7 @@ export const toggleAnnotations = () => { annotationsEnabled.send(!annotationsEnabled.get()); }; -IframeIPC.setIframeContextHandler({ +setIframeContextHandler({ read(context: any, key: string): any { return context?.getAsQueryResult ? context?.getAsQueryResult([key]) : context?.[key]; }, diff --git a/typescript/packages/pnpm-lock.yaml b/typescript/packages/pnpm-lock.yaml index 8dcfb40f5..b5b5b8570 100644 --- a/typescript/packages/pnpm-lock.yaml +++ b/typescript/packages/pnpm-lock.yaml @@ -95,6 +95,49 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/node@22.10.10) + common-iframe-sandbox: + dependencies: + lit: + specifier: ^3.2.1 + version: 3.2.1 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@eslint/js': + specifier: ^9.19.0 + version: 9.19.0 + '@types/node': + specifier: ^22.10.10 + version: 22.10.10 + '@web/test-runner': + specifier: ^0.19.0 + version: 0.19.0 + eslint: + specifier: ^9.19.0 + version: 9.19.0 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.0.1(eslint@9.19.0) + eslint-plugin-prettier: + specifier: ^5.2.3 + version: 5.2.3(eslint-config-prettier@10.0.1(eslint@9.19.0))(eslint@9.19.0)(prettier@3.4.2) + globals: + specifier: ^15.14.0 + version: 15.14.0 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10) + vite-plugin-dts: + specifier: ^4.5.0 + version: 4.5.0(@types/node@22.10.10)(rollup@4.32.0)(typescript@5.7.3)(vite@6.0.11(@types/node@22.10.10)) + common-os-ui: dependencies: '@cfworker/json-schema': @@ -246,9 +289,9 @@ importers: '@cfworker/json-schema': specifier: ^4.1.0 version: 4.1.0 - '@commontools/runner': + '@commontools/iframe-sandbox': specifier: workspace:* - version: link:../common-runner + version: link:../common-iframe-sandbox '@shoelace-style/shoelace': specifier: ^2.19.1 version: 2.19.1(@floating-ui/utils@0.2.9)(@types/react@19.0.8) @@ -329,6 +372,9 @@ importers: '@commontools/html': specifier: workspace:* version: link:../common-html + '@commontools/iframe-sandbox': + specifier: workspace:* + version: link:../common-iframe-sandbox '@commontools/llm-client': specifier: workspace:* version: link:../llm-client diff --git a/typescript/packages/pnpm-workspace.yaml b/typescript/packages/pnpm-workspace.yaml index 82bba3052..bd741b6d5 100644 --- a/typescript/packages/pnpm-workspace.yaml +++ b/typescript/packages/pnpm-workspace.yaml @@ -4,5 +4,6 @@ packages: - "common-runner" - "llm-client" - "common-ui" + - "common-iframe-sandbox" - "common-os-ui" - "common-html"