@bytecodealliance/preview2-shim 0.26.0 → 0.27.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.
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,7 +411,7 @@ 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
416
  const target = readlinkSync(fullPath);
432
417
  return isWindows ? target.replace(/\\/g, "/") : target;
@@ -436,7 +421,7 @@ class Descriptor {
436
421
  }
437
422
  }
438
423
  removeDirectoryAt(path) {
439
- const fullPath = this.#getFullPath(path);
424
+ const fullPath = this.#getFullPath(path, false);
440
425
  try {
441
426
  rmdirSync(fullPath);
442
427
  }
@@ -448,8 +433,8 @@ class Descriptor {
448
433
  }
449
434
  }
450
435
  renameAt(oldPath, newDescriptor, newPath) {
451
- const oldFullPath = this.#getFullPath(oldPath);
452
- const newFullPath = newDescriptor.#getFullPath(newPath);
436
+ const oldFullPath = this.#getFullPath(oldPath, false);
437
+ const newFullPath = newDescriptor.#getFullPath(newPath, false);
453
438
  try {
454
439
  renameSync(oldFullPath, newFullPath);
455
440
  }
@@ -461,7 +446,7 @@ class Descriptor {
461
446
  }
462
447
  }
463
448
  symlinkAt(target, path) {
464
- const fullPath = this.#getFullPath(path);
449
+ const fullPath = this.#getFullPath(path, false);
465
450
  if (target.startsWith("/")) {
466
451
  throw "not-permitted";
467
452
  }
@@ -490,7 +475,7 @@ class Descriptor {
490
475
  }
491
476
  }
492
477
  unlinkFileAt(path) {
493
- const fullPath = this.#getFullPath(path);
478
+ const fullPath = this.#getFullPath(path, false);
494
479
  try {
495
480
  if (fullPath.endsWith("/")) {
496
481
  let isDir = false;
@@ -532,7 +517,7 @@ class Descriptor {
532
517
  }
533
518
  }
534
519
  metadataHashAt(pathFlags, path) {
535
- const fullPath = this.#getFullPath(path);
520
+ const fullPath = this.#getFullPath(path, pathFlags.symlinkFollow);
536
521
  try {
537
522
  const stats = (pathFlags.symlinkFollow ? statSync : lstatSync)(fullPath, {
538
523
  bigint: true,
@@ -543,70 +528,100 @@ class Descriptor {
543
528
  throw convertFsError(e);
544
529
  }
545
530
  }
546
- // Join paths lexically; each operation supplies its own symlink semantics.
547
- #getFullPath(subpath) {
548
- let descriptor = this;
549
- if (subpath.indexOf("\\") !== -1) {
550
- subpath = subpath.replace(/\\/g, "/");
551
- }
552
- if (subpath.indexOf("//") !== -1) {
553
- subpath = subpath.replace(/\/\/+/g, "/");
554
- }
555
- if (subpath[0] === "/") {
531
+ #getFullPath(subpath, followFinalSymlink) {
532
+ subpath = subpath.replace(/\\/g, "/").replace(/\/\/+/g, "/");
533
+ if (subpath.startsWith("/")) {
556
534
  throw "not-permitted";
557
535
  }
558
- // segment resolution
536
+ const trailingSlash = subpath.endsWith("/");
559
537
  const segments = [];
560
- let segmentIndex = -1;
561
- for (let i = 0; i < subpath.length; i++) {
562
- // busy reading a segment - only terminate on '/'
563
- if (segmentIndex !== -1) {
564
- if (subpath[i] === "/") {
565
- segments.push(subpath.slice(segmentIndex, i + 1));
566
- segmentIndex = -1;
567
- }
538
+ for (const segment of subpath.split("/")) {
539
+ if (segment === "" || segment === ".") {
568
540
  continue;
569
541
  }
570
- // new segment - check if it is relative
571
- else if (subpath[i] === ".") {
572
- // ../ segment
573
- if (subpath[i + 1] === "." &&
574
- (subpath[i + 2] === "/" || i + 2 === subpath.length)) {
575
- if (segments.pop() === undefined) {
576
- throw "not-permitted";
577
- }
578
- i += 2;
579
- continue;
542
+ if (segment === "..") {
543
+ if (segments.pop() === undefined) {
544
+ throw "not-permitted";
580
545
  }
581
- // ./ segment
582
- else if (subpath[i + 1] === "/" || i + 1 === subpath.length) {
583
- i += 1;
584
- 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;
585
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);
586
599
  }
587
- // it is the start of a new segment
588
- while (subpath[i] === "/") {
589
- i++;
600
+ let targetSegments;
601
+ if (hostPath.isAbsolute(target)) {
602
+ throw "not-permitted";
590
603
  }
591
- segmentIndex = i;
592
- }
593
- // finish reading out the last segment
594
- if (segmentIndex !== -1) {
595
- segments.push(subpath.slice(segmentIndex));
596
- }
597
- subpath = segments.join("");
598
- // The default Windows preopen represents all drives.
599
- //
600
- // Convert drive paths to namespaced paths so joining them to the preopen
601
- // does not produce an invalid UNC server name such as //C:/Users/...
602
- if (isWindows && descriptor.#hostPreopen === "//" && /^[a-zA-Z]:\//.test(subpath)) {
603
- return "//?/" + subpath;
604
- }
605
- return descriptor.#hostPreopen
606
- ? descriptor.#hostPreopen +
607
- (descriptor.#hostPreopen.endsWith("/") ? "" : subpath.length > 0 ? "/" : "") +
608
- subpath
609
- : 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;
610
625
  }
611
626
  }
612
627
  const descriptorCreatePreopen = Descriptor._createPreopen;
@@ -655,7 +670,6 @@ export const preopens = {
655
670
  return preopenEntries;
656
671
  },
657
672
  };
658
- _addPreopen("/", isWindows ? "//" : "/");
659
673
  export const types = {
660
674
  Descriptor,
661
675
  DirectoryEntryStream,
@@ -694,11 +708,10 @@ export function _addPreopen(virtualPath, hostPreopen) {
694
708
  }
695
709
  /**
696
710
  * Clear all preopens, giving the guest no filesystem access.
697
- * Call this immediately after import to disable default full filesystem access.
698
711
  *
699
712
  * @example
700
713
  * import { _clearPreopens } from '@bytecodealliance/preview2-shim/filesystem';
701
- * _clearPreopens(); // Now guest has no filesystem access by default
714
+ * _clearPreopens();
702
715
  */
703
716
  export function _clearPreopens() {
704
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.26.0",
3
+ "version": "0.27.0",
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"