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.
- package/LICENSE +21 -0
- package/README.md +224 -2
- package/assets/wasi/LICENSE +220 -0
- package/assets/wasi/cli.d.ts +43 -0
- package/assets/wasi/cli.js +147 -0
- package/assets/wasi/clocks.d.ts +3 -0
- package/assets/wasi/clocks.js +55 -0
- package/assets/wasi/common.d.ts +5 -0
- package/assets/wasi/common.js +16 -0
- package/assets/wasi/config.d.ts +2 -0
- package/assets/wasi/config.js +7 -0
- package/assets/wasi/environment.d.ts +5 -0
- package/assets/wasi/environment.js +24 -0
- package/assets/wasi/filesystem.d.ts +117 -0
- package/assets/wasi/filesystem.js +341 -0
- package/assets/wasi/helpers/cli.d.ts +7 -0
- package/assets/wasi/helpers/cli.js +25 -0
- package/assets/wasi/helpers/filesystem.d.ts +6 -0
- package/assets/wasi/helpers/filesystem.js +10 -0
- package/assets/wasi/http.d.ts +13 -0
- package/assets/wasi/http.js +953 -0
- package/assets/wasi/in-memory-filesystem.d.ts +38 -0
- package/assets/wasi/in-memory-filesystem.js +620 -0
- package/assets/wasi/in-memory-http.d.ts +8 -0
- package/assets/wasi/in-memory-http.js +11 -0
- package/assets/wasi/in-memory-sockets.d.ts +34 -0
- package/assets/wasi/in-memory-sockets.js +294 -0
- package/assets/wasi/index.d.ts +7 -0
- package/assets/wasi/index.js +7 -0
- package/assets/wasi/io.d.ts +67 -0
- package/assets/wasi/io.js +366 -0
- package/assets/wasi/opfs-filesystem.d.ts +52 -0
- package/assets/wasi/opfs-filesystem.js +262 -0
- package/assets/wasi/random.d.ts +4 -0
- package/assets/wasi/random.js +51 -0
- package/assets/wasi/sockets.d.ts +9 -0
- package/assets/wasi/sockets.js +85 -0
- package/browser.mjs +209 -0
- package/client.mjs +138 -0
- package/index.d.ts +199 -0
- package/install.d.ts +22 -0
- package/install.mjs +176 -0
- package/native-registration.mjs +70 -0
- package/node.mjs +332 -0
- package/package.json +66 -4
- package/policy.mjs +433 -0
- package/worker.mjs +389 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { checkedU64AsNumber } from "./common.js";
|
|
2
|
+
const MAX_BYTES = 65536;
|
|
3
|
+
let insecureRandomValue1, insecureRandomValue2;
|
|
4
|
+
export const insecure = {
|
|
5
|
+
getInsecureRandomBytes(len) {
|
|
6
|
+
return random.getRandomBytes(len);
|
|
7
|
+
},
|
|
8
|
+
getInsecureRandomU64() {
|
|
9
|
+
return random.getRandomU64();
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
let insecureSeedValue1, insecureSeedValue2;
|
|
13
|
+
export const insecureSeed = {
|
|
14
|
+
insecureSeed() {
|
|
15
|
+
if (insecureSeedValue1 === undefined || insecureSeedValue2 === undefined) {
|
|
16
|
+
insecureSeedValue1 = random.getRandomU64();
|
|
17
|
+
insecureSeedValue2 = random.getRandomU64();
|
|
18
|
+
}
|
|
19
|
+
return [insecureSeedValue1, insecureSeedValue2];
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export const random = {
|
|
23
|
+
getRandomBytes(len) {
|
|
24
|
+
const byteLength = checkedU64AsNumber(len, "random byte length");
|
|
25
|
+
const bytes = new Uint8Array(byteLength);
|
|
26
|
+
if (byteLength > MAX_BYTES) {
|
|
27
|
+
// this is the max bytes crypto.getRandomValues
|
|
28
|
+
// can do at once see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues
|
|
29
|
+
for (let generated = 0; generated < byteLength; generated += MAX_BYTES) {
|
|
30
|
+
// buffer.slice automatically checks if the end is past the end of
|
|
31
|
+
// the buffer so we don't have to here
|
|
32
|
+
crypto.getRandomValues(bytes.subarray(generated, generated + MAX_BYTES));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
crypto.getRandomValues(bytes);
|
|
37
|
+
}
|
|
38
|
+
return bytes;
|
|
39
|
+
},
|
|
40
|
+
getRandomU64() {
|
|
41
|
+
return crypto.getRandomValues(new BigUint64Array(1))[0];
|
|
42
|
+
},
|
|
43
|
+
// @ts-expect-error Not defined in WIT
|
|
44
|
+
insecureRandom() {
|
|
45
|
+
if (insecureRandomValue1 === undefined || insecureRandomValue2 === undefined) {
|
|
46
|
+
insecureRandomValue1 = random.getRandomU64();
|
|
47
|
+
insecureRandomValue2 = random.getRandomU64();
|
|
48
|
+
}
|
|
49
|
+
return [insecureRandomValue1, insecureRandomValue2];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { instanceNetwork as InstanceNetworkNamespace, ipNameLookup as IpNameLookupNamespace, network as NetworkNamespace, tcpCreateSocket as TcpCreateSocketNamespace, tcp as TcpNamespace, udpCreateSocket as UdpCreateSocketNamespace, udp as UdpNamespace } from "../../types/sockets.js";
|
|
2
|
+
export { InMemoryTcpClient, InMemoryTcpSockets, InMemoryUdpClient, InMemoryUdpSockets, } from "./in-memory-sockets.js";
|
|
3
|
+
export declare const instanceNetwork: typeof InstanceNetworkNamespace;
|
|
4
|
+
export declare const network: typeof NetworkNamespace;
|
|
5
|
+
export declare const ipNameLookup: typeof IpNameLookupNamespace;
|
|
6
|
+
export declare const tcpCreateSocket: typeof TcpCreateSocketNamespace;
|
|
7
|
+
export declare const tcp: typeof TcpNamespace;
|
|
8
|
+
export declare const udpCreateSocket: typeof UdpCreateSocketNamespace;
|
|
9
|
+
export declare const udp: typeof UdpNamespace;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export { InMemoryTcpClient, InMemoryTcpSockets, InMemoryUdpClient, InMemoryUdpSockets, } from "./in-memory-sockets.js";
|
|
2
|
+
const unsupported = () => {
|
|
3
|
+
throw "not-supported";
|
|
4
|
+
};
|
|
5
|
+
class Network {
|
|
6
|
+
}
|
|
7
|
+
const defaultNetwork = new Network();
|
|
8
|
+
export const instanceNetwork = {
|
|
9
|
+
instanceNetwork: () => defaultNetwork,
|
|
10
|
+
};
|
|
11
|
+
export const network = { Network };
|
|
12
|
+
class ResolveAddressStream {
|
|
13
|
+
resolveNextAddress = unsupported;
|
|
14
|
+
subscribe = unsupported;
|
|
15
|
+
}
|
|
16
|
+
export const ipNameLookup = {
|
|
17
|
+
ResolveAddressStream,
|
|
18
|
+
resolveAddresses: unsupported,
|
|
19
|
+
};
|
|
20
|
+
class TcpSocket {
|
|
21
|
+
startBind = unsupported;
|
|
22
|
+
finishBind = unsupported;
|
|
23
|
+
startConnect = unsupported;
|
|
24
|
+
finishConnect = unsupported;
|
|
25
|
+
startListen = unsupported;
|
|
26
|
+
finishListen = unsupported;
|
|
27
|
+
accept = unsupported;
|
|
28
|
+
localAddress = unsupported;
|
|
29
|
+
remoteAddress = unsupported;
|
|
30
|
+
isListening = unsupported;
|
|
31
|
+
addressFamily = unsupported;
|
|
32
|
+
setListenBacklogSize = unsupported;
|
|
33
|
+
keepAliveEnabled = unsupported;
|
|
34
|
+
setKeepAliveEnabled = unsupported;
|
|
35
|
+
keepAliveIdleTime = unsupported;
|
|
36
|
+
setKeepAliveIdleTime = unsupported;
|
|
37
|
+
keepAliveInterval = unsupported;
|
|
38
|
+
setKeepAliveInterval = unsupported;
|
|
39
|
+
keepAliveCount = unsupported;
|
|
40
|
+
setKeepAliveCount = unsupported;
|
|
41
|
+
hopLimit = unsupported;
|
|
42
|
+
setHopLimit = unsupported;
|
|
43
|
+
receiveBufferSize = unsupported;
|
|
44
|
+
setReceiveBufferSize = unsupported;
|
|
45
|
+
sendBufferSize = unsupported;
|
|
46
|
+
setSendBufferSize = unsupported;
|
|
47
|
+
subscribe = unsupported;
|
|
48
|
+
shutdown = unsupported;
|
|
49
|
+
}
|
|
50
|
+
export const tcpCreateSocket = {
|
|
51
|
+
createTcpSocket: unsupported,
|
|
52
|
+
};
|
|
53
|
+
export const tcp = { TcpSocket };
|
|
54
|
+
class IncomingDatagramStream {
|
|
55
|
+
receive = unsupported;
|
|
56
|
+
subscribe = unsupported;
|
|
57
|
+
}
|
|
58
|
+
class OutgoingDatagramStream {
|
|
59
|
+
checkSend = unsupported;
|
|
60
|
+
send = unsupported;
|
|
61
|
+
subscribe = unsupported;
|
|
62
|
+
}
|
|
63
|
+
class UdpSocket {
|
|
64
|
+
startBind = unsupported;
|
|
65
|
+
finishBind = unsupported;
|
|
66
|
+
stream = unsupported;
|
|
67
|
+
localAddress = unsupported;
|
|
68
|
+
remoteAddress = unsupported;
|
|
69
|
+
addressFamily = unsupported;
|
|
70
|
+
unicastHopLimit = unsupported;
|
|
71
|
+
setUnicastHopLimit = unsupported;
|
|
72
|
+
receiveBufferSize = unsupported;
|
|
73
|
+
setReceiveBufferSize = unsupported;
|
|
74
|
+
sendBufferSize = unsupported;
|
|
75
|
+
setSendBufferSize = unsupported;
|
|
76
|
+
subscribe = unsupported;
|
|
77
|
+
}
|
|
78
|
+
export const udpCreateSocket = {
|
|
79
|
+
createUdpSocket: unsupported,
|
|
80
|
+
};
|
|
81
|
+
export const udp = {
|
|
82
|
+
IncomingDatagramStream,
|
|
83
|
+
OutgoingDatagramStream,
|
|
84
|
+
UdpSocket,
|
|
85
|
+
};
|
package/browser.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Privileged handles and provider functions stay in the embedding page.
|
|
2
|
+
// ref:DEC-host-sdk-direct-haskell
|
|
3
|
+
import {
|
|
4
|
+
HostError,
|
|
5
|
+
fail,
|
|
6
|
+
configure,
|
|
7
|
+
createBroker,
|
|
8
|
+
byteLength,
|
|
9
|
+
createResourceScope,
|
|
10
|
+
trustedJavaScript,
|
|
11
|
+
} from './policy.mjs';
|
|
12
|
+
export {HostError, trustedJavaScript};
|
|
13
|
+
export {trustedDOM, trustedNativeFFI, trustedProcess} from './policy.mjs';
|
|
14
|
+
|
|
15
|
+
const hex = bytes =>
|
|
16
|
+
[...new Uint8Array(bytes)].map(x => x.toString(16).padStart(2, '0')).join('');
|
|
17
|
+
/** Starts a registered component in a worker with grants retained by the page broker. */
|
|
18
|
+
export async function createHost(options = {})
|
|
19
|
+
{
|
|
20
|
+
const registration = options.registration;
|
|
21
|
+
if (
|
|
22
|
+
!registration ||
|
|
23
|
+
!Array.isArray(registration.manifests) ||
|
|
24
|
+
!registration.workerURL
|
|
25
|
+
)
|
|
26
|
+
fail('unsupported', 'host-generated browser registration required');
|
|
27
|
+
const trustedManifests = new Set(registration.manifests);
|
|
28
|
+
const resources = createResourceScope();
|
|
29
|
+
const configuration = configure(options),
|
|
30
|
+
{limits, grants} = configuration;
|
|
31
|
+
if (
|
|
32
|
+
!globalThis.isSecureContext ||
|
|
33
|
+
!globalThis.crypto?.subtle ||
|
|
34
|
+
typeof Worker === 'undefined'
|
|
35
|
+
)
|
|
36
|
+
fail('unsupported', 'secure context, workers and WebCrypto required');
|
|
37
|
+
if (
|
|
38
|
+
typeof WebAssembly.Suspending !== 'function' ||
|
|
39
|
+
typeof WebAssembly.promising !== 'function'
|
|
40
|
+
)
|
|
41
|
+
fail('unsupported', 'WebAssembly JSPI required; no fallback runtime');
|
|
42
|
+
if (!options.manifest)
|
|
43
|
+
fail('failed', 'an embedding-approved component manifest is required');
|
|
44
|
+
const manifestURL = new URL(options.manifest, import.meta.url);
|
|
45
|
+
const response = await fetch(manifestURL, {
|
|
46
|
+
credentials: 'omit',
|
|
47
|
+
redirect: 'error',
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) fail('failed', 'manifest unavailable');
|
|
50
|
+
const manifest = await response.json();
|
|
51
|
+
const manifestDigest = hex(
|
|
52
|
+
await crypto.subtle.digest(
|
|
53
|
+
'SHA-256',
|
|
54
|
+
new TextEncoder().encode(JSON.stringify(manifest)),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
if (!trustedManifests.has(manifestDigest))
|
|
58
|
+
fail('denied', 'component is not in the host-generated artifact registry');
|
|
59
|
+
if (options.deployment === 'isolated' && !globalThis.crossOriginIsolated)
|
|
60
|
+
fail('unsupported', 'isolated deployment requires COOP/COEP headers');
|
|
61
|
+
if (
|
|
62
|
+
options.deployment &&
|
|
63
|
+
!['standard', 'isolated'].includes(options.deployment)
|
|
64
|
+
)
|
|
65
|
+
fail('unsupported', 'unknown deployment profile');
|
|
66
|
+
if (manifest.version !== 1 || manifest.browser?.memory > limits.memory)
|
|
67
|
+
fail('unsupported', 'artifact exceeds browser memory policy');
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const broker = createBroker(configuration, {
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
storage: options.storage,
|
|
72
|
+
});
|
|
73
|
+
const worker = new Worker(registration.workerURL, {
|
|
74
|
+
type: 'module',
|
|
75
|
+
});
|
|
76
|
+
let closed = false,
|
|
77
|
+
pending,
|
|
78
|
+
timer,
|
|
79
|
+
queue = Promise.resolve();
|
|
80
|
+
const stop = (error = new HostError('cancelled', 'instance revoked')) =>
|
|
81
|
+
{
|
|
82
|
+
if (closed) return;
|
|
83
|
+
closed = true;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
controller.abort(error);
|
|
86
|
+
worker.terminate();
|
|
87
|
+
broker.dispose();
|
|
88
|
+
pending?.reject(error);
|
|
89
|
+
pending = undefined;
|
|
90
|
+
};
|
|
91
|
+
const wait = () =>
|
|
92
|
+
new Promise((resolve, reject) =>
|
|
93
|
+
{
|
|
94
|
+
pending = {resolve, reject};
|
|
95
|
+
timer = setTimeout(
|
|
96
|
+
() => stop(new HostError('exhausted', 'execution deadline')),
|
|
97
|
+
limits.timeout,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
worker.onerror = event => stop(new HostError('failed', event.message));
|
|
101
|
+
worker.onmessage = event =>
|
|
102
|
+
{
|
|
103
|
+
const message = event.data;
|
|
104
|
+
if (message.hostRequest)
|
|
105
|
+
{
|
|
106
|
+
const {provider, request} = message.hostRequest;
|
|
107
|
+
broker.perform(provider, request).then(
|
|
108
|
+
value =>
|
|
109
|
+
{
|
|
110
|
+
if (!closed)
|
|
111
|
+
worker.postMessage({hostResponse: true, id: message.id, value});
|
|
112
|
+
},
|
|
113
|
+
error =>
|
|
114
|
+
{
|
|
115
|
+
if (!closed)
|
|
116
|
+
worker.postMessage({
|
|
117
|
+
hostResponse: true,
|
|
118
|
+
id: message.id,
|
|
119
|
+
error: {code: error.code ?? 'failed', message: error.message},
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const active = pending;
|
|
126
|
+
pending = undefined;
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
if (!active)
|
|
129
|
+
return stop(new HostError('failed', 'unexpected worker response'));
|
|
130
|
+
if (message.error)
|
|
131
|
+
{
|
|
132
|
+
active.reject(
|
|
133
|
+
new HostError(
|
|
134
|
+
message.error.code,
|
|
135
|
+
message.error.message,
|
|
136
|
+
resources.decode(message.error.payload),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
// Guest-returned capability errors leave the component usable; traps do not.
|
|
140
|
+
if (message.fatal) stop();
|
|
141
|
+
}
|
|
142
|
+
else active.resolve(message);
|
|
143
|
+
};
|
|
144
|
+
const ready = wait();
|
|
145
|
+
worker.postMessage({
|
|
146
|
+
init: true,
|
|
147
|
+
manifestURL: manifestURL.href,
|
|
148
|
+
manifest,
|
|
149
|
+
limits,
|
|
150
|
+
providers: [...grants.keys()],
|
|
151
|
+
});
|
|
152
|
+
try
|
|
153
|
+
{
|
|
154
|
+
await ready;
|
|
155
|
+
}
|
|
156
|
+
catch (error)
|
|
157
|
+
{
|
|
158
|
+
stop(error);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
const policy = await crypto.subtle.digest(
|
|
162
|
+
'SHA-256',
|
|
163
|
+
new TextEncoder().encode(
|
|
164
|
+
JSON.stringify([...grants]) + JSON.stringify(limits),
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
identity: Object.freeze({
|
|
169
|
+
runtime: `jco-${manifest.generators.jco}`,
|
|
170
|
+
artifact: manifest.sha256,
|
|
171
|
+
provider: manifest.providers,
|
|
172
|
+
instance: crypto.randomUUID(),
|
|
173
|
+
policy: hex(policy),
|
|
174
|
+
interface: manifest.interface,
|
|
175
|
+
}),
|
|
176
|
+
invoke(method, args = [], {signal} = {})
|
|
177
|
+
{
|
|
178
|
+
const run = async () =>
|
|
179
|
+
{
|
|
180
|
+
if (closed || signal?.aborted) fail('cancelled', 'instance revoked');
|
|
181
|
+
const encoded = resources.encode(args);
|
|
182
|
+
if (byteLength(encoded) > limits.bytes)
|
|
183
|
+
fail('exhausted', 'input byte limit');
|
|
184
|
+
const abort = () => stop();
|
|
185
|
+
signal?.addEventListener('abort', abort, {once: true});
|
|
186
|
+
try
|
|
187
|
+
{
|
|
188
|
+
const result = wait();
|
|
189
|
+
worker.postMessage({method, args: encoded});
|
|
190
|
+
return resources.decode((await result).value);
|
|
191
|
+
}
|
|
192
|
+
finally
|
|
193
|
+
{
|
|
194
|
+
signal?.removeEventListener('abort', abort);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const result = queue.then(run);
|
|
198
|
+
queue = result.catch(() =>
|
|
199
|
+
{});
|
|
200
|
+
return result;
|
|
201
|
+
},
|
|
202
|
+
drop(resource, controls)
|
|
203
|
+
{
|
|
204
|
+
return this.invoke('[resource-drop]', [resource], controls);
|
|
205
|
+
},
|
|
206
|
+
revoke: () => stop(),
|
|
207
|
+
close: () => stop(),
|
|
208
|
+
});
|
|
209
|
+
}
|
package/client.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Typed façades use WIT metadata; application decisions never live in this module.
|
|
2
|
+
// ref:DEC-host-sdk-typed-application-boundary
|
|
3
|
+
import {HostError} from './policy.mjs';
|
|
4
|
+
const camel = name =>
|
|
5
|
+
name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
6
|
+
const pascal = name =>
|
|
7
|
+
{
|
|
8
|
+
const value = camel(name);
|
|
9
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Adds Promise-returning WIT operations and scoped resources to a generic host. */
|
|
13
|
+
export function createClient(host, contract, interfaceName)
|
|
14
|
+
{
|
|
15
|
+
const resultType = type =>
|
|
16
|
+
{
|
|
17
|
+
if (typeof type !== 'number') return undefined;
|
|
18
|
+
const kind = contract.types[type].kind;
|
|
19
|
+
return kind.type !== undefined ? resultType(kind.type) : kind.result;
|
|
20
|
+
};
|
|
21
|
+
const functions = contract.interfaces[interfaceName];
|
|
22
|
+
const wrappers = new WeakMap();
|
|
23
|
+
function transform(type, value, output)
|
|
24
|
+
{
|
|
25
|
+
if (type == null || typeof type === 'string') return value;
|
|
26
|
+
const {kind} = contract.types[type];
|
|
27
|
+
if (kind.type !== undefined) return transform(kind.type, value, output);
|
|
28
|
+
if (kind.handle)
|
|
29
|
+
{
|
|
30
|
+
const target = kind.handle.own ?? kind.handle.borrow;
|
|
31
|
+
if (!output)
|
|
32
|
+
{
|
|
33
|
+
const entry = wrappers.get(value);
|
|
34
|
+
if (!entry || entry.type !== target)
|
|
35
|
+
throw new HostError(
|
|
36
|
+
'denied',
|
|
37
|
+
'resource belongs to another client or type',
|
|
38
|
+
);
|
|
39
|
+
return entry.token;
|
|
40
|
+
}
|
|
41
|
+
const resource = {};
|
|
42
|
+
wrappers.set(resource, {type: target, token: value});
|
|
43
|
+
for (const fn of Object.values(functions))
|
|
44
|
+
if (fn.kind?.method === target)
|
|
45
|
+
{
|
|
46
|
+
const name = camel(fn.name.split('.')[1]);
|
|
47
|
+
if (name === 'dispose' || name === 'then')
|
|
48
|
+
throw new HostError(
|
|
49
|
+
'unsupported',
|
|
50
|
+
'resource method conflicts with lifecycle',
|
|
51
|
+
);
|
|
52
|
+
resource[name] = (...args) => call(fn, [resource, ...args]);
|
|
53
|
+
}
|
|
54
|
+
resource.dispose = controls => host.drop(value, controls);
|
|
55
|
+
return Object.freeze(resource);
|
|
56
|
+
}
|
|
57
|
+
if (kind.list !== undefined)
|
|
58
|
+
{
|
|
59
|
+
const items = Array.from(value, item =>
|
|
60
|
+
transform(kind.list, item, output),
|
|
61
|
+
);
|
|
62
|
+
return output && kind.list === 'u8' ? new Uint8Array(items) : items;
|
|
63
|
+
}
|
|
64
|
+
if (kind.record)
|
|
65
|
+
return Object.fromEntries(
|
|
66
|
+
kind.record.fields.map(field => [
|
|
67
|
+
output ? camel(field.name) : field.name,
|
|
68
|
+
transform(
|
|
69
|
+
field.type,
|
|
70
|
+
value[output ? field.name : camel(field.name)],
|
|
71
|
+
output,
|
|
72
|
+
),
|
|
73
|
+
]),
|
|
74
|
+
);
|
|
75
|
+
if (kind.option !== undefined)
|
|
76
|
+
return value == null ? null : transform(kind.option, value, output);
|
|
77
|
+
if (kind.variant)
|
|
78
|
+
{
|
|
79
|
+
const variant = kind.variant.cases.find(item => item.name === value?.tag);
|
|
80
|
+
if (!variant) throw new HostError('failed', 'invalid variant');
|
|
81
|
+
return {
|
|
82
|
+
tag: value.tag,
|
|
83
|
+
...(variant.type == null
|
|
84
|
+
? {}
|
|
85
|
+
: {val: transform(variant.type, value.val, output)}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (kind.result)
|
|
89
|
+
{
|
|
90
|
+
if (value?.tag !== 'ok' && value?.tag !== 'err')
|
|
91
|
+
throw new HostError('failed', 'invalid result');
|
|
92
|
+
return {
|
|
93
|
+
tag: value.tag,
|
|
94
|
+
val: transform(kind.result[value.tag], value.val, output),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (kind.enum) return value;
|
|
98
|
+
throw new HostError('unsupported', 'unsupported façade type');
|
|
99
|
+
}
|
|
100
|
+
async function call(fn, values)
|
|
101
|
+
{
|
|
102
|
+
if (
|
|
103
|
+
values.length < fn.params.length ||
|
|
104
|
+
values.length > fn.params.length + 1
|
|
105
|
+
)
|
|
106
|
+
throw new HostError('failed', 'argument count mismatch');
|
|
107
|
+
const args = fn.params.map((param, i) =>
|
|
108
|
+
transform(param.type, values[i], false),
|
|
109
|
+
);
|
|
110
|
+
const result = resultType(fn.result);
|
|
111
|
+
try
|
|
112
|
+
{
|
|
113
|
+
const value = await host.invoke(fn.name, args, values[fn.params.length]);
|
|
114
|
+
return transform(result ? result.ok : fn.result, value, true);
|
|
115
|
+
}
|
|
116
|
+
catch (error)
|
|
117
|
+
{
|
|
118
|
+
if (result && Object.hasOwn(error, 'payload'))
|
|
119
|
+
error.payload = transform(result.err, error.payload, true);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const client = {...host};
|
|
124
|
+
for (const fn of Object.values(functions))
|
|
125
|
+
{
|
|
126
|
+
if (fn.kind?.method !== undefined) continue;
|
|
127
|
+
const name = fn.name.startsWith('[constructor]')
|
|
128
|
+
? 'new' + pascal(fn.name.slice(13))
|
|
129
|
+
: camel(fn.name.replace(/^\[static\]/, '').replace('.', '-'));
|
|
130
|
+
if (Object.hasOwn(client, name) || name === 'then')
|
|
131
|
+
throw new HostError(
|
|
132
|
+
'unsupported',
|
|
133
|
+
'export conflicts with client lifecycle',
|
|
134
|
+
);
|
|
135
|
+
client[name] = (...args) => call(fn, args);
|
|
136
|
+
}
|
|
137
|
+
return Object.freeze(client);
|
|
138
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/** Stable refusal and failure classifications shared with WIT. ref:DEC-host-sdk-direct-haskell */
|
|
2
|
+
export type Status =
|
|
3
|
+
'denied' | 'unsupported' | 'cancelled' | 'exhausted' | 'failed';
|
|
4
|
+
/** Reports an operation outcome separately from an exception message. */
|
|
5
|
+
export class HostError extends Error
|
|
6
|
+
{
|
|
7
|
+
/** Classifies the outcome for programmatic handling. */
|
|
8
|
+
readonly code: Status;
|
|
9
|
+
/** Original typed error value when a component returns result::err. */
|
|
10
|
+
readonly payload?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** Immutable resource budgets for one runtime instance. */
|
|
13
|
+
export interface Limits
|
|
14
|
+
{
|
|
15
|
+
/** Aggregate guest linear-memory budget in bytes; excludes trusted host heaps. */
|
|
16
|
+
memory: number;
|
|
17
|
+
/** Maximum request or response bytes, with bounded transport overhead. */
|
|
18
|
+
bytes: number;
|
|
19
|
+
/** Maximum imported capability calls during the instance lifetime. */
|
|
20
|
+
requests: number;
|
|
21
|
+
/** Wasmtime instruction fuel per invocation; browsers use the execution deadline. */
|
|
22
|
+
fuel: number;
|
|
23
|
+
/** Startup or invocation deadline in milliseconds; expiration terminates the instance. */
|
|
24
|
+
timeout: number;
|
|
25
|
+
}
|
|
26
|
+
/** Lets trusted provider implementations cooperate with revocation. */
|
|
27
|
+
export interface ExecutionContext
|
|
28
|
+
{
|
|
29
|
+
/** Aborts when the instance is revoked or an invocation is cancelled. */
|
|
30
|
+
signal: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
/** Authority supplied by the embedding application, never by guest declarations. */
|
|
33
|
+
export type Grant =
|
|
34
|
+
| {
|
|
35
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
36
|
+
kind: 'files';
|
|
37
|
+
/** Node directory opened as a capability; never exposed as an ambient WASI preopen. */
|
|
38
|
+
root?: string;
|
|
39
|
+
/** Browser-owned directory handle; consent remains the embedding application responsibility. */
|
|
40
|
+
handle?: FileSystemDirectoryHandle;
|
|
41
|
+
/** Allows reads within this provider scope. */
|
|
42
|
+
read?: boolean;
|
|
43
|
+
/** Allows writes within this provider scope. */
|
|
44
|
+
write?: boolean;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
48
|
+
kind: 'network';
|
|
49
|
+
/** Explicitly permits HTTP in addition to the HTTPS default. */
|
|
50
|
+
allowHttp?: boolean;
|
|
51
|
+
/** Approved origin, path and operation combinations; redirects cannot widen them. */
|
|
52
|
+
destinations: {
|
|
53
|
+
/** Exact normalized URL origin, including an explicit non-default port. */
|
|
54
|
+
origin: string;
|
|
55
|
+
/** Exact URL pathname; defaults to the origin root. */
|
|
56
|
+
path?: string;
|
|
57
|
+
/** Also permits descendants under the path segment boundary. */
|
|
58
|
+
subpaths?: boolean;
|
|
59
|
+
/** Allowed HTTP methods for this destination. */
|
|
60
|
+
methods: ('GET' | 'POST')[];
|
|
61
|
+
}[];
|
|
62
|
+
/** Explicit application-supplied headers, including any mediated credentials. */
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
67
|
+
kind: 'storage';
|
|
68
|
+
/** Application/plugin storage namespace; no ambient storage lookup. */
|
|
69
|
+
namespace: string;
|
|
70
|
+
/** Maximum UTF-8 bytes of keys and values in the namespace. */
|
|
71
|
+
quota: number;
|
|
72
|
+
/** Allows reads within this provider scope. */
|
|
73
|
+
read?: boolean;
|
|
74
|
+
/** Allows writes within this provider scope. */
|
|
75
|
+
write?: boolean;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
79
|
+
kind: 'environment';
|
|
80
|
+
/** Explicit environment values; process and page environment are never inherited. */
|
|
81
|
+
values: Record<string, string>;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
85
|
+
kind: 'ui';
|
|
86
|
+
/** Embedding-selected element hosting an isolated text-rendering frame. */
|
|
87
|
+
surface: HTMLElement;
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
91
|
+
kind: 'native';
|
|
92
|
+
/** Must return true before this native provider executes an input. */
|
|
93
|
+
validate(input: string): boolean;
|
|
94
|
+
/** Trusted application implementation; should cooperate with the cancellation signal. */
|
|
95
|
+
execute(
|
|
96
|
+
input: string,
|
|
97
|
+
context: ExecutionContext,
|
|
98
|
+
): string | Promise<string>;
|
|
99
|
+
}
|
|
100
|
+
| TrustedGrant;
|
|
101
|
+
/** Embedding-owned escape callback, rejected in the hostile profile. */
|
|
102
|
+
export interface TrustedGrant
|
|
103
|
+
{
|
|
104
|
+
/** Selects the provider whose operations and scope are authorized. */
|
|
105
|
+
readonly kind: 'trusted';
|
|
106
|
+
/** Trusted application implementation; should cooperate with the cancellation signal. */
|
|
107
|
+
readonly execute: (
|
|
108
|
+
request: unknown,
|
|
109
|
+
context: ExecutionContext,
|
|
110
|
+
) => string | Promise<string>;
|
|
111
|
+
}
|
|
112
|
+
/** Wraps arbitrary JavaScript execution for the explicit trusted profile. */
|
|
113
|
+
export function trustedJavaScript(
|
|
114
|
+
execute: TrustedGrant['execute'],
|
|
115
|
+
): TrustedGrant;
|
|
116
|
+
/** Wraps unrestricted DOM execution for the explicit trusted profile. */
|
|
117
|
+
export function trustedDOM(execute: TrustedGrant['execute']): TrustedGrant;
|
|
118
|
+
/** Wraps native FFI execution for the explicit trusted profile. */
|
|
119
|
+
export function trustedNativeFFI(
|
|
120
|
+
execute: TrustedGrant['execute'],
|
|
121
|
+
): TrustedGrant;
|
|
122
|
+
/** Wraps unsandboxed process execution for the explicit trusted profile. */
|
|
123
|
+
export function trustedProcess(execute: TrustedGrant['execute']): TrustedGrant;
|
|
124
|
+
/** Trusted build output; never construct this authority from a guest manifest. */
|
|
125
|
+
export interface NativeArtifactRegistration
|
|
126
|
+
{
|
|
127
|
+
readonly artifact: URL | string;
|
|
128
|
+
readonly componentSha256: string;
|
|
129
|
+
readonly nativeSha256: string;
|
|
130
|
+
readonly configuration: string;
|
|
131
|
+
readonly target: string;
|
|
132
|
+
}
|
|
133
|
+
/** Selects an application-approved artifact and immutable per-instance grants. */
|
|
134
|
+
export interface HostOptions
|
|
135
|
+
{
|
|
136
|
+
/** Node loads trusted native code prepared during installation; runtime compilation is unavailable. */
|
|
137
|
+
compilation?: 'precompiled';
|
|
138
|
+
/** Node authority supplied by trusted embedding code, independent of guest declarations. */
|
|
139
|
+
nativeArtifacts?: Readonly<Record<string, NativeArtifactRegistration>>;
|
|
140
|
+
/** Node only: false allows an idle embedding process to exit and releases its helper. */
|
|
141
|
+
keepAlive?: boolean;
|
|
142
|
+
/** Required by browser callers; generated by the embedding application's build. */
|
|
143
|
+
registration?: {
|
|
144
|
+
readonly manifests: readonly string[];
|
|
145
|
+
readonly workerURL: URL | string;
|
|
146
|
+
};
|
|
147
|
+
/** Host-built component manifest URL; browsers additionally require registry membership. */
|
|
148
|
+
manifest: URL | string;
|
|
149
|
+
/** Defaults to hostile; trusted explicitly permits escape callbacks. */
|
|
150
|
+
profile?: 'hostile' | 'trusted';
|
|
151
|
+
/** Browser deployment requirements, separate from capability grants. */
|
|
152
|
+
deployment?: 'standard' | 'isolated';
|
|
153
|
+
/** Named providers granted by the embedding application. */
|
|
154
|
+
grants?: Record<string, Grant>;
|
|
155
|
+
/** Overrides to bounded runtime defaults. */
|
|
156
|
+
limits?: Partial<Limits>;
|
|
157
|
+
/** Optional application-owned store shared only through authorized namespaces. */
|
|
158
|
+
storage?: Map<string, Map<string, string>>;
|
|
159
|
+
}
|
|
160
|
+
/** Records exact artifact and runtime identity without implying proof or cache validity. */
|
|
161
|
+
export interface Identity
|
|
162
|
+
{
|
|
163
|
+
readonly compilation?: 'precompiled';
|
|
164
|
+
readonly configuration?: string;
|
|
165
|
+
readonly target?: string;
|
|
166
|
+
readonly nativeArtifact?: string | null;
|
|
167
|
+
/** Unique instance identifier; opaque callbacks and handles prevent general cache equivalence. */
|
|
168
|
+
readonly instance: string;
|
|
169
|
+
/** Runtime version and, on Node, the helper binary digest. */
|
|
170
|
+
readonly runtime: string;
|
|
171
|
+
/** SHA-256 digest of the original component. */
|
|
172
|
+
readonly artifact: string;
|
|
173
|
+
/** Build digest of the SDK provider and loader implementation. */
|
|
174
|
+
readonly provider: string;
|
|
175
|
+
/** Digest of serializable grants and limits; opaque callback identity is instance-scoped. */
|
|
176
|
+
readonly policy: string;
|
|
177
|
+
/** Versioned exported WIT interface. */
|
|
178
|
+
readonly interface: string;
|
|
179
|
+
}
|
|
180
|
+
/** Serializes component calls while keeping separate instances independent. */
|
|
181
|
+
export interface HostInstance
|
|
182
|
+
{
|
|
183
|
+
/** Integrity and policy identifiers for this invocation context. */
|
|
184
|
+
readonly identity: Identity;
|
|
185
|
+
/** Calls an exported method; aborting cancels and revokes the whole instance. */
|
|
186
|
+
invoke(
|
|
187
|
+
method: string,
|
|
188
|
+
args?: unknown[],
|
|
189
|
+
controls?: {signal?: AbortSignal},
|
|
190
|
+
): Promise<unknown>;
|
|
191
|
+
/** Releases an owned exported resource; handles cannot cross instances. */
|
|
192
|
+
drop(resource: object, controls?: {signal?: AbortSignal}): Promise<void>;
|
|
193
|
+
/** Terminates the runtime and disposes broker resources. */
|
|
194
|
+
close(): void;
|
|
195
|
+
/** Terminates the runtime, rejecting pending and subsequent work as cancelled. */
|
|
196
|
+
revoke(): void;
|
|
197
|
+
}
|
|
198
|
+
/** Creates an instance or reports unsupported deployment without downgrading isolation. */
|
|
199
|
+
export function createHost(options: HostOptions): Promise<HostInstance>;
|