@marimo-team/frontend 0.23.1-dev21 → 0.23.1-dev23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/assets/{edit-page-BDgzn0ig.js → edit-page-DD4uEDmX.js} +2 -2
  2. package/dist/assets/{index-C9DyCFTe.js → index-y6osgSWB.js} +17 -17
  3. package/dist/assets/{panels--5tTbFBo.js → panels-1u-RE72f.js} +1 -1
  4. package/dist/assets/{run-page-DN26u83D.js → run-page-DfWH_1mz.js} +1 -1
  5. package/dist/assets/state-D1n-olwf.js +3 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +4 -4
  8. package/src/core/islands/__tests__/bridge.test.ts +2 -12
  9. package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
  10. package/src/core/islands/__tests__/parse.test.ts +466 -24
  11. package/src/core/islands/__tests__/test-utils.tsx +263 -0
  12. package/src/core/islands/bootstrap.ts +265 -0
  13. package/src/core/islands/bridge.ts +154 -75
  14. package/src/core/islands/components/IslandControls.tsx +103 -0
  15. package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
  16. package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
  17. package/src/core/islands/components/output-wrapper.tsx +76 -93
  18. package/src/core/islands/components/useIslandControls.ts +60 -0
  19. package/src/core/islands/components/web-components.tsx +168 -40
  20. package/src/core/islands/constants.ts +28 -0
  21. package/src/core/islands/main.ts +7 -205
  22. package/src/core/islands/parse.ts +73 -26
  23. package/src/core/islands/worker-factory.ts +86 -0
  24. package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
  25. package/src/plugins/core/trusted-url.ts +20 -0
  26. package/src/plugins/impl/DataTablePlugin.tsx +7 -3
  27. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
  28. package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
  29. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
  30. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
  31. package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
  32. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
  33. package/dist/assets/state-DAlJ-NbL.js +0 -3
