@bytecodealliance/preview2-shim 0.17.5 → 0.17.7
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 +48 -0
- package/lib/browser/config.js +0 -1
- package/lib/browser/filesystem.js +169 -0
- package/lib/browser/http.js +10 -0
- package/lib/browser/io.js +5 -2
- package/lib/common/instantiation.js +126 -3
- package/lib/nodejs/filesystem.js +39 -0
- package/package.json +1 -1
- package/types/filesystem.d.ts +27 -0
- package/types/instantiation.d.ts +64 -1
package/README.md
CHANGED
|
@@ -51,6 +51,54 @@ const component = await instantiate(loader, new WASIShim().getImportObject());
|
|
|
51
51
|
// TODO: Code that uses your component's exports goes here.
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Sandboxing
|
|
55
|
+
|
|
56
|
+
By default, the preview2-shim provides full access to the host filesystem, environment variables,
|
|
57
|
+
and network - matching the default behavior of Node.js libraries. However, you can configure
|
|
58
|
+
sandboxing to restrict what guests can access.
|
|
59
|
+
|
|
60
|
+
### Using WASIShim for sandboxing
|
|
61
|
+
|
|
62
|
+
The `WASIShim` class accepts a `sandbox` configuration option to control access:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';
|
|
66
|
+
|
|
67
|
+
// Fully sandboxed - no filesystem, network, or env access
|
|
68
|
+
const sandboxedShim = new WASIShim({
|
|
69
|
+
sandbox: {
|
|
70
|
+
preopens: {}, // No filesystem access
|
|
71
|
+
env: {}, // No environment variables
|
|
72
|
+
args: ['arg1'], // Custom arguments
|
|
73
|
+
enableNetwork: false, // Disable network access
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Limited filesystem access - map virtual paths to host paths
|
|
78
|
+
const limitedShim = new WASIShim({
|
|
79
|
+
sandbox: {
|
|
80
|
+
preopens: {
|
|
81
|
+
'/data': '/tmp/guest-data', // Guest sees /data, maps to /tmp/guest-data
|
|
82
|
+
'/config': '/etc/app' // Guest sees /config, maps to /etc/app
|
|
83
|
+
},
|
|
84
|
+
env: { 'ENV1': '42' }, // Only expose specific env vars
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const component = await instantiate(loader, sandboxedShim.getImportObject());
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Notes on sandboxing
|
|
92
|
+
|
|
93
|
+
- By default (when no options are passed), the shim is providing full access to match typical
|
|
94
|
+
Node.js library behavior.
|
|
95
|
+
- Each `WASIShim` instance has its own isolated preopens, environment variables, and arguments.
|
|
96
|
+
Multiple instances with different configurations will not affect each other.
|
|
97
|
+
- The direct preopen functions (`_setPreopens`, `_clearPreopens`, etc.) modify global state and
|
|
98
|
+
affect all components not using `WASIShim` with explicit configuration. For isolation, prefer
|
|
99
|
+
using `WASIShim` with the `sandbox` option containing `preopens` and `env`.
|
|
100
|
+
- When `sandbox.enableNetwork: false`, all socket and HTTP operations will throw "access-denied" errors.
|
|
101
|
+
|
|
54
102
|
[jco]: https://www.npmjs.com/package/@bytecodealliance/jco
|
|
55
103
|
|
|
56
104
|
# License
|
package/lib/browser/config.js
CHANGED
|
@@ -6,6 +6,25 @@ export { _setCwd } from './config.js';
|
|
|
6
6
|
|
|
7
7
|
const { InputStream, OutputStream } = streams;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} FileDataEntry
|
|
11
|
+
* @property {Record<string, FileDataEntry>} [dir] - Directory contents (present for directories)
|
|
12
|
+
* @property {Uint8Array|string} [source] - File contents (present for files)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {FileDataEntry} FileData
|
|
17
|
+
* Root file data structure representing a filesystem tree.
|
|
18
|
+
* Each entry is either a directory (has `dir` property) or a file (has `source` property).
|
|
19
|
+
* @example
|
|
20
|
+
* // A simple filesystem with one directory containing one file:
|
|
21
|
+
* const fileData = {
|
|
22
|
+
* dir: {
|
|
23
|
+
* 'myfile.txt': { source: new Uint8Array([72, 101, 108, 108, 111]) }
|
|
24
|
+
* }
|
|
25
|
+
* };
|
|
26
|
+
*/
|
|
27
|
+
|
|
9
28
|
export function _setFileData(fileData) {
|
|
10
29
|
_fileData = fileData;
|
|
11
30
|
_rootPreopen[0] = new Descriptor(fileData);
|
|
@@ -322,9 +341,159 @@ export const preopens = {
|
|
|
322
341
|
},
|
|
323
342
|
};
|
|
324
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Replace all preopens with the given set.
|
|
346
|
+
* @param {Record<string, FileData>} preopensConfig - Map of virtual paths to file data entries
|
|
347
|
+
*/
|
|
348
|
+
export function _setPreopens(preopensConfig) {
|
|
349
|
+
_preopens = [];
|
|
350
|
+
for (const [virtualPath, fileData] of Object.entries(preopensConfig)) {
|
|
351
|
+
_addPreopen(virtualPath, fileData);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Add a single preopen mapping.
|
|
357
|
+
* @param {string} virtualPath - The virtual path visible to the guest
|
|
358
|
+
* @param {FileData} fileData - The file data object representing the directory
|
|
359
|
+
*/
|
|
360
|
+
export function _addPreopen(virtualPath, fileData) {
|
|
361
|
+
const descriptor = new Descriptor(fileData);
|
|
362
|
+
_preopens.push([descriptor, virtualPath]);
|
|
363
|
+
if (virtualPath === '/') {
|
|
364
|
+
_rootPreopen = [descriptor, virtualPath];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Clear all preopens, giving the guest no filesystem access.
|
|
370
|
+
*
|
|
371
|
+
* This functionality exists mostly to maintain backwards compatibility. Prefer setting preopens
|
|
372
|
+
* via `WASIShim` rather than making top level changes to preopens using these functions.
|
|
373
|
+
*/
|
|
374
|
+
export function _clearPreopens() {
|
|
375
|
+
_preopens = [];
|
|
376
|
+
_rootPreopen = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get current preopens configuration.
|
|
381
|
+
* @returns {Array<[Descriptor, string]>} Array of [descriptor, virtualPath] pairs
|
|
382
|
+
*/
|
|
383
|
+
export function _getPreopens() {
|
|
384
|
+
return [..._preopens];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Create a preopen descriptor for file data.
|
|
389
|
+
* This is used internally to create isolated preopen instances.
|
|
390
|
+
* @param {FileData} fileData - The file data object representing the directory
|
|
391
|
+
* @returns {Descriptor} A preopen descriptor
|
|
392
|
+
*/
|
|
393
|
+
export function _createPreopenDescriptor(fileData) {
|
|
394
|
+
return new Descriptor(fileData);
|
|
395
|
+
}
|
|
396
|
+
|
|
325
397
|
export const types = {
|
|
326
398
|
Descriptor,
|
|
327
399
|
DirectoryEntryStream,
|
|
400
|
+
filesystemErrorCode(err) {
|
|
401
|
+
return convertFsError(err.payload);
|
|
402
|
+
},
|
|
328
403
|
};
|
|
329
404
|
|
|
405
|
+
function convertFsError(e) {
|
|
406
|
+
switch (e.code) {
|
|
407
|
+
case 'EACCES':
|
|
408
|
+
return 'access';
|
|
409
|
+
case 'EAGAIN':
|
|
410
|
+
case 'EWOULDBLOCK':
|
|
411
|
+
return 'would-block';
|
|
412
|
+
case 'EALREADY':
|
|
413
|
+
return 'already';
|
|
414
|
+
case 'EBADF':
|
|
415
|
+
return 'bad-descriptor';
|
|
416
|
+
case 'EBUSY':
|
|
417
|
+
return 'busy';
|
|
418
|
+
case 'EDEADLK':
|
|
419
|
+
return 'deadlock';
|
|
420
|
+
case 'EDQUOT':
|
|
421
|
+
return 'quota';
|
|
422
|
+
case 'EEXIST':
|
|
423
|
+
return 'exist';
|
|
424
|
+
case 'EFBIG':
|
|
425
|
+
return 'file-too-large';
|
|
426
|
+
case 'EILSEQ':
|
|
427
|
+
return 'illegal-byte-sequence';
|
|
428
|
+
case 'EINPROGRESS':
|
|
429
|
+
return 'in-progress';
|
|
430
|
+
case 'EINTR':
|
|
431
|
+
return 'interrupted';
|
|
432
|
+
case 'EINVAL':
|
|
433
|
+
return 'invalid';
|
|
434
|
+
case 'EIO':
|
|
435
|
+
return 'io';
|
|
436
|
+
case 'EISDIR':
|
|
437
|
+
return 'is-directory';
|
|
438
|
+
case 'ELOOP':
|
|
439
|
+
return 'loop';
|
|
440
|
+
case 'EMLINK':
|
|
441
|
+
return 'too-many-links';
|
|
442
|
+
case 'EMSGSIZE':
|
|
443
|
+
return 'message-size';
|
|
444
|
+
case 'ENAMETOOLONG':
|
|
445
|
+
return 'name-too-long';
|
|
446
|
+
case 'ENODEV':
|
|
447
|
+
return 'no-device';
|
|
448
|
+
case 'ENOENT':
|
|
449
|
+
return 'no-entry';
|
|
450
|
+
case 'ENOLCK':
|
|
451
|
+
return 'no-lock';
|
|
452
|
+
case 'ENOMEM':
|
|
453
|
+
return 'insufficient-memory';
|
|
454
|
+
case 'ENOSPC':
|
|
455
|
+
return 'insufficient-space';
|
|
456
|
+
case 'ENOTDIR':
|
|
457
|
+
case 'ERR_FS_EISDIR':
|
|
458
|
+
return 'not-directory';
|
|
459
|
+
case 'ENOTEMPTY':
|
|
460
|
+
return 'not-empty';
|
|
461
|
+
case 'ENOTRECOVERABLE':
|
|
462
|
+
return 'not-recoverable';
|
|
463
|
+
case 'ENOTSUP':
|
|
464
|
+
return 'unsupported';
|
|
465
|
+
case 'ENOTTY':
|
|
466
|
+
return 'no-tty';
|
|
467
|
+
// windows gives this error for badly structured `//` reads
|
|
468
|
+
// this seems like a slightly better error than unknown given
|
|
469
|
+
// that it's a common footgun
|
|
470
|
+
case -4094:
|
|
471
|
+
case 'ENXIO':
|
|
472
|
+
return 'no-such-device';
|
|
473
|
+
case 'EOVERFLOW':
|
|
474
|
+
return 'overflow';
|
|
475
|
+
case 'EPERM':
|
|
476
|
+
return 'not-permitted';
|
|
477
|
+
case 'EPIPE':
|
|
478
|
+
return 'pipe';
|
|
479
|
+
case 'EROFS':
|
|
480
|
+
return 'read-only';
|
|
481
|
+
case 'ESPIPE':
|
|
482
|
+
return 'invalid-seek';
|
|
483
|
+
case 'ETXTBSY':
|
|
484
|
+
return 'text-file-busy';
|
|
485
|
+
case 'EXDEV':
|
|
486
|
+
return 'cross-device';
|
|
487
|
+
case 'UNKNOWN':
|
|
488
|
+
switch (e.errno) {
|
|
489
|
+
case -4094:
|
|
490
|
+
return 'no-such-device';
|
|
491
|
+
default:
|
|
492
|
+
throw e;
|
|
493
|
+
}
|
|
494
|
+
default:
|
|
495
|
+
throw e;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
330
499
|
export { types as filesystemTypes };
|
package/lib/browser/http.js
CHANGED
|
@@ -142,4 +142,14 @@ export const types = {
|
|
|
142
142
|
listenToFutureIncomingResponse(_f) {
|
|
143
143
|
console.log('[types] Listen to future incoming response');
|
|
144
144
|
},
|
|
145
|
+
Fields: class Fields {},
|
|
146
|
+
FutureIncomingResponse: new class FutureIncomingResponse {},
|
|
147
|
+
IncomingBody: new class IncomingBody {},
|
|
148
|
+
IncomingRequest: new class IncomingRequest {},
|
|
149
|
+
IncomingResponse: new class IncomingResponse {},
|
|
150
|
+
OutgoingBody: new class OutgoingBody {},
|
|
151
|
+
OutgoingRequest: new class OutgoingRequest {},
|
|
152
|
+
OutgoingResponse: new class OutgoingResponse {},
|
|
153
|
+
RequestOptions: new class RequestOptions {},
|
|
154
|
+
ResponseOutparam: new class ResponseOutparam {},
|
|
145
155
|
};
|
package/lib/browser/io.js
CHANGED
|
@@ -17,7 +17,7 @@ const IoError = class Error {
|
|
|
17
17
|
* blockingRead: (len: BigInt) => Uint8Array,
|
|
18
18
|
* skip?: (len: BigInt) => BigInt,
|
|
19
19
|
* blockingSkip?: (len: BigInt) => BigInt,
|
|
20
|
-
* subscribe: () =>
|
|
20
|
+
* subscribe: () => Pollable,
|
|
21
21
|
* drop?: () => void,
|
|
22
22
|
* }} InputStreamHandler
|
|
23
23
|
*
|
|
@@ -33,7 +33,7 @@ const IoError = class Error {
|
|
|
33
33
|
* splice?: (src: InputStream, len: BigInt) => BigInt,
|
|
34
34
|
* blockingSplice?: (src: InputStream, len: BigInt) => BigInt,
|
|
35
35
|
* forward?: (src: InputStream) => void,
|
|
36
|
-
* subscribe?: () =>
|
|
36
|
+
* subscribe?: () => Pollable,
|
|
37
37
|
* drop?: () => void,
|
|
38
38
|
* }} OutputStreamHandler
|
|
39
39
|
*
|
|
@@ -78,6 +78,7 @@ class InputStream {
|
|
|
78
78
|
}
|
|
79
79
|
subscribe() {
|
|
80
80
|
console.log(`[streams] Subscribe to input stream ${this.id}`);
|
|
81
|
+
return new Pollable();
|
|
81
82
|
}
|
|
82
83
|
[symbolDispose]() {
|
|
83
84
|
if (this.handler.drop) {
|
|
@@ -168,6 +169,7 @@ class OutputStream {
|
|
|
168
169
|
}
|
|
169
170
|
subscribe() {
|
|
170
171
|
console.log(`[streams] Subscribe to output stream ${this.id}`);
|
|
172
|
+
return new Pollable();
|
|
171
173
|
}
|
|
172
174
|
[symbolDispose]() {}
|
|
173
175
|
}
|
|
@@ -190,4 +192,5 @@ export const poll = {
|
|
|
190
192
|
Pollable,
|
|
191
193
|
pollList,
|
|
192
194
|
pollOne,
|
|
195
|
+
poll: pollOne,
|
|
193
196
|
};
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/lib/nodejs/filesystem.js
CHANGED
|
@@ -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
package/types/filesystem.d.ts
CHANGED
|
@@ -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]>;
|
package/types/instantiation.d.ts
CHANGED
|
@@ -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(
|
|
182
|
+
constructor(config?: WASIShimConfig);
|
|
120
183
|
|
|
121
184
|
/**
|
|
122
185
|
* Generate an import object for the shim that can be used with
|