@marimo-team/islands 0.23.3-dev9 → 0.23.4-dev0

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 (42) hide show
  1. package/dist/{chat-ui-BLFhPclV.js → chat-ui-DEd_Ndal.js} +82 -82
  2. package/dist/{html-to-image-XYwXqg2E.js → html-to-image-DBosi5GK.js} +2240 -2214
  3. package/dist/main.js +2627 -2746
  4. package/dist/{process-output-BDVjDpbu.js → process-output-k-4WHpxz.js} +1 -1
  5. package/dist/{reveal-component-CrnLosc4.js → reveal-component-CFuofbBD.js} +827 -561
  6. package/dist/{slide-Dl7Rf496.js → slide-form-DgMI37ES.js} +1729 -894
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/components/editor/file-tree/renderers.tsx +1 -1
  10. package/src/components/editor/output/JsonOutput.tsx +187 -4
  11. package/src/components/editor/output/__tests__/JsonOutput-mimetype.test.tsx +80 -0
  12. package/src/components/editor/output/__tests__/json-output.test.ts +185 -2
  13. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +150 -0
  14. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +298 -0
  15. package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +50 -0
  16. package/src/components/editor/renderers/slides-layout/plugin.tsx +54 -9
  17. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +30 -12
  18. package/src/components/editor/renderers/slides-layout/types.ts +31 -3
  19. package/src/components/editor/renderers/types.ts +2 -0
  20. package/src/components/slides/__tests__/compose-slides.test.ts +433 -0
  21. package/src/components/slides/compose-slides.ts +337 -0
  22. package/src/components/slides/minimap.tsx +133 -12
  23. package/src/components/slides/reveal-component.tsx +337 -74
  24. package/src/components/slides/reveal-slides.css +33 -1
  25. package/src/components/slides/slide-form.tsx +347 -0
  26. package/src/components/ui/radio-group.tsx +5 -3
  27. package/src/core/cells/types.ts +2 -0
  28. package/src/core/islands/__tests__/bridge.test.ts +116 -5
  29. package/src/core/islands/bridge.ts +5 -1
  30. package/src/core/layout/layout.ts +6 -2
  31. package/src/core/static/__tests__/export-context.test.ts +122 -0
  32. package/src/core/static/__tests__/static-state.test.ts +80 -0
  33. package/src/core/static/export-context.ts +84 -0
  34. package/src/core/static/static-state.ts +44 -6
  35. package/src/plugins/core/RenderHTML.tsx +23 -2
  36. package/src/plugins/core/__test__/RenderHTML.test.ts +86 -1
  37. package/src/plugins/core/__test__/trusted-url.test.ts +130 -18
  38. package/src/plugins/core/sanitize.ts +11 -5
  39. package/src/plugins/core/trusted-url.ts +32 -10
  40. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +29 -1
  41. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +34 -0
  42. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +35 -2
@@ -3,25 +3,31 @@ import { atom, useAtomValue } from "jotai";
3
3
  import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
4
4
  import { autoInstantiateAtom } from "@/core/config/config";
5
5
  import { getInitialAppMode } from "@/core/mode";
6
+ import { hasTrustedExportContext } from "@/core/static/export-context";
6
7
 
7
8
  // Re-export so existing consumers don't break.
8
9
  export { sanitizeHtml } from "./sanitize-html";
9
10
 
10
11
  /**
11
- * Whether to sanitize the html.
12
- * When running as an app or with auto_instantiate enabled
13
- * we ignore sanitization because they should be treated as a website.
12
+ * Whether to sanitize the html. Trust signals match
13
+ * `hasTrustedNotebookContext` in `@/core/static/export-context`.
14
+ * Kept as a jotai atom so consumers re-render when `hasRunAnyCellAtom`
15
+ * flips after the user runs a cell.
14
16
  */
