@marimo-team/frontend 0.23.3-dev39 → 0.23.3-dev41

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 (41) hide show
  1. package/dist/assets/JsonOutput-zJfSu-Vk.js +49 -0
  2. package/dist/assets/{MarimoErrorOutput-BWFFx2VQ.js → MarimoErrorOutput-BGCTswmw.js} +1 -1
  3. package/dist/assets/RenderHTML-DT8P6T1l.js +1 -0
  4. package/dist/assets/{add-connection-dialog-C1aIq9FP.js → add-connection-dialog-DChyqh-3.js} +1 -1
  5. package/dist/assets/{agent-panel-BF6fYTEU.js → agent-panel-CcLuIYse.js} +1 -1
  6. package/dist/assets/{cell-editor-DL4womh8.js → cell-editor-BqklzNDV.js} +1 -1
  7. package/dist/assets/{chat-display-DyTcEZFU.js → chat-display-7P_28VVE.js} +1 -1
  8. package/dist/assets/{chat-panel-DVFjhDIb.js → chat-panel-BQp1b73y.js} +1 -1
  9. package/dist/assets/{chat-ui-CyIhz4-T.js → chat-ui-BNeL7YZB.js} +1 -1
  10. package/dist/assets/{column-preview-rQE4qc5R.js → column-preview-C2_n8Cec.js} +1 -1
  11. package/dist/assets/{command-palette-Cd2q-HUO.js → command-palette-Ck1uQB30.js} +1 -1
  12. package/dist/assets/{documentation-panel-CZt-M5JN.js → documentation-panel-BJnrDC1G.js} +1 -1
  13. package/dist/assets/{edit-page-BQ7AdKwV.js → edit-page-BUArB_ce.js} +3 -3
  14. package/dist/assets/{error-panel-BN05vshu.js → error-panel-C3s8IlRa.js} +1 -1
  15. package/dist/assets/{file-explorer-panel-Ci0Cz4JU.js → file-explorer-panel-Dk5S33G1.js} +1 -1
  16. package/dist/assets/{form-BON4sAdY.js → form-kVlV8UJh.js} +1 -1
  17. package/dist/assets/{hooks-CLXoEljg.js → hooks-CVOMPk9n.js} +1 -1
  18. package/dist/assets/{index-Flgwmpg_.js → index-AQQQ__hU.js} +4 -4
  19. package/dist/assets/{layout-B31_eP6P.js → layout-BZFYst8S.js} +3 -3
  20. package/dist/assets/{panels-CfgYkAvQ.js → panels-Dp6Q1Mvz.js} +1 -1
  21. package/dist/assets/{reveal-component-Bt3Epro6.js → reveal-component-DWnqsE1_.js} +1 -1
  22. package/dist/assets/{run-page-4lA-192f.js → run-page-DiTeOCSw.js} +1 -1
  23. package/dist/assets/{scratchpad-panel-CiXkVXhL.js → scratchpad-panel-DDqb_fyf.js} +1 -1
  24. package/dist/assets/{session-panel-oGdw6hvF.js → session-panel-NjZNmi9U.js} +1 -1
  25. package/dist/assets/{slide-C5wPx0qL.js → slide-C5Pp2eO8.js} +1 -1
  26. package/dist/assets/{snippets-panel-C1g0FaLN.js → snippets-panel-BYaK2OYv.js} +1 -1
  27. package/dist/assets/state-BHl5TPGk.js +3 -0
  28. package/dist/assets/{useNotebookActions-C-Vve9tT.js → useNotebookActions-Cg9IU5j7.js} +1 -1
  29. package/dist/index.html +12 -8
  30. package/package.json +1 -1
  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 +41 -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/sanitize.ts +11 -5
  38. package/src/plugins/core/trusted-url.ts +9 -0
  39. package/dist/assets/JsonOutput-BLiYwyAC.js +0 -49
  40. package/dist/assets/RenderHTML-DweIff7M.js +0 -1
  41. package/dist/assets/state-DpMLCkRP.js +0 -3
