@immediately-run/sandpack-client 2.21.1 → 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.21.1",
4
- "srcHash": "89d50cca03a74c4165b1fc5bdb8b0056beb6c7922b7938dc996456632b144338",
5
- "builtAt": "2026-08-16T19:29:38.984Z"
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;
@@ -17,8 +18,19 @@ export declare class SandpackRuntime extends SandpackClient {
17
18
  unsubscribeFsWatcher?: () => void;
18
19
  iframeProtocol: IFrameProtocol;
19
20
  fs: SandpackFS;
21
+ /** Set once the teardown below has run, so a rogue document posting in a loop
22
+ * cannot re-run `destroy()` or re-fire the host callback. */
23
+ private refusedBoot;
20
24
  /** Parent-owned Babel transpiler worker, connected to the iframe by port. */
21
25
  private babelWorker;
26
+ /**
27
+ * Which `initialized` messages this client will honour (R3-353). Armed by
28
+ * {@link setLocationURLIntoIFrame} — i.e. by every navigation THIS CLIENT
29
+ * causes — and spent when a boot arrives. A boot the host did not cause finds
30
+ * nothing armed, which is the whole check. See `initialization-guard.ts` for
31
+ * why the parent asks "did I put you there?" rather than "where are you?".
32
+ */
33
+ private readonly initGuard;
22
34
  constructor(selector: string | HTMLIFrameElement, sandboxSetup: SandboxSetup, options?: ClientOptions);
23
35
  private createBundlerURL;
24
36
  /**
@@ -33,6 +45,16 @@ export declare class SandpackRuntime extends SandpackClient {
33
45
  private serviceWorkerHandshake;
34
46
  private handleWorkerRequest;
35
47
  setLocationURLIntoIFrame(): void;
48
+ /**
49
+ * Spend one armed `initialized`, or refuse this boot (R3-353).
50
+ *
51
+ * Refusing is terminal for this client: the frame is blanked so the rogue
52
+ * document — which still holds whatever it scraped before navigating — stops
53
+ * executing, the client detaches, and the host is told through
54
+ * `onUnexpectedNavigation` so it can surface or re-create the frame. Nothing is
55
+ * connected and nothing is registered, so no fs port is ever minted for it.
56
+ */
57
+ private consumeExpectedInitialization;
36
58
  destroy(): void;
37
59
  updateOptions(options: ClientOptions): void;
38
60
  /**
@@ -60,7 +82,17 @@ export declare class SandpackRuntime extends SandpackClient {
60
82
  dispatch(message: SandpackRuntimeMessage): void;
61
83
  listen(listener: ListenerFunction): UnsubscribeFunction;
62
84
  /**
63
- * 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).
64
96
  */
65
97
  getCodeSandboxURL(): Promise<{
66
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();
@@ -364,6 +428,99 @@ function handleImmutableFetch(url, integrity) {
364
428
  });
365
429
  }
366
430
 