15
17
  const sanitizeHtmlAtom = atom<boolean>((get) => {
16
18
  const hasRunAnyCell = get(hasRunAnyCellAtom);
17
19
  const autoInstantiate = get(autoInstantiateAtom);
18
20
 
19
- // If a user has specifically run at least one cell or auto_instantiate is enabled, we don't need to sanitize.
20
- // HTML needs to be rich to allow for interactive widgets and other dynamic content.
21
21
  if (hasRunAnyCell || autoInstantiate) {
22
22
  return false;
23
23
  }
24
24
 
25
+ // Trusted export context is installed once at page load by a first-party
26
+ // script (frozen + non-configurable), so a direct read is safe and stable.
27
+ if (hasTrustedExportContext()) {
28
+ return false;
29
+ }
30
+
25
31
  let isInAppMode = true;
26
32
  try {
27
33
  isInAppMode = getInitialAppMode() === "read";
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
4
+ import { hasTrustedExportContext } from "@/core/static/export-context";
4
5
  import { store } from "@/core/state/jotai";
5
6
 
6
7
  /**
@@ -15,13 +16,13 @@ import { store } from "@/core/state/jotai";
15
16
  * attacker-controlled JavaScript at same origin, since the HTML sanitizer
16
17
  * lets arbitrary marimo custom elements and attributes through.
17
18
  *
18
- * Some runtimes (WASM, VS Code) have no backend to serve virtual files, so
19
- * `VirtualFile` falls back to inline base64 data URLs (see `virtual_file.py`).
19
+ * Some runtimes (WASM, VS Code, and trusted exported notebook contexts such as
20
+ * Quarto islands) have no backend to serve virtual files, so `VirtualFile`
21
+ * falls back to inline base64 data URLs (see `virtual_file.py`).
20
22
  * We accept those only once the user has explicitly run a cell in the current
21
- * notebook the same trust signal `sanitize.ts` uses to lift HTML
22
- * sanitization. Running a cell requires deliberate user action and already
23
- * executes arbitrary Python, so a data URL script loaded afterwards is not a
24
- * new attack surface.
23
+ * notebook, or when a first-party export script has installed a trusted
24
+ * notebook export context. Both cases already imply trust in notebook-authored
25
+ * code, so loading the matching data URL is not a new attack surface.
25
26
  */
26
27
  export function isTrustedVirtualFileUrl(url: unknown): url is string {
27
28
  if (typeof url !== "string" || url.length === 0) {
@@ -30,16 +31,37 @@ export function isTrustedVirtualFileUrl(url: unknown): url is string {
30
31
  if (/^(\.?\/)?@file\/[^?#]+$/.test(url)) {
31
32
  return true;
32
33
  }
33
- if (isSafeDataUrl(url) && store.get(hasRunAnyCellAtom)) {
34
+ if (isSafeDataUrl(url)) {
34
35
  return true;
35
36
  }
36
37
  return false;
37
38
  }
38
39
 
40
+ /**
41
+ * Intentionally narrower than `hasTrustedNotebookContext` in
42
+ * `@/core/static/export-context`: `auto_instantiate` and `read` mode are
43
+ * deliberately excluded here. Both can be triggered by DOM-observable page
44
+ * shape, and accepting inline base64 `data:` JS/CSS payloads on their
45
+ * strength would let a hostile notebook page smuggle attacker-controlled
46
+ * script into the same origin. Keep this gate tied only to "user actively
47
+ * ran a cell" or "first-party exporter installed a trusted runtime marker".
48
+ */
49
+ function hasNotebookTrustedDataUrlContext(): boolean {
50
+ return store.get(hasRunAnyCellAtom) || hasTrustedExportContext();
51
+ }
52
+
53
+ /**
54
+ * Safe data URL formats: JS/CSS inlined as base64. Non-base64 data URLs and
55
+ * other MIME types (HTML, SVG, octet-stream, etc.) are refused because they
56
+ * broaden the surface for attacker-controlled inline content.
57
+ */
39
58
  function isSafeDataUrl(url: string): boolean {
40
- return (
59
+ const isSafeKind =
41
60
  url.startsWith("data:text/javascript;base64,") ||
42
61
  url.startsWith("data:application/javascript;base64,") ||
43
- url.startsWith("data:text/css;base64,")
44
- );
62
+ url.startsWith("data:text/css;base64,");
63
+ if (!isSafeKind) {
64
+ return false;
65
+ }
66
+ return hasNotebookTrustedDataUrlContext();
45
67
  }
@@ -1,5 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { ExtractAtomValue } from "jotai";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
5
+ import { userConfigAtom } from "@/core/config/config";
6
+ import { parseUserConfig } from "@/core/config/config-schema";
7
+ import { initialModeAtom } from "@/core/mode";
8
+ import { store } from "@/core/state/jotai";
3
9
  import { Model } from "../model";
4
10
  import type { ModelState, WidgetModelId } from "../types";
5
11
  import { visibleForTesting } from "../widget-binding";
@@ -18,9 +24,31 @@ function createMockComm() {
18
24
 
19
25
  describe("WidgetDefRegistry", () => {
20
26
  let registry: InstanceType<typeof WidgetDefRegistry>;
27
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
28
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
29
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
21
30
 
22
31
  beforeEach(() => {
23
32
  registry = new WidgetDefRegistry();
33
+ // Force "no notebook trust" so the `data:` rejection test below
34
+ // exercises the untrusted branch. The positive trust path is covered
35
+ // centrally in trusted-url.test.ts.
36
+ previousConfig = store.get(userConfigAtom);
37
+ previousMode = store.get(initialModeAtom);
38
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
39
+ store.set(hasRunAnyCellAtom, false);
40
+ const cleared = parseUserConfig({});
41
+ store.set(userConfigAtom, {
42
+ ...cleared,
43
+ runtime: { ...cleared.runtime, auto_instantiate: false },
44
+ });
45
+ store.set(initialModeAtom, "edit");
46
+ });
47
+
48
+ afterEach(() => {
49
+ store.set(userConfigAtom, previousConfig);
50
+ store.set(initialModeAtom, previousMode);
51
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
24
52
  });
25
53
 
26
54
  it("should cache modules by jsHash and return same promise", () => {
@@ -1,12 +1,41 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { ExtractAtomValue } from "jotai";
2
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
5
+ import { userConfigAtom } from "@/core/config/config";
6
+ import { parseUserConfig } from "@/core/config/config-schema";
7
+ import { initialModeAtom } from "@/core/mode";
8
+ import { store } from "@/core/state/jotai";
3
9
  import { Logger } from "@/utils/Logger";
4
10
  import { visibleForTesting } from "../MplInteractivePlugin";
5
11
 
6
12
  const { ensureMplJs, injectCss, resetMplJsLoading } = visibleForTesting;
7
13
 
14
+ /**
15
+ * Clear every "notebook trust" signal `isTrustedVirtualFileUrl` consults so
16
+ * the rejection cases below test the actually-untrusted branch. Positive
17
+ * export-context trust is covered centrally in trusted-url.test.ts.
18
+ */
19
+ function clearTrustSignals() {
20
+ store.set(hasRunAnyCellAtom, false);
21
+ const cleared = parseUserConfig({});
22
+ store.set(userConfigAtom, {
23
+ ...cleared,
24
+ runtime: { ...cleared.runtime, auto_instantiate: false },
25
+ });
26
+ store.set(initialModeAtom, "edit");
27
+ }
28
+
8
29
  describe("MplInteractivePlugin URL validation", () => {
30
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
31
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
32
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
33
+
9
34
  beforeEach(() => {
35
+ previousConfig = store.get(userConfigAtom);
36
+ previousMode = store.get(initialModeAtom);
37
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
38
+ clearTrustSignals();
10
39
  // Reset module-level script-loading state and any stubs.
11
40
  delete (window as { mpl?: unknown }).mpl;
12
41
  resetMplJsLoading();
@@ -20,6 +49,9 @@ describe("MplInteractivePlugin URL validation", () => {
20
49
 
21
50
  afterEach(() => {
22
51
  vi.restoreAllMocks();
52
+ store.set(userConfigAtom, previousConfig);
53
+ store.set(initialModeAtom, previousMode);
54
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
23
55
  });
24
56
 
25
57
  describe("ensureMplJs", () => {
@@ -35,6 +67,8 @@ describe("MplInteractivePlugin URL validation", () => {
35
67
  "https://evil.example.com/x.js",
36
68
  "//evil.example.com/x.js",
37
69
  "javascript:alert(1)",
70
+ // Data URL is rejected only in an untrusted context. WASM/autoInstantiate
71
+ // intentionally accepts it — covered by trusted-url.test.ts.
38
72
  "data:text/javascript;base64,YWxlcnQoMSk=",
39
73
  "./@file/x.js?redirect=http://evil.com",
40
74
  ])("rejects %s", async (url) => {
@@ -1,10 +1,39 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { ExtractAtomValue } from "jotai";
2
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
5
+ import { userConfigAtom } from "@/core/config/config";
6
+ import { parseUserConfig } from "@/core/config/config-schema";
7
+ import { initialModeAtom } from "@/core/mode";
8
+ import { store } from "@/core/state/jotai";
3
9
  import { Logger } from "@/utils/Logger";
4
10
  import { loadPanelExtension } from "../PanelPlugin";
5
11
 
12
+ /**
13
+ * Force the "no notebook trust" branch so the `data:` URL rejection below
14
+ * actually exercises the untrusted path. Positive export-context trust is
15
+ * covered centrally in trusted-url.test.ts.
16
+ */
17
+ function clearTrustSignals() {
18
+ store.set(hasRunAnyCellAtom, false);
19
+ const cleared = parseUserConfig({});
20
+ store.set(userConfigAtom, {
21
+ ...cleared,
22
+ runtime: { ...cleared.runtime, auto_instantiate: false },
23
+ });
24
+ store.set(initialModeAtom, "edit");
25
+ }
26
+
6
27
  describe("loadPanelExtension", () => {
28
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
29
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
30
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
31
+
7
32
  beforeEach(() => {
33
+ previousConfig = store.get(userConfigAtom);
34
+ previousMode = store.get(initialModeAtom);
35
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
36
+ clearTrustSignals();
8
37
  for (const el of document.head.querySelectorAll("script")) {
9
38
  el.remove();
10
39
  }
@@ -12,6 +41,9 @@ describe("loadPanelExtension", () => {
12
41
 
13
42
  afterEach(() => {
14
43
  vi.restoreAllMocks();
44
+ store.set(userConfigAtom, previousConfig);
45
+ store.set(initialModeAtom, previousMode);
46
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
15
47
  });
16
48
 
17
49
  it("does nothing and returns false for null URL", () => {
@@ -35,8 +67,9 @@ describe("loadPanelExtension", () => {
35
67
  it.each([
36
68
  "https://evil.example.com/x.js",
37
69
  "//evil.example.com/x.js",
38
- // An attacker embedding inline JS as a data URLwhat the old plugin
39
- // would have executed verbatim via script.innerHTML.
70
+ // Data URL is rejected only in an untrusted context — the WASM fallback
71
+ // legitimately produces these, so trusted-url.test.ts covers the
72
+ // positive path when a notebook trust signal is set.
40
73
  "data:text/javascript;base64,YWxlcnQoMSk=",
41
74
  "javascript:alert(1)",
42
75
  "./@file/x.js#http://evil.com",