@bytecodealliance/preview2-shim 0.25.0 → 0.26.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/README.md CHANGED
@@ -107,8 +107,7 @@ const shim = new WASIShim({
107
107
  ```
108
108
 
109
109
  This keeps permission prompts, handle acquisition, persistence, and synchronization policy in
110
- application code. Raw TCP, UDP, and DNS are denied by default; outbound HTTP remains a separate
111
- `fetch` capability.
110
+ application code. `sandbox.enableNetwork: false` denies raw TCP, UDP, DNS, and outbound HTTP.
112
111
 
113
112
  Deterministic in-memory transports are available for components that act as servers and for tests:
114
113
 
@@ -240,9 +239,8 @@ const component = await instantiate(loader, sandboxedShim.getImportObject());
240
239
  - The direct preopen functions (`_setPreopens`, `_clearPreopens`, etc.) modify global state and
241
240
  affect all components not using `WASIShim` with explicit configuration. For isolation, prefer
242
241
  using `WASIShim` with the `sandbox` option containing `preopens` and `env`.
243
- - When `sandbox.enableNetwork: false`, Node.js socket operations receive an instance-local denied
244
- network capability. Outbound HTTP is a separate Fetch capability; replace or omit the HTTP
245
- namespace when the embedding must deny it as well.
242
+ - When `sandbox.enableNetwork: false`, socket operations and outbound HTTP receive instance-local
243
+ denied network capabilities.
246
244
 
247
245
  [jco]: https://www.npmjs.com/package/@bytecodealliance/jco
248
246
 
@@ -180,6 +180,17 @@ export class WASIShim {
180
180
  };
181
181
  }
182
182
  this.#http = shims?.http ?? wasi.http;
183
+ if (sandbox?.enableNetwork === false) {
184
+ this.#http = {
185
+ ...this.#http,
186
+ outgoingHandler: {
187
+ ...this.#http.outgoingHandler,
188
+ handle() {
189
+ throw "access-denied";
190
+ },
191
+ },
192
+ };
193
+ }
183
194
  if (shims?.incomingHandler) {
184
195
  if (!this.#http.createIncomingHandler) {
185
196
  throw new TypeError("the selected HTTP shim does not support Web incoming handlers");
@@ -95,11 +95,10 @@ export declare function _setPreopens(preopens: any): void;
95
95
  export declare function _addPreopen(virtualPath: any, hostPreopen: any): void;
96
96
  /**
97
97
  * Clear all preopens, giving the guest no filesystem access.
98
- * Call this immediately after import to disable default full filesystem access.
99
98
  *
100
99
  * @example
101
100
  * import { _clearPreopens } from '@bytecodealliance/preview2-shim/filesystem';
102
- * _clearPreopens(); // Now guest has no filesystem access by default
101
+ * _clearPreopens();
103
102
  */
104
103
  export declare function _clearPreopens(): void;
105
104
  /**
@@ -1,10 +1,16 @@
1
1
  import { earlyDispose, inputStreamCreate, ioCall, outputStreamCreate, registerDispose, } from "../io/worker-io.js";
2
2
  import { FILE, FILESYSTEM_DESCRIPTOR_ADVISE, FILESYSTEM_DESCRIPTOR_CLOSE, FILESYSTEM_DESCRIPTOR_GET_TYPE, FILESYSTEM_DESCRIPTOR_METADATA_HASH, FILESYSTEM_DESCRIPTOR_OPEN, FILESYSTEM_DESCRIPTOR_READ, FILESYSTEM_DESCRIPTOR_SET_SIZE, FILESYSTEM_DESCRIPTOR_SET_TIMES, FILESYSTEM_DESCRIPTOR_STAT, FILESYSTEM_DESCRIPTOR_SYNC, FILESYSTEM_DESCRIPTOR_SYNC_DATA, FILESYSTEM_DESCRIPTOR_WRITE, INPUT_STREAM_CREATE, OUTPUT_STREAM_CREATE, } from "../io/calls.js";
3
3
  import { constants, linkSync, lstatSync, lutimesSync, mkdirSync, opendirSync, readlinkSync, realpathSync, renameSync, rmdirSync, statSync, symlinkSync, unlinkSync, utimesSync, } from "node:fs";
4
+ import { posix, win32 } from "node:path";
4
5
  import { platform } from "node:process";
5
6
  const symbolDispose = Symbol.dispose || Symbol.for("dispose");
6
7
  const isWindows = platform === "win32";
7
8
  const isMac = platform === "darwin";
9
+ const hostPath = isWindows ? win32 : posix;
10
+ const MAX_SYMLINKS = 40;
11
+ function normalizeHostPath(path) {
12
+ return isWindows ? path.replace(/\\/g, "/") : path;
13
+ }
8
14
  const nsMagnitude = 1000000000n;
9
15
  function nsToDateTime(ns) {
10
16
  const seconds = ns / nsMagnitude;
@@ -226,7 +232,7 @@ class Descriptor {
226
232
  }
227
233
  }
228
234
  createDirectoryAt(path) {
229
- const fullPath = this.#getFullPath(path);
235
+ const fullPath = this.#getFullPath(path, false);
230
236
  try {
231
237
  mkdirSync(fullPath);
232
238
  }
@@ -256,7 +262,7 @@ class Descriptor {
256
262
  };
257
263
  }
258
264
  statAt(pathFlags, path) {
259
- const fullPath = this.#getFullPath(path);
265
+ const fullPath = this.#getFullPath(path, pathFlags.symlinkFollow);
260
266
  let stats;
261
267
  try {
262
268
  stats = (pathFlags.symlinkFollow ? statSync : lstatSync)(fullPath, {
@@ -277,7 +283,7 @@ class Descriptor {
277
283
  };
278
284
  }
279
285
  setTimesAt(pathFlags, path, dataAccessTimestamp, dataModificationTimestamp) {
280
- const fullPath = this.#getFullPath(path);
286
+ const fullPath = this.#getFullPath(path, pathFlags.symlinkFollow);
281
287
  let stats;
282
288
  if (dataAccessTimestamp.tag === "no-change" ||
283
289
  dataModificationTimestamp.tag === "no-change") {
@@ -288,7 +294,7 @@ class Descriptor {
288
294
  if (!pathFlags.symlinkFollow && !lutimesSync) {
289
295
  throw new Error("Changing the timestamps of symlinks isn't supported");
290
296
  }
291
- let metadataSetFn = pathFlags.symlinkFollow ? lutimesSync : utimesSync;
297
+ let metadataSetFn = pathFlags.symlinkFollow ? utimesSync : lutimesSync;
292
298
  try {
293
299
  metadataSetFn(fullPath, atime / 1000, mtime / 1000);
294
300
  }
@@ -297,42 +303,21 @@ class Descriptor {
297
303
  }
298
304
  }
299
305
  linkAt(oldPathFlags, oldPath, newDescriptor, newPath) {
300
- const oldFullPath = this.#getFullPath(oldPath);
301
- const newFullPath = newDescriptor.#getFullPath(newPath);
306
+ const oldFullPath = this.#getFullPath(oldPath, oldPathFlags.symlinkFollow);
307
+ const newFullPath = newDescriptor.#getFullPath(newPath, false);
302
308
  // Windows doesn't automatically fail on trailing slashes
303
309
  if (isWindows && newFullPath.endsWith("/")) {
304
310
  throw "no-entry";
305
311
  }
306
312
  try {
307
- // Unlike open/stat, Node's link does not follow the final symlink.
308
- // Resolve only this operation's source, propagating resolution errors
309
- // instead of silently hard-linking a dangling or cyclic symlink.
310
- let source = oldFullPath;
311
- if (oldPathFlags.symlinkFollow) {
312
- source = realpathSync(source);
313
- // realpath strips trailing slashes; keep the caller's directory
314
- // requirement so a regular-file source ending in '/' still fails.
315
- if (oldFullPath.endsWith("/")) {
316
- // Windows may ignore a trailing slash even in linkSync.
317
- if (!statSync(source).isDirectory()) {
318
- throw "not-directory";
319
- }
320
- if (!source.endsWith("/")) {
321
- source += "/";
322
- }
323
- }
324
- }
325
- linkSync(source, newFullPath);
313
+ linkSync(oldFullPath, newFullPath);
326
314
  }
327
315
  catch (e) {
328
316
  throw convertFsError(e);
329
317
  }
330
318
  }
331
319
  openAt(pathFlags, path, openFlags, descriptorFlags) {
332
- if (preopenEntries.length === 0) {
333
- throw "access";
334
- }
335
- const fullPath = this.#getFullPath(path);
320
+ const fullPath = this.#getFullPath(path, pathFlags.symlinkFollow);
336
321
  let fsOpenFlags = 0x0;
337
322
  if (openFlags.create) {
338
323
  fsOpenFlags |= constants.O_CREAT;
@@ -426,16 +411,17 @@ class Descriptor {
426
411
  }
427
412
  }
428
413
  readlinkAt(path) {
429
- const fullPath = this.#getFullPath(path);
414
+ const fullPath = this.#getFullPath(path, false);
430
415
  try {
431
- return readlinkSync(fullPath);
416
+ const target = readlinkSync(fullPath);
417
+ return isWindows ? target.replace(/\\/g, "/") : target;
432
418
  }
433
419
  catch (e) {
434
420
  throw convertFsError(e);
435
421
  }
436
422
  }
437
423
  removeDirectoryAt(path) {
438
- const fullPath = this.#getFullPath(path);
424
+ const fullPath = this.#getFullPath(path, false);
439
425
  try {
440
426
  rmdirSync(fullPath);
441
427
  }
@@ -447,8 +433,8 @@ class Descriptor {
447
433
  }
448
434
  }
449
435
  renameAt(oldPath, newDescriptor, newPath) {
450
- const oldFullPath = this.#getFullPath(oldPath);
451
- const newFullPath = newDescriptor.#getFullPath(newPath);
436
+ const oldFullPath = this.#getFullPath(oldPath, false);
437
+ const newFullPath = newDescriptor.#getFullPath(newPath, false);
452
438
  try {
453
439
  renameSync(oldFullPath, newFullPath);
454
440
  }
@@ -460,7 +446,7 @@ class Descriptor {
460
446
  }
461
447
  }
462
448
  symlinkAt(target, path) {
463
- const fullPath = this.#getFullPath(path);
449
+ const fullPath = this.#getFullPath(path, false);
464
450
  if (target.startsWith("/")) {
465
451
  throw "not-permitted";
466
452
  }
@@ -489,7 +475,7 @@ class Descriptor {
489
475
  }
490
476
  }
491
477
  unlinkFileAt(path) {
492
- const fullPath = this.#getFullPath(path);
478
+ const fullPath = this.#getFullPath(path, false);
493
479
  try {
494
480
  if (fullPath.endsWith("/")) {
495
481
  let isDir = false;
@@ -531,7 +517,7 @@ class Descriptor {
531
517
  }
532
518
  }
533
519
  metadataHashAt(pathFlags, path) {
534
- const fullPath = this.#getFullPath(path);
520
+ const fullPath = this.#getFullPath(path, pathFlags.symlinkFollow);
535
521
  try {
536
522
  const stats = (pathFlags.symlinkFollow ? statSync : lstatSync)(fullPath, {
537
523
  bigint: true,
@@ -542,70 +528,100 @@ class Descriptor {
542
528
  throw convertFsError(e);
543
529
  }
544
530
  }
545
- // Join paths lexically; each operation supplies its own symlink semantics.
546
- #getFullPath(subpath) {
547
- let descriptor = this;
548
- if (subpath.indexOf("\\") !== -1) {
549
- subpath = subpath.replace(/\\/g, "/");
550
- }
551
- if (subpath.indexOf("//") !== -1) {
552
- subpath = subpath.replace(/\/\/+/g, "/");
553
- }
554
- if (subpath[0] === "/") {
531
+ #getFullPath(subpath, followFinalSymlink) {
532
+ subpath = subpath.replace(/\\/g, "/").replace(/\/\/+/g, "/");
533
+ if (subpath.startsWith("/")) {
555
534
  throw "not-permitted";
556
535
  }
557
- // segment resolution
536
+ const trailingSlash = subpath.endsWith("/");
558
537
  const segments = [];
559
- let segmentIndex = -1;
560
- for (let i = 0; i < subpath.length; i++) {
561
- // busy reading a segment - only terminate on '/'
562
- if (segmentIndex !== -1) {
563
- if (subpath[i] === "/") {
564
- segments.push(subpath.slice(segmentIndex, i + 1));
565
- segmentIndex = -1;
566
- }
538
+ for (const segment of subpath.split("/")) {
539
+ if (segment === "" || segment === ".") {
567
540
  continue;
568
541
  }
569
- // new segment - check if it is relative
570
- else if (subpath[i] === ".") {
571
- // ../ segment
572
- if (subpath[i + 1] === "." &&
573
- (subpath[i + 2] === "/" || i + 2 === subpath.length)) {
574
- if (segments.pop() === undefined) {
575
- throw "not-permitted";
576
- }
577
- i += 2;
578
- continue;
542
+ if (segment === "..") {
543
+ if (segments.pop() === undefined) {
544
+ throw "not-permitted";
579
545
  }
580
- // ./ segment
581
- else if (subpath[i + 1] === "/" || i + 1 === subpath.length) {
582
- i += 1;
583
- continue;
546
+ }
547
+ else {
548
+ segments.push(segment);
549
+ }
550
+ }
551
+ const base = this.#hostPreopen ?? this.#fullPath;
552
+ // The default Windows preopen intentionally represents every drive and
553
+ // UNC share, so there is no narrower host boundary to enforce.
554
+ if (isWindows && this.#hostPreopen === "//") {
555
+ const normalized = segments.join("/");
556
+ const fullPath = /^[a-zA-Z]:\//.test(normalized)
557
+ ? `//?/${normalized}`
558
+ : `//${normalized}`;
559
+ return trailingSlash && normalized ? `${fullPath}/` : fullPath;
560
+ }
561
+ let baseResolved;
562
+ try {
563
+ baseResolved = normalizeHostPath(realpathSync(base));
564
+ }
565
+ catch (error) {
566
+ throw convertFsError(error);
567
+ }
568
+ let current = baseResolved;
569
+ let pending = [...segments];
570
+ let followedSymlinks = 0;
571
+ while (pending.length > 0) {
572
+ const segment = pending.shift();
573
+ const candidate = normalizeHostPath(hostPath.join(current, segment));
574
+ let stats;
575
+ try {
576
+ stats = lstatSync(candidate);
577
+ }
578
+ catch (error) {
579
+ if (error.code === "ENOENT") {
580
+ const unresolved = normalizeHostPath(hostPath.join(candidate, ...pending));
581
+ return trailingSlash ? `${unresolved}/` : unresolved;
584
582
  }
583
+ throw convertFsError(error);
584
+ }
585
+ const isFinal = pending.length === 0;
586
+ if (!stats.isSymbolicLink() || (isFinal && !followFinalSymlink && !trailingSlash)) {
587
+ current = candidate;
588
+ continue;
589
+ }
590
+ if (++followedSymlinks > MAX_SYMLINKS) {
591
+ throw "loop";
592
+ }
593
+ let target;
594
+ try {
595
+ target = readlinkSync(candidate);
596
+ }
597
+ catch (error) {
598
+ throw convertFsError(error);
585
599
  }
586
- // it is the start of a new segment
587
- while (subpath[i] === "/") {
588
- i++;
600
+ let targetSegments;
601
+ if (hostPath.isAbsolute(target)) {
602
+ throw "not-permitted";
589
603
  }
590
- segmentIndex = i;
591
- }
592
- // finish reading out the last segment
593
- if (segmentIndex !== -1) {
594
- segments.push(subpath.slice(segmentIndex));
595
- }
596
- subpath = segments.join("");
597
- // The default Windows preopen represents all drives.
598
- //
599
- // Convert drive paths to namespaced paths so joining them to the preopen
600
- // does not produce an invalid UNC server name such as //C:/Users/...
601
- if (isWindows && descriptor.#hostPreopen === "//" && /^[a-zA-Z]:\//.test(subpath)) {
602
- return "//?/" + subpath;
603
- }
604
- return descriptor.#hostPreopen
605
- ? descriptor.#hostPreopen +
606
- (descriptor.#hostPreopen.endsWith("/") ? "" : subpath.length > 0 ? "/" : "") +
607
- subpath
608
- : descriptor.#fullPath + (subpath.length > 0 ? "/" : "") + subpath;
604
+ else {
605
+ const relativeCurrent = hostPath.relative(baseResolved, current);
606
+ targetSegments = relativeCurrent ? relativeCurrent.split(hostPath.sep) : [];
607
+ for (const targetSegment of target.replace(/\\/g, "/").split("/")) {
608
+ if (targetSegment === "" || targetSegment === ".") {
609
+ continue;
610
+ }
611
+ if (targetSegment === "..") {
612
+ if (targetSegments.pop() === undefined) {
613
+ throw "not-permitted";
614
+ }
615
+ }
616
+ else {
617
+ targetSegments.push(targetSegment);
618
+ }
619
+ }
620
+ }
621
+ pending = [...targetSegments, ...pending];
622
+ current = baseResolved;
623
+ }
624
+ return trailingSlash && current !== baseResolved ? `${current}/` : current;
609
625
  }
610
626
  }