431
+ /**
432
+ * The re-register guard (R3-353; `TRUST_MODES_SPEC` §6, `UI_AS_APPS_SPEC` §G1a).
433
+ *
434
+ * ## What it defends
435
+ *
436
+ * A sandboxed frame may always navigate **itself** — no sandbox flag governs
437
+ * that, and `navigate-to` was dropped from CSP3, so neither half of the M3
438
+ * containment can prevent it. The finding that led here reads that as an egress
439
+ * problem (the M3 CSP travels with the birth document, so a frame that
440
+ * re-births itself at the policy-free baseline document gets unrestricted
441
+ * `connect-src` back). The bigger half is that **the host relationship travels
442
+ * with the document too, and the browsing context does not change**:
443
+ *
444
+ * - `iframe.contentWindow` returns the SAME `WindowProxy` across a navigation,
445
+ * so `IFrameProtocol`'s `evt.source !== this.frameWindow` intake check — which
446
+ * is correct, and is the only identity the parent has — still passes;
447
+ * - the client's `initialized` handler is a plain listener with no notion of how
448
+ * many boots it has seen, so it re-runs `fs.connectRemote()` and `register(…)`;
449
+ * - whatever document is in the frame now — the baseline bundler document, or a
450
+ * page on an origin the app chose — is therefore handed a **fresh fs port** and
451
+ * a fresh registration, inheriting the frame's grants.
452
+ *
453
+ * So the escalation is not "a one-shot GET carrying a small secret" (the residual
454
+ * `TRUST_MODES_SPEC` §6 books); it is a persistent execution context, possibly at
455
+ * an attacker's own origin, still attached to the host with the victim frame's
456
+ * authority.
457
+ *
458
+ * ## Why it is shaped as a counter
459
+ *
460
+ * The parent cannot read a cross-origin frame's `location`, so it cannot ask
461
+ * *"where are you?"*. It can ask *"did I put you there?"* — the same question,
462
+ * and one it can answer without reading anything: every legitimate boot follows a
463
+ * navigation the CLIENT performed (its constructor, and its `refresh` dispatch,
464
+ * both through `setLocationURLIntoIFrame`). Arm on navigate, spend on boot; a
465
+ * boot with nothing armed was not ours.
466
+ *
467
+ * A counter rather than a boolean because a rapid navigate–navigate–boot–boot
468
+ * sequence is legitimate and must not eat its own credit. A latch rather than
469
+ * decrement-below-zero because a refusal is terminal: once a rogue document has
470
+ * been refused, nothing it posts may re-arm anything.
471
+ *
472
+ * Kept framework-free and separate from `SandpackRuntime` so the decision can be
473
+ * driven directly by tests — the client itself needs a real `SandpackFS`, a Babel
474
+ * worker and a live iframe to construct.
475
+ */
476
+ var InitializationGuard = /** @class */ (function () {
477
+ function InitializationGuard() {
478
+ this.expected = 0;
479
+ this.detached = false;
480
+ }
481
+ /** Record that the host has navigated the frame, so ONE boot is now expected. */
482
+ InitializationGuard.prototype.arm = function () {
483
+ if (this.detached)
484
+ return;
485
+ this.expected++;
486
+ };
487
+ /**
488
+ * Spend one armed boot. Returns `false` when this boot was not caused by the
489
+ * host — the caller must then refuse to connect or register anything.
490
+ *
491
+ * The first `false` **detaches** the guard permanently: every later call
492
+ * returns `false` too, including after an `arm()`, so a refused frame cannot be
493
+ * brought back by any sequence of messages.
494
+ */
495
+ InitializationGuard.prototype.consume = function () {
496
+ if (this.detached)
497
+ return false;
498
+ if (this.expected > 0) {
499
+ this.expected--;
500
+ return true;
501
+ }
502
+ this.detached = true;
503
+ return false;
504
+ };
505
+ Object.defineProperty(InitializationGuard.prototype, "isDetached", {
506
+ /** True once an unexpected boot has been refused. Terminal. */
507
+ get: function () {
508
+ return this.detached;
509
+ },
510
+ enumerable: false,
511
+ configurable: true
512
+ });
513
+ Object.defineProperty(InitializationGuard.prototype, "pending", {
514
+ /** How many host-initiated boots are still outstanding (diagnostics/tests). */
515
+ get: function () {
516
+ return this.expected;
517
+ },
518
+ enumerable: false,
519
+ configurable: true
520
+ });
521
+ return InitializationGuard;
522
+ }());
523
+
367
524
  var extensionMap = new Map();
368
525
  var entries = Object.entries(mimeDB);
369
526
  for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
@@ -528,16 +685,62 @@ function getExtension(filepath) {
528
685
  }
529
686
  }
530
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
+
531
723
  var _a;
532
724
  var SUFFIX_PLACEHOLDER = "-{{suffix}}";
