@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.
- package/dist/.ir-build-stamp.json +3 -3
- package/dist/clients/node/index.js +2 -2
- package/dist/clients/node/index.mjs +2 -2
- package/dist/clients/runtime/gestureGate.d.ts +19 -0
- package/dist/clients/runtime/iframe-protocol.d.ts +1 -1
- package/dist/clients/runtime/index.d.ts +33 -1
- package/dist/clients/runtime/index.js +297 -21
- package/dist/clients/runtime/index.mjs +296 -22
- package/dist/clients/runtime/initialization-guard.d.ts +64 -0
- package/dist/{consoleHook-BYuGaxe8.mjs → consoleHook-DT9Wd42J.mjs} +1 -1
- package/dist/{consoleHook-BRSVCdB0.js → consoleHook-vrsBdedj.js} +1 -1
- package/dist/{index-CetxPB1x.mjs → index-CSAGdFtt.mjs} +2 -2
- package/dist/{index-dX5nvjo5.js → index-Dnq1i5aM.js} +2 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{types-KEkDKvIe.js → types-B3g_70H8.js} +34 -7
- package/dist/{types-BgalzxpH.mjs → types-DRg992RB.mjs} +34 -7
- package/dist/types.d.ts +19 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandpack-client",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"srcHash": "
|
|
5
|
-
"builtAt": "2026-
|
|
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-
|
|
5
|
+
require('../../types-B3g_70H8.js');
|
|
6
6
|
var base = require('../../base-DelKLlDk.js');
|
|
7
|
-
var consoleHook = require('../../consoleHook-
|
|
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-
|
|
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-
|
|
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:
|
|
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-
|
|
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
|
|
78
|
-
|
|
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
|
-
//
|
|
105
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
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;
|