@@ -0,0 +1,86 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { getMarimoVersion } from "../meta/globals";
4
+ import workerUrl from "./worker/worker.tsx?worker&url";
5
+
6
+ /**
7
+ * Interface for creating Web Workers for islands
8
+ */
9
+ export interface WorkerFactory {
10
+ /**
11
+ * Creates a new worker instance
12
+ */
13
+ create(): Worker;
14
+ }
15
+
16
+ /**
17
+ * Configuration for the default worker factory
18
+ */
19
+ export interface DefaultWorkerFactoryConfig {
20
+ /**
21
+ * The URL to the worker script
22
+ * Defaults to the bundled worker
23
+ */
24
+ workerUrl?: string;
25
+
26
+ /**
27
+ * The name to give the worker (shows in DevTools)
28
+ * Defaults to the marimo version
29
+ */
30
+ workerName?: string;
31
+ }
32
+
33
+ /**
34
+ * Default implementation of WorkerFactory that creates Pyodide workers
35
+ * for islands mode.
36
+ */
37
+ export class DefaultWorkerFactory implements WorkerFactory {
38
+ private readonly url: string;
39
+ private readonly name: string;
40
+
41
+ constructor(config: DefaultWorkerFactoryConfig = {}) {
42
+ this.url = config.workerUrl || this.getDefaultWorkerUrl();
43
+ this.name = config.workerName || getMarimoVersion();
44
+ }
45
+
46
+ /**
47
+ * Creates a new Pyodide worker
48
+ */
49
+ create(): Worker {
50
+ const js = `import ${JSON.stringify(new URL(this.url, import.meta.url))}`;
51
+ const blob = new Blob([js], { type: "application/javascript" });
52
+ const objURL = URL.createObjectURL(blob);
53
+
54
+ const worker = new Worker(objURL, {
55
+ type: "module",
56
+ /* @vite-ignore */
57
+ name: this.name,
58
+ });
59
+
60
+ // Blob URL can be revoked once the worker has loaded the script
61
+ URL.revokeObjectURL(objURL);
62
+
63
+ return worker;
64
+ }
65
+
66
+ /**
67
+ * Gets the default worker URL based on environment
68
+ */
69
+ private getDefaultWorkerUrl(): string {
70
+ const url = import.meta.env.DEV
71
+ ? workerUrl
72
+ : makeRelativeWorkerUrl(workerUrl);
73
+ return url;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Makes worker URLs relative for production builds
79
+ */
80
+ function makeRelativeWorkerUrl(url: string): string {
81
+ return url.startsWith("./")
82
+ ? url
83
+ : url.startsWith("/")
84
+ ? `.${url}`
85
+ : `./${url}`;
86
+ }
@@ -0,0 +1,48 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import { isTrustedVirtualFileUrl } from "../trusted-url";
4
+
5
+ describe("isTrustedVirtualFileUrl", () => {
6
+ it.each([
7
+ "./@file/123-mpl.js",
8
+ "./@file/456-mpl.css",
9
+ "@file/789-bokeh.js",
10
+ "/@file/0-empty.txt",
11
+ "./@file/1234-name.with.dots.js",
12
+ ])("accepts virtual file path %s", (url) => {
13
+ expect(isTrustedVirtualFileUrl(url)).toBe(true);
14
+ });
15
+
16
+ it.each([
17
+ // Attack vector from the vulnerability report
18
+ "http://127.0.0.1:8820/poc.js",
19
+ "https://evil.example.com/x.js",
20
+ // Protocol-relative → takes attacker's origin
21
+ "//evil.example.com/x.js",
22
+ // Dangerous schemes
23
+ "javascript:alert(1)",
24
+ "data:text/javascript;base64,YWxlcnQoMSk=",
25
+ "file:///etc/passwd",
26
+ "blob:http://127.0.0.1/abc",
27
+ // Almost-but-not virtual file paths
28
+ "./evil.js",
29
+ "../@file/x.js",
30
+ "./malicious/@file/x.js",
31
+ "@file",
32
+ "@files/x.js",
33
+ // Query/fragment smuggling
34
+ "./@file/x.js?redirect=http://evil.com",
35
+ "./@file/x.js#http://evil.com",
36
+ // Empty and non-string
37
+ "",
38
+ ])("rejects %s", (url) => {
39
+ expect(isTrustedVirtualFileUrl(url)).toBe(false);
40
+ });
41
+
42
+ it("rejects non-string input", () => {
43
+ expect(isTrustedVirtualFileUrl(null)).toBe(false);
44
+ expect(isTrustedVirtualFileUrl(undefined)).toBe(false);
45
+ expect(isTrustedVirtualFileUrl(42)).toBe(false);
46
+ expect(isTrustedVirtualFileUrl({})).toBe(false);
47
+ });
48
+ });
@@ -0,0 +1,20 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Whether a URL can be trusted to point at a marimo-served virtual file.
5
+ *
6
+ * Plugins that load remote scripts or stylesheets (e.g. MplInteractive, Panel)
7
+ * must call this before turning a plugin-supplied URL into a `<script src>` or
8
+ * `<link href>`. The backend always serializes these URLs as virtual file
9
+ * paths of the form `./@file/<byte_length>-<filename>` (see
10
+ * `VirtualFile.create_and_register`). Accepting anything else would let a
11
+ * maliciously crafted `<marimo-*>` element embedded in markdown load
12
+ * attacker-controlled JavaScript at same origin, since the HTML sanitizer
13
+ * lets arbitrary marimo custom elements and attributes through.
14
+ */
15
+ export function isTrustedVirtualFileUrl(url: unknown): url is string {
16
+ if (typeof url !== "string" || url.length === 0) {
17
+ return false;
18
+ }
19
+ return /^(\.?\/)?@file\/[^?#]+$/.test(url);
20
+ }
@@ -68,6 +68,7 @@ import {
68
68
  import { slotsController } from "@/core/slots/slots";
69
69
  import { store } from "@/core/state/jotai";
70
70
  import { isStaticNotebook } from "@/core/static/static-state";
71
+ import { isIslands } from "@/core/islands/utils";
71
72
  import { isInVscodeExtension } from "@/core/vscode/is-in-vscode";
72
73
  import { useAsyncData } from "@/hooks/useAsyncData";
73
74
  import { useDeepCompareMemoize } from "@/hooks/useDeepCompareMemoize";
@@ -1006,6 +1007,7 @@ const DataTableComponent = ({
1006
1007
  const canShowColumnExplorer = showColumnExplorer && !!preview_column;
1007
1008
 
1008
1009
  const isInVscode = isInVscodeExtension();
1010
+ const isIslandsMode = isIslands();
1009
1011
 
1010
1012
  return (
1011
1013
  <>
@@ -1091,13 +1093,15 @@ const DataTableComponent = ({
1091
1093
  onCellSelectionChange={handleCellSelectionChange}
1092
1094
  getRowIds={get_row_ids}
1093
1095
  toggleDisplayHeader={toggleDisplayHeader}
1094
- showChartBuilder={showChartBuilder}
1096
+ showChartBuilder={showChartBuilder && !isIslandsMode}
1095
1097
  isChartBuilderOpen={isChartBuilderOpen}
1096
1098
  showPageSizeSelector={showPageSizeSelector}
1097
- // Hidden in VSCode (for now) because we don't have a panel to show
1099
+ // Hidden in VSCode and islands because there's no panel to show
1098
1100
  // the table explorer.
1099
1101
  showTableExplorer={
1100
- (showRowExplorer || canShowColumnExplorer) && !isInVscode
1102
+ (showRowExplorer || canShowColumnExplorer) &&
1103
+ !isInVscode &&
1104
+ !isIslandsMode
1101
1105
  }
1102
1106
  togglePanel={togglePanel}
1103
1107
  isPanelOpen={isPanelOpen}
@@ -59,13 +59,39 @@ describe("WidgetDefRegistry", () => {
59
59
 
60
60
  it("should remove from cache on import failure so retry creates new promise", async () => {
61
61
  const promise1 = registry.getModule("http://localhost/a.js", "fail-hash");
62
- // The import will fail in Node (http: scheme not supported)
62
+ // The URL is rejected by the trusted-URL validator.
63
63
  await expect(promise1).rejects.toThrow();
64
64
  // After failure, cache should be cleared, so next call creates a new promise
65
65
  const promise2 = registry.getModule("http://localhost/a.js", "fail-hash");
66
66
  expect(promise1).not.toBe(promise2);
67
67
  promise2.catch(() => undefined);
68
68
  });
69
+
70
+ describe("URL validation", () => {
71
+ it.each([
72
+ // Attack vector: raw <marimo-anywidget data-js-url=...> in markdown
73
+ "http://127.0.0.1:8820/poc.mjs",
74
+ "https://evil.example.com/widget.mjs",
75
+ "//evil.example.com/widget.mjs",
76
+ "javascript:alert(1)",
77
+ "data:text/javascript;base64,YWxlcnQoMSk=",
78
+ "./@file/x.js?redirect=http://evil.com",
79
+ "",
80
+ ])("rejects untrusted URL: %s", async (url) => {
81
+ await expect(registry.getModule(url, `hash-${url}`)).rejects.toThrow(
82
+ /untrusted/i,
83
+ );
84
+ });
85
+
86
+ it("accepts virtual file paths (fails later at import time)", async () => {
87
+ // The URL passes validation but the import still fails because this
88
+ // is a Node test environment with no server. We only assert that
89
+ // the rejection reason is NOT the "untrusted URL" refusal.
90
+ await expect(
91
+ registry.getModule("./@file/123-widget.js", "trusted-hash"),
92
+ ).rejects.not.toThrow(/untrusted/i);
93
+ });
94
+ });
69
95
  });
70
96
 
71
97
  describe("WidgetBinding", () => {
@@ -5,6 +5,7 @@ import type { AnyWidget, Experimental } from "@anywidget/types";
5
5
  import { asRemoteURL } from "@/core/runtime/config";
6
6
  import { resolveVirtualFileURL } from "@/core/static/files";
7
7
  import { isStaticNotebook } from "@/core/static/static-state";
8
+ import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
8
9
  import { Logger } from "@/utils/Logger";
9
10
  import type { Model } from "./model";
10
11
  import type { ModelState, WidgetModelId } from "./types";
@@ -80,6 +81,18 @@ class WidgetDefRegistry {
80
81
  }
81
82
 
82
83
  async #doImport(jsUrl: string): Promise<any> {
84
+ // Only trust marimo virtual file paths. Accepting arbitrary URLs
85
+ // would let a raw `<marimo-anywidget data-js-url=...>` element
86
+ // embedded in a markdown cell dynamically import attacker-controlled
87
+ // JavaScript at same origin (the HTML sanitizer allows any marimo-*
88
+ // custom element with any attribute through to the plugin layer).
89
+ if (!isTrustedVirtualFileUrl(jsUrl)) {
90
+ throw new Error(
91
+ `Refusing to load anywidget module from untrusted URL: ${String(
92
+ jsUrl,
93
+ )}`,
94
+ );
95
+ }
83
96
  let url = asRemoteURL(jsUrl).toString();
84
97
  if (isStaticNotebook()) {
85
98
  url = resolveVirtualFileURL(url);
@@ -5,12 +5,14 @@ import { useCallback, useEffect, useRef } from "react";
5
5
  import { z } from "zod";
6
6
  import { useEventListener } from "@/hooks/useEventListener";
7
7
  import { createPlugin } from "@/plugins/core/builder";
8
+ import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
8
9
  import { MODEL_MANAGER, type Model } from "@/plugins/impl/anywidget/model";
9
10
  import type { ModelState, WidgetModelId } from "@/plugins/impl/anywidget/types";
10
11
  import type { IPluginProps } from "@/plugins/types";
11
12
  import { downloadBlob } from "@/utils/download";
12
13
  import { Logger } from "@/utils/Logger";
13
14
  import { MplCommWebSocket } from "./mpl-websocket-shim";
15
+ import { Functions } from "@/utils/functions";
14
16
 
15
17
  const MPL_SCOPE_CLASS = "mpl-interactive-figure";
16
18
 
@@ -73,6 +75,11 @@ async function ensureMplJs(jsUrl: string): Promise<void> {
73
75
  if (window.mpl) {
74
76
  return;
75
77
  }
78
+ if (!isTrustedVirtualFileUrl(jsUrl)) {
79
+ throw new Error(
80
+ `Refusing to load mpl.js from untrusted URL: ${String(jsUrl)}`,
81
+ );
82
+ }
76
83
  if (mplJsLoading) {
77
84
  return mplJsLoading;
78
85
  }
@@ -148,6 +155,12 @@ function patchToolbarImages(
148
155
  }
149
156
 
150
157
  function injectCss(container: HTMLElement, cssUrl: string): () => void {
158
+ if (!isTrustedVirtualFileUrl(cssUrl)) {
159
+ Logger.error(
160
+ `Refusing to load mpl CSS from untrusted URL: ${String(cssUrl)}`,
161
+ );
162
+ return Functions.NOOP;
163
+ }
151
164
  const link = document.createElement("link");
152
165
  link.rel = "stylesheet";
153
166
  link.href = cssUrl;
@@ -307,3 +320,11 @@ const MplInteractiveSlot = (props: IPluginProps<ModelIdRef, Data>) => {
307
320
  // Must match _MPL_SCOPE in from_mpl_interactive.py
308
321
  return <div ref={containerRef} className={MPL_SCOPE_CLASS} />;
309
322
  };
323
+
324
+ export const visibleForTesting = {
325
+ ensureMplJs,
326
+ injectCss,
327
+ resetMplJsLoading: () => {
328
+ mplJsLoading = null;
329
+ },
330
+ };
@@ -0,0 +1,119 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { Logger } from "@/utils/Logger";
4
+ import { visibleForTesting } from "../MplInteractivePlugin";
5
+
6
+ const { ensureMplJs, injectCss, resetMplJsLoading } = visibleForTesting;
7
+
8
+ describe("MplInteractivePlugin URL validation", () => {
9
+ beforeEach(() => {
10
+ // Reset module-level script-loading state and any stubs.
11
+ delete (window as { mpl?: unknown }).mpl;
12
+ resetMplJsLoading();
13
+ // Remove any scripts the tests added to document.head.
14
+ for (const el of document.head.querySelectorAll(
15
+ "script[data-test-mpl],link[data-test-mpl]",
16
+ )) {
17
+ el.remove();
18
+ }
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ describe("ensureMplJs", () => {
26
+ it("rejects the PoC attack URL without creating a <script>", async () => {
27
+ const appendSpy = vi.spyOn(document.head, "append");
28
+ await expect(ensureMplJs("http://127.0.0.1:8820/poc.js")).rejects.toThrow(
29
+ /untrusted/i,
30
+ );
31
+ expect(appendSpy).not.toHaveBeenCalled();
32
+ });
33
+
34
+ it.each([
35
+ "https://evil.example.com/x.js",
36
+ "//evil.example.com/x.js",
37
+ "javascript:alert(1)",
38
+ "data:text/javascript;base64,YWxlcnQoMSk=",
39
+ "./@file/x.js?redirect=http://evil.com",
40
+ ])("rejects %s", async (url) => {
41
+ const appendSpy = vi.spyOn(document.head, "append");
42
+ await expect(ensureMplJs(url)).rejects.toThrow(/untrusted/i);
43
+ expect(appendSpy).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it("is a no-op when window.mpl is already present", async () => {
47
+ (window as { mpl?: unknown }).mpl = {};
48
+ const appendSpy = vi.spyOn(document.head, "append");
49
+ // Even a malicious URL should be ignored — short-circuit happens first.
50
+ await expect(
51
+ ensureMplJs("http://evil.example.com/x.js"),
52
+ ).resolves.toBeUndefined();
53
+ expect(appendSpy).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("creates a <script src> for a trusted virtual file URL", async () => {
57
+ const appendSpy = vi
58
+ .spyOn(document.head, "append")
59
+ .mockImplementation((...nodes) => {
60
+ // Simulate a successful load so ensureMplJs resolves.
61
+ for (const node of nodes) {
62
+ if (node instanceof HTMLScriptElement) {
63
+ queueMicrotask(() => node.onload?.(new Event("load")));
64
+ }
65
+ }
66
+ });
67
+
68
+ await expect(ensureMplJs("./@file/123-mpl.js")).resolves.toBeUndefined();
69
+
70
+ expect(appendSpy).toHaveBeenCalledTimes(1);
71
+ const appended = appendSpy.mock.calls[0][0] as HTMLScriptElement;
72
+ expect(appended.tagName).toBe("SCRIPT");
73
+ expect(appended.src).toContain("@file/123-mpl.js");
74
+ });
75
+ });
76
+
77
+ describe("injectCss", () => {
78
+ it("refuses to append <link> for the PoC attack CSS URL", () => {
79
+ const container = document.createElement("div");
80
+ const loggerSpy = vi.spyOn(Logger, "error").mockImplementation(() => {});
81
+
82
+ const cleanup = injectCss(container, "http://127.0.0.1:8820/x.css");
83
+
84
+ expect(container.querySelector("link")).toBeNull();
85
+ expect(loggerSpy).toHaveBeenCalledWith(
86
+ expect.stringContaining("untrusted"),
87
+ );
88
+ // Cleanup must be safe to call even when nothing was appended.
89
+ expect(() => cleanup()).not.toThrow();
90
+ });
91
+
92
+ it.each([
93
+ "https://evil.example.com/x.css",
94
+ "javascript:alert(1)",
95
+ "data:text/css,body{background:red}",
96
+ ])("refuses to append <link> for %s", (url) => {
97
+ const container = document.createElement("div");
98
+ vi.spyOn(Logger, "error").mockImplementation(() => {});
99
+
100
+ injectCss(container, url);
101
+
102
+ expect(container.querySelector("link")).toBeNull();
103
+ });
104
+
105
+ it("appends a <link> for a trusted virtual file URL", () => {
106
+ const container = document.createElement("div");
107
+
108
+ const cleanup = injectCss(container, "./@file/456-mpl.css");
109
+
110
+ const link = container.querySelector("link");
111
+ expect(link).not.toBeNull();
112
+ expect(link?.rel).toBe("stylesheet");
113
+ expect(link?.getAttribute("href")).toBe("./@file/456-mpl.css");
114
+
115
+ cleanup();
116
+ expect(container.querySelector("link")).toBeNull();
117
+ });
118
+ });
119
+ });
@@ -10,6 +10,7 @@ import {
10
10
  } from "@/hooks/useEventListener";
11
11
  import { createPlugin } from "@/plugins/core/builder";
12
12
  import { rpc } from "@/plugins/core/rpc";
13
+ import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
13
14
  import type { IPluginProps } from "@/plugins/types";
14
15
  import { Logger } from "@/utils/Logger";
15
16
  import { EventBuffer, extractBuffers, MessageSchema } from "./utils";
@@ -64,7 +65,7 @@ declare global {
64
65
  }
65
66
 
66
67
  interface PanelData {
67
- extension: string | null;
68
+ extensionUrl: string | null;
68
69
  docs_json: Record<string, unknown>;
69
70
  render_json: {
70
71
  roots: Record<string, string>;
@@ -85,7 +86,7 @@ type PluginFunctions = {
85
86
  export const PanelPlugin = createPlugin<T>("marimo-panel")
86
87
  .withData(
87
88
  z.object({
88
- extension: z.string().nullable(),
89
+ extensionUrl: z.string().nullable(),
89
90
  docs_json: z.record(z.string(), z.unknown()),
90
91
  render_json: z
91
92
  .object({
@@ -110,9 +111,34 @@ function isBokehLoaded() {
110
111
  return window.Bokeh != null;
111
112
  }
112
113
 
114
+ /**
115
+ * Append a `<script src>` for the bokeh/panel extension.
116
+ *
117
+ * The URL must be a marimo virtual file path; anything else (e.g. an
118
+ * attacker-controlled URL injected via a raw `<marimo-panel>` element in a
119
+ * markdown cell) is refused.
120
+ */
121
+ export function loadPanelExtension(extensionUrl: string | null): boolean {
122
+ if (!extensionUrl) {
123
+ return false;
124
+ }
125
+ if (!isTrustedVirtualFileUrl(extensionUrl)) {
126
+ Logger.error(
127
+ `Refusing to load Panel extension from untrusted URL: ${String(
128
+ extensionUrl,
129
+ )}`,
130
+ );
131
+ return false;
132
+ }
133
+ const script = document.createElement("script");
134
+ script.src = extensionUrl;
135
+ document.head.append(script);
136
+ return true;
137
+ }
138
+
113
139
  const PanelSlot = (props: Props) => {
114
140
  const { data, functions, host } = props;
115
- const { extension, docs_json: docsJson, render_json: renderJson } = data;
141
+ const { extensionUrl, docs_json: docsJson, render_json: renderJson } = data;
116
142
  const ref = useRef<HTMLDivElement>(null);
117
143
  const rootModelIdRef = useRef<string | null>(null);
118
144
  const receiverRef = useRef<InstanceType<
@@ -173,12 +199,7 @@ const PanelSlot = (props: Props) => {
173
199
  return;
174
200
  }
175
201
 
176
- // Load the extension
177
- if (extension) {
178
- const script = document.createElement("script");
179
- script.innerHTML = extension;
180
- document.head.append(script);
181
- }
202
+ loadPanelExtension(extensionUrl);
182
203
 
183
204
  // Check if Bokeh is loaded every 10ms
184
205
  const checkBokeh = setInterval(() => {
@@ -189,7 +210,7 @@ const PanelSlot = (props: Props) => {
189
210
  }, 10);
190
211
 
191
212
  return () => clearInterval(checkBokeh);
192
- }, [extension, setLoaded]);
213
+ }, [extensionUrl, setLoaded]);
193
214
 
194
215
  // Listen for incoming messages
195
216
  useEventListener(
@@ -0,0 +1,60 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { Logger } from "@/utils/Logger";
4
+ import { loadPanelExtension } from "../PanelPlugin";
5
+
6
+ describe("loadPanelExtension", () => {
7
+ beforeEach(() => {
8
+ for (const el of document.head.querySelectorAll("script")) {
9
+ el.remove();
10
+ }
11
+ });
12
+
13
+ afterEach(() => {
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ it("does nothing and returns false for null URL", () => {
18
+ const appendSpy = vi.spyOn(document.head, "append");
19
+ expect(loadPanelExtension(null)).toBe(false);
20
+ expect(appendSpy).not.toHaveBeenCalled();
21
+ });
22
+
23
+ it("refuses to load the PoC attack URL", () => {
24
+ const appendSpy = vi.spyOn(document.head, "append");
25
+ const loggerSpy = vi.spyOn(Logger, "error").mockImplementation(() => {});
26
+
27
+ expect(loadPanelExtension("http://127.0.0.1:8820/poc.js")).toBe(false);
28
+
29
+ expect(appendSpy).not.toHaveBeenCalled();
30
+ expect(loggerSpy).toHaveBeenCalledWith(
31
+ expect.stringContaining("untrusted"),
32
+ );
33
+ });
34
+
35
+ it.each([
36
+ "https://evil.example.com/x.js",
37
+ "//evil.example.com/x.js",
38
+ // An attacker embedding inline JS as a data URL — what the old plugin
39
+ // would have executed verbatim via script.innerHTML.
40
+ "data:text/javascript;base64,YWxlcnQoMSk=",
41
+ "javascript:alert(1)",
42
+ "./@file/x.js#http://evil.com",
43
+ ])("refuses to load %s", (url) => {
44
+ const appendSpy = vi.spyOn(document.head, "append");
45
+ vi.spyOn(Logger, "error").mockImplementation(() => {});
46
+
47
+ expect(loadPanelExtension(url)).toBe(false);
48
+ expect(appendSpy).not.toHaveBeenCalled();
49
+ });
50
+
51
+ it("appends a <script src> for a trusted virtual file URL", () => {
52
+ expect(loadPanelExtension("./@file/42-bokeh.js")).toBe(true);
53
+
54
+ const script = document.head.querySelector("script");
55
+ expect(script).not.toBeNull();
56
+ expect(script?.src).toContain("@file/42-bokeh.js");
57
+ // Must NOT populate innerHTML — that was the original vulnerability sink.
58
+ expect(script?.innerHTML).toBe("");
59
+ });
60
+ });
@@ -1,3 +0,0 @@
1
- var Ht=Object.defineProperty;var De=f=>{throw TypeError(f)};var Ot=(f,h,y)=>h in f?Ht(f,h,{enumerable:!0,configurable:!0,writable:!0,value:y}):f[h]=y;var n=(f,h,y)=>Ot(f,typeof h!="symbol"?h+"":h,y),ue=(f,h,y)=>h.has(f)||De("Cannot "+y);var o=(f,h,y)=>(ue(f,h,"read from private field"),y?y.call(f):h.get(f)),_=(f,h,y)=>h.has(f)?De("Cannot add the same private member more than once"):h instanceof WeakSet?h.add(f):h.set(f,y),C=(f,h,y,z)=>(ue(f,h,"write to private field"),z?z.call(f,y):h.set(f,y),y),A=(f,h,y)=>(ue(f,h,"access private method"),y);import{i as Q,p as de,u as $t}from"./useEvent-D91BmmQi.js";import{An as zt,Si as Y,b as Vt,ci as Qt,di as Yt,gi as Gt,it as He,pi as Jt,x as Kt,xi as Xt,__tla as Zt}from"./cells-BqYYXi6G.js";import{t as er}from"./get-D7jeR2wg.js";import{t as tr}from"./debounce-BbSyBDA8.js";import{t as rr}from"./_baseSet-49dbm-yb.js";import{_ as p,y as g}from"./useEventListener-DGjKht0c.js";import{t as sr}from"./invariant-CkiqGLcK.js";import{S as nr}from"./utils-8btzWeZg.js";import{S as ar,n as ir}from"./config-DoZCLcOb.js";import{n as or}from"./switch-YkPg_CVc.js";import{n as Oe}from"./globals-Bu6OEURn.js";import{at as ce,ot as $e,__tla as lr}from"./JsonOutput-CavtrueA.js";import{t as ur}from"./createReducer-CYlwteGD.js";import{r as dr}from"./strings-pfr2N700.js";import{t as pe}from"./requests-9-v2bhoi.js";import{a as ze,l as cr,r as Ve,__tla as pr}from"./markdown-renderer-DNANigO8.js";import{t as fr}from"./DeferredRequestRegistry-BLWK1eKa.js";import{t as hr}from"./preload-helper-D2MJg03u.js";import{t as Qe}from"./Deferred-DxQeE5uh.js";import{t as Ye}from"./uuid-DXdzqzcr.js";import{t as mr}from"./use-toast-BDYuj3zG.js";import{n as gr,r as yr,t as wr}from"./share-0LPgOyiW.js";import{t as br}from"./blob-CTort_or.js";import{i as Ge,r as vr,t as _r}from"./events-Qeh-bHlj.js";let G,fe,he,Je,Ke,Xe,Ze,et,J,tt,me,rt,st,nt,ge,at,ne,it,ot,ye,lt,ut,Cr=Promise.all([(()=>{try{return Zt}catch{}})(),(()=>{try{return lr}catch{}})(),(()=>{try{return pr}catch{}})()]).then(async()=>{var U,ee,ct,Ie,M,T,E,Le,F,Ne,P,V,D,ae,Ae,Ue,te,I,L,H,R,j,N,pt,ft,ht,re;function f(t,e,r){return t==null?t:rr(t,e,r)}G=f,ut=de(null);var{valueAtom:h,useActions:y}=ur(()=>({banners:[]}),{addBanner:(t,e)=>({...t,banners:[...t.banners,{...e,id:Ye()}]}),removeBanner:(t,e)=>({...t,banners:t.banners.filter(r=>r.id!==e)}),clearBanners:t=>({...t,banners:[]})});Je=()=>$t(h),lt=function(){return y()};let z,be,ve;z=class{constructor(){n(this,"subscriptions",new Map)}addSubscription(t,e){var r;this.subscriptions.has(t)||this.subscriptions.set(t,new Set),(r=this.subscriptions.get(t))==null||r.add(e)}removeSubscription(t,e){var r;(r=this.subscriptions.get(t))==null||r.delete(e)}notify(t,e){for(let r of this.subscriptions.get(t)??[])r(e)}},ye=class we{constructor(e){n(this,"subscriptions",new z);this.producer=e}static withProducerCallback(e){return new we(e)}static empty(){return new we}startProducer(){this.producer&&this.producer(e=>{this.subscriptions.notify("message",e)})}connect(){return new Promise(e=>setTimeout(e,0)).then(()=>{this.subscriptions.notify("open",new Event("open"))})}get readyState(){return WebSocket.OPEN}reconnect(e,r){this.close(),this.connect()}close(){this.subscriptions.notify("close",new Event("close"))}send(e){return this.subscriptions.notify("message",new MessageEvent("message",{data:e})),Promise.resolve()}addEventListener(e,r){this.subscriptions.addSubscription(e,r),e==="open"&&r(new Event("open")),e==="message"&&this.startProducer()}removeEventListener(e,r){this.subscriptions.removeSubscription(e,r)}},be=1e10,ve=1e3;function K(t,e){let r=t.map(s=>`"${s}"`).join(", ");return Error(`This RPC instance cannot ${e} because the transport did not provide one or more of these methods: ${r}`)}function gt(t={}){let e={};function r(l){e=l}let s={};function a(l){var c;s.unregisterHandler&&s.unregisterHandler(),s=l,(c=s.registerHandler)==null||c.call(s,Dt)}let i;function u(l){if(typeof l=="function"){i=l;return}i=(c,w)=>{let m=l[c];if(m)return m(w);let b=l._;if(!b)throw Error(`The requested method has no handler: ${c}`);return b(c,w)}}let{maxRequestTime:d=ve}=t;t.transport&&a(t.transport),t.requestHandler&&u(t.requestHandler),t._debugHooks&&r(t._debugHooks);let v=0;function q(){return v<=be?++v:v=0}let x=new Map,S=new Map;function O(l,...c){let w=c[0];return new Promise((m,b)=>{var $;if(!s.send)throw K(["send"],"make requests");let k=q(),W={type:"request",id:k,method:l,params:w};x.set(k,{resolve:m,reject:b}),d!==1/0&&S.set(k,setTimeout(()=>{S.delete(k),b(Error("RPC request timed out."))},d)),($=e.onSend)==null||$.call(e,W),s.send(W)})}let se=new Proxy(O,{get:(l,c,w)=>c in l?Reflect.get(l,c,w):m=>O(c,m)}),Te=se;function je(l,...c){var b;let w=c[0];if(!s.send)throw K(["send"],"send messages");let m={type:"message",id:l,payload:w};(b=e.onSend)==null||b.call(e,m),s.send(m)}let Be=new Proxy(je,{get:(l,c,w)=>c in l?Reflect.get(l,c,w):m=>je(c,m)}),We=Be,B=new Map,le=new Set;function Bt(l,c){var w;if(!s.registerHandler)throw K(["registerHandler"],"register message listeners");if(l==="*"){le.add(c);return}B.has(l)||B.set(l,new Set),(w=B.get(l))==null||w.add(c)}function Wt(l,c){var w,m;if(l==="*"){le.delete(c);return}(w=B.get(l))==null||w.delete(c),((m=B.get(l))==null?void 0:m.size)===0&&B.delete(l)}async function Dt(l){var c,w;if((c=e.onReceive)==null||c.call(e,l),!("type"in l))throw Error("Message does not contain a type.");if(l.type==="request"){if(!s.send||!i)throw K(["send","requestHandler"],"handle requests");let{id:m,method:b,params:k}=l,W;try{W={type:"response",id:m,success:!0,payload:await i(b,k)}}catch($){if(!($ instanceof Error))throw $;W={type:"response",id:m,success:!1,error:$.message}}(w=e.onSend)==null||w.call(e,W),s.send(W);return}if(l.type==="response"){let m=S.get(l.id);m!=null&&clearTimeout(m);let{resolve:b,reject:k}=x.get(l.id)??{};l.success?b==null||b(l.payload):k==null||k(Error(l.error));return}if(l.type==="message"){for(let b of le)b(l.id,l.payload);let m=B.get(l.id);if(!m)return;for(let b of m)b(l.payload);return}throw Error(`Unexpected RPC message type: ${l.type}`)}return{setTransport:a,setRequestHandler:u,request:se,requestProxy:Te,send:Be,sendProxy:We,addMessageListener:Bt,removeMessageListener:Wt,proxy:{send:We,request:Te},_setDebugHooks:r}}function yt(t){return gt(t)}var _e="[transport-id]";function wt(t,e){let{transportId:r}=e;return r==null?t:{[_e]:r,data:t}}function bt(t,e){let{transportId:r,filter:s}=e,a=s==null?void 0:s();if(r!=null&&a!=null)throw Error("Cannot use both `transportId` and `filter` at the same time");let i=t;if(r){if(t[_e]!==r)return[!0];i=t.data}return a===!1?[!0]:[!1,i]}function vt(t,e={}){let{transportId:r,filter:s,remotePort:a}=e,i=t,u=a??t,d;return{send(v){u.postMessage(wt(v,{transportId:r}))},registerHandler(v){d=q=>{let x=q.data,[S,O]=bt(x,{transportId:r,filter:()=>s==null?void 0:s(q)});S||v(O)},i.addEventListener("message",d)},unregisterHandler(){d&&i.removeEventListener("message",d)}}}function _t(t,e){return vt(t,e)}function Ce(t){return yt({transport:_t(t,{transportId:"marimo-transport"}),maxRequestTime:2e4,_debugHooks:{onSend:e=>{p.debug("[rpc] Parent -> Worker",e)},onReceive:e=>{p.debug("[rpc] Worker -> Parent",e)}}})}fe=de("Initializing..."),et=de(t=>{let e=t(Vt),r=Object.values(e.cellRuntime);return r.some(s=>!cr(s.output))?!0:r.every(s=>s.status==="idle")});var Re=gr(),qe="marimo:file",xe=new Qt(null);const Ct={saveFile(t){xe.set(qe,t)},readFile(){return xe.get(qe)}};var Rt={saveFile(t){Y.setCodeForHash((0,Re.compressToEncodedURIComponent)(t))},readFile(){let t=Y.getCodeFromHash()||Y.getCodeFromSearchParam();return t?(0,Re.decompressFromEncodedURIComponent)(t):null}};const qt={saveFile(t){},readFile(){let t=document.querySelector("marimo-code");return t?decodeURIComponent(t.textContent||"").trim():null}};var xt={saveFile(t){},readFile(){if(window.location.hostname!=="marimo.app")return null;let t=new URL("files/wasm-intro.py",document.baseURI);return fetch(t.toString()).then(e=>e.ok?e.text():null).catch(()=>null)}};const St={saveFile(t){},readFile(){return Q.get(Yt)??null}};var Et={saveFile(t){},readFile(){return["import marimo","app = marimo.App()","","@app.cell","def __():"," return","",'if __name__ == "__main__":'," app.run()"].join(`
2
- `)}},Se=class{constructor(t){this.stores=t}insert(t,e){this.stores.splice(t,0,e)}saveFile(t){this.stores.forEach(e=>e.saveFile(t))}readFile(){for(let t of this.stores){let e=t.readFile();if(e)return e}return null}};let X;J=new Se([St,qt,Rt]),X=new Se([Ct,xt,Et]),ge=class dt{constructor(){n(this,"initialized",new Qe);n(this,"sendRename",async({filename:e})=>(e===null||(Y.setFilename(e),await this.rpc.proxy.request.bridge({functionName:"rename_file",payload:e})),null));n(this,"sendSave",async e=>{if(!this.saveRpc)return p.warn("Save RPC not initialized"),null;await this.saveRpc.saveNotebook(e);let r=await this.readCode();return r.contents&&(J.saveFile(r.contents),X.saveFile(r.contents)),this.rpc.proxy.request.saveNotebook(e).catch(s=>{p.error(s)}),null});n(this,"sendCopy",async()=>{g()});n(this,"sendStdin",async e=>(await this.rpc.proxy.request.bridge({functionName:"put_input",payload:e.text}),null));n(this,"sendPdb",async()=>{g()});n(this,"sendRun",async e=>(await this.rpc.proxy.request.loadPackages(e.codes.join(`
3
- `)),await this.putControlRequest({type:"execute-cells",...e}),null));n(this,"sendRunScratchpad",async e=>(await this.rpc.proxy.request.loadPackages(e.code),await this.putControlRequest({type:"execute-scratchpad",...e}),null));n(this,"sendInterrupt",async()=>(this.interruptBuffer!==void 0&&(this.interruptBuffer[0]=2),null));n(this,"sendShutdown",async()=>(window.close(),null));n(this,"sendFormat",async e=>await this.rpc.proxy.request.bridge({functionName:"format",payload:e}));n(this,"sendDeleteCell",async e=>(await this.putControlRequest({type:"delete-cell",...e}),null));n(this,"sendInstallMissingPackages",async e=>(this.putControlRequest({type:"install-packages",...e}),null));n(this,"sendCodeCompletionRequest",async e=>(Q.get(Kt)||await this.rpc.proxy.request.bridge({functionName:"code_complete",payload:e}),null));n(this,"saveUserConfig",async e=>(await this.rpc.proxy.request.bridge({functionName:"save_user_config",payload:e}),or.post("/kernel/save_user_config",e,{baseUrl:"/"}).catch(r=>(p.error(r),null))));n(this,"saveAppConfig",async e=>(await this.rpc.proxy.request.bridge({functionName:"save_app_config",payload:e}),null));n(this,"saveCellConfig",async e=>(await this.putControlRequest({type:"update-cell-config",...e}),null));n(this,"sendRestart",async()=>{let e=await this.readCode();return e.contents&&(J.saveFile(e.contents),X.saveFile(e.contents)),yr(),null});n(this,"readCode",async()=>this.saveRpc?{contents:await this.saveRpc.readNotebook()}:(p.warn("Save RPC not initialized"),{contents:""}));n(this,"readSnippets",async()=>await this.rpc.proxy.request.bridge({functionName:"read_snippets",payload:void 0}));n(this,"openFile",async({path:e})=>{let r=wr({code:null,baseUrl:window.location.origin});return window.open(r,"_blank"),null});n(this,"sendListFiles",async e=>await this.rpc.proxy.request.bridge({functionName:"list_files",payload:e}));n(this,"sendSearchFiles",async e=>await this.rpc.proxy.request.bridge({functionName:"search_files",payload:e}));n(this,"sendComponentValues",async e=>(await this.putControlRequest({type:"update-ui-element",...e,token:Ye()}),null));n(this,"sendInstantiate",async e=>null);n(this,"sendFunctionRequest",async e=>(await this.putControlRequest({type:"invoke-function",...e}),null));n(this,"sendCreateFileOrFolder",async e=>await this.rpc.proxy.request.bridge({functionName:"create_file_or_directory",payload:e}));n(this,"sendDeleteFileOrFolder",async e=>await this.rpc.proxy.request.bridge({functionName:"delete_file_or_directory",payload:e}));n(this,"sendRenameFileOrFolder",async e=>await this.rpc.proxy.request.bridge({functionName:"move_file_or_directory",payload:e}));n(this,"sendUpdateFile",async e=>await this.rpc.proxy.request.bridge({functionName:"update_file",payload:e}));n(this,"sendFileDetails",async e=>await this.rpc.proxy.request.bridge({functionName:"file_details",payload:e}));n(this,"exportAsHTML",async e=>await this.rpc.proxy.request.bridge({functionName:"export_html",payload:e}));n(this,"exportAsMarkdown",async e=>await this.rpc.proxy.request.bridge({functionName:"export_markdown",payload:e}));n(this,"previewDatasetColumn",async e=>(await this.putControlRequest({type:"preview-dataset-column",...e}),null));n(this,"previewSQLTable",async e=>(await this.putControlRequest({type:"preview-sql-table",...e}),null));n(this,"previewSQLTableList",async e=>(await this.putControlRequest({type:"list-sql-tables",...e}),null));n(this,"previewSQLSchemaList",async e=>(await this.putControlRequest({type:"list-sql-schemas",...e}),null));n(this,"previewDataSourceConnection",async e=>(await this.putControlRequest({type:"list-data-source-connection",...e}),null));n(this,"validateSQL",async e=>(await this.putControlRequest({type:"validate-sql",...e}),null));n(this,"sendModelValue",async e=>(await this.putControlRequest({type:"model",...e}),null));n(this,"sendDocumentTransaction",()=>Promise.resolve(null));n(this,"addPackage",async e=>this.rpc.proxy.request.addPackage(e));n(this,"removePackage",async e=>this.rpc.proxy.request.removePackage(e));n(this,"getPackageList",async()=>await this.rpc.proxy.request.listPackages());n(this,"getDependencyTree",async()=>({tree:{dependencies:[],name:"",tags:[],version:null}}));n(this,"listSecretKeys",async e=>(await this.putControlRequest({type:"list-secret-keys",...e}),null));n(this,"getUsageStats",g);n(this,"openTutorial",g);n(this,"getRecentFiles",g);n(this,"getWorkspaceFiles",g);n(this,"getRunningNotebooks",g);n(this,"shutdownSession",g);n(this,"exportAsIPYNB",g);n(this,"exportAsPDF",g);n(this,"autoExportAsHTML",g);n(this,"autoExportAsMarkdown",g);n(this,"autoExportAsIPYNB",g);n(this,"updateCellOutputs",g);n(this,"writeSecret",g);n(this,"invokeAiTool",g);n(this,"clearCache",g);n(this,"getCacheInfo",g);n(this,"listStorageEntries",g);n(this,"downloadStorage",g);ar()&&(this.rpc=Ce(new Worker(new URL(""+new URL("worker-BPV9SmHz.js",import.meta.url).href,""+import.meta.url),{type:"module",name:Oe()})),this.rpc.addMessageListener("ready",()=>{this.startSession()}),this.rpc.addMessageListener("initialized",()=>{this.saveRpc=this.getSaveWorker(),this.setInterruptBuffer(),this.initialized.resolve()}),this.rpc.addMessageListener("initializingMessage",({message:e})=>{Q.set(fe,e)}),this.rpc.addMessageListener("initializedError",({error:e})=>{this.initialized.status==="resolved"&&(p.error(e),mr({title:"Error initializing",description:e,variant:"danger"})),this.initialized.reject(Error(e))}),this.rpc.addMessageListener("kernelMessage",({message:e})=>{var r;(r=this.messageConsumer)==null||r.call(this,new MessageEvent("message",{data:e}))}))}static get INSTANCE(){let e="_marimo_private_PyodideBridge";return window[e]||(window[e]=new dt),window[e]}getSaveWorker(){return He()==="read"?(p.debug("Skipping SaveWorker in read-mode"),{readFile:g,readNotebook:g,saveNotebook:g}):Ce(new Worker(new URL(""+new URL("save-worker-CtJsIYIM.js",import.meta.url).href,""+import.meta.url),{type:"module",name:Oe()})).proxy.request}async startSession(){let e=await J.readFile(),r=await X.readFile(),s=Q.get(Jt)??Y.getFilename(),a=Q.get(nr),i={},u=new URLSearchParams(window.location.search);for(let d of u.keys()){let v=u.getAll(d);i[d]=v.length===1?v[0]:v}await this.rpc.proxy.request.startSession({queryParameters:i,code:e||r||"",filename:s,userConfig:{...a,runtime:{...a.runtime,auto_instantiate:He()==="read"?!0:a.runtime.auto_instantiate}}})}setInterruptBuffer(){crossOriginIsolated?(this.interruptBuffer=new Uint8Array(new SharedArrayBuffer(1)),this.rpc.proxy.request.setInterruptBuffer(this.interruptBuffer)):p.warn("Not running in a secure context; interrupts are not available.")}attachMessageConsumer(e){this.messageConsumer=e,this.rpc.proxy.send.consumerReady({})}async putControlRequest(e){await this.rpc.proxy.request.bridge({functionName:"put_control_request",payload:e})}},rt=function(){return ye.withProducerCallback(t=>{ge.INSTANCE.attachMessageConsumer(t)})},Xe=function(t=ce()){let e=window.fetch;return window.fetch=async(r,s)=>{let a=r instanceof Request?r.url:r.toString();if(a.startsWith("data:"))return e(r,s);try{let i=Z(a,t);if(i){let u=await(await e(i)).arrayBuffer();return new Response(u,{headers:{"Content-Type":kt(a)}})}return e(r,s)}catch(i){return p.error("Error parsing URL",i),e(r,s)}},()=>{window.fetch=e}};function kt(t){return t.endsWith(".csv")?"text/csv":t.endsWith(".json")?"application/json":t.endsWith(".txt")?"text/plain":"application/octet-stream"}Ze=function(t,e=ce()){let r=t.http.bind(t),s=t.load.bind(t);return t.http=async a=>{let i=Z(a,e);if(i)return await window.fetch(i).then(u=>u.text());try{return await r(a)}catch(u){if(a.startsWith("data:"))return await window.fetch(a).then(d=>d.text());throw u}},t.load=async a=>{let i=Z(a,e);if(i)return await window.fetch(i).then(u=>u.text());try{return await s(a)}catch(u){if(a.startsWith("data:"))return await window.fetch(a).then(d=>d.text());throw u}},()=>{t.http=r,t.load=s}};function Ee(t){return t.startsWith(".")?t.slice(1):t}function Mt(t,e=ce()){let r=Z(t,e);if(!r)return t;let s=br(r);return URL.createObjectURL(s)}function Z(t,e){let r=document.baseURI;r.startsWith("blob:")&&(r=r.replace("blob:",""));let s=new URL(t,r).pathname,a=ke(t),i=ke(s);return e[t]||e[Ee(t)]||e[s]||e[Ee(s)]||a&&e[a]||i&&e[i]}function ke(t){let e=t.indexOf("/@file/");return e===-1?null:t.slice(e)}function ie(t,e=[]){let r=[];if(t instanceof DataView)r.push(e);else if(Array.isArray(t))for(let[s,a]of t.entries())r.push(...ie(a,[...e,s]));else if(typeof t=="object"&&t)for(let[s,a]of Object.entries(t))r.push(...ie(a,[...e,s]));return r}function Pt(t){let e=ie(t);if(e.length===0)return{state:t,buffers:[],bufferPaths:[]};let r=structuredClone(t),s=[],a=[];for(let i of e){let u=er(t,i);if(u instanceof DataView){let d=ze(u);s.push(d),a.push(i),G(r,i,d)}}return{state:r,buffers:s,bufferPaths:a}}function Me(t){let{state:e,bufferPaths:r,buffers:s}=t;if(!r||r.length===0)return e;s&&sr(s.length===r.length,"Buffers and buffer paths not the same length");let a=e;for(let[i,u]of r.entries()){let d=s==null?void 0:s[i];if(d==null){p.warn("[anywidget] Could not find buffer at path",u);continue}typeof d=="string"?G(a,u,Ve(d)):G(a,u,d)}return a}const Pe={invoke:async()=>{let t="anywidget.invoke not supported in marimo. Please file an issue at https://github.com/marimo-team/marimo/issues";throw p.warn(t),Error(t)}};function Ft(t){if(typeof AbortSignal.any=="function")return AbortSignal.any(t);let e=new AbortController;for(let r of t){if(r.aborted)return e.abort(r.reason),e.signal;r.addEventListener("abort",()=>e.abort(r.reason),{once:!0})}return e.signal}var It=(Ie=class{constructor(){_(this,ee);_(this,U,new Map)}getModule(t,e){let r=o(this,U).get(e);if(r)return r;let s=A(this,ee,ct).call(this,t).catch(a=>{throw o(this,U).delete(e),a});return o(this,U).set(e,s),s}invalidate(t){p.debug(`[WidgetDefRegistry] Invalidating module cache for hash=${t}`),o(this,U).delete(t)}},U=new WeakMap,ee=new WeakSet,ct=async function(t){let e=ir(t).toString();return $e()&&(e=Mt(e)),hr(()=>import(e).then(async r=>(await r.__tla,r)),[],import.meta.url)},Ie),Lt=(Le=class{constructor(){_(this,M);_(this,T);_(this,E)}async bind(t,e){var i,u;if(o(this,E)&&o(this,T)===t)return o(this,E);o(this,E)&&o(this,T)!==t&&(p.debug("[WidgetBinding] Hot-reload detected, aborting previous binding"),(i=o(this,M))==null||i.abort(),C(this,M,void 0),C(this,E,void 0)),C(this,T,t),C(this,M,new AbortController);let r=o(this,M).signal,s=typeof t=="function"?await t():t,a=await((u=s.initialize)==null?void 0:u.call(s,{model:e,experimental:Pe}));return a&&r.addEventListener("abort",a),C(this,E,async(d,v)=>{var x;let q=await((x=s.render)==null?void 0:x.call(s,{model:e,el:d,experimental:Pe}));q&&Ft([v,r]).addEventListener("abort",()=>{let S=v.aborted?"view unmount":"binding destroyed";p.debug(`[WidgetBinding] Render cleanup triggered (reason: ${S})`),q()})}),o(this,E)}destroy(){var t;p.debug("[WidgetBinding] Destroying binding, aborting initialize lifecycle"),(t=o(this,M))==null||t.abort(),C(this,M,void 0),C(this,T,void 0),C(this,E,void 0)}},M=new WeakMap,T=new WeakMap,E=new WeakMap,Le),Nt=(Ne=class{constructor(){_(this,F,new Map)}getOrCreate(t){let e=o(this,F).get(t);return e||(e=new Lt,o(this,F).set(t,e)),e}destroy(t){let e=o(this,F).get(t);e&&(p.debug(`[BindingManager] Destroying binding for model=${t}`),e.destroy(),o(this,F).delete(t))}has(t){return o(this,F).has(t)}},F=new WeakMap,Ne);ot=new It,me=new Nt;var At=(Ae=class{constructor(t=1e4){_(this,D);_(this,P,new Map);_(this,V);C(this,V,t)}get(t){let e=A(this,D,ae).call(this,t);return e.deferred.status==="pending"&&setTimeout(()=>{e.deferred.status==="pending"&&(e.deferred.reject(Error(`Model not found for key: ${t}`)),o(this,P).delete(t))},o(this,V)),e.deferred.promise}create(t,e){let r=A(this,D,ae).call(this,t);r.deferred.resolve(e(r.controller.signal))}set(t,e){A(this,D,ae).call(this,t).deferred.resolve(e)}getSync(t){let e=o(this,P).get(t);if(e&&e.deferred.status==="resolved")return e.deferred.value}delete(t){var e;p.debug(`[ModelManager] Deleting model=${t}, aborting lifecycle signal`),(e=o(this,P).get(t))==null||e.controller.abort(),o(this,P).delete(t)}},P=new WeakMap,V=new WeakMap,D=new WeakSet,ae=function(t){let e=o(this,P).get(t);return e||(e={deferred:new Qe,controller:new AbortController},o(this,P).set(t,e)),e},Ae),Fe=Symbol("marimo");function oe(t){return t[Fe]}ne=new At;var Ut=(Ue=Fe,j=class{constructor(e,r,s){_(this,N);_(this,te,"change");_(this,I);_(this,L);_(this,H);_(this,R,{});n(this,Ue,{updateAndEmitDiffs:e=>A(this,N,ft).call(this,e),emitCustomMessage:(e,r)=>A(this,N,ht).call(this,e,r)});n(this,"widget_manager",{async get_model(e){let r=await j._modelManager.get(e);if(!r)throw Error(`Model not found with id: ${e}. This is likely because the model was not registered.`);return r}});_(this,re,tr(()=>{let e=o(this,R)[o(this,te)];if(e)for(let r of e)try{r()}catch(s){p.error("Error emitting event",s)}},0));C(this,L,e),C(this,H,r),C(this,I,new Map),s&&s.addEventListener("abort",()=>{p.debug("[Model] Signal aborted, clearing all listeners"),C(this,R,{})})}off(e,r){var s;if(!e){C(this,R,{});return}if(!r){o(this,R)[e]=new Set;return}(s=o(this,R)[e])==null||s.delete(r)}send(e,r,s){let a=(s??[]).map(i=>i instanceof ArrayBuffer?new DataView(i):new DataView(i.buffer,i.byteOffset,i.byteLength));return o(this,H).sendCustomMessage(e,a).then(()=>r==null?void 0:r())}get(e){return o(this,L)[e]}set(e,r){C(this,L,{...o(this,L),[e]:r}),o(this,I).set(e,r),A(this,N,pt).call(this,`change:${e}`,r),o(this,re).call(this)}save_changes(){if(o(this,I).size===0)return;let e=Object.fromEntries(o(this,I).entries());o(this,I).clear(),o(this,H).sendUpdate(e)}on(e,r){o(this,R)[e]||(o(this,R)[e]=new Set),o(this,R)[e].add(r)}},te=new WeakMap,I=new WeakMap,L=new WeakMap,H=new WeakMap,R=new WeakMap,N=new WeakSet,pt=function(e,r){if(!o(this,R)[e])return;let s=o(this,R)[e];for(let a of s)try{a(r)}catch(i){p.error("Error emitting event",i)}},ft=function(e){e!=null&&Object.keys(e).forEach(r=>{let s=r;o(this,L)[s]!==e[s]&&this.set(s,e[s])})},ht=function(e,r=[]){let s=o(this,R)["msg:custom"];if(s)for(let a of s)try{a(e.content,r)}catch(i){p.error("Error emitting event",i)}},re=new WeakMap,n(j,"_modelManager",ne),j);Ke=async function(t,e){let r=e.model_id,s=e.message,a=("buffers"in s?s.buffers:[]).map(Ve);switch(s.method){case"open":{let{state:i,buffer_paths:u=[]}=s,d=Me({state:i,bufferPaths:u,buffers:a}),v=t.getSync(r);if(v){oe(v).updateAndEmitDiffs(d);return}t.create(r,q=>new Ut(d,$e()?{sendUpdate:async()=>{},sendCustomMessage:async()=>{}}:{async sendUpdate(x){if(q.aborted){p.debug(`[Model] sendUpdate suppressed for model=${r} (signal aborted)`);return}let{state:S,buffers:O,bufferPaths:se}=Pt(x);await pe().sendModelValue({modelId:r,message:{method:"update",state:S,bufferPaths:se},buffers:O})},async sendCustomMessage(x,S){if(q.aborted){p.debug(`[Model] sendCustomMessage suppressed for model=${r} (signal aborted)`);return}await pe().sendModelValue({modelId:r,message:{method:"custom",content:x},buffers:S.map(ze)})}},q));return}case"custom":oe(await t.get(r)).emitCustomMessage({method:"custom",content:s.content},a);return;case"close":me.destroy(r),t.delete(r);return;case"update":{let{state:i,buffer_paths:u=[]}=s,d=Me({state:i,bufferPaths:u,buffers:a});oe(await t.get(r)).updateAndEmitDiffs(d);return}default:dr(s)}},zt(ne,"MODEL_MANAGER");function Tt(t){return typeof t!="object"||!t?!1:"type"in t&&t.type==="marimo-ui-value-update"}he=class mt{static get INSTANCE(){let e="_marimo_private_UIElementRegistry";return window[e]||(window[e]=new mt),window[e]}constructor(){this.entries=new Map}has(e){return this.entries.has(e)}set(e,r){this.entries.has(e)&&p.debug("UIElementRegistry overwriting entry for objectId.",e),this.entries.set(e,{objectId:e,value:r,elements:new Set})}registerInstance(e,r){let s=this.entries.get(e);s===void 0?this.entries.set(e,{objectId:e,value:Xt(r,this),elements:new Set([r])}):s.elements.add(r)}removeInstance(e,r){let s=this.entries.get(e);s!=null&&s.elements.has(r)&&s.elements.delete(r)}removeElementsByCell(e){[...this.entries.keys()].filter(r=>r.startsWith(`${e}-`)).forEach(r=>{this.entries.delete(r)})}lookupValue(e){let r=this.entries.get(e);return r===void 0?void 0:r.value}broadcastMessage(e,r,s){let a=this.entries.get(e);if(a===void 0){p.warn("UIElementRegistry missing entry",e);return}if(Tt(r)){a.value=r.value,a.elements.forEach(i=>{i.dispatchEvent(Ge.create({bubbles:!1,composed:!0,detail:{value:r.value,element:i}}))});return}a.elements.forEach(i=>{i.dispatchEvent(_r.create({bubbles:!1,composed:!0,detail:{objectId:e,message:r,buffers:s}}))})}broadcastValueUpdate(e,r,s){let a=this.entries.get(r);a===void 0?p.warn("UIElementRegistry missing entry",r):(a.value=s,a.elements.forEach(i=>{i!==e&&i.dispatchEvent(Ge.create({bubbles:!1,composed:!0,detail:{value:s,element:i}}))}),document.dispatchEvent(vr.create({bubbles:!0,composed:!0,detail:{objectId:r}})))}},nt=he.INSTANCE,tt=new fr("function-call-result",async(t,e)=>{await pe().sendFunctionRequest({functionCallId:t,...e})}),it="68px";var jt="288px";st=t=>t?/^\d+$/.test(t)?`${t}px`:t:jt,at=Gt({isOpen:!0},(t,e)=>{if(!e)return t;switch(e.type){case"toggle":return{...t,isOpen:e.isOpen??t.isOpen};case"setWidth":return{...t,width:e.width};default:return t}})});export{G as S,fe as _,Cr as __tla,he as a,Je as b,Ke as c,Xe as d,Ze as f,et as g,J as h,tt as i,me as l,rt as m,st as n,nt as o,ge as p,at as r,ne as s,it as t,ot as u,ye as v,lt as x,ut as y};