611
627
  const descriptorCreatePreopen = Descriptor._createPreopen;
@@ -654,7 +670,6 @@ export const preopens = {
654
670
  return preopenEntries;
655
671
  },
656
672
  };
657
- _addPreopen("/", isWindows ? "//" : "/");
658
673
  export const types = {
659
674
  Descriptor,
660
675
  DirectoryEntryStream,
@@ -693,11 +708,10 @@ export function _addPreopen(virtualPath, hostPreopen) {
693
708
  }
694
709
  /**
695
710
  * Clear all preopens, giving the guest no filesystem access.
696
- * Call this immediately after import to disable default full filesystem access.
697
711
  *
698
712
  * @example
699
713
  * import { _clearPreopens } from '@bytecodealliance/preview2-shim/filesystem';
700
- * _clearPreopens(); // Now guest has no filesystem access by default
714
+ * _clearPreopens();
701
715
  */
702
716
  export function _clearPreopens() {
703
717
  preopenEntries = [];
@@ -1,31 +1,7 @@
1
1
  import type { types as TypesNamespace } from "../../types/http.js";
2
- import { OutputStream, Pollable } from "../../types/interfaces/wasi-io-streams.js";
2
+ import { Pollable } from "../../types/interfaces/wasi-io-streams.js";
3
3
  declare const symbolDispose: symbol;
4
4
  export declare const _forbiddenHeaders: Set<string>;
5
- declare class OutgoingRequest implements TypesNamespace.OutgoingRequest {
6
- #private;
7
- [symbolDispose]: () => void;
8
- constructor(headers: any);
9
- body(): OutgoingBody;
10
- method(): TypesNamespace.Method;
11
- setMethod(method: any): void;
12
- pathWithQuery(): string | undefined;
13
- setPathWithQuery(pathWithQuery: any): void;
14
- scheme(): TypesNamespace.Scheme | undefined;
15
- setScheme(scheme: any): void;
16
- authority(): string | undefined;
17
- setAuthority(authority: any): void;
18
- headers(): Fields;
19
- static _handle(request: any, options: any): FutureIncomingResponse;
20
- }
21
- declare class OutgoingBody implements TypesNamespace.OutgoingBody {
22
- #private;
23
- [symbolDispose]: () => void;
24
- write(): OutputStream;
25
- static finish(body: OutgoingBody, trailers: Fields | undefined): void;
26
- static _outputStreamId(outgoingBody: any): any;
27
- static _create(contentLength: any): OutgoingBody;
28
- }
29
5
  declare class FutureIncomingResponse implements TypesNamespace.FutureIncomingResponse {
30
6
  #private;
31
7
  [symbolDispose]: () => void;
@@ -33,42 +9,15 @@ declare class FutureIncomingResponse implements TypesNamespace.FutureIncomingRes
33
9
  get(): any;
34
10
  static _create(method: any, scheme: any, authority: any, pathWithQuery: any, headers: any, body: any, connectTimeout: any, betweenBytesTimeout: any, firstByteTimeout: any): FutureIncomingResponse;
35
11
  }
36
- declare class Fields {
37
- #private;
38
- /**
39
- * @param {[string, Uint8Array[][]][]} entries
40
- */
41
- static fromList(entries: any): Fields;
42
- get(name: any): TypesNamespace.FieldValue[];
43
- /**
44
- * WIT spec (https://github.com/WebAssembly/WASI/blob/91bb44c3c3a9b1e09187db23c85fa844d1fd6b15/proposals/http/wit/types.wit#L215-L223):
45
- *
46
- * > Set all of the values for a name. Clears any existing values for that
47
- * > name, if they have been set.
48
- * >
49
- * > Fails with `header-error.immutable` if the `fields` are immutable.
50
- * >
51
- * > Fails with `header-error.invalid-syntax` if the `field-name` or any of
52
- * > the `field-value`s are syntactically invalid.
53
- *
54
- * The existing-branch splice/reuse is an allocation optimization — values are cleared, not retained.
55
- */
56
- set(name: any, values: any): void;
57
- has(name: any): boolean;
58
- delete(name: any): void;
59
- append(name: any, value: any): void;
60
- entries(): [string, TypesNamespace.FieldValue][];
61
- clone(): Fields;
62
- static _lock(fields: any): any;
63
- static _fromEntriesChecked(entries: any): Fields;
64
- }
12
+ /** Deny outbound HTTP for the process-global HTTP namespace. */
13
+ export declare function _denyHttp(): void;
65
14
  export declare const outgoingHandler: {
66
15
  /**
67
16
  * @param {OutgoingRequest} request
68
17
  * @param {RequestOptions | undefined} options
69
18
  * @returns {FutureIncomingResponse}
70
19
  */
71
- handle: typeof OutgoingRequest._handle;
20
+ handle(request: any, options: any): FutureIncomingResponse;
72
21
  };
73
22
  export declare const types: typeof TypesNamespace;
74
23
  export declare class HTTPServer {
@@ -546,13 +546,23 @@ delete Fields._lock;
546
546
  const fieldsFromEntriesChecked = Fields._fromEntriesChecked;
547
547
  // @ts-expect-error - Deleting static method
548
548
  delete Fields._fromEntriesChecked;
549
+ let httpEnabled = true;
550
+ /** Deny outbound HTTP for the process-global HTTP namespace. */
551
+ export function _denyHttp() {
552
+ httpEnabled = false;
553
+ }
549
554
  export const outgoingHandler = {
550
555
  /**
551
556
  * @param {OutgoingRequest} request
552
557
  * @param {RequestOptions | undefined} options
553
558
  * @returns {FutureIncomingResponse}
554
559
  */
555
- handle: outgoingRequestHandle,
560
+ handle(request, options) {
561
+ if (!httpEnabled) {
562
+ throw "access-denied";
563
+ }
564
+ return outgoingRequestHandle(request, options);
565
+ },
556
566
  };
557
567
  function httpErrorCode(err) {
558
568
  if (err.payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytecodealliance/preview2-shim",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "WASI Preview2 shim for JS environments",
5
5
  "homepage": "https://github.com/bytecodealliance/jco#readme",
6
6
  "bugs": {
@@ -68,7 +68,7 @@
68
68
  "@bytecodealliance/jco": "1.25.2",
69
69
  "@types/node": "^24.12.4",
70
70
  "mime": "^4.0.7",
71
- "puppeteer": "^25.1.0",
71
+ "puppeteer": "^25.12.0",
72
72
  "rolldown": "^1.2.4",
73
73
  "typescript": "7.0.2",
74
74
  "vitest": "^4.1.11"