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