@immediately-run/sandpack-client 2.22.0 → 2.22.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandpack-client",
3
- "version": "2.22.0",
4
- "srcHash": "0ad007879d4260ed144b1aef82d68bc2332a9b291d777aed95b8320499351fac",
5
- "builtAt": "2026-08-26T22:56:07.275Z"
3
+ "version": "2.22.1",
4
+ "srcHash": "10bae547420c39b69868a88e4aa7810abbbd8490309a16a6154ca3fdc16e7b4f",
5
+ "builtAt": "2026-09-13T05:42:11.272Z"
6
6
  }
@@ -2,9 +2,9 @@
2
2
 
3
3
  var utils = require('../../utils-BiVyytui.js');
4
4
  var nodebox = require('@codesandbox/nodebox');
5
- require('../../types-KEkDKvIe.js');
5
+ require('../../types-B3g_70H8.js');
6
6
  var base = require('../../base-DelKLlDk.js');
7
- var consoleHook = require('../../consoleHook-BRSVCdB0.js');
7
+ var consoleHook = require('../../consoleHook-vrsBdedj.js');
8
8
  require('outvariant');
9
9
  require('@zenfs/core');
10
10
  require('dequal');
@@ -1,8 +1,8 @@
1
1
  import { _ as __awaiter, a as __generator, f as nullthrows, c as createError, g as __extends, h as __assign } from '../../utils-DG1HA4RZ.mjs';
2
2
  import { INJECT_MESSAGE_TYPE, Nodebox, PREVIEW_LOADED_MESSAGE_TYPE } from '@codesandbox/nodebox';
3
- import '../../types-BgalzxpH.mjs';
3
+ import '../../types-DRg992RB.mjs';
4
4
  import { S as SandpackClient } from '../../base-DBh7xJX9.mjs';
5
- import { c as consoleHook, g as generateRandomId, E as EventEmitter, a as getMessageFromError, f as findStartScriptPackageJson } from '../../consoleHook-BYuGaxe8.mjs';
5
+ import { c as consoleHook, g as generateRandomId, E as EventEmitter, a as getMessageFromError, f as findStartScriptPackageJson } from '../../consoleHook-DT9Wd42J.mjs';
6
6
  import 'outvariant';
7
7
  import '@zenfs/core';
8
8
  import 'dequal';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * R3-367 — the getCodeSandboxURL gesture gate, extracted so it is testable
