wasm-platform-kit 0.0.0-stage → 0.2.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -2
  3. package/assets/wasi/LICENSE +220 -0
  4. package/assets/wasi/cli.d.ts +43 -0
  5. package/assets/wasi/cli.js +147 -0
  6. package/assets/wasi/clocks.d.ts +3 -0
  7. package/assets/wasi/clocks.js +55 -0
  8. package/assets/wasi/common.d.ts +5 -0
  9. package/assets/wasi/common.js +16 -0
  10. package/assets/wasi/config.d.ts +2 -0
  11. package/assets/wasi/config.js +7 -0
  12. package/assets/wasi/environment.d.ts +5 -0
  13. package/assets/wasi/environment.js +24 -0
  14. package/assets/wasi/filesystem.d.ts +117 -0
  15. package/assets/wasi/filesystem.js +341 -0
  16. package/assets/wasi/helpers/cli.d.ts +7 -0
  17. package/assets/wasi/helpers/cli.js +25 -0
  18. package/assets/wasi/helpers/filesystem.d.ts +6 -0
  19. package/assets/wasi/helpers/filesystem.js +10 -0
  20. package/assets/wasi/http.d.ts +13 -0
  21. package/assets/wasi/http.js +953 -0
  22. package/assets/wasi/in-memory-filesystem.d.ts +38 -0
  23. package/assets/wasi/in-memory-filesystem.js +620 -0
  24. package/assets/wasi/in-memory-http.d.ts +8 -0
  25. package/assets/wasi/in-memory-http.js +11 -0
  26. package/assets/wasi/in-memory-sockets.d.ts +34 -0
  27. package/assets/wasi/in-memory-sockets.js +294 -0
  28. package/assets/wasi/index.d.ts +7 -0
  29. package/assets/wasi/index.js +7 -0
  30. package/assets/wasi/io.d.ts +67 -0
  31. package/assets/wasi/io.js +366 -0
  32. package/assets/wasi/opfs-filesystem.d.ts +52 -0
  33. package/assets/wasi/opfs-filesystem.js +262 -0
  34. package/assets/wasi/random.d.ts +4 -0
  35. package/assets/wasi/random.js +51 -0
  36. package/assets/wasi/sockets.d.ts +9 -0
  37. package/assets/wasi/sockets.js +85 -0
  38. package/browser.mjs +209 -0
  39. package/client.mjs +138 -0
  40. package/index.d.ts +199 -0
  41. package/install.d.ts +22 -0
  42. package/install.mjs +176 -0
  43. package/native-registration.mjs +70 -0
  44. package/node.mjs +332 -0
  45. package/package.json +66 -4
  46. package/policy.mjs +433 -0
  47. package/worker.mjs +389 -0