533
- var BUNDLER_URL = "https://".concat((_a = "2.21.1") === 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/");
534
726
  var SandpackRuntime = /** @class */ (function (_super) {
535
727
  utils.__extends(SandpackRuntime, _super);
536
728
  function SandpackRuntime(selector, sandboxSetup, options) {
537
729
  if (options === void 0) { options = {}; }
538
730
  var _this = _super.call(this, selector, sandboxSetup, options) || this;
731
+ /** Set once the teardown below has run, so a rogue document posting in a loop
732
+ * cannot re-run `destroy()` or re-fire the host callback. */
733
+ _this.refusedBoot = false;
539
734
  /** Parent-owned Babel transpiler worker, connected to the iframe by port. */
540
735
  _this.babelWorker = null;
736
+ /**
737
+ * Which `initialized` messages this client will honour (R3-353). Armed by
738
+ * {@link setLocationURLIntoIFrame} — i.e. by every navigation THIS CLIENT
739
+ * causes — and spent when a boot arrives. A boot the host did not cause finds
740
+ * nothing armed, which is the whole check. See `initialization-guard.ts` for
741
+ * why the parent asks "did I put you there?" rather than "where are you?".
742
+ */
743
+ _this.initGuard = new InitializationGuard();
541
744
  _this.getTranspilerContext = function () {
542
745
  return new Promise(function (resolve) {
543
746
  var unsubscribe = _this.listen(function (message) {
@@ -592,6 +795,26 @@ var SandpackRuntime = /** @class */ (function (_super) {
592
795
  if (mes.type !== "initialized" || !_this.iframe.contentWindow) {
593
796
  return;
594
797
  }
798
+ // R3-353 — the re-register guard. A sandboxed frame may always navigate
799
+ // ITSELF (no sandbox flag governs that), and after it does, the browsing
800
+ // context is the same one: `iframe.contentWindow` returns the same
801
+ // WindowProxy, so `IFrameProtocol`'s `evt.source !== this.frameWindow`
802
+ // check still passes. Whatever document is in the frame now — the
803
+ // policy-free baseline bundler document, or a page on an origin the app
804
+ // chose — can therefore post `initialized` and be handed a FRESH fs port
805
+ // and a fresh registration, inheriting this frame's grants.
806
+ //
807
+ // That is the escalation, and it is bigger than the CSP loss the finding
808
+ // leads with: the CSP travels with the document, but so does the host
809
+ // RELATIONSHIP, and the relationship is worth more.
810
+ //
811
+ // The host cannot read a cross-origin frame's location, so it cannot ask
812
+ // "where are you?". It can ask "did I put you there?" — which is the same
813
+ // question and one it can answer: every legitimate (re)boot follows a
814
+ // navigation THIS CLIENT performed (the constructor, and the `refresh`
815
+ // dispatch), both of which go through `setLocationURLIntoIFrame`.
816
+ if (!_this.consumeExpectedInitialization())
817
+ return;
595
818
  // this may not work with a boundedcontext, it may require an actual FS instance
596
819
  var remotePortPromise = _this.fs.connectRemote();
597
820
  remotePortPromise.then(function (remotePort) { return utils.__awaiter(_this, void 0, void 0, function () {
@@ -835,9 +1058,42 @@ var SandpackRuntime = /** @class */ (function (_super) {
835
1058
  var urlSource = this.options.startRoute
836
1059
  ? new URL(this.options.startRoute, this.bundlerURL).toString()
837
1060
  : this.bundlerURL;
1061
+ // Arm one expected `initialized` (R3-353): this navigation is host-initiated,
1062
+ // so the boot that follows it is legitimate. Every legitimate (re)boot in the
1063
+ // system passes through here — the constructor and the `refresh` dispatch —
1064
+ // which is exactly what makes "nothing armed" mean "not ours".
1065
+ this.initGuard.arm();
838
1066
  (_a = this.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.location.replace(urlSource);
839
1067
  this.iframe.src = urlSource;
840
1068
  };
1069
+ /**
1070
+ * Spend one armed `initialized`, or refuse this boot (R3-353).
1071
+ *
1072
+ * Refusing is terminal for this client: the frame is blanked so the rogue
1073
+ * document — which still holds whatever it scraped before navigating — stops
1074
+ * executing, the client detaches, and the host is told through
1075
+ * `onUnexpectedNavigation` so it can surface or re-create the frame. Nothing is
1076
+ * connected and nothing is registered, so no fs port is ever minted for it.
1077
+ */
1078
+ SandpackRuntime.prototype.consumeExpectedInitialization = function () {
1079
+ var _a, _b;
1080
+ if (this.initGuard.consume())
1081
+ return true;
1082
+ if (this.refusedBoot)
1083
+ return false; // already torn down; stay quiet
1084
+ this.refusedBoot = true;
1085
+ console.error("[Sandpack] Refusing to register a frame that booted from a navigation " +
1086
+ "this client did not perform (R3-353). The frame is being torn down.");
1087
+ try {
1088
+ this.iframe.src = "about:blank";
1089
+ }
1090
+ catch (_c) {
1091
+ /* the element may already be gone */
1092
+ }
1093
+ this.destroy();
1094
+ (_b = (_a = this.options).onUnexpectedNavigation) === null || _b === void 0 ? void 0 : _b.call(_a);
1095
+ return false;
1096
+ };
841
1097
  SandpackRuntime.prototype.destroy = function () {
842
1098
  var _a, _b, _c, _d;
843
1099
  this.unsubscribeChannelListener();
@@ -965,14 +1221,32 @@ var SandpackRuntime = /** @class */ (function (_super) {
965
1221
  return this.iframeProtocol.channelListen(listener);
966
1222
  };
967
1223
  /**
968
- * 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).
969
1235
  */
970
1236
  SandpackRuntime.prototype.getCodeSandboxURL = function () {
971
1237
  return utils.__awaiter(this, void 0, void 0, function () {
972
1238
  var snapshot, paramFiles, res, sandboxId;
973
1239
  return utils.__generator(this, function (_a) {
974
1240
  switch (_a.label) {
975
- 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)];
976
1250
  case 1:
977
1251
  snapshot = _a.sent();
978
1252
  paramFiles = Object.keys(snapshot).reduce(function (prev, next) {
@@ -1052,3 +1326,5 @@ function snapshotFS(fs) {
1052
1326
  }
1053
1327
 
1054
1328
  exports.SandpackRuntime = SandpackRuntime;
1329
+ exports.codeSandboxExportAllowed = codeSandboxExportAllowed$1;
1330
+ exports.notifyCodeSandboxExportRefused = notifyCodeSandboxExportRefused$1;