@marimo-team/islands 0.23.16-dev5 → 0.23.16-dev51

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 (156) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
  2. package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
  3. package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
  4. package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
  5. package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
  6. package/dist/{chat-ui-BdSDiVjk.js → chat-ui-1e7PcW56.js} +3117 -3120
  7. package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
  8. package/dist/{code-visibility-C4wLKnSb.js → common-CD6phlRu.js} +1046 -1014
  9. package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
  10. package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
  11. package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
  12. package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
  13. package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
  14. package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
  15. package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
  16. package/dist/{html-to-image-DDENewl2.js → html-to-image-3w0AakWW.js} +7164 -6240
  17. package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
  18. package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
  19. package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
  20. package/dist/main.js +1010 -1019
  21. package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
  22. package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
  23. package/dist/{process-output-2QPcgBQg.js → process-output-BRwR4QGs.js} +32 -28
  24. package/dist/{reveal-component-BSD4w8YU.js → reveal-component-DmGGKS8z.js} +725 -643
  25. package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
  26. package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
  27. package/dist/style.css +1 -1
  28. package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
  29. package/dist/toDate-CqhBWjvO.js +713 -0
  30. package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
  31. package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
  32. package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
  33. package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
  34. package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
  35. package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
  36. package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
  37. package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
  38. package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
  39. package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
  40. package/package.json +3 -4
  41. package/src/__mocks__/requests.ts +45 -4
  42. package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
  43. package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
  44. package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
  45. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  46. package/src/components/data-table/column-summary/utils.ts +26 -7
  47. package/src/components/data-table/types.ts +1 -1
  48. package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
  49. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
  50. package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
  51. package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
  52. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  53. package/src/components/editor/actions/pdf-export.ts +11 -14
  54. package/src/components/editor/actions/useNotebookActions.tsx +18 -18
  55. package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
  56. package/src/components/editor/cell/code/cell-editor.tsx +13 -7
  57. package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
  58. package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
  59. package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
  60. package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
  61. package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
  62. package/src/components/editor/connections/components.tsx +6 -2
  63. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +21 -5
  64. package/src/components/editor/connections/database/__tests__/as-code.test.ts +17 -0
  65. package/src/components/editor/connections/database/as-code.ts +16 -2
  66. package/src/components/editor/connections/database/schemas.ts +6 -5
  67. package/src/components/editor/connections/form-renderers.tsx +99 -1
  68. package/src/components/editor/connections/json-credentials.ts +57 -0
  69. package/src/components/editor/connections/paths.ts +21 -0
  70. package/src/components/editor/connections/secret-combobox.tsx +41 -20
  71. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
  72. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
  73. package/src/components/editor/connections/storage/as-code.ts +79 -36
  74. package/src/components/editor/connections/storage/schemas.ts +85 -44
  75. package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
  76. package/src/components/editor/file-tree/file-viewer.tsx +11 -7
  77. package/src/components/editor/file-tree/renderers.tsx +21 -0
  78. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  79. package/src/components/editor/viewer-banner.tsx +6 -1
  80. package/src/components/forms/options.ts +2 -1
  81. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  82. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  83. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
  84. package/src/components/slides/reveal-component.tsx +25 -19
  85. package/src/components/slides/slide-cell-view.tsx +1 -1
  86. package/src/components/slides/slide-scroll-hint.tsx +161 -0
  87. package/src/components/slides/slides.css +4 -2
  88. package/src/components/static-html/share-modal.tsx +1 -1
  89. package/src/components/ui/input.tsx +1 -1
  90. package/src/core/ai/context/providers/file.ts +8 -29
  91. package/src/core/cells/__tests__/cells.test.ts +12 -2
  92. package/src/core/cells/__tests__/logs.test.ts +5 -5
  93. package/src/core/cells/cells.ts +17 -4
  94. package/src/core/cells/document-changes.ts +1 -0
  95. package/src/core/cells/logs.ts +14 -4
  96. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
  97. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
  98. package/src/core/codemirror/copilot/language-server.ts +182 -70
  99. package/src/core/codemirror/copilot/transport.ts +15 -207
  100. package/src/core/codemirror/copilot/types.ts +9 -2
  101. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  102. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  103. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  104. package/src/core/codemirror/go-to-definition/commands.ts +3 -0
  105. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  106. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  107. package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
  108. package/src/core/codemirror/language/languages/python.ts +23 -11
  109. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  110. package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
  111. package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
  112. package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
  113. package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
  114. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
  115. package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
  116. package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
  117. package/src/core/codemirror/lsp/log-messages.ts +37 -0
  118. package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
  119. package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
  120. package/src/core/codemirror/lsp/transports.ts +8 -1
  121. package/src/core/codemirror/lsp/types.ts +68 -8
  122. package/src/core/codemirror/lsp/utils.ts +14 -0
  123. package/src/core/codemirror/utils.ts +28 -0
  124. package/src/core/edit-app.tsx +1 -2
  125. package/src/core/export/__tests__/hooks.test.ts +2 -4
  126. package/src/core/export/hooks.ts +4 -3
  127. package/src/core/hotkeys/hotkeys.ts +0 -2
  128. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  129. package/src/core/islands/bridge.ts +16 -0
  130. package/src/core/lsp/__tests__/transport.test.ts +509 -337
  131. package/src/core/lsp/transport.ts +304 -132
  132. package/src/core/mode.ts +10 -0
  133. package/src/core/network/__tests__/api.test.ts +42 -0
  134. package/src/core/network/__tests__/connection.test.ts +45 -0
  135. package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
  136. package/src/core/network/__tests__/requests-network.test.ts +21 -0
  137. package/src/core/network/api.ts +27 -0
  138. package/src/core/network/connection.ts +15 -0
  139. package/src/core/network/requests-lazy.ts +1 -0
  140. package/src/core/network/requests-network.ts +14 -8
  141. package/src/core/network/requests-static.ts +1 -0
  142. package/src/core/network/requests-toasting.tsx +1 -0
  143. package/src/core/network/types.ts +22 -6
  144. package/src/core/rtc/state.ts +9 -0
  145. package/src/core/static/download-html.ts +5 -14
  146. package/src/core/wasm/bridge.ts +4 -2
  147. package/src/core/wasm/worker/types.ts +5 -2
  148. package/src/css/app/codemirror.css +16 -2
  149. package/src/plugins/impl/FormPlugin.tsx +1 -1
  150. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  151. package/src/stories/log-viewer.stories.tsx +12 -6
  152. package/src/utils/__tests__/blob.test.ts +27 -1
  153. package/src/utils/__tests__/download.test.tsx +44 -4
  154. package/src/utils/blob.ts +24 -1
  155. package/src/utils/download.ts +14 -15
  156. package/dist/toDate-B3IRfHhw.js +0 -692
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
+ import { prefixPath } from "../../paths";
3
4
  import { prefixSecret } from "../../secrets";
