@bytecodealliance/preview2-shim 0.17.6 → 0.17.8

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/lib/browser/io.js CHANGED
@@ -77,7 +77,9 @@ class InputStream {
77
77
  return BigInt(bytes.byteLength);
78
78
  }
79
79
  subscribe() {
80
- console.log(`[streams] Subscribe to input stream ${this.id}`);
80
+ if (this.handler.subscribe) {
81
+ return this.handler.subscribe();
82
+ }
81
83
  return new Pollable();
82
84
  }
83
85
  [symbolDispose]() {
@@ -168,7 +170,9 @@ class OutputStream {
168
170
  console.log(`[streams] Forward ${this.id}`);
169
171
  }
170
172
  subscribe() {
171
- console.log(`[streams] Subscribe to output stream ${this.id}`);
173
+ if (this.handler.subscribe) {
174
+ return this.handler.subscribe();
175
+ }
172
176
  return new Pollable();
173
177
  }
174
178
  [symbolDispose]() {}
@@ -178,19 +182,75 @@ export const error = { Error: IoError };
178
182
 
179
183
  export const streams = { InputStream, OutputStream };
180
184
 
181
- class Pollable {}
185
+ class Pollable {
186
+ #ready = false;
187
+ #promise = null;
188
+
189
+ constructor(promise) {
190
+ if (!promise) {
191
+ this.#ready = true;
192
+ } else {
193
+ this.#promise = promise.then(
194
+ () => { this.#ready = true; },
195
+ () => { this.#ready = true; }
196
+ );
197
+ }
198
+ }
199
+
200
+ ready() {
201
+ return this.#ready;
202
+ }
203
+
204
+ block() {
205
+ if (this.#ready) {
206
+ return Promise.resolve();
207
+ }
208
+ return this.#promise;
209
+ }
182
210
 
183
- function pollList(_list) {
184
- // TODO
211
+ [symbolDispose]() {
212
+ this.#promise = null;
213
+ }
214
+ }
215
+
216
+ function pollList(list) {
217
+ if (list.length === 0) {
218
+ throw new Error('poll list must not be empty');
219
+ }
220
+ if (list.length > 0xFFFFFFFF) {
221
+ throw new Error('poll list length exceeds u32 index range');
222
+ }
223
+ const ready = [];
224
+ for (let i = 0; i < list.length; i++) {
225
+ if (list[i].ready()) {
226
+ ready.push(i);
227
+ }
228
+ }
229
+ if (ready.length > 0) {
230
+ return new Uint32Array(ready);
231
+ }
232
+ // None ready synchronously. Wait for the first to resolve via Promise.race,
233
+ // then sweep for any others that became ready concurrently.
234
+ return Promise.race(
235
+ list.map((p, i) => p.block().then(() => {
236
+ const result = [i];
237
+ for (let j = 0; j < list.length; j++) {
238
+ if (j !== i && list[j].ready()) {
239
+ result.push(j);
240
+ }
241
+ }
242
+ return new Uint32Array(result);
243
+ }))
244
+ );
185
245
  }
186
246
 
187
- function pollOne(_poll) {
188
- // TODO
247
+ function pollOne(poll) {
248
+ return poll.block();
189
249
  }
190
250
 
191
251
  export const poll = {
192
252
  Pollable,
193
253
  pollList,
194
254
  pollOne,
195
- poll: pollOne,
255
+ poll: pollList,
196
256
  };
@@ -44,6 +44,33 @@ import * as wasi from '@bytecodealliance/preview2-shim';
44
44
  * const component = await instantiate(null, customWASIShim.getImportObject())
45
45
  * ```
46
46
  *
47
+ * For sandboxing, you can configure preopens, environment variables, and other
48
+ * capabilities via the `sandbox` option:
49
+ *
50
+ * ```js
51
+ * import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"
52
+ *
53
+ * // Fully sandboxed - no filesystem, network, or env access
54
+ * const sandboxedShim = new WASIShim({
55
+ * sandbox: {
56
+ * preopens: {}, // No filesystem access
57
+ * env: {}, // No environment variables
58
+ * args: ['program'], // Custom arguments
59
+ * enableNetwork: false, // Disable network (default: true for backward compat)
60
+ * }
61
+ * });
62
+ *
63
+ * // Limited filesystem access
64
+ * const limitedShim = new WASIShim({
65
+ * sandbox: {
66
+ * preopens: {
67
+ * '/data': '/tmp/guest-data', // Guest sees /data, maps to /tmp/guest-data
68
+ * '/config': '/etc/app' // Guest sees /config, maps to /etc/app
69
+ * }
70
+ * }
71
+ * });
72
+ * ```
73
+ *
47
74
  * Note that this object is similar but not identical to the Node `WASI` object --
48
75
  * it is solely concerned with shimming of preview2 when dealing with a WebAssembly
49
76
  * component transpiled by Jco. While this object *does* work with Node (and the browser)
@@ -66,8 +93,20 @@ export class WASIShim {
66
93
  #sockets;
67
94
  /** Object that confirms to the shim interface for `wasi:http` */
68
95
  #http;
96
+ /** Isolated preopens for this instance */
97
+ #preopens;
98
+ /** Isolated environment for this instance */
99
+ #environment;
100
+
101
+ /**
102
+ * Create a new WASIShim instance.
103
+ *
104
+ * @param {import('../types/instantiation.d.ts').WASIShimConfig} [config] - Configuration options
105
+ */
106
+ constructor(config) {
107
+ // Support both old 'shims' parameter name and new 'config' style
108
+ const shims = config;
69
109
 
70
- constructor(shims) {
71
110
  this.#cli = shims?.cli ?? wasi.cli;
72
111
  this.#filesystem = shims?.filesystem ?? wasi.filesystem;
73
112
  this.#io = shims?.io ?? wasi.io;
@@ -75,6 +114,37 @@ export class WASIShim {
75
114
  this.#clocks = shims?.clocks ?? wasi.clocks;
76
115
  this.#sockets = shims?.sockets ?? wasi.sockets;
77
116
  this.#http = shims?.http ?? wasi.http;
117
+
118
+ // Extract sandbox options
119
+ const sandbox = shims?.sandbox;
120
+
121
+ // Create isolated preopens if configured
122
+ if (sandbox?.preopens !== undefined) {
123
+ this.#preopens = createIsolatedPreopens(sandbox.preopens);
124
+ }
125
+
126
+ // Create isolated environment if env or args are configured
127
+ if (sandbox?.env !== undefined || sandbox?.args !== undefined) {
128
+ this.#environment = createIsolatedEnvironment(
129
+ sandbox?.env,
130
+ sandbox?.args,
131
+ this.#cli
132
+ );
133
+ }
134
+
135
+ // Apply network restrictions if disabled
136
+ if (sandbox?.enableNetwork === false) {
137
+ // Use the sockets module's built-in deny functions
138
+ if (this.#sockets._denyTcp) {
139
+ this.#sockets._denyTcp();
140
+ }
141
+ if (this.#sockets._denyUdp) {
142
+ this.#sockets._denyUdp();
143
+ }
144
+ if (this.#sockets._denyDnsLookup) {
145
+ this.#sockets._denyDnsLookup();
146
+ }
147
+ }
78
148
  }
79
149
 
80
150
  /**
@@ -93,7 +163,8 @@ export class WASIShim {
93
163
  const versionSuffix = opts?.asVersion ? `@${opts.asVersion}` : '';
94
164
 
95
165
  const obj = {};
96
- obj[`wasi:cli/environment${versionSuffix}`] = this.#cli.environment;
166
+
167
+ obj[`wasi:cli/environment${versionSuffix}`] = this.#environment ?? this.#cli.environment;
97
168
  obj[`wasi:cli/exit${versionSuffix}`] = this.#cli.exit;
98
169
  obj[`wasi:cli/stderr${versionSuffix}`] = this.#cli.stderr;
99
170
  obj[`wasi:cli/stdin${versionSuffix}`] = this.#cli.stdin;
@@ -112,7 +183,7 @@ export class WASIShim {
112
183
  obj[`wasi:sockets/udp${versionSuffix}`] = this.#sockets.udp;
113
184
  obj[`wasi:sockets/udp-create-socket${versionSuffix}`] = this.#sockets.udpCreateSocket;
114
185
 
115
- obj[`wasi:filesystem/preopens${versionSuffix}`] = this.#filesystem.preopens;
186
+ obj[`wasi:filesystem/preopens${versionSuffix}`] = this.#preopens ?? this.#filesystem.preopens;
116
187
  obj[`wasi:filesystem/types${versionSuffix}`] = this.#filesystem.types;
117
188
 
118
189
  obj[`wasi:io/error${versionSuffix}`] = this.#io.error;
@@ -132,3 +203,55 @@ export class WASIShim {
132
203
  return obj;
133
204
  }
134
205
  }
206
+
207
+ /**
208
+ * Create an isolated preopens object with its own preopen entries.
209
+ *
210
+ * @param {Record<string, string>} preopensConfig - Map of virtual paths to host paths
211
+ * @returns {object} A preopens object with Descriptor and getDirectories()
212
+ */
213
+ function createIsolatedPreopens(preopensConfig) {
214
+ const { types, _createPreopenDescriptor } = wasi.filesystem;
215
+ const entries = [];
216
+
217
+ // Populate entries using the filesystem's descriptor creation
218
+ if (_createPreopenDescriptor) {
219
+ for (const [virtualPath, hostPath] of Object.entries(preopensConfig)) {
220
+ const descriptor = _createPreopenDescriptor(hostPath);
221
+ entries.push([descriptor, virtualPath]);
222
+ }
223
+ }
224
+
225
+ return {
226
+ Descriptor: types.Descriptor,
227
+ getDirectories() {
228
+ return entries;
229
+ },
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Create an isolated CLI environment with its own env and args.
235
+ *
236
+ * @param {Record<string, string>} env - Environment variables
237
+ * @param {string[]} args - Command-line arguments
238
+ * @param {object} baseCli - The base CLI module to extend
239
+ * @returns {object} An isolated CLI environment object
240
+ */
241
+ function createIsolatedEnvironment(env, args, baseCli) {
242
+ const envEntries = env ? Object.entries(env) : null;
243
+ const argsArray = args || null;
244
+
245
+ return {
246
+ ...baseCli.environment,
247
+ getEnvironment() {
248
+ return envEntries ?? baseCli.environment.getEnvironment();
249
+ },
250
+ getArguments() {
251
+ return argsArray ?? baseCli.environment.getArguments();
252
+ },
253
+ initialCwd() {
254
+ return baseCli.environment.initialCwd();
255
+ },
256
+ };
257
+ }
@@ -738,6 +738,10 @@ export const types = {
738
738
  },
739
739
  };
740
740
 
741
+ /**
742
+ * Replace all preopens with the given set.
743
+ * @param {Record<string, string>} preopens - Map of virtual paths to host paths
744
+ */
741
745
  export function _setPreopens(preopens) {
742
746
  preopenEntries = [];
743
747
  for (const [virtualPath, hostPreopen] of Object.entries(preopens)) {
@@ -745,11 +749,46 @@ export function _setPreopens(preopens) {
745
749
  }
746
750
  }
747
751
 
752
+ /**
753
+ * Add a single preopen mapping.
754
+ * @param {string} virtualPath - The virtual path visible to the guest
755
+ * @param {string} hostPreopen - The host filesystem path
756
+ */
748
757
  export function _addPreopen(virtualPath, hostPreopen) {
749
758
  const preopenEntry = [descriptorCreatePreopen(hostPreopen), virtualPath];
750
759
  preopenEntries.push(preopenEntry);
751
760
  }
752
761
 
762
+ /**
763
+ * Clear all preopens, giving the guest no filesystem access.
764
+ * Call this immediately after import to disable default full filesystem access.
765
+ *
766
+ * @example
767
+ * import { _clearPreopens } from '@bytecodealliance/preview2-shim/filesystem';
768
+ * _clearPreopens(); // Now guest has no filesystem access by default
769
+ */
770
+ export function _clearPreopens() {
771
+ preopenEntries = [];
772
+ }
773
+
774
+ /**
775
+ * Get current preopens configuration.
776
+ * @returns {Array<[Descriptor, string]>} Array of [descriptor, virtualPath] pairs
777
+ */
778
+ export function _getPreopens() {
779
+ return [...preopenEntries];
780
+ }
781
+
782
+ /**
783
+ * Create a preopen descriptor for a host path.
784
+ * This is used internally to create isolated preopen instances.
785
+ * @param {string} hostPreopen - The host filesystem path
786
+ * @returns {Descriptor} A preopen descriptor
787
+ */
788
+ export function _createPreopenDescriptor(hostPreopen) {
789
+ return descriptorCreatePreopen(hostPreopen);
790
+ }
791
+
753
792
  function convertFsError(e) {
754
793
  switch (e.code) {
755
794
  case 'EACCES':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytecodealliance/preview2-shim",
3
- "version": "0.17.6",
3
+ "version": "0.17.8",
4
4
  "description": "WASI Preview2 shim for JS environments",
5
5
  "author": "Guy Bedford, Eduardo Rodrigues<16357187+eduardomourar@users.noreply.github.com>",
6
6
  "contributors": [
@@ -1,2 +1,29 @@
1
1
  export type * as preopens from './interfaces/wasi-filesystem-preopens.d.ts';
2
2
  export type * as types from './interfaces/wasi-filesystem-types.d.ts';
3
+
4
+ import type { Descriptor } from './interfaces/wasi-filesystem-types.d.ts';
5
+
6
+ /**
7
+ * Replace all preopens with the given set.
8
+ * @param preopens - Map of virtual paths to host paths
9
+ */
10
+ export function _setPreopens(preopens: Record<string, string>): void;
11
+
12
+ /**
13
+ * Add a single preopen mapping.
14
+ * @param virtualPath - The virtual path visible to the guest
15
+ * @param hostPreopen - The host filesystem path
16
+ */
17
+ export function _addPreopen(virtualPath: string, hostPreopen: string): void;
18
+
19
+ /**
20
+ * Clear all preopens, giving the guest no filesystem access.
21
+ * Call this immediately after import to disable default full filesystem access.
22
+ */
23
+ export function _clearPreopens(): void;
24
+
25
+ /**
26
+ * Get current preopens configuration.
27
+ * @returns Array of [descriptor, virtualPath] pairs
28
+ */
29
+ export function _getPreopens(): Array<[Descriptor, string]>;
@@ -64,6 +64,42 @@ type AppendVersion<
64
64
  : never
65
65
  : never;
66
66
 
67
+ /**
68
+ * Sandbox configuration options for WASIShim
69
+ */
70
+ interface SandboxConfig {
71
+ /** Filesystem preopens mapping (virtual path -> host path) */
72
+ preopens?: Record<string, string>;
73
+ /** Environment variables visible to the guest */
74
+ env?: Record<string, string>;
75
+ /** Command-line arguments */
76
+ args?: string[];
77
+ /** Whether to enable network access (sockets, HTTP). Default: true */
78
+ enableNetwork?: boolean;
79
+ }
80
+
81
+ /**
82
+ * Configuration options for WASIShim
83
+ */
84
+ interface WASIShimConfig {
85
+ /** Custom CLI shim */
86
+ cli?: object;
87
+ /** Custom filesystem shim */
88
+ filesystem?: object;
89
+ /** Custom I/O shim */
90
+ io?: object;
91
+ /** Custom random shim */
92
+ random?: object;
93
+ /** Custom clocks shim */
94
+ clocks?: object;
95
+ /** Custom sockets shim */
96
+ sockets?: object;
97
+ /** Custom HTTP shim */
98
+ http?: object;
99
+ /** Sandbox configuration for restricting guest capabilities */
100
+ sandbox?: SandboxConfig;
101
+ }
102
+
67
103
  /**
68
104
  * (EXPERIMENTAL) A class that holds WASI shims and can be used to configure
69
105
  * an instantiation of a WebAssembly component transpiled with jco
@@ -108,6 +144,33 @@ type AppendVersion<
108
144
  * const component = await instantiate(null, customWASIShim.getImportObject())
109
145
  * ```
110
146
  *
147
+ * For sandboxing, you can configure preopens, environment variables, and other
148
+ * capabilities via the `sandbox` option:
149
+ *
150
+ * ```js
151
+ * import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"
152
+ *
153
+ * // Fully sandboxed - no filesystem, network, or env access
154
+ * const sandboxedShim = new WASIShim({
155
+ * sandbox: {
156
+ * preopens: {}, // No filesystem access
157
+ * env: {}, // No environment variables
158
+ * args: ['program'], // Custom arguments
159
+ * enableNetwork: false, // Disable network
160
+ * }
161
+ * });
162
+ *
163
+ * // Limited filesystem access
164
+ * const limitedShim = new WASIShim({
165
+ * sandbox: {
166
+ * preopens: {
167
+ * '/data': '/tmp/guest-data', // Guest sees /data, maps to /tmp/guest-data
168
+ * '/config': '/etc/app' // Guest sees /config, maps to /etc/app
169
+ * }
170
+ * }
171
+ * });
172
+ * ```
173
+ *
111
174
  * Note that this object is similar but not identical to the Node `WASI` object --
112
175
  * it is solely concerned with shimming of preview2 when dealing with a WebAssembly
113
176
  * component transpiled by Jco. While this object *does* work with Node (and the browser)
@@ -116,7 +179,7 @@ type AppendVersion<
116
179
  * @class WASIShim
117
180
  */
118
181
  export class WASIShim {
119
- constructor(shims?: Partial<WASIImportObject>);
182
+ constructor(config?: WASIShimConfig);
120
183
 
121
184
  /**
122
185
  * Generate an import object for the shim that can be used with