package/worker.mjs ADDED
@@ -0,0 +1,389 @@
1
+ // Only this host-generated worker and validated jco output execute JavaScript.
2
+ // ref:DEC-host-sdk-direct-haskell
3
+
4
+ import * as wasi from './assets/wasi/index.js';
5
+ import {HostError, byteLength, outcomeCode} from './policy.mjs';
6
+ /** Installs one worker using the embedding build's immutable artifact allowlist. */
7
+ export function startWorker(manifestDigests)
8
+ {
9
+ const trustedManifests = new Set(manifestDigests);
10
+ let guest,
11
+ limits,
12
+ manifest,
13
+ remaining,
14
+ nextRequest = 0;
15
+ const pending = new Map();
16
+ const handles = new WeakMap();
17
+ const budgetFlags = [];
18
+ const fail = (code, message) =>
19
+ {
20
+ throw new HostError(code, message);
21
+ };
22
+ const camel = name =>
23
+ name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
24
+ const hex = bytes =>
25
+ [...new Uint8Array(bytes)]
26
+ .map(x => x.toString(16).padStart(2, '0'))
27
+ .join('');
28
+ const resources = new Map();
29
+ let nextResource = 0;
30
+ const className = name =>
31
+ {
32
+ const value = camel(name);
33
+ return value[0].toUpperCase() + value.slice(1);
34
+ };
35
+ // Conversion follows trusted WIT metadata, including ownership and field spelling.
36
+ // jco's resource objects never leave this worker.
37
+ function resultType(type)
38
+ {
39
+ if (typeof type !== 'number') return undefined;
40
+ const kind = manifest.contract.types[type].kind;
41
+ return kind.type !== undefined ? resultType(kind.type) : kind.result;
42
+ }
43
+ function convert(type, value, output)
44
+ {
45
+ if (type == null) return undefined;
46
+ if (typeof type === 'string')
47
+ {
48
+ if (/^[us]64$/.test(type))
49
+ {
50
+ const low = type === 's64' ? -(1n << 63n) : 0n;
51
+ const high = type === 's64' ? (1n << 63n) - 1n : (1n << 64n) - 1n;
52
+ if (typeof value !== 'bigint' || value < low || value > high)
53
+ fail('failed', 'expected ' + type + ' bigint');
54
+ }
55
+ else if (/^[us](8|16|32)$/.test(type))
56
+ {
57
+ const bits = Number(type.slice(1)),
58
+ signed = type[0] === 's';
59
+ if (
60
+ !Number.isInteger(value) ||
61
+ value < (signed ? -(2 ** (bits - 1)) : 0) ||
62
+ value > 2 ** (bits - (signed ? 1 : 0)) - 1
63
+ )
64
+ fail('failed', 'expected ' + type);
65
+ }
66
+ else if (
67
+ type === 'bool'
68
+ ? typeof value !== 'boolean'
69
+ : type === 'string'
70
+ ? typeof value !== 'string'
71
+ : true
72
+ )
73
+ fail('failed', 'unsupported or invalid scalar ' + type);
74
+ return value;
75
+ }
76
+ const definition = manifest.contract.types[type];
77
+ const kind = definition.kind;
78
+ if (kind.type !== undefined) return convert(kind.type, value, output);
79
+ if (kind.handle)
80
+ {
81
+ const target = kind.handle.own ?? kind.handle.borrow;
82
+ if (output)
83
+ {
84
+ if (kind.handle.own === undefined)
85
+ fail('unsupported', 'borrowed public result');
86
+ if (resources.size >= 1024)
87
+ fail('exhausted', 'resource allocation exhausted');
88
+ const id = String(++nextResource);
89
+ resources.set(id, {value, type: target});
90
+ return {'$host-sdk-resource': id};
91
+ }
92
+ const id = value?.['$host-sdk-resource'];
93
+ const entry = resources.get(id);
94
+ if (!entry || entry.type !== target)
95
+ fail('failed', 'invalid or released resource');
96
+ if (kind.handle.own !== undefined) resources.delete(id);
97
+ return entry.value;
98
+ }
99
+ if (kind.list !== undefined)
100
+ {
101
+ if (!Array.isArray(value) && !(value instanceof Uint8Array))
102
+ fail('failed', 'expected list');
103
+ return Array.from(value, item => convert(kind.list, item, output));
104
+ }
105
+ if (kind.record)
106
+ return Object.fromEntries(
107
+ kind.record.fields.map(field => [
108
+ output ? field.name : camel(field.name),
109
+ convert(
110
+ field.type,
111
+ value[output ? camel(field.name) : field.name],
112
+ output,
113
+ ),
114
+ ]),
115
+ );
116
+ if (kind.option !== undefined)
117
+ return value == null
118
+ ? output
119
+ ? null
120
+ : undefined
121
+ : convert(kind.option, value, output);
122
+ if (kind.variant)
123
+ {
124
+ const variant = kind.variant.cases.find(item => item.name === value?.tag);
125
+ if (!variant) fail('failed', 'invalid variant');
126
+ return {
127
+ tag: value.tag,
128
+ ...(variant.type == null
129
+ ? {}
130
+ : {val: convert(variant.type, value.val, output)}),
131
+ };
132
+ }
133
+ if (kind.enum)
134
+ {
135
+ if (!kind.enum.cases.some(item => item.name === value))
136
+ fail('failed', 'invalid enum');
137
+ return value;
138
+ }
139
+ if (kind.result)
140
+ {
141
+ if (value?.tag !== 'ok' && value?.tag !== 'err')
142
+ fail('failed', 'invalid result');
143
+ return {
144
+ tag: value.tag,
145
+ val: convert(kind.result[value.tag], value.val, output),
146
+ };
147
+ }
148
+ fail('unsupported', 'unsupported public type');
149
+ }
150
+ async function validated(url, expected)
151
+ {
152
+ const response = await fetch(url, {credentials: 'omit', redirect: 'error'});
153
+ if (!response.ok) fail('failed', 'asset unavailable');
154
+ const bytes = await response.arrayBuffer();
155
+ if (hex(await crypto.subtle.digest('SHA-256', bytes)) !== expected)
156
+ fail('failed', 'asset integrity mismatch');
157
+ return bytes;
158
+ }
159
+ function count()
160
+ {
161
+ if (--remaining < 0) fail('exhausted', 'request limit');
162
+ }
163
+ class Provider
164
+ {
165
+ [Symbol.dispose ?? Symbol.for('dispose')]()
166
+ {
167
+ handles.delete(this);
168
+ }
169
+ }
170
+ self.onmessage = async event =>
171
+ {
172
+ const message = event.data;
173
+ if (message.hostResponse)
174
+ {
175
+ const active = pending.get(message.id);
176
+ pending.delete(message.id);
177
+ if (!active) return;
178
+ if (message.error) active.reject(message.error);
179
+ else active.resolve(message.value);
180
+ return;
181
+ }
182
+ try
183
+ {
184
+ if (message.init)
185
+ {
186
+ ({limits, manifest} = message);
187
+ remaining = limits.requests;
188
+ const manifestDigest = hex(
189
+ await crypto.subtle.digest(
190
+ 'SHA-256',
191
+ new TextEncoder().encode(JSON.stringify(manifest)),
192
+ ),
193
+ );
194
+ if (!trustedManifests.has(manifestDigest))
195
+ fail('denied', 'unregistered component glue');
196
+ if (
197
+ typeof WebAssembly.Suspending !== 'function' ||
198
+ typeof WebAssembly.promising !== 'function'
199
+ )
200
+ fail('unsupported', 'WebAssembly JSPI required');
201
+ const allowed = new Set(message.providers);
202
+ const imports = {};
203
+ for (const group of ['cli', 'clocks', 'filesystem', 'io', 'random'])
204
+ {
205
+ for (const [name, value] of Object.entries(wasi[group]))
206
+ imports[
207
+ `wasi:${group}/${name.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}`
208
+ ] = value;
209
+ }
210
+ imports['wasi:cli/environment'] = {
211
+ getEnvironment: () => [],
212
+ getArguments: () => [],
213
+ initialCwd: () => undefined,
214
+ };
215
+ imports['wasi:filesystem/preopens'] = {getDirectories: () => []};
216
+ let output = 0;
217
+ const consume = bytes =>
218
+ {
219
+ output += bytes.byteLength;
220
+ if (output > limits.bytes) fail('exhausted', 'stdio output limit');
221
+ };
222
+ wasi.cli._setStdout({write: consume, flush()
223
+ {}});
224
+ wasi.cli._setStderr({write: consume, flush()
225
+ {}});
226
+ imports['host:sdk/host'] = {
227
+ Provider,
228
+ acquire(name)
229
+ {
230
+ try
231
+ {
232
+ count();
233
+ if (!allowed.has(name)) fail('denied', 'provider not granted');
234
+ const provider = new Provider();
235
+ handles.set(provider, name);
236
+ return {tag: 'ok', val: provider};
237
+ }
238
+ catch (error)
239
+ {
240
+ return {
241
+ tag: 'err',
242
+ val: {code: error.code ?? 'failed', message: error.message},
243
+ };
244
+ }
245
+ },
246
+ async perform(provider, request)
247
+ {
248
+ try
249
+ {
250
+ count();
251
+ const name = handles.get(provider);
252
+ if (!name) fail('denied', 'invalid or cross-instance provider');
253
+ if (byteLength(request) > limits.bytes)
254
+ fail('exhausted', 'request bytes');
255
+ const id = ++nextRequest;
256
+ const value = await new Promise((resolve, reject) =>
257
+ {
258
+ pending.set(id, {resolve, reject});
259
+ self.postMessage({hostRequest: {provider: name, request}, id});
260
+ });
261
+ return {tag: 'ok', val: value};
262
+ }
263
+ catch (error)
264
+ {
265
+ return {
266
+ tag: 'err',
267
+ val: {code: error.code ?? 'failed', message: error.message},
268
+ };
269
+ }
270
+ },
271
+ };
272
+ const code = await validated(
273
+ new URL('guest.js', message.manifestURL),
274
+ manifest.assets['guest.js'],
275
+ );
276
+ const blob = URL.createObjectURL(
277
+ new Blob([code], {type: 'text/javascript'}),
278
+ );
279
+ try
280
+ {
281
+ const {instantiate} = await import(blob);
282
+ guest = await instantiate(
283
+ async name =>
284
+ {
285
+ if (
286
+ !Object.hasOwn(manifest.assets, name) ||
287
+ name.includes('/') ||
288
+ name.includes('\\')
289
+ )
290
+ fail('denied', 'unregistered core asset');
291
+ return WebAssembly.compile(
292
+ await validated(
293
+ new URL(name, message.manifestURL),
294
+ manifest.assets[name],
295
+ ),
296
+ );
297
+ },
298
+ imports,
299
+ async (module, coreImports) =>
300
+ {
301
+ const instance = await WebAssembly.instantiate(
302
+ module,
303
+ coreImports,
304
+ );
305
+ const flag = instance.exports['host-sdk:memory-exhausted'];
306
+ if (flag) budgetFlags.push(flag);
307
+ return instance;
308
+ },
309
+ );
310
+ }
311
+ finally
312
+ {
313
+ URL.revokeObjectURL(blob);
314
+ }
315
+ self.postMessage({ready: true});
316
+ return;
317
+ }
318
+ if (!guest) fail('failed', 'instance not initialized');
319
+ if (byteLength(message.args) > limits.bytes)
320
+ fail('exhausted', 'input bytes');
321
+ const exported =
322
+ guest[manifest.interface] ?? guest[manifest.interface.split('@')[0]];
323
+ for (const flag of budgetFlags) flag.value = 0;
324
+ if (message.method === '[resource-drop]')
325
+ {
326
+ const id = message.args[0]?.['$host-sdk-resource'];
327
+ const entry = resources.get(id);
328
+ if (!entry) fail('failed', 'invalid or released resource');
329
+ resources.delete(id);
330
+ const dispose = Symbol.dispose ?? Symbol.for('dispose');
331
+ await entry.value[dispose]();
332
+ self.postMessage({value: undefined});
333
+ return;
334
+ }
335
+ const fn =
336
+ manifest.contract?.interfaces[manifest.interface]?.[message.method];
337
+ if (!fn) fail('unsupported', 'unknown guest method');
338
+ if (message.args.length !== fn.params.length)
339
+ fail('failed', 'argument count mismatch');
340
+ const args = fn.params.map((param, index) =>
341
+ convert(param.type, message.args[index], false),
342
+ );
343
+ let raw;
344
+ if (message.method.startsWith('[constructor]'))
345
+ raw = await new exported[className(message.method.slice(13))](...args);
346
+ else if (message.method.startsWith('[method]'))
347
+ {
348
+ const [, method] = message.method.slice(8).split('.');
349
+ raw = await args[0][camel(method)](...args.slice(1));
350
+ }
351
+ else if (message.method.startsWith('[static]'))
352
+ {
353
+ const [resource, method] = message.method.slice(8).split('.');
354
+ raw = await exported[className(resource)][camel(method)](...args);
355
+ }
356
+ else raw = await exported[camel(message.method)](...args);
357
+ const result = resultType(fn.result);
358
+ const value = convert(result ? result.ok : fn.result, raw, true);
359
+ if (byteLength(value) > limits.bytes) fail('exhausted', 'output bytes');
360
+ self.postMessage({value});
361
+ }
362
+ catch (error)
363
+ {
364
+ const componentError = error != null && Object.hasOwn(error, 'payload');
365
+ const schema = resultType(
366
+ manifest?.contract?.interfaces[manifest.interface]?.[message.method]
367
+ ?.result,
368
+ );
369
+ const payload =
370
+ componentError && schema
371
+ ? convert(schema.err, error.payload, true)
372
+ : error;
373
+
374
+ self.postMessage({
375
+ fatal: !componentError || budgetFlags.some(flag => flag.value !== 0),
376
+ error: {
377
+ code: budgetFlags.some(flag => flag.value !== 0)
378
+ ? 'exhausted'
379
+ : outcomeCode(payload?.code),
380
+ message:
381
+ typeof payload === 'string'
382
+ ? payload
383
+ : (payload?.message ?? String(error)),
384
+ ...(componentError ? {payload} : {}),
385
+ },
386
+ });
387
+ }
388
+ };
389
+ }