4
5
  import { generateStorageCode } from "../as-code";
5
6
  import type { StorageConnection } from "../schemas";
@@ -9,9 +10,13 @@ describe("generateStorageCode", () => {
9
10
  type: "s3",
10
11
  bucket: "my-bucket",
11
12
  region: "us-east-1",
12
- access_key_id: "AKIAIOSFODNN7EXAMPLE",
13
- secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
14
13
  endpoint_url: undefined,
14
+ allow_http: false,
15
+ auth: {
16
+ type: "Access keys",
17
+ access_key_id: "AKIAIOSFODNN7EXAMPLE",
18
+ secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
19
+ },
15
20
  };
16
21
 
17
22
  const baseGCS: StorageConnection = {
@@ -31,8 +36,11 @@ describe("generateStorageCode", () => {
31
36
  type: "coreweave",
32
37
  bucket: "operator-bucket",
33
38
  region: "US-EAST-04A",
34
- access_key_id: "AKIAIOSFODNN7EXAMPLE",
35
- secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
39
+ auth: {
40
+ type: "Access keys",
41
+ access_key_id: "AKIAIOSFODNN7EXAMPLE",
42
+ secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
43
+ },
36
44
  };
37
45
 
38
46
  const baseGDrive: StorageConnection = {
@@ -52,6 +60,8 @@ describe("generateStorageCode", () => {
52
60
  const conn: StorageConnection = {
53
61
  type: "s3",
54
62
  bucket: "my-bucket",
63
+ allow_http: false,
64
+ auth: { type: "Access keys" },
55
65
  };
56
66
  expect(
57
67
  generateStorageCode(conn, { library: "obstore" }),
@@ -68,13 +78,61 @@ describe("generateStorageCode", () => {
68
78
  ).toMatchSnapshot();
69
79
  });
70
80
 
81
+ it("with plain-http endpoint", () => {
82
+ const conn: StorageConnection = {
83
+ ...baseS3,
84
+ endpoint_url: "http://cwlota.com",
85
+ allow_http: true,
86
+ };
87
+ expect(
88
+ generateStorageCode(conn, { library: "obstore" }),
89
+ ).toMatchSnapshot();
90
+ });
91
+
92
+ it("with container credentials", () => {
93
+ const conn: StorageConnection = {
94
+ ...baseS3,
95
+ auth: {
96
+ type: "Container credentials",
97
+ container_credentials_full_uri:
98
+ "http://169.254.170.23/v1/credentials",
99
+ container_authorization_token_file: "/var/run/secrets/token",
100
+ },
101
+ };
102
+ expect(
103
+ generateStorageCode(conn, { library: "obstore" }),
104
+ ).toMatchSnapshot();
105
+ });
106
+
107
+ it("with container credentials from secrets", () => {
108
+ const conn: StorageConnection = {
109
+ ...baseS3,
110
+ auth: {
111
+ type: "Container credentials",
112
+ container_credentials_full_uri: prefixSecret(
113
+ "AWS_CONTAINER_CREDENTIALS_FULL_URI",
114
+ ),
115
+ container_authorization_token_file: prefixSecret(
116
+ "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
117
+ ),
118
+ },
119
+ };
120
+ expect(
121
+ generateStorageCode(conn, { library: "obstore" }),
122
+ ).toMatchSnapshot();
123
+ });
124
+
71
125
  it("with secrets", () => {
72
126
  const conn: StorageConnection = {
73
127
  type: "s3",
74
128
  bucket: "my-bucket",
75
129
  region: "us-east-1",
76
- access_key_id: prefixSecret("AWS_ACCESS_KEY_ID"),
77
- secret_access_key: prefixSecret("AWS_SECRET_ACCESS_KEY"),
130
+ allow_http: false,
131
+ auth: {
132
+ type: "Access keys",
133
+ access_key_id: prefixSecret("AWS_ACCESS_KEY_ID"),
134
+ secret_access_key: prefixSecret("AWS_SECRET_ACCESS_KEY"),
135
+ },
78
136
  };
79
137
  expect(
80
138
  generateStorageCode(conn, { library: "obstore" }),
@@ -98,6 +156,26 @@ describe("generateStorageCode", () => {
98
156
  generateStorageCode(conn, { library: "obstore" }),
99
157
  ).toMatchSnapshot();
100
158
  });
159
+
160
+ it("with service account key from secrets", () => {
161
+ const conn: StorageConnection = {
162
+ ...baseGCS,
163
+ service_account_key: prefixSecret("GCS_SERVICE_ACCOUNT_KEY"),
164
+ };
165
+ expect(
166
+ generateStorageCode(conn, { library: "obstore" }),
167
+ ).toMatchSnapshot();
168
+ });
169
+
170
+ it("with service account key from a file path", () => {
171
+ const conn: StorageConnection = {
172
+ ...baseGCS,
173
+ service_account_key: prefixPath("/etc/secrets/gcs.json"),
174
+ };
175
+ expect(
176
+ generateStorageCode(conn, { library: "obstore" }),
177
+ ).toMatchSnapshot();
178
+ });
101
179
  });
102
180
 
103
181
  describe("Azure", () => {
@@ -143,6 +221,25 @@ describe("generateStorageCode", () => {
143
221
  type: "coreweave",
144
222
  bucket: "operator-bucket",
145
223
  region: "US-EAST-04A",
224
+ auth: { type: "Access keys" },
225
+ };
226
+ expect(
227
+ generateStorageCode(conn, { library: "obstore" }),
228
+ ).toMatchSnapshot();
229
+ });
230
+
231
+ it("with container credentials", () => {
232
+ const conn: StorageConnection = {
233
+ ...baseCoreWeave,
234
+ auth: {
235
+ type: "Container credentials",
236
+ container_credentials_full_uri: prefixSecret(
237
+ "AWS_CONTAINER_CREDENTIALS_FULL_URI",
238
+ ),
239
+ container_authorization_token_file: prefixSecret(
240
+ "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
241
+ ),
242
+ },
146
243
  };
147
244
  expect(
148
245
  generateStorageCode(conn, { library: "obstore" }),
@@ -154,8 +251,11 @@ describe("generateStorageCode", () => {
154
251
  type: "coreweave",
155
252
  bucket: "operator-bucket",
156
253
  region: "US-EAST-04A",
157
- access_key_id: prefixSecret("COREWEAVE_OBJECT_STORAGE_KEY"),
158
- secret_access_key: prefixSecret("COREWEAVE_OBJECT_STORAGE_SECRET"),
254
+ auth: {
255
+ type: "Access keys",
256
+ access_key_id: prefixSecret("COREWEAVE_OBJECT_STORAGE_KEY"),
257
+ secret_access_key: prefixSecret("COREWEAVE_OBJECT_STORAGE_SECRET"),
258
+ },
159
259
  };
160
260
  expect(
161
261
  generateStorageCode(conn, { library: "obstore" }),
@@ -187,6 +287,26 @@ describe("generateStorageCode", () => {
187
287
  generateStorageCode(conn, { library: "fsspec", isEmbedded: true }),
188
288
  ).toMatchSnapshot();
189
289
  });
290
+
291
+ it("with service account credentials from secrets", () => {
292
+ const conn: StorageConnection = {
293
+ ...baseGDrive,
294
+ credentials_json: prefixSecret("GDRIVE_CREDENTIALS_JSON"),
295
+ };
296
+ expect(
297
+ generateStorageCode(conn, { library: "fsspec" }),
298
+ ).toMatchSnapshot();
299
+ });
300
+
301
+ it("with service account credentials from a file path", () => {
302
+ const conn: StorageConnection = {
303
+ ...baseGDrive,
304
+ credentials_json: prefixPath("/etc/secrets/gdrive.json"),
305
+ };
306
+ expect(
307
+ generateStorageCode(conn, { library: "fsspec" }),
308
+ ).toMatchSnapshot();
309
+ });
190
310
  });
191
311
 
192
312
  describe("invalid cases", () => {
@@ -2,8 +2,13 @@
2
2
 
3
3
  import dedent from "string-dedent";
4
4
  import { assertNever } from "@/utils/assertNever";
5
+ import { escapePythonString, resolveJsonCredential } from "../json-credentials";
5
6
  import { isSecret, unprefixSecret } from "../secrets";
6
- import { type StorageConnection, StorageConnectionSchema } from "./schemas";
7
+ import {
8
+ type S3Auth,
9
+ type StorageConnection,
10
+ StorageConnectionSchema,
11
+ } from "./schemas";
7
12
 
8
13
  export type StorageLibrary = "obstore" | "fsspec";
9
14
 
@@ -50,6 +55,36 @@ class SecretContainer {
50
55
  }
51
56
  }
52
57
 
58
+ /**
59
+ * Credential kwargs shared by every S3-compatible store.
60
+ */
61
+ function s3AuthParams(
62
+ auth: S3Auth | undefined,
63
+ secrets: SecretContainer,
64
+ ): string[] {
65
+ // obstore polls the credential endpoint with the token read from the file,
66
+ // refreshing credentials for the lifetime of the store.
67
+ if (auth?.type === "Container credentials") {
68
+ return [
69
+ ` container_credentials_full_uri=${secrets.print("container_credentials_full_uri", auth.container_credentials_full_uri)},`,
70
+ ` container_authorization_token_file=${secrets.print("container_authorization_token_file", auth.container_authorization_token_file)},`,
71
+ ];
72
+ }
73
+
74
+ const params: string[] = [];
75
+ if (auth?.access_key_id) {
76
+ params.push(
77
+ ` access_key_id=${secrets.print("access_key_id", auth.access_key_id)},`,
78
+ );
79
+ }
80
+ if (auth?.secret_access_key) {
81
+ params.push(
82
+ ` secret_access_key=${secrets.print("secret_access_key", auth.secret_access_key)},`,
83
+ );
84
+ }
85
+ return params;
86
+ }
87
+
53
88
  function generateS3Code(
54
89
  connection: Extract<StorageConnection, { type: "s3" }>,
55
90
  secrets: SecretContainer,
@@ -61,21 +96,15 @@ function generateS3Code(
61
96
  if (connection.region) {
62
97
  params.push(` region=${secrets.print("region", connection.region)},`);
63
98
  }
64
- if (connection.access_key_id) {
65
- params.push(
66
- ` access_key_id=${secrets.print("access_key_id", connection.access_key_id)},`,
67
- );
68
- }
69
- if (connection.secret_access_key) {
70
- params.push(
71
- ` secret_access_key=${secrets.print("secret_access_key", connection.secret_access_key)},`,
72
- );
73
- }
99
+ params.push(...s3AuthParams(connection.auth, secrets));
74
100
  if (connection.endpoint_url) {
75
101
  params.push(
76
102
  ` endpoint_url=${secrets.print("endpoint_url", connection.endpoint_url)},`,
77
103
  );
78
104
  }
105
+ if (connection.allow_http) {
106
+ params.push(" allow_http=True,");
107
+ }
79
108
 
80
109
  const paramsStr = params.length > 0 ? `\n${params.join("\n")}\n` : "";
81
110
 
@@ -92,20 +121,36 @@ function generateGCSCode(
92
121
  const bucket = secrets.print("bucket", connection.bucket);
93
122
  const imports = new Set(["from obstore.store import GCSStore"]);
94
123
 
95
- let code: string;
96
- if (connection.service_account_key) {
97
- imports.add("import json");
98
- code = dedent(`
99
- _credentials = json.loads("""${connection.service_account_key}""")
124
+ if (!connection.service_account_key) {
125
+ return {
126
+ imports,
127
+ code: dedent(`
128
+ store = GCSStore(${bucket})
129
+ `),
130
+ };
131
+ }
132
+
133
+ const credential = resolveJsonCredential(
134
+ connection.service_account_key,
135
+ (v) => secrets.print("service_account_key", v),
136
+ );
137
+
138
+ if (credential.kind === "path") {
139
+ const code = dedent(`
100
140
  store = GCSStore(${bucket},
101
- service_account_key=_credentials,
141
+ service_account_path="${escapePythonString(credential.path)}",
102
142
  )
103
143
  `);
104
- } else {
105
- code = dedent(`
106
- store = GCSStore(${bucket})
107
- `);
144
+ return { imports, code };
108
145
  }
146
+
147
+ imports.add("import json");
148
+ const code = dedent(`
149
+ _credentials = ${credential.expr}
150
+ store = GCSStore(${bucket},
151
+ service_account_key=_credentials,
152
+ )
153
+ `);
109
154
  return { imports, code };
110
155
  }
111
156
 
@@ -140,16 +185,7 @@ function generateCoreWeaveCode(
140
185
  ` region=${secrets.print("region", connection.region)},`,
141
186
  ];
142
187
 
143
- if (connection.access_key_id) {
144
- params.push(
145
- ` access_key_id=${secrets.print("access_key_id", connection.access_key_id)},`,
146
- );
147
- }
148
- if (connection.secret_access_key) {
149
- params.push(
150
- ` secret_access_key=${secrets.print("secret_access_key", connection.secret_access_key)},`,
151
- );
152
- }
188
+ params.push(...s3AuthParams(connection.auth, secrets));
153
189
 
154
190
  params.push(
155
191
  ` endpoint="https://${connection.bucket}.cwobject.com",`,
@@ -176,13 +212,20 @@ function generateGDriveCode(
176
212
  const imports = new Set(["from gdrive_fsspec import GoogleDriveFileSystem"]);
177
213
 
178
214
  if (connection.credentials_json) {
179
- imports.add("import json");
180
- const creds = secrets.print(
181
- "credentials_json",
182
- connection.credentials_json,
215
+ const credential = resolveJsonCredential(connection.credentials_json, (v) =>
216
+ secrets.print("credentials_json", v),
183
217
  );
218
+ // gdrive-fsspec accepts a filesystem path as `creds` when the string
219
+ // doesn't start with "{"; otherwise it expects a JSON object/string.
220
+ if (credential.kind === "path") {
221
+ const code = dedent(`
222
+ fs = GoogleDriveFileSystem(creds="${escapePythonString(credential.path)}", token="service_account", use_listings_cache=False, skip_instance_cache=True)
223
+ `);
224
+ return { imports, code };
225
+ }
226
+ imports.add("import json");
184
227
  const code = dedent(`
185
- _creds = json.loads("""${connection.credentials_json?.startsWith("ENV:") ? `{${creds}}` : connection.credentials_json}""")
228
+ _creds = ${credential.expr}
186
229
  fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)
187
230
  `);
188
231
  return { imports, code };
@@ -2,6 +2,83 @@
2
2
  import { z } from "zod";
3
3
  import { FieldOptions } from "@/components/forms/options";
4
4
 
5
+ /**
6
+ * Credentials shared by every S3-compatible store (Amazon S3, CoreWeave, ...).
7
+ *
8
+ * Rendered as tabs: either long-lived access keys, or a credential-vending
9
+ * endpoint (ECS/EKS task roles, CoreWeave sandboxes, ...) that obstore polls
10
+ * with a bearer token read from disk.
11
+ */
12
+ const s3AuthField = () =>
13
+ z
14
+ .discriminatedUnion("type", [
15
+ z.object({
16
+ type: z.literal("Access keys"),
17
+ access_key_id: z
18
+ .string()
19
+ .optional()
20
+ .describe(
21
+ FieldOptions.of({
22
+ label: "Access Key ID",
23
+ description: "Leave empty to use the default credential chain",
24
+ inputType: "password",
25
+ optionRegex:
26
+ "(access.?key.?id|object.?storage.?key|aws.?access.?key)",
27
+ }),
28
+ ),
29
+ secret_access_key: z
30
+ .string()
31
+ .optional()
32
+ .describe(
33
+ FieldOptions.of({
34
+ label: "Secret Access Key",
35
+ inputType: "password",
36
+ optionRegex:
37
+ "(secret.?access.?key|object.?storage.?secret|aws.?secret)",
38
+ }),
39
+ ),
40
+ }),
41
+ z.object({
42
+ type: z.literal("Container credentials"),
43
+ container_credentials_full_uri: z
44
+ .string()
45
+ .nonempty()
46
+ .describe(
47
+ FieldOptions.of({
48
+ label: "Credentials URI",
49
+ description: "Endpoint that vends credentials to the container",
50
+ placeholder: "http://169.254.170.23/v1/credentials",
51
+ optionRegex: "container.?credentials",
52
+ }),
53
+ ),
54
+ container_authorization_token_file: z
55
+ .string()
56
+ .nonempty()
57
+ .describe(
58
+ FieldOptions.of({
59
+ label: "Authorization Token File",
60
+ description: "File holding the token sent to the credentials URI",
61
+ placeholder: "/var/run/secrets/token",
62
+ optionRegex:
63
+ "(container.?authorization.?token.?file|container.?auth.?token)",
64
+ }),
65
+ ),
66
+ }),
67
+ ])
68
+ .default({ type: "Access keys" })
69
+ .describe(FieldOptions.of({ label: "Credentials", special: "tabs" }));
70
+
71
+ const allowHttpField = () =>
72
+ z
73
+ .boolean()
74
+ .default(false)
75
+ .describe(
76
+ FieldOptions.of({
77
+ label: "Allow HTTP",
78
+ description: "Required for plain-http (non-TLS) endpoints",
79
+ }),
80
+ );
81
+
5
82
  export const S3StorageSchema = z
6
83
  .object({
7
84
  type: z.literal("s3"),
@@ -25,36 +102,20 @@ export const S3StorageSchema = z
25
102
  optionRegex: "(region|aws.?region)",
26
103
  }),
27
104
  ),
28
- access_key_id: z
29
- .string()
30
- .optional()
31
- .describe(
32
- FieldOptions.of({
33
- label: "Access Key ID",
34
- inputType: "password",
35
- optionRegex: "(access.?key.?id|aws.?access.?key)",
36
- }),
37
- ),
38
- secret_access_key: z
39
- .string()
40
- .optional()
41
- .describe(
42
- FieldOptions.of({
43
- label: "Secret Access Key",
44
- inputType: "password",
45
- optionRegex: "(secret.?access.?key|aws.?secret)",
46
- }),
47
- ),
48
105
  endpoint_url: z
49
106
  .string()
50
107
  .optional()
51
108
  .describe(
52
109
  FieldOptions.of({
53
110
  label: "Endpoint URL",
111
+ description:
112
+ "Ignored if the AWS_ENDPOINT_URL_S3 environment variable is set",
54
113
  placeholder: "https://s3.amazonaws.com",
55
114
  optionRegex: "(endpoint|s3.?url|s3.?endpoint)",
56
115
  }),
57
116
  ),
117
+ allow_http: allowHttpField(),
118
+ auth: s3AuthField(),
58
119
  })
59
120
  .describe(FieldOptions.of({ direction: "two-columns" }));
60
121
 
@@ -77,7 +138,7 @@ export const GCSStorageSchema = z
77
138
  .describe(
78
139
  FieldOptions.of({
79
140
  label: "Service Account Key (JSON)",
80
- inputType: "textarea",
141
+ special: "secret_textarea",
81
142
  }),
82
143
  ),
83
144
  })
@@ -139,28 +200,7 @@ export const CoreWeaveStorageSchema = z
139
200
  placeholder: "US-EAST-04A",
140
201
  }),
141
202
  ),
142
- access_key_id: z
143
- .string()
144
- .optional()
145
- .describe(
146
- FieldOptions.of({
147
- label: "Access Key ID",
148
- inputType: "password",
149
- optionRegex:
150
- "(access.?key.?id|object.?storage.?key|aws.?access.?key)",
151
- }),
152
- ),
153
- secret_access_key: z
154
- .string()
155
- .optional()
156
- .describe(
157
- FieldOptions.of({
158
- label: "Secret Access Key",
159
- inputType: "password",
160
- optionRegex:
161
- "(secret.?access.?key|object.?storage.?secret|aws.?secret)",
162
- }),
163
- ),
203
+ auth: s3AuthField(),
164
204
  })
165
205
  .describe(FieldOptions.of({ direction: "two-columns" }));
166
206
 
@@ -174,7 +214,7 @@ export const GoogleDriveStorageSchema = z
174
214
  FieldOptions.of({
175
215
  label: "Service Account JSON",
176
216
  description: "Leave empty to use browser-based authentication",
177
- inputType: "textarea",
217
+ special: "secret_textarea",
178
218
  }),
179
219
  ),
180
220
  })
@@ -189,3 +229,4 @@ export const StorageConnectionSchema = z.discriminatedUnion("type", [
189
229
  ]);
190
230
 
191
231
  export type StorageConnection = z.infer<typeof StorageConnectionSchema>;
232
+ export type S3Auth = Extract<StorageConnection, { type: "s3" }>["auth"];
@@ -0,0 +1,31 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import { buildMediaSource } from "../renderers";
4
+
5
+ describe("buildMediaSource", () => {
6
+ it("returns base64 source for binary media", () => {
7
+ expect(
8
+ buildMediaSource({
9
+ contents: "aGVsbG8=",
10
+ mimeType: "image/png",
11
+ isBase64: true,
12
+ }),
13
+ ).toEqual({
14
+ base64: "aGVsbG8=",
15
+ mime: "image/png",
16
+ });
17
+ });
18
+
19
+ it("returns UTF-8 data URL for text-based media", () => {
20
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg"></svg>';
21
+ expect(
22
+ buildMediaSource({
23
+ contents: svg,
24
+ mimeType: "image/svg+xml",
25
+ isBase64: false,
26
+ }),
27
+ ).toEqual({
28
+ url: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`,
29
+ });
30
+ });
31
+ });
@@ -22,12 +22,11 @@ import { isWasm } from "@/core/wasm/utils";
22
22
  import { useAsyncData } from "@/hooks/useAsyncData";
23
23
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
24
24
  import { copyToClipboard } from "@/utils/copy";
25
- import type { Base64String } from "@/utils/json/base64";
26
25
  import { downloadFile } from "./download";
27
26
  import { FilePreviewHeader } from "./file-header";
28
27
  import { FilePreviewMetadata } from "./file-preview-metadata";
29
28
  import { getFileRenderMode } from "./file-render-mode";
30
- import { FileContentRenderer } from "./renderers";
29
+ import { buildMediaSource, FileContentRenderer } from "./renderers";
31
30
 
32
31
  export const MAX_FILE_PREVIEW_BYTES = 10 * 1024 * 1024;
33
32
 
@@ -226,6 +225,15 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
226
225
  </Alert>
227
226
  );
228
227
 
228
+ const mediaSource =
229
+ isMedia && data.contents
230
+ ? buildMediaSource({
231
+ contents: data.contents,
232
+ mimeType,
233
+ isBase64: data.isBase64 ?? false,
234
+ })
235
+ : undefined;
236
+
229
237
  return (
230
238
  <>
231
239
  {header}
@@ -239,11 +247,7 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
239
247
  ? (data.contents ?? undefined)
240
248
  : undefined
241
249
  }
242
- mediaSource={
243
- isMedia && data.contents
244
- ? { base64: data.contents as Base64String, mime: mimeType }
245
- : undefined
246
- }
250
+ mediaSource={mediaSource}
247
251
  readOnly={!isText}
248
252
  onChange={isText ? setInternalValue : undefined}
249
253
  extensions={isText ? [saveKeymapExtension] : []}
@@ -60,6 +60,27 @@ function resolveMediaSrc(source: MediaSource): string {
60
60
  return base64ToDataURL(source.base64, source.mime);
61
61
  }
62
62
 
63
+ /**
64
+ * Build a {@link MediaSource} from file details.
65
+ * Binary media is base64-encoded; text-based media (e.g. SVG) uses a UTF-8 data URL.
66
+ */
67
+ export function buildMediaSource({
68
+ contents,
69
+ mimeType,
70
+ isBase64,
71
+ }: {
72
+ contents: string;
73
+ mimeType: string;
74
+ isBase64: boolean;
75
+ }): MediaSource {
76
+ if (isBase64) {
77
+ return { base64: contents as Base64String, mime: mimeType };
78
+ }
79
+ return {
80
+ url: `data:${mimeType};charset=utf-8,${encodeURIComponent(contents)}`,
81
+ };
82
+ }
83
+
63
84
  export const CsvViewer: React.FC<{ contents: string }> = ({ contents }) => {
64
85
  const data = useMemo(() => parseCsvData(contents), [contents]);
65
86
  const [pagination, setPagination] = useState<PaginationState>({
@@ -202,7 +202,7 @@ const ActionButtons: React.FC<{
202
202
  if (!app) {
203
203
  return;
204
204
  }
205
- await downloadAsHTML({ filename: document.title, includeCode: true });
205
+ await downloadAsHTML({ includeCode: true });
206
206
  };
207
207
 
208
208
  const handleDownloadAsPython = async () => {
@@ -51,8 +51,13 @@ export const ViewerBanner = () => {
51
51
  }
52
52
  };
53
53
 
54
+ // A zero-height sticky row pins the banner to the top-left of the notebook
55
+ // scroll area, so it stays visible while scrolling without reserving space in
56
+ // the flow. Because it reserves no space, the banner visually overlaps the
57
+ // app header (sticky at z-50); setting the banner to z-100 keeps the header
58
+ // from covering the button and swallowing its clicks.
54
59
  return (
55
- <div className="absolute top-2 left-2 z-50 w-fit print:hidden">
60
+ <div className="sticky top-2 left-2 z-100 h-0 ml-2 w-fit print:hidden">
56
61
  <Banner
57
62
  kind="info"
58
63
  className="flex items-center gap-2 rounded px-2 py-1 text-xs shadow-sm"
@@ -28,7 +28,8 @@ export interface FieldOptions {
28
28
  | "date"
29
29
  | "datetime"
30
30
  | "time"
31
- | "tabs";
31
+ | "tabs"
32
+ | "secret_textarea";
32
33
  /**
33
34
  * Only show options that match the regex
34
35
  */