@@ -0,0 +1,122 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import type { ExtractAtomValue } from "jotai";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
7
+ import { userConfigAtom } from "@/core/config/config";
8
+ import { parseUserConfig } from "@/core/config/config-schema";
9
+ import { initialModeAtom } from "@/core/mode";
10
+ import { store } from "@/core/state/jotai";
11
+ import {
12
+ getMarimoExportContext,
13
+ hasTrustedExportContext,
14
+ hasTrustedNotebookContext,
15
+ } from "../export-context";
16
+
17
+ type ExportContextWindow = Window & {
18
+ __MARIMO_EXPORT_CONTEXT__?: {
19
+ trusted: boolean;
20
+ notebookCode?: string;
21
+ };
22
+ };
23
+
24
+ function setAutoInstantiate(value: boolean) {
25
+ const cleared = parseUserConfig({});
26
+ store.set(userConfigAtom, {
27
+ ...cleared,
28
+ runtime: { ...cleared.runtime, auto_instantiate: value },
29
+ });
30
+ }
31
+
32
+ describe("hasTrustedNotebookContext", () => {
33
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
34
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
35
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
36
+ let w: ExportContextWindow;
37
+
38
+ beforeEach(() => {
39
+ w = window as ExportContextWindow;
40
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
41
+ previousConfig = store.get(userConfigAtom);
42
+ previousMode = store.get(initialModeAtom);
43
+ store.set(hasRunAnyCellAtom, false);
44
+ setAutoInstantiate(false);
45
+ store.set(initialModeAtom, "edit");
46
+ delete w.__MARIMO_EXPORT_CONTEXT__;
47
+ });
48
+
49
+ afterEach(() => {
50
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
51
+ store.set(userConfigAtom, previousConfig);
52
+ store.set(initialModeAtom, previousMode);
53
+ delete w.__MARIMO_EXPORT_CONTEXT__;
54
+ });
55
+
56
+ it("returns false in untrusted edit mode before interaction", () => {
57
+ expect(hasTrustedNotebookContext()).toBe(false);
58
+ });
59
+
60
+ it("returns true once the user has run a cell", () => {
61
+ store.set(hasRunAnyCellAtom, true);
62
+ expect(hasTrustedNotebookContext()).toBe(true);
63
+ });
64
+
65
+ it("returns true when a trusted export context is installed", () => {
66
+ w.__MARIMO_EXPORT_CONTEXT__ = { trusted: true };
67
+ expect(hasTrustedNotebookContext()).toBe(true);
68
+ });
69
+
70
+ it("returns true when auto_instantiate is enabled", () => {
71
+ setAutoInstantiate(true);
72
+ expect(hasTrustedNotebookContext()).toBe(true);
73
+ });
74
+
75
+ it("returns true in read mode", () => {
76
+ store.set(initialModeAtom, "read");
77
+ expect(hasTrustedNotebookContext()).toBe(true);
78
+ });
79
+
80
+ it("returns false if initialMode throws (config not yet applied)", () => {
81
+ store.set(initialModeAtom, undefined);
82
+ expect(hasTrustedNotebookContext()).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe("hasTrustedExportContext / getMarimoExportContext shape validation", () => {
87
+ let w: ExportContextWindow;
88
+
89
+ beforeEach(() => {
90
+ w = window as ExportContextWindow;
91
+ delete w.__MARIMO_EXPORT_CONTEXT__;
92
+ });
93
+
94
+ afterEach(() => {
95
+ delete w.__MARIMO_EXPORT_CONTEXT__;
96
+ });
97
+
98
+ it("accepts a valid context", () => {
99
+ w.__MARIMO_EXPORT_CONTEXT__ = { trusted: true, notebookCode: "x = 1" };
100
+ expect(hasTrustedExportContext()).toBe(true);
101
+ expect(getMarimoExportContext()).toEqual({
102
+ trusted: true,
103
+ notebookCode: "x = 1",
104
+ });
105
+ });
106
+
107
+ it("rejects a context where `trusted` is not exactly true", () => {
108
+ w.__MARIMO_EXPORT_CONTEXT__ = {
109
+ trusted: "yes" as unknown as true,
110
+ };
111
+ expect(hasTrustedExportContext()).toBe(false);
112
+ expect(getMarimoExportContext()).toBeUndefined();
113
+ });
114
+
115
+ it("rejects a context with non-string notebookCode", () => {
116
+ w.__MARIMO_EXPORT_CONTEXT__ = {
117
+ trusted: true,
118
+ notebookCode: 42 as unknown as string,
119
+ };
120
+ expect(getMarimoExportContext()).toBeUndefined();
121
+ });
122
+ });
@@ -0,0 +1,80 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import {
6
+ getStaticModelNotifications,
7
+ getStaticVirtualFiles,
8
+ isStaticNotebook,
9
+ } from "../static-state";
10
+
11
+ function setMarimoStatic(value: unknown): void {
12
+ (window as unknown as { __MARIMO_STATIC__?: unknown }).__MARIMO_STATIC__ =
13
+ value;
14
+ }
15
+
16
+ function clearMarimoStatic(): void {
17
+ delete (window as unknown as { __MARIMO_STATIC__?: unknown })
18
+ .__MARIMO_STATIC__;
19
+ }
20
+
21
+ describe("static-state shape validation", () => {
22
+ beforeEach(() => {
23
+ clearMarimoStatic();
24
+ });
25
+
26
+ afterEach(() => {
27
+ clearMarimoStatic();
28
+ });
29
+
30
+ it("treats an absent global as not-a-static-notebook", () => {
31
+ expect(isStaticNotebook()).toBe(false);
32
+ expect(getStaticModelNotifications()).toBeUndefined();
33
+ });
34
+
35
+ it("accepts a well-formed state", () => {
36
+ setMarimoStatic({
37
+ files: { "/@file/a.txt": "data:text/plain;base64,YQ==" },
38
+ modelNotifications: [],
39
+ });
40
+ expect(isStaticNotebook()).toBe(true);
41
+ expect(getStaticVirtualFiles()).toEqual({
42
+ "/@file/a.txt": "data:text/plain;base64,YQ==",
43
+ });
44
+ expect(getStaticModelNotifications()).toEqual([]);
45
+ });
46
+
47
+ it("rejects a malformed global (missing files)", () => {
48
+ setMarimoStatic({ modelNotifications: [] });
49
+ expect(isStaticNotebook()).toBe(false);
50
+ expect(getStaticModelNotifications()).toBeUndefined();
51
+ });
52
+
53
+ it("rejects a malformed global (non-array modelNotifications)", () => {
54
+ setMarimoStatic({ files: {}, modelNotifications: "oops" });
55
+ expect(isStaticNotebook()).toBe(false);
56
+ });
57
+
58
+ it("rejects a non-object global", () => {
59
+ setMarimoStatic("pwned");
60
+ expect(isStaticNotebook()).toBe(false);
61
+ });
62
+
63
+ it("rejects an array as the state", () => {
64
+ setMarimoStatic([]);
65
+ expect(isStaticNotebook()).toBe(false);
66
+ });
67
+
68
+ it("rejects files when it is an array", () => {
69
+ setMarimoStatic({ files: [], modelNotifications: [] });
70
+ expect(isStaticNotebook()).toBe(false);
71
+ });
72
+
73
+ it("rejects files that contain non-string values", () => {
74
+ setMarimoStatic({
75
+ files: { "/@file/a.txt": 42 },
76
+ modelNotifications: [],
77
+ });
78
+ expect(isStaticNotebook()).toBe(false);
79
+ });
80
+ });
@@ -1,5 +1,10 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
4
+ import { autoInstantiateAtom } from "@/core/config/config";
5
+ import { getInitialAppMode } from "@/core/mode";
6
+ import { store } from "@/core/state/jotai";
7
+
3
8
  export interface MarimoExportContext {
4
9
  trusted: true;
5
10
  notebookCode?: string;
@@ -41,3 +46,39 @@ export function getMarimoExportContext():
41
46
  export function hasTrustedExportContext(): boolean {
42
47
  return getMarimoExportContext()?.trusted === true;
43
48
  }
49
+
50
+ /**
51
+ * True when the current page is a context where notebook-authored script
52
+ * execution is expected, and therefore the user has consented (explicitly or
53
+ * by the nature of the page) to running arbitrary notebook content:
54
+ *
55
+ * - the user has run at least one cell, OR
56
+ * - a first-party exported notebook page installed a trusted export context
57
+ * (islands / static exports / Quarto islands), OR
58
+ * - `auto_instantiate` is enabled (the notebook runs on page load by user
59
+ * configuration), OR
60
+ * - the page was loaded in `read` / app mode (served by marimo as an app).
61
+ *
62
+ * Edit mode before any user interaction is intentionally NOT trusted — that
63
+ * is the only surface where we must prevent notebook-authored content from
64
+ * loading scripts or bypassing HTML sanitization.
65
+ */
66
+ export function hasTrustedNotebookContext(): boolean {
67
+ if (store.get(hasRunAnyCellAtom)) {
68
+ return true;
69
+ }
70
+ if (hasTrustedExportContext()) {
71
+ return true;
72
+ }
73
+ if (store.get(autoInstantiateAtom)) {
74
+ return true;
75
+ }
76
+ try {
77
+ if (getInitialAppMode() === "read") {
78
+ return true;
79
+ }
80
+ } catch {
81
+ // getInitialAppMode throws before mount config is applied; treat as untrusted.
82
+ }
83
+ return false;
84
+ }
@@ -5,20 +5,58 @@ import type { MarimoStaticState, StaticVirtualFiles } from "./types";
5
5
 
6
6
  declare global {
7
7
  interface Window {
8
- __MARIMO_STATIC__?: MarimoStaticState;
8
+ __MARIMO_STATIC__?: Readonly<MarimoStaticState>;
9
9
  }
10
10
  }
11
11
 
12
+ function isStringToStringRecord(
13
+ value: unknown,
14
+ ): value is Record<string, string> {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
16
+ return false;
17
+ }
18
+ for (const entry of Object.values(value)) {
19
+ if (typeof entry !== "string") {
20
+ return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+
26
+ function isMarimoStaticState(
27
+ value: unknown,
28
+ ): value is Readonly<MarimoStaticState> {
29
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
30
+ return false;
31
+ }
32
+ const candidate = value as MarimoStaticState;
33
+ if (!isStringToStringRecord(candidate.files)) {
34
+ return false;
35
+ }
36
+ if (
37
+ candidate.modelNotifications !== undefined &&
38
+ !Array.isArray(candidate.modelNotifications)
39
+ ) {
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ function getMarimoStaticState(): Readonly<MarimoStaticState> | undefined {
46
+ const state = window?.__MARIMO_STATIC__;
47
+ return isMarimoStaticState(state) ? state : undefined;
48
+ }
49
+
12
50
  export function isStaticNotebook(): boolean {
13
- return window?.__MARIMO_STATIC__ !== undefined;
51
+ return getMarimoStaticState() !== undefined;
14
52
  }
15
53
 
16
54
  export function getStaticVirtualFiles(): StaticVirtualFiles {
17
- invariant(window.__MARIMO_STATIC__ !== undefined, "Not a static notebook");
18
-
19
- return window.__MARIMO_STATIC__.files;
55
+ const state = getMarimoStaticState();
56
+ invariant(state !== undefined, "Not a static notebook");
57
+ return state.files;
20
58
  }
21
59
 
22
60
  export function getStaticModelNotifications(): ModelLifecycle[] | undefined {
23
- return window?.__MARIMO_STATIC__?.modelNotifications;
61
+ return getMarimoStaticState()?.modelNotifications;
24
62
  }
@@ -16,6 +16,8 @@ import { CopyClipboardIcon } from "@/components/icons/copy-icon";
16
16
  import { QueryParamPreservingLink } from "@/components/ui/query-param-preserving-link";
17
17
  import { Tooltip } from "@/components/ui/tooltip";
18
18
  import { DocHoverTarget } from "@/core/documentation/DocHoverTarget";
19
+ import { hasTrustedNotebookContext } from "@/core/static/export-context";
20
+ import { Logger } from "@/utils/Logger";
19
21
  import { sanitizeHtml, useSanitizeHtml } from "./sanitize";
20
22
 
21
23
  type ReplacementFn = NonNullable<HTMLReactParserOptions["replace"]>;
@@ -98,8 +100,27 @@ const replaceSrcScripts = (domNode: DOMNode): JSX.Element | undefined => {
98
100
  if (!src) {
99
101
  return;
100
102
  }
101
- // Check if script already exists
102
- if (!document.querySelector(`script[src="${src}"]`)) {
103
+ // Only append notebook-authored scripts when the page is a trusted
104
+ // context (the user has run a cell, the page is a trusted export, or
105
+ // we're running in read/app mode). In untrusted edit mode before any
106
+ // user interaction, drop the script and log a warning. Outer
107
+ // sanitization will normally strip <script> tags already; this is
108
+ // defense-in-depth for flows that reparse children with
109
+ // alwaysSanitizeHtml: false (see registerReactComponent.getChildren).
110
+ if (!hasTrustedNotebookContext()) {
111
+ Logger.warn(
112
+ `[RenderHTML] refusing <script src> in untrusted context: ${src}`,
113
+ );
114
+ // oxlint-disable-next-line react/jsx-no-useless-fragment
115
+ return <></>;
116
+ }
117
+ // Check if script already exists. Avoid building a CSS selector from
118
+ // notebook-provided input, which can throw for valid URLs containing
119
+ // selector-significant characters (e.g. IPv6 hosts with `[`/`]`).
120
+ const scriptExists = [...document.querySelectorAll("script[src]")].some(
121
+ (existingScript) => existingScript.getAttribute("src") === src,
122
+ );
123
+ if (!scriptExists) {
103
124
  const script = document.createElement("script");
104
125
  script.src = src;
105
126
  document.head.append(script);
@@ -1,5 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { describe, expect, test } from "vitest";
2
+ import type { ExtractAtomValue } from "jotai";
3
+ import { afterEach, beforeEach, describe, expect, test } 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 { visibleForTesting } from "../RenderHTML";
4
10
 
5
11
  const { parseHtml } = visibleForTesting;
@@ -197,6 +203,85 @@ describe("parseHtml", () => {
197
203
  });
198
204
  });
199
205
 
206
+ describe("replaceSrcScripts trust gate", () => {
207
+ let previousHasRunAnyCell: ExtractAtomValue<typeof hasRunAnyCellAtom>;
208
+ let previousConfig: ExtractAtomValue<typeof userConfigAtom>;
209
+ let previousMode: ExtractAtomValue<typeof initialModeAtom>;
210
+ const windowWithExport = window as Window & {
211
+ __MARIMO_EXPORT_CONTEXT__?: unknown;
212
+ };
213
+
214
+ function clearTrustSignals() {
215
+ const cleared = parseUserConfig({});
216
+ store.set(hasRunAnyCellAtom, false);
217
+ store.set(userConfigAtom, {
218
+ ...cleared,
219
+ runtime: { ...cleared.runtime, auto_instantiate: false },
220
+ });
221
+ store.set(initialModeAtom, "edit");
222
+ delete windowWithExport.__MARIMO_EXPORT_CONTEXT__;
223
+ }
224
+
225
+ beforeEach(() => {
226
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
227
+ previousConfig = store.get(userConfigAtom);
228
+ previousMode = store.get(initialModeAtom);
229
+ clearTrustSignals();
230
+ for (const s of document.head.querySelectorAll(
231
+ 'script[src^="https://cdn.example.com/"]',
232
+ )) {
233
+ s.remove();
234
+ }
235
+ });
236
+
237
+ afterEach(() => {
238
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
239
+ store.set(userConfigAtom, previousConfig);
240
+ store.set(initialModeAtom, previousMode);
241
+ delete windowWithExport.__MARIMO_EXPORT_CONTEXT__;
242
+ for (const s of document.head.querySelectorAll(
243
+ 'script[src^="https://cdn.example.com/"]',
244
+ )) {
245
+ s.remove();
246
+ }
247
+ });
248
+
249
+ test("drops <script src> in untrusted edit mode", () => {
250
+ parseHtml({
251
+ html: '<script src="https://cdn.example.com/unrun.js"></script>',
252
+ });
253
+ expect(
254
+ document.head.querySelector(
255
+ 'script[src="https://cdn.example.com/unrun.js"]',
256
+ ),
257
+ ).toBeNull();
258
+ });
259
+
260
+ test("loads <script src> once a cell has been run", () => {
261
+ store.set(hasRunAnyCellAtom, true);
262
+ parseHtml({
263
+ html: '<script src="https://cdn.example.com/ok.js"></script>',
264
+ });
265
+ expect(
266
+ document.head.querySelector(
267
+ 'script[src="https://cdn.example.com/ok.js"]',
268
+ ),
269
+ ).not.toBeNull();
270
+ });
271
+
272
+ test("loads <script src> when a trusted export context is installed", () => {
273
+ windowWithExport.__MARIMO_EXPORT_CONTEXT__ = { trusted: true };
274
+ parseHtml({
275
+ html: '<script src="https://cdn.example.com/export.js"></script>',
276
+ });
277
+ expect(
278
+ document.head.querySelector(
279
+ 'script[src="https://cdn.example.com/export.js"]',
280
+ ),
281
+ ).not.toBeNull();
282
+ });
283
+ });
284
+
200
285
  describe("wrapTooltipTargets", () => {
201
286
  test("data-tooltip wraps element in Tooltip component", () => {
202
287
  const html = '<span data-tooltip="Hello world">Hover me</span>';
@@ -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";
@@ -37,6 +37,15 @@ export function isTrustedVirtualFileUrl(url: unknown): url is string {
37
37
  return false;
38
38
  }
39
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
+ */
40
49
  function hasNotebookTrustedDataUrlContext(): boolean {
41
50
  return store.get(hasRunAnyCellAtom) || hasTrustedExportContext();
42
51
  }