3
+ * without standing the whole runtime graph up (and so the fork's node-env
4
+ * suites can import it too).
5
+ */
6
+ /**
7
+ * Whether a codesandbox.io export may run: the user has been ACTIVE on this
8
+ * page (sticky activation — the fs snapshot can outlive a transient window).
9
+ * False ⇒ app/script-initiated context ⇒ the export must refuse before
10
+ * reading the filesystem.
11
+ */
12
+ export declare const codeSandboxExportAllowed: () => boolean;
13
+ /**
14
+ * Make a gesture-gate refusal observable: a `sandpack-security-violation`
15
+ * CustomEvent the host (site-main) can listen for and journal into its
16
+ * security-events stream. Never throws — observability must not become a
17
+ * second failure mode on the refusal path.
18
+ */
19
+ export declare const notifyCodeSandboxExportRefused: () => void;
@@ -6,7 +6,7 @@ export declare class IFrameProtocol {
6
6
  private globalListenersCount;
7
7
  channelListeners: Record<number, ListenerFunction>;
8
8
  private channelListenersCount;
9
- readonly channelId: number;
9
+ readonly channelId: string;
10
10
  constructor(iframe: HTMLIFrameElement, _origin: string);
11
11
  cleanup(): void;
12
12
  register(port?: MessagePort, config?: Record<string, unknown>, babelPort?: MessagePort): void;
@@ -4,6 +4,7 @@ import { SandpackClient } from "../base";
4
4
  import Protocol from "./file-resolver-protocol";
5
5
  import { IFrameProtocol } from "./iframe-protocol";
6
6
  import { type SandpackRuntimeMessage } from "./types";
7
+ export { codeSandboxExportAllowed, notifyCodeSandboxExportRefused, } from "./gestureGate";
7
8
  export declare class SandpackRuntime extends SandpackClient {
8
9
  fileResolverProtocol?: Protocol;
9
10
  immutableFetchProtocol?: Protocol;
@@ -81,7 +82,17 @@ export declare class SandpackRuntime extends SandpackClient {
81
82
  dispatch(message: SandpackRuntimeMessage): void;
82
83
  listen(listener: ListenerFunction): UnsubscribeFunction;
83
84
  /**
84
- * Get the URL of the contents of the current sandbox
85
+ * Get the URL of the contents of the current sandbox.
86
+ *
87
+ * R3-367 — GESTURE-GATED. This POSTs the ENTIRE app filesystem to
88
+ * codesandbox.io from the parent page; the fs may contain the user's data
89
+ * (spaces, mounts), so the export is reachable only when the user has been
90
+ * active on the page (`navigator.userActivation.hasBeenActive` — the sticky
91
+ * flag, because the fs snapshot can outlive a transient-activation window).
92
+ * Without any user activation this refuses BEFORE reading the filesystem:
93
+ * nothing is posted, and a `sandpack-security-violation` CustomEvent is
94
+ * dispatched on the window for the host to journal (site-main wires the
95
+ * listener into its security-events seam).
85
96
  */
86
97
  getCodeSandboxURL(): Promise<{
87
98
  sandboxId: string;
@@ -2,7 +2,7 @@
2
2
 
3
3
  var utils = require('../../utils-BiVyytui.js');
4
4
  var dequal = require('dequal');
5
- var types = require('../../types-KEkDKvIe.js');
5
+ var types = require('../../types-B3g_70H8.js');
6
6
  var base = require('../../base-DelKLlDk.js');
7
7
  var iframeFactory = require('../../iframe-factory-Bc7tcyQZ.js');
8
8
  var mimeDB = require('mime-db');
@@ -66,6 +66,21 @@ var Protocol = /** @class */ (function () {
66
66
  return Protocol;
67
67
  }());
68
68
 
69
+ // R3-367: crypto-random, 64-bit channel id (16 hex chars from 8 random bytes).
70
+ // This used to be Math.floor(Math.random()*1e6) — guessable, and the id was
71
+ // console.logged at registration. SECURITY INVARIANT (do not weaken): the id is
72
+ // a CORRELATION key only — every incoming message is authenticated by the
73
+ // `event.source === this.frameWindow` check in eventListener() below, NEVER by
74
+ // the id alone. The crypto entropy is defense-in-depth on top of that check
75
+ // (the frame-side twin comment lives in the sandbox bundler's protocol layer,
76
+ // R3-352 C1). `string | number` on the wire stays compatible with ids older
77
+ // peers minted as numbers: both sides compare the echoed value they themselves
78
+ // sent, so the type never has to agree across versions.
79
+ var randomChannelId = function () {
80
+ var bytes = new Uint8Array(8);
81
+ crypto.getRandomValues(bytes);
82
+ return Array.from(bytes, function (b) { return b.toString(16).padStart(2, "0"); }).join("");
83
+ };
69
84
  var IFrameProtocol = /** @class */ (function () {
70
85
  function IFrameProtocol(iframe, _origin) {
71
86
  // React to messages from any iframe
@@ -74,8 +89,9 @@ var IFrameProtocol = /** @class */ (function () {
74
89
  // React to messages from the iframe owned by this instance
75
90
  this.channelListeners = {};
76
91
  this.channelListenersCount = 0;
77
- // Random number to identify this instance of the client when messages are coming from multiple iframes
78
- this.channelId = Math.floor(Math.random() * 1000000);
92
+ // Random id to identify this instance of the client when messages are coming
93
+ // from multiple iframes — crypto-random (see randomChannelId above).
94
+ this.channelId = randomChannelId();
79
95
  this.frameWindow = iframe.contentWindow;
80
96
  this.origin = "*"; //origin;
81
97
  this.globalListeners = [];
@@ -101,8 +117,9 @@ var IFrameProtocol = /** @class */ (function () {
101
117
  if (!this.frameWindow) {
102
118
  return;
103
119
  }
104
- // eslint-disable-next-line no-console -- dev registration trace
105
- console.log("[IFrameProtocol] Registering iframe with channelId", this.channelId, this);
120
+ // R3-367: no channelId log — the id is not secret-critical, but logging it
121
+ // at registration handed an observer the correlation key for free (the
122
+ // evt.source check below is the actual authentication).
106
123
  // Order matters: the bundler reads ports[0] as the fs port and ports[1] as
107
124
  // the Babel worker port. `filter` keeps that order as long as the fs port
108
125
  // is always present (it is, in normal operation).
@@ -153,7 +170,11 @@ var IFrameProtocol = /** @class */ (function () {
153
170
  };
154
171
  // Handles message windows coming from iframes
155
172
  IFrameProtocol.prototype.eventListener = function (evt) {
156
- // skip events originating from different iframes
173
+ // SECURITY INVARIANT (R3-367, twin of the randomChannelId comment): this
174
+ // source check — not the channelId — is what authenticates an incoming
175
+ // message. The id below only routes a message ALREADY accepted here to the
176
+ // owning instance. Weakening or reordering this check would let any iframe
177
+ // on the page speak on the bundler channel.
157
178
  if (evt.source !== this.frameWindow) {
158
179
  return;
159
180
  }
@@ -190,24 +211,50 @@ var IFrameProtocol = /** @class */ (function () {
190
211
  * registers via `registerImmutableUrlPrefix` (see sandpack-bundler
191
212
  * `src/utils/fetch.ts`) — keep the two in sync.
192
213
  */
193
- /**
194
- * URL prefixes whose responses never change for a given URL (the URL encodes
195
- * the exact content version). Only these may be fetched on the iframe's
196
- * behalf, and they are safe to cache forever.
197
- */
198
214
  var IMMUTABLE_URL_ALLOWLIST = [
199
215
  // Module CDN, exact-versioned package bundles. (NOT /dep_tree/, which
200
216
  // resolves semver ranges and changes as new versions publish.)
201
- "https://sandpack-cdn-staging.blazingly.io/package/",
217
+ {
218
+ origin: "https://sandpack-cdn-staging.blazingly.io",
219
+ pathPrefix: "/package/",
220
+ },
202
221
  // unpkg files, requested by the bundler at registry-resolved exact versions.
203
- "https://unpkg.com/",
222
+ { origin: "https://unpkg.com", pathPrefix: "/" },
204
223
  // Self-hosted, versioned @immediately-run/sdk builds (SDK_PACKAGING_SPEC
205
224
  // §5/§11, Option A). The /v/<version>/ path encodes the exact version, so
206
225
  // responses are immutable; the bundler fetches these when an app opts the SDK
207
226
  // into immediately.run.resolveFromRegistry. Keep in sync with the prefix the
208
227
  // bundler registers via registerImmutableUrlPrefix (sandbox bundler.ts).
209
- "https://immediately-run.github.io/immediately-run-sdk/v/",
228
+ {
229
+ origin: "https://immediately-run.github.io",
230
+ pathPrefix: "/immediately-run-sdk/v/",
231
+ },
210
232
  ];
233
+ /**
234
+ * Is a PARSED URL inside the allowlist? Origin is compared exactly (a userinfo
235
+ * or lookalike host cannot match) and the path prefix against the URL parser's
236
+ * NORMALIZED pathname; additionally, any dot segment — literal or
237
+ * percent-encoded, in any hex case — is refused, so no spelling of `..` can
238
+ * cross the prefix boundary after a server-side decode.
239
+ */
240
+ var inPolicy = function (u) {
241
+ var segments = u.pathname.split("/").map(function (segment) {
242
+ try {
243
+ return decodeURIComponent(segment).toLowerCase();
244
+ }
245
+ catch (_a) {
246
+ return segment.toLowerCase();
247
+ }
248
+ });
249
+ if (segments.some(function (s) { return s === ".." || s === "."; }))
250
+ return false;
251
+ if (/%2e/.test(u.pathname.toLowerCase()))
252
+ return false;
253
+ return IMMUTABLE_URL_ALLOWLIST.some(function (_a) {
254
+ var origin = _a.origin, pathPrefix = _a.pathPrefix;
255
+ return u.origin === origin && u.pathname.startsWith(pathPrefix);
256
+ });
257
+ };
211
258
  var IMMUTABLE_CACHE_NAME = "sandpack-immutable-fetch-v1";
212
259
  var serializeResponse = function (res) { return utils.__awaiter(void 0, void 0, void 0, function () {
213
260
  var _a;
@@ -299,14 +346,22 @@ var matchesIntegrity = function (body, integrity) { return utils.__awaiter(void
299
346
  */
300
347
  function handleImmutableFetch(url, integrity) {
301
348
  return utils.__awaiter(this, void 0, void 0, function () {
302
- var expected, cache, hit, body, res, result, _a;
349
+ var parsed, expected, cache, hit, body, res, result, _a;
303
350
  return utils.__generator(this, function (_b) {
304
351
  switch (_b.label) {
305
352
  case 0:
306
- if (typeof url !== "string" ||
307
- !IMMUTABLE_URL_ALLOWLIST.some(function (prefix) { return url.startsWith(prefix); })) {
353
+ if (typeof url !== "string") {
308
354
  throw new Error("URL not allowed for immutable fetch: ".concat(String(url)));
309
355
  }
356
+ try {
357
+ parsed = new URL(url);
358
+ }
359
+ catch (_c) {
360
+ throw new Error("URL not allowed for immutable fetch: ".concat(url));
361
+ }
362
+ if (!inPolicy(parsed)) {
363
+ throw new Error("URL not allowed for immutable fetch: ".concat(url));
364
+ }
310
365
  expected = typeof integrity === "string" ? integrity : undefined;
311
366
  return [4 /*yield*/, openCache()];
312
367
  case 1:
@@ -330,12 +385,21 @@ function handleImmutableFetch(url, integrity) {
330
385
  // Stale/poisoned entry: drop it and fall through to a fresh fetch.
331
386
  _b.sent();
332
387
  _b.label = 6;
333
- case 6: return [4 /*yield*/, fetch(url)];
388
+ case 6: return [4 /*yield*/, fetch(parsed)];
334
389
  case 7:
335
390
  res = _b.sent();
336
391
  if (!res.ok) {
337
392
  throw new Error("Immutable fetch failed with status ".concat(res.status, ": ").concat(url));
338
393
  }
394
+ // R3-364: the prefix hosts are exact-version content hosts; a redirect that
395
+ // leaves the allowlist means the bytes did NOT come from an in-policy origin —
396
+ // refuse rather than serve (and never cache) them. The browser follows
397
+ // redirects itself (cross-origin `redirect: 'manual'` responses are opaque),
398
+ // so this is the final-URL check: `res.url` is where the bytes actually came
399
+ // from. A response that reports no URL (test doubles) skips the check.
400
+ if (res.redirected && res.url !== "" && !inPolicy(new URL(res.url))) {
401
+ throw new Error("Immutable fetch redirected outside the allowlist: ".concat(res.url));
402
+ }
339
403
  return [4 /*yield*/, serializeResponse(res)];
340
404
  case 8:
341
405
  result = _b.sent();
@@ -621,9 +685,44 @@ function getExtension(filepath) {
621
685
  }
622
686
  }
623
687
 
688
+ /**
689
+ * R3-367 — the getCodeSandboxURL gesture gate, extracted so it is testable
690
+ * without standing the whole runtime graph up (and so the fork's node-env
691
+ * suites can import it too).
692
+ */
693
+ /**
694
+ * Whether a codesandbox.io export may run: the user has been ACTIVE on this
695
+ * page (sticky activation — the fs snapshot can outlive a transient window).
696
+ * False ⇒ app/script-initiated context ⇒ the export must refuse before
697
+ * reading the filesystem.
698
+ */
699
+ var codeSandboxExportAllowed$1 = function () {
700
+ var _a;
701
+ return ((_a = navigator.userActivation) === null || _a === void 0 ? void 0 : _a.hasBeenActive) === true;
702
+ };
703
+ /**
704
+ * Make a gesture-gate refusal observable: a `sandpack-security-violation`
705
+ * CustomEvent the host (site-main) can listen for and journal into its
706
+ * security-events stream. Never throws — observability must not become a
707
+ * second failure mode on the refusal path.
708
+ */
709
+ var notifyCodeSandboxExportRefused$1 = function () {
710
+ try {
711
+ window.dispatchEvent(new CustomEvent("sandpack-security-violation", {
712
+ detail: {
713
+ kind: "gesture-gate.getCodeSandboxURL",
714
+ reason: "no user activation",
715
+ },
716
+ }));
717
+ }
718
+ catch (_a) {
719
+ /* no window / dispatch unavailable */
720
+ }
721
+ };
722
+
624
723
  var _a;
625
724
  var SUFFIX_PLACEHOLDER = "-{{suffix}}";
626
- var BUNDLER_URL = "https://".concat((_a = "2.22.0") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
725
+ var BUNDLER_URL = "https://".concat((_a = "2.22.1") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
627
726
  var SandpackRuntime = /** @class */ (function (_super) {
628
727
  utils.__extends(SandpackRuntime, _super);
629
728
  function SandpackRuntime(selector, sandboxSetup, options) {
@@ -1122,14 +1221,32 @@ var SandpackRuntime = /** @class */ (function (_super) {
1122
1221
  return this.iframeProtocol.channelListen(listener);
1123
1222
  };
1124
1223
  /**
1125
- * Get the URL of the contents of the current sandbox
1224
+ * Get the URL of the contents of the current sandbox.
1225
+ *
1226
+ * R3-367 — GESTURE-GATED. This POSTs the ENTIRE app filesystem to
1227
+ * codesandbox.io from the parent page; the fs may contain the user's data
1228
+ * (spaces, mounts), so the export is reachable only when the user has been
1229
+ * active on the page (`navigator.userActivation.hasBeenActive` — the sticky
1230
+ * flag, because the fs snapshot can outlive a transient-activation window).
1231
+ * Without any user activation this refuses BEFORE reading the filesystem:
1232
+ * nothing is posted, and a `sandpack-security-violation` CustomEvent is
1233
+ * dispatched on the window for the host to journal (site-main wires the
1234
+ * listener into its security-events seam).
1126
1235
  */
1127
1236
  SandpackRuntime.prototype.getCodeSandboxURL = function () {
1128
1237
  return utils.__awaiter(this, void 0, void 0, function () {
1129
1238
  var snapshot, paramFiles, res, sandboxId;
1130
1239
  return utils.__generator(this, function (_a) {
1131
1240
  switch (_a.label) {
1132
- case 0: return [4 /*yield*/, snapshotFS(this.sandboxSetup.fs)];
1241
+ case 0:
1242
+ if (!codeSandboxExportAllowed()) {
1243
+ // No user has ever interacted with this page: an export initiated here
1244
+ // would be app- or script-initiated, not user-initiated. Refuse and make
1245
+ // the refusal observable — BEFORE reading the filesystem.
1246
+ notifyCodeSandboxExportRefused();
1247
+ throw new Error("getCodeSandboxURL requires user activation (R3-367 gesture gate)");
1248
+ }
1249
+ return [4 /*yield*/, snapshotFS(this.sandboxSetup.fs)];
1133
1250
  case 1:
1134
1251
  snapshot = _a.sent();
1135
1252
  paramFiles = Object.keys(snapshot).reduce(function (prev, next) {
@@ -1209,3 +1326,5 @@ function snapshotFS(fs) {
1209
1326
  }
1210
1327
 
1211
1328
  exports.SandpackRuntime = SandpackRuntime;
1329
+ exports.codeSandboxExportAllowed = codeSandboxExportAllowed$1;
1330
+ exports.notifyCodeSandboxExportRefused = notifyCodeSandboxExportRefused$1;
@@ -1,6 +1,6 @@
1
1
  import { _ as __awaiter, a as __generator, h as __assign, i as __rest, j as __spreadArray, g as __extends, f as nullthrows, e as extractErrorDetails, c as createError, d as createPackageJSON } from '../../utils-DG1HA4RZ.mjs';
2
2
  import { dequal } from 'dequal';
3
- import { a as SandpackLogLevel } from '../../types-BgalzxpH.mjs';
3
+ import { a as SandpackLogLevel } from '../../types-DRg992RB.mjs';
4
4
  import { S as SandpackClient } from '../../base-DBh7xJX9.mjs';
5
5
  import { c as createSandboxedIframe, e as ensureSandboxed } from '../../iframe-factory-C8M0b9uf.mjs';
6
6
  import mimeDB from 'mime-db';
@@ -64,6 +64,21 @@ var Protocol = /** @class */ (function () {
64
64
  return Protocol;
65
65
  }());
66
66
 
67
+ // R3-367: crypto-random, 64-bit channel id (16 hex chars from 8 random bytes).
68
+ // This used to be Math.floor(Math.random()*1e6) — guessable, and the id was
69
+ // console.logged at registration. SECURITY INVARIANT (do not weaken): the id is
70
+ // a CORRELATION key only — every incoming message is authenticated by the
71
+ // `event.source === this.frameWindow` check in eventListener() below, NEVER by
72
+ // the id alone. The crypto entropy is defense-in-depth on top of that check
73
+ // (the frame-side twin comment lives in the sandbox bundler's protocol layer,
74
+ // R3-352 C1). `string | number` on the wire stays compatible with ids older
75
+ // peers minted as numbers: both sides compare the echoed value they themselves
76
+ // sent, so the type never has to agree across versions.
77
+ var randomChannelId = function () {
78
+ var bytes = new Uint8Array(8);
79
+ crypto.getRandomValues(bytes);
80
+ return Array.from(bytes, function (b) { return b.toString(16).padStart(2, "0"); }).join("");
81
+ };
67
82
  var IFrameProtocol = /** @class */ (function () {
68
83
  function IFrameProtocol(iframe, _origin) {
69
84
  // React to messages from any iframe
@@ -72,8 +87,9 @@ var IFrameProtocol = /** @class */ (function () {
72
87
  // React to messages from the iframe owned by this instance
73
88
  this.channelListeners = {};
74
89
  this.channelListenersCount = 0;
75
- // Random number to identify this instance of the client when messages are coming from multiple iframes
76
- this.channelId = Math.floor(Math.random() * 1000000);
90
+ // Random id to identify this instance of the client when messages are coming
91
+ // from multiple iframes — crypto-random (see randomChannelId above).
92
+ this.channelId = randomChannelId();
77
93
  this.frameWindow = iframe.contentWindow;
78
94
  this.origin = "*"; //origin;
79
95
  this.globalListeners = [];
@@ -99,8 +115,9 @@ var IFrameProtocol = /** @class */ (function () {
99
115
  if (!this.frameWindow) {
100
116
  return;
101
117
  }
102
- // eslint-disable-next-line no-console -- dev registration trace
103
- console.log("[IFrameProtocol] Registering iframe with channelId", this.channelId, this);
118
+ // R3-367: no channelId log — the id is not secret-critical, but logging it
119
+ // at registration handed an observer the correlation key for free (the
120
+ // evt.source check below is the actual authentication).
104
121
  // Order matters: the bundler reads ports[0] as the fs port and ports[1] as
105
122
  // the Babel worker port. `filter` keeps that order as long as the fs port
106
123
  // is always present (it is, in normal operation).
@@ -151,7 +168,11 @@ var IFrameProtocol = /** @class */ (function () {
151
168
  };
152
169
  // Handles message windows coming from iframes
153
170
  IFrameProtocol.prototype.eventListener = function (evt) {
154
- // skip events originating from different iframes
171
+ // SECURITY INVARIANT (R3-367, twin of the randomChannelId comment): this
172
+ // source check — not the channelId — is what authenticates an incoming
173
+ // message. The id below only routes a message ALREADY accepted here to the
174
+ // owning instance. Weakening or reordering this check would let any iframe
175
+ // on the page speak on the bundler channel.
155
176
  if (evt.source !== this.frameWindow) {
156
177
  return;
157
178
  }
@@ -188,24 +209,50 @@ var IFrameProtocol = /** @class */ (function () {
188
209
  * registers via `registerImmutableUrlPrefix` (see sandpack-bundler
189
210
  * `src/utils/fetch.ts`) — keep the two in sync.
190
211
  */
191
- /**
192
- * URL prefixes whose responses never change for a given URL (the URL encodes
193
- * the exact content version). Only these may be fetched on the iframe's
194
- * behalf, and they are safe to cache forever.
195
- */
196
212
  var IMMUTABLE_URL_ALLOWLIST = [
197
213
  // Module CDN, exact-versioned package bundles. (NOT /dep_tree/, which
198
214
  // resolves semver ranges and changes as new versions publish.)
199
- "https://sandpack-cdn-staging.blazingly.io/package/",
215
+ {
216
+ origin: "https://sandpack-cdn-staging.blazingly.io",
217
+ pathPrefix: "/package/",
218
+ },
200
219
  // unpkg files, requested by the bundler at registry-resolved exact versions.
201
- "https://unpkg.com/",
220
+ { origin: "https://unpkg.com", pathPrefix: "/" },
202
221
  // Self-hosted, versioned @immediately-run/sdk builds (SDK_PACKAGING_SPEC
203
222
  // §5/§11, Option A). The /v/<version>/ path encodes the exact version, so
204
223
  // responses are immutable; the bundler fetches these when an app opts the SDK
205
224
  // into immediately.run.resolveFromRegistry. Keep in sync with the prefix the
206
225
  // bundler registers via registerImmutableUrlPrefix (sandbox bundler.ts).
207
- "https://immediately-run.github.io/immediately-run-sdk/v/",
226
+ {
227
+ origin: "https://immediately-run.github.io",
228
+ pathPrefix: "/immediately-run-sdk/v/",
229
+ },
208
230
  ];
231
+ /**
232
+ * Is a PARSED URL inside the allowlist? Origin is compared exactly (a userinfo
233
+ * or lookalike host cannot match) and the path prefix against the URL parser's
234
+ * NORMALIZED pathname; additionally, any dot segment — literal or
235
+ * percent-encoded, in any hex case — is refused, so no spelling of `..` can
236
+ * cross the prefix boundary after a server-side decode.
237
+ */
238
+ var inPolicy = function (u) {
239
+ var segments = u.pathname.split("/").map(function (segment) {
240
+ try {
241
+ return decodeURIComponent(segment).toLowerCase();
242
+ }
243
+ catch (_a) {
244
+ return segment.toLowerCase();
245
+ }
246
+ });
247
+ if (segments.some(function (s) { return s === ".." || s === "."; }))
248
+ return false;
249
+ if (/%2e/.test(u.pathname.toLowerCase()))
250
+ return false;
251
+ return IMMUTABLE_URL_ALLOWLIST.some(function (_a) {
252
+ var origin = _a.origin, pathPrefix = _a.pathPrefix;
253
+ return u.origin === origin && u.pathname.startsWith(pathPrefix);
254
+ });
255
+ };
209
256
  var IMMUTABLE_CACHE_NAME = "sandpack-immutable-fetch-v1";
210
257
  var serializeResponse = function (res) { return __awaiter(void 0, void 0, void 0, function () {
211
258
  var _a;
@@ -297,14 +344,22 @@ var matchesIntegrity = function (body, integrity) { return __awaiter(void 0, voi
297
344
  */
298
345
  function handleImmutableFetch(url, integrity) {
299
346
  return __awaiter(this, void 0, void 0, function () {
300
- var expected, cache, hit, body, res, result, _a;
347
+ var parsed, expected, cache, hit, body, res, result, _a;
301
348
  return __generator(this, function (_b) {
302
349
  switch (_b.label) {
303
350
  case 0:
304
- if (typeof url !== "string" ||
305
- !IMMUTABLE_URL_ALLOWLIST.some(function (prefix) { return url.startsWith(prefix); })) {
351
+ if (typeof url !== "string") {
306
352
  throw new Error("URL not allowed for immutable fetch: ".concat(String(url)));
307
353
  }
354
+ try {
355
+ parsed = new URL(url);
356
+ }
357
+ catch (_c) {
358
+ throw new Error("URL not allowed for immutable fetch: ".concat(url));
359
+ }
360
+ if (!inPolicy(parsed)) {
361
+ throw new Error("URL not allowed for immutable fetch: ".concat(url));
362
+ }
308
363
  expected = typeof integrity === "string" ? integrity : undefined;
309
364
  return [4 /*yield*/, openCache()];
310
365
  case 1:
@@ -328,12 +383,21 @@ function handleImmutableFetch(url, integrity) {
328
383
  // Stale/poisoned entry: drop it and fall through to a fresh fetch.
329
384
  _b.sent();
330
385
  _b.label = 6;
331
- case 6: return [4 /*yield*/, fetch(url)];
386
+ case 6: return [4 /*yield*/, fetch(parsed)];
332
387
  case 7:
333
388
  res = _b.sent();
334
389
  if (!res.ok) {
335
390
  throw new Error("Immutable fetch failed with status ".concat(res.status, ": ").concat(url));
336
391
  }
392
+ // R3-364: the prefix hosts are exact-version content hosts; a redirect that
393
+ // leaves the allowlist means the bytes did NOT come from an in-policy origin —
394
+ // refuse rather than serve (and never cache) them. The browser follows
395
+ // redirects itself (cross-origin `redirect: 'manual'` responses are opaque),
396
+ // so this is the final-URL check: `res.url` is where the bytes actually came
397
+ // from. A response that reports no URL (test doubles) skips the check.
398
+ if (res.redirected && res.url !== "" && !inPolicy(new URL(res.url))) {
399
+ throw new Error("Immutable fetch redirected outside the allowlist: ".concat(res.url));
400
+ }
337
401
  return [4 /*yield*/, serializeResponse(res)];
338
402
  case 8:
339
403
  result = _b.sent();
@@ -619,9 +683,44 @@ function getExtension(filepath) {
619
683
  }
620
684
  }
621
685
 
686
+ /**
687
+ * R3-367 — the getCodeSandboxURL gesture gate, extracted so it is testable
688
+ * without standing the whole runtime graph up (and so the fork's node-env
689
+ * suites can import it too).
690
+ */
691
+ /**
692
+ * Whether a codesandbox.io export may run: the user has been ACTIVE on this
693
+ * page (sticky activation — the fs snapshot can outlive a transient window).
694
+ * False ⇒ app/script-initiated context ⇒ the export must refuse before
695
+ * reading the filesystem.
696
+ */
697
+ var codeSandboxExportAllowed$1 = function () {
698
+ var _a;
699
+ return ((_a = navigator.userActivation) === null || _a === void 0 ? void 0 : _a.hasBeenActive) === true;
700
+ };
701
+ /**
702
+ * Make a gesture-gate refusal observable: a `sandpack-security-violation`
703
+ * CustomEvent the host (site-main) can listen for and journal into its
704
+ * security-events stream. Never throws — observability must not become a
705
+ * second failure mode on the refusal path.
706
+ */
707
+ var notifyCodeSandboxExportRefused$1 = function () {
708
+ try {
709
+ window.dispatchEvent(new CustomEvent("sandpack-security-violation", {
710
+ detail: {
711
+ kind: "gesture-gate.getCodeSandboxURL",
712
+ reason: "no user activation",
713
+ },
714
+ }));
715
+ }
716
+ catch (_a) {
717
+ /* no window / dispatch unavailable */
718
+ }
719
+ };
720
+
622
721
  var _a;
623
722
  var SUFFIX_PLACEHOLDER = "-{{suffix}}";
624
- var BUNDLER_URL = "https://".concat((_a = "2.22.0") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
723
+ var BUNDLER_URL = "https://".concat((_a = "2.22.1") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
625
724
  var SandpackRuntime = /** @class */ (function (_super) {
626
725
  __extends(SandpackRuntime, _super);
627
726
  function SandpackRuntime(selector, sandboxSetup, options) {
@@ -1120,14 +1219,32 @@ var SandpackRuntime = /** @class */ (function (_super) {
1120
1219
  return this.iframeProtocol.channelListen(listener);
1121
1220
  };
1122
1221
  /**
1123
- * Get the URL of the contents of the current sandbox
1222
+ * Get the URL of the contents of the current sandbox.
1223
+ *
1224
+ * R3-367 — GESTURE-GATED. This POSTs the ENTIRE app filesystem to
1225
+ * codesandbox.io from the parent page; the fs may contain the user's data
1226
+ * (spaces, mounts), so the export is reachable only when the user has been
1227
+ * active on the page (`navigator.userActivation.hasBeenActive` — the sticky
1228
+ * flag, because the fs snapshot can outlive a transient-activation window).
1229
+ * Without any user activation this refuses BEFORE reading the filesystem:
1230
+ * nothing is posted, and a `sandpack-security-violation` CustomEvent is
1231
+ * dispatched on the window for the host to journal (site-main wires the
1232
+ * listener into its security-events seam).
1124
1233
  */
1125
1234
  SandpackRuntime.prototype.getCodeSandboxURL = function () {
1126
1235
  return __awaiter(this, void 0, void 0, function () {
1127
1236
  var snapshot, paramFiles, res, sandboxId;
1128
1237
  return __generator(this, function (_a) {
1129
1238
  switch (_a.label) {
1130
- case 0: return [4 /*yield*/, snapshotFS(this.sandboxSetup.fs)];
1239
+ case 0:
1240
+ if (!codeSandboxExportAllowed()) {
1241
+ // No user has ever interacted with this page: an export initiated here
1242
+ // would be app- or script-initiated, not user-initiated. Refuse and make
1243
+ // the refusal observable — BEFORE reading the filesystem.
1244
+ notifyCodeSandboxExportRefused();
1245
+ throw new Error("getCodeSandboxURL requires user activation (R3-367 gesture gate)");
1246
+ }
1247
+ return [4 /*yield*/, snapshotFS(this.sandboxSetup.fs)];
1131
1248
  case 1:
1132
1249
  snapshot = _a.sent();
1133
1250
  paramFiles = Object.keys(snapshot).reduce(function (prev, next) {
@@ -1206,4 +1323,4 @@ function snapshotFS(fs) {
1206
1323
  });
1207
1324
  }
1208
1325
 
1209
- export { SandpackRuntime };
1326
+ export { SandpackRuntime, codeSandboxExportAllowed$1 as codeSandboxExportAllowed, notifyCodeSandboxExportRefused$1 as notifyCodeSandboxExportRefused };
@@ -1,6 +1,6 @@
1
1
  import { invariant } from 'outvariant';
2
2
  import { c as createError } from './utils-DG1HA4RZ.mjs';
3
- import './types-BgalzxpH.mjs';
3
+ import './types-DRg992RB.mjs';
4
4
 
5
5
  var EventEmitter = /** @class */ (function () {
6
6
  function EventEmitter() {
@@ -2,7 +2,7 @@
2
2
 
3
3
  var outvariant = require('outvariant');
4
4
  var utils = require('./utils-BiVyytui.js');
5
- require('./types-KEkDKvIe.js');
5
+ require('./types-B3g_70H8.js');
6
6
 
7
7
  var EventEmitter = /** @class */ (function () {
8
8
  function EventEmitter() {
@@ -1,10 +1,10 @@
1
1
  import { g as __extends, h as __assign, _ as __awaiter, a as __generator } from './utils-DG1HA4RZ.mjs';
2
2
  import { PreviewController } from 'static-browser-server';
3
- import { E as EventEmitter, g as generateRandomId, c as consoleHook } from './consoleHook-BYuGaxe8.mjs';
3
+ import { E as EventEmitter, g as generateRandomId, c as consoleHook } from './consoleHook-DT9Wd42J.mjs';
4
4
  import { S as SandpackClient } from './base-DBh7xJX9.mjs';
5
5
  import { c as createSandboxedIframe, e as ensureSandboxed } from './iframe-factory-C8M0b9uf.mjs';
6
6
  import 'outvariant';
7
- import './types-BgalzxpH.mjs';
7
+ import './types-DRg992RB.mjs';
8
8
  import '@zenfs/core';
9
9
  import 'dequal';
10
10
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  var utils = require('./utils-BiVyytui.js');
4
4
  var staticBrowserServer = require('static-browser-server');
5
- var consoleHook = require('./consoleHook-BRSVCdB0.js');
5
+ var consoleHook = require('./consoleHook-vrsBdedj.js');
6
6
  var base = require('./base-DelKLlDk.js');
7
7
  var iframeFactory = require('./iframe-factory-Bc7tcyQZ.js');
8
8
  require('outvariant');
9
- require('./types-KEkDKvIe.js');
9
+ require('./types-B3g_70H8.js');
10
10
  require('@zenfs/core');
11
11
  require('dequal');
12
12
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var utils = require('./utils-BiVyytui.js');
4
- var types = require('./types-KEkDKvIe.js');
4
+ var types = require('./types-B3g_70H8.js');
5
5
  require('outvariant');
6
6
  require('@zenfs/core');
7
7
 
@@ -24,7 +24,7 @@ function loadSandpackClient(iframeSelector_1, sandboxSetup_1) {
24
24
  case 2:
25
25
  Client = _c.sent();
26
26
  return [3 /*break*/, 7];
27
- case 3: return [4 /*yield*/, Promise.resolve().then(function () { return require('./index-dX5nvjo5.js'); }).then(function (m) { return m.SandpackStatic; })];
27
+ case 3: return [4 /*yield*/, Promise.resolve().then(function () { return require('./index-Dnq1i5aM.js'); }).then(function (m) { return m.SandpackStatic; })];
28
28
  case 4:
29
29
  Client = _c.sent();
30
30
  return [3 /*break*/, 7];
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { _ as __awaiter, a as __generator } from './utils-DG1HA4RZ.mjs';
2
2
  export { b as addPackageJSONIfNeededToMap, c as createError, d as createPackageJSON, e as extractErrorDetails, n as normalizePath, f as nullthrows } from './utils-DG1HA4RZ.mjs';
3
- export { M as META_PATH, S as SandpackFS, a as SandpackLogLevel } from './types-BgalzxpH.mjs';
3
+ export { M as META_PATH, S as SandpackFS, a as SandpackLogLevel } from './types-DRg992RB.mjs';
4
4
  import 'outvariant';
5
5
  import '@zenfs/core';
6
6
 
@@ -23,7 +23,7 @@ function loadSandpackClient(iframeSelector_1, sandboxSetup_1) {
23
23
  case 2:
24
24
  Client = _c.sent();
25
25
  return [3 /*break*/, 7];
26
- case 3: return [4 /*yield*/, import('./index-CetxPB1x.mjs').then(function (m) { return m.SandpackStatic; })];
26
+ case 3: return [4 /*yield*/, import('./index-CSAGdFtt.mjs').then(function (m) { return m.SandpackStatic; })];
27
27
  case 4:
28
28
  Client = _c.sent();
29
29
  return [3 /*break*/, 7];
@@ -45,7 +45,7 @@ var GUARDED_WRITE_METHODS = [
45
45
  * (roadmap R3-110).
46
46
  *
47
47
  * This is a module-level function (not a class method) and its **only** reference
48
- * is behind the `if (IS_DEV)` branch in the constructor — so once a consumer's
48
+ * is behind the `if (IS_DEV)` branch in {@link ensureGuard} — so once a consumer's
49
49
  * production build folds `IS_DEV` to `false`, the branch and this whole function
50
50
  * tree-shake away (a class method would be retained). No-op in production.
51
51
  */
@@ -73,6 +73,35 @@ function installOutOfBandGuard(fsContext) {
73
73
  _loop_1(method);
74
74
  }
75
75
  }
76
+ /**
77
+ * The true raw write methods per bound context, captured once and shared by every
78
+ * SandpackFS instance that adopts that context. Without this, a second instance's
79
+ * constructor captures the first instance's wrapper as its "raw" method — because the
80
+ * guard has already replaced `fsContext.fs.promises` entries — so SandpackFS's own
81
+ * writes trip the guard once per prior adoption (the R3-614 stacking bug).
82
+ */
83
+ var RAW = new WeakMap();
84
+ /**
85
+ * Return the context's raw write methods, capturing them and installing the out-of-band
86
+ * guard exactly once per context (see {@link installOutOfBandGuard}). Reads `RAW` first,
87
+ * so adopting the same context any number of times never re-wraps, never re-captures a
88
+ * wrapper as raw, and never disarms a sibling instance.
89
+ */
90
+ function ensureGuard(fsContext) {
91
+ var existing = RAW.get(fsContext);
92
+ if (existing)
93
+ return existing;
94
+ var p = fsContext.fs.promises;
95
+ var raw = {
96
+ writeFile: p.writeFile.bind(p),
97
+ unlink: p.unlink.bind(p),
98
+ mkdir: p.mkdir.bind(p),
99
+ };
100
+ RAW.set(fsContext, raw);
101
+ if (IS_DEV)
102
+ installOutOfBandGuard(fsContext);
103
+ return raw;
104
+ }
76
105
  var mountCounter = 0;
77
106
  var normalize = function (path) {
78
107
  return path.startsWith("/") ? path : "/".concat(path);
@@ -119,12 +148,10 @@ var SandpackFS = /** @class */ (function () {
119
148
  * unmount in {@link dispose}. Unset for an adopted context, whose lifecycle
120
149
  * belongs to the caller. */
121
150
  this.ownedMountPoint = undefined;
122
- var p = fsContext.fs.promises;
123
- this.rawWriteFile = p.writeFile.bind(p);
124
- this.rawUnlink = p.unlink.bind(p);
125
- this.rawMkdir = p.mkdir.bind(p);
126
- if (IS_DEV)
127
- installOutOfBandGuard(fsContext);
151
+ var raw = ensureGuard(fsContext);
152
+ this.rawWriteFile = raw.writeFile;
153
+ this.rawUnlink = raw.unlink;
154
+ this.rawMkdir = raw.mkdir;
128
155
  }
129
156
  /**
130
157
  * Create the `MessagePort` shared with the child iframe, wiring the iframe's
@@ -43,7 +43,7 @@ var GUARDED_WRITE_METHODS = [
43
43
  * (roadmap R3-110).
44
44
  *
45
45
  * This is a module-level function (not a class method) and its **only** reference
46
- * is behind the `if (IS_DEV)` branch in the constructor — so once a consumer's
46
+ * is behind the `if (IS_DEV)` branch in {@link ensureGuard} — so once a consumer's
47
47
  * production build folds `IS_DEV` to `false`, the branch and this whole function
48
48
  * tree-shake away (a class method would be retained). No-op in production.
49
49
  */
@@ -71,6 +71,35 @@ function installOutOfBandGuard(fsContext) {
71
71
  _loop_1(method);
72
72
  }
73
73
  }
74
+ /**
75
+ * The true raw write methods per bound context, captured once and shared by every
76
+ * SandpackFS instance that adopts that context. Without this, a second instance's
77
+ * constructor captures the first instance's wrapper as its "raw" method — because the
78
+ * guard has already replaced `fsContext.fs.promises` entries — so SandpackFS's own
79
+ * writes trip the guard once per prior adoption (the R3-614 stacking bug).
80
+ */
81
+ var RAW = new WeakMap();
82
+ /**
83
+ * Return the context's raw write methods, capturing them and installing the out-of-band
84
+ * guard exactly once per context (see {@link installOutOfBandGuard}). Reads `RAW` first,
85
+ * so adopting the same context any number of times never re-wraps, never re-captures a
86
+ * wrapper as raw, and never disarms a sibling instance.
87
+ */
88
+ function ensureGuard(fsContext) {
89
+ var existing = RAW.get(fsContext);
90
+ if (existing)
91
+ return existing;
92
+ var p = fsContext.fs.promises;
93
+ var raw = {
94
+ writeFile: p.writeFile.bind(p),
95
+ unlink: p.unlink.bind(p),
96
+ mkdir: p.mkdir.bind(p),
97
+ };
98
+ RAW.set(fsContext, raw);
99
+ if (IS_DEV)
100
+ installOutOfBandGuard(fsContext);
101
+ return raw;
102
+ }
74
103
  var mountCounter = 0;
75
104
  var normalize = function (path) {
76
105
  return path.startsWith("/") ? path : "/".concat(path);
@@ -117,12 +146,10 @@ var SandpackFS = /** @class */ (function () {
117
146
  * unmount in {@link dispose}. Unset for an adopted context, whose lifecycle
118
147
  * belongs to the caller. */
119
148
  this.ownedMountPoint = undefined;
120
- var p = fsContext.fs.promises;
121
- this.rawWriteFile = p.writeFile.bind(p);
122
- this.rawUnlink = p.unlink.bind(p);
123
- this.rawMkdir = p.mkdir.bind(p);
124
- if (IS_DEV)
125
- installOutOfBandGuard(fsContext);
149
+ var raw = ensureGuard(fsContext);
150
+ this.rawWriteFile = raw.writeFile;
151
+ this.rawUnlink = raw.unlink;
152
+ this.rawMkdir = raw.mkdir;
126
153
  }
127
154
  /**
128
155
  * Create the `MessagePort` shared with the child iframe, wiring the iframe's
package/dist/types.d.ts CHANGED
@@ -251,7 +251,13 @@ export type ClientStatus = "initializing" | "installing-dependencies" | "transpi
251
251
  export type SandpackMessageConsoleMethods = "log" | "debug" | "info" | "warn" | "error" | "table" | "clear" | "time" | "timeEnd" | "count" | "assert";
252
252
  export interface BaseSandpackMessage {
253
253
  type: string;
254
- $id?: number;
254
+ /**
255
+ * The sender's channel id. Newer peers mint it as a crypto-random hex STRING
256
+ * (R3-367 — 64 bits); ids older peers minted as numbers still flow, because
257
+ * each side compares the value it itself sent. Correlation only — never an
258
+ * authenticator (the evt.source check in IFrameProtocol is).
259
+ */
260
+ $id?: string | number;
255
261
  codesandbox?: boolean;
256
262
  /**
257
263
  * Transferable objects (e.g. a `MessagePort`) to hand to the iframe alongside
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sandpack-client",
3
- "version": "2.22.0",
3
+ "version": "2.22.1",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {