@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/README.md +48 -0
- package/lib/browser/clocks.js +10 -6
- package/lib/browser/filesystem.js +72 -0
- package/lib/browser/http.js +677 -140
- package/lib/browser/io.js +68 -8
- 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/lib/browser/io.js
CHANGED
|
@@ -77,7 +77,9 @@ class InputStream {
|
|
|
77
77
|
return BigInt(bytes.byteLength);
|
|
78
78
|
}
|
|
79
79
|
subscribe() {
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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(
|
|
188
|
-
|
|
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:
|
|
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
|
-
|
|
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
|