space-data-module-sdk 0.5.21 → 0.6.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/README.md +9 -2
- package/package.json +2 -2
- package/src/AGENTS.md +9 -10
- package/src/auth/AGENTS.md +6 -9
- package/src/bundle/AGENTS.md +8 -12
- package/src/compiler/AGENTS.md +14 -12
- package/src/compliance/AGENTS.md +9 -11
- package/src/host/AGENTS.md +13 -14
- package/src/host/abi.js +31 -3
- package/src/host/browserEdgeShims.js +4 -0
- package/src/host/browserHost.js +236 -2
- package/src/host/isomorphicLoader.js +43 -20
- package/src/host/nodeHost.js +179 -2
- package/src/manifest/AGENTS.md +6 -9
- package/src/runtime-host/AGENTS.md +12 -10
- package/src/runtime-host/index.js +56 -0
- package/src/testing/AGENTS.md +12 -11
- package/src/testing/browserModuleHarness.js +8 -2
- package/src/testing/index.js +38 -1
package/README.md
CHANGED
|
@@ -176,8 +176,15 @@ This repo currently includes:
|
|
|
176
176
|
- non-JS bundle reference clients in
|
|
177
177
|
[`examples/single-file-bundle/go`](./examples/single-file-bundle/go) and
|
|
178
178
|
[`examples/single-file-bundle/python`](./examples/single-file-bundle/python)
|
|
179
|
-
-
|
|
180
|
-
|
|
179
|
+
- reference Node and browser hosts plus the legacy sync `sdn_host` bridge for
|
|
180
|
+
sync-safe guest hostcalls
|
|
181
|
+
|
|
182
|
+
The current host/runtime contract is two-tiered:
|
|
183
|
+
|
|
184
|
+
- raw guest `sdn_host` imports stay sync-only
|
|
185
|
+
- generic async capabilities such as filesystem, network, IPFS, and protocol
|
|
186
|
+
adapters are exposed through `NodeHost`, `BrowserHost`, and the browser/module
|
|
187
|
+
harness APIs
|
|
181
188
|
|
|
182
189
|
## Runtime Targets
|
|
183
190
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "space-data-module-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
53
|
"test": "node --test",
|
|
54
|
-
"test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js",
|
|
54
|
+
"test:host-surfaces": "node --test test/node-host.test.js test/host-abi.test.js test/browser-host.test.js test/browser-module-harness.test.js test/testing-harness.test.js",
|
|
55
55
|
"test:runtime-matrix": "SPACE_DATA_MODULE_SDK_ENABLE_RUNTIME_MATRIX=1 node --test test/runtime-matrix.test.js",
|
|
56
56
|
"test:stream-ingest": "node --test test/runtime-host-stream-ingest.test.js",
|
|
57
57
|
"test:module-stream": "node --test test/module-flatbuffer-stream-pump.test.js",
|
package/src/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
Apply the root `AGENTS.md` first. This file
|
|
4
|
-
|
|
3
|
+
Apply the root `AGENTS.md` first. This file is a map for module authors reading
|
|
4
|
+
the SDK source, not a prompt to edit SDK internals by default.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Use `src/` As A Reference Map
|
|
7
7
|
|
|
8
8
|
- `src/manifest`, `src/compliance`, `src/standards`: manifest shape, type refs,
|
|
9
9
|
standards validation, and compliance warnings/errors.
|
|
@@ -23,11 +23,10 @@ subsystem.
|
|
|
23
23
|
- `src/generated`: generated code. Do not hand-edit unless regeneration is part
|
|
24
24
|
of the task and you update the generation path or fixtures as needed.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Author Rules
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
repo-level verification set in `test/AGENTS.md`.
|
|
28
|
+
- Prefer public APIs and examples over editing `src/*`.
|
|
29
|
+
- Read the nearest child `AGENTS.md` to understand which source files define the
|
|
30
|
+
contract you are using.
|
|
31
|
+
- Only edit internals here when the task explicitly says to change the SDK
|
|
32
|
+
standard for everyone.
|
package/src/auth/AGENTS.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This directory is the reference implementation for deployment authorization and
|
|
4
|
+
signing helpers used by compliant module distribution.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## What Authors Should Use It For
|
|
7
7
|
|
|
8
8
|
- Keep signing and authorization flows deterministic and explicit.
|
|
9
9
|
- Do not invent app-local signature records when a repo-level contract belongs
|
|
10
10
|
here.
|
|
11
|
-
- When auth changes affect transport or deployment plans, update the related
|
|
12
|
-
surfaces together rather than splitting incompatible changes across repos.
|
|
13
11
|
|
|
14
12
|
## Related Surfaces
|
|
15
13
|
|
|
@@ -18,8 +16,7 @@ and related record handling used by module distribution.
|
|
|
18
16
|
- `test/transport-records.test.js`
|
|
19
17
|
- `test/deployment-plan.test.js`
|
|
20
18
|
|
|
21
|
-
##
|
|
19
|
+
## Note
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `node --test test/transport-records.test.js test/deployment-plan.test.js`
|
|
21
|
+
Use the exported helpers here rather than inventing your own signing records.
|
|
22
|
+
Only edit this directory when you are intentionally changing the auth contract.
|
package/src/bundle/AGENTS.md
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
packaging
|
|
3
|
+
This directory is the reference implementation for `sds.bundle` and wasm
|
|
4
|
+
custom-section packaging.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## What Authors Should Use It For
|
|
7
7
|
|
|
8
8
|
- `sds.bundle` is the single-file delivery format.
|
|
9
9
|
- Put bundle data in a wasm custom section; do not append raw bytes after the
|
|
10
10
|
wasm binary.
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
- If the bundle contract changes, regenerate vectors and update the bundle docs
|
|
14
|
-
and demos in the same change.
|
|
11
|
+
- Use the public bundle helpers and CLI from this repo rather than inventing a
|
|
12
|
+
custom one-file format.
|
|
15
13
|
|
|
16
|
-
##
|
|
14
|
+
## Note
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- `node --test test/module-bundle.test.js test/module-bundle-vectors.test.js test/module-bundle-cli.test.js test/module-bundle-go.test.js test/module-bundle-python.test.js`
|
|
21
|
-
- `npm run generate:vectors` when vectors or the contract changed
|
|
16
|
+
Only edit this directory when you are intentionally changing bundle behavior for
|
|
17
|
+
all consumers.
|
package/src/compiler/AGENTS.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
Apply the root and `src/AGENTS.md` files first.
|
|
3
|
+
Apply the root and `src/AGENTS.md` files first. This directory tells module
|
|
4
|
+
authors how the compiler behaves.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## What Authors Should Take From This Directory
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
- `compileModuleFromSource(...)` and the CLI are the canonical ways to produce a
|
|
9
|
+
compliant artifact.
|
|
10
|
+
- Manifest embedding and required exports are generated together with the wasm
|
|
11
|
+
bytes.
|
|
12
|
+
- Shared browser/WasmEdge artifacts should land at
|
|
13
|
+
`dist/isomorphic/module.wasm`.
|
|
9
14
|
|
|
10
|
-
## Compiler Rules
|
|
15
|
+
## Compiler Rules Authors Should Follow
|
|
11
16
|
|
|
12
17
|
- Preserve the canonical manifest accessor exports and validate them together
|
|
13
18
|
with compiled wasm.
|
|
@@ -18,18 +23,15 @@ target inference, toolchain integration, and canonical artifact layout.
|
|
|
18
23
|
imports.
|
|
19
24
|
- Prefer repo-local Emscripten or `sdn-emception`. Do not assume Homebrew
|
|
20
25
|
toolchains.
|
|
21
|
-
- If you change runtime-target inference, keep the browser/WasmEdge shared path
|
|
22
|
-
on the standalone profile unless the docs and tests are updated deliberately.
|
|
23
26
|
|
|
24
|
-
## Key Files
|
|
27
|
+
## Key Files To Read
|
|
25
28
|
|
|
26
29
|
- `compileModule.js`
|
|
27
30
|
- `compileModuleFromSource.js`
|
|
28
31
|
- `emception*.js`
|
|
29
32
|
- `invokeGlue.js`
|
|
30
33
|
|
|
31
|
-
##
|
|
34
|
+
## Note
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `node --test test/compiler-emception-subpath-export.test.js`
|
|
36
|
+
Do not edit compiler internals just to build one module. Only change this
|
|
37
|
+
directory when you are intentionally changing the repo-wide compile contract.
|
package/src/compliance/AGENTS.md
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This directory is the reference policy for what counts as a compliant module or
|
|
4
4
|
artifact.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## What Authors Should Use It For
|
|
7
7
|
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
describe the broader contract accurately.
|
|
8
|
+
- Use the CLI and public validation helpers to understand whether a manifest or
|
|
9
|
+
artifact is compliant.
|
|
10
|
+
- Read this directory when you need to know why a module was rejected.
|
|
11
|
+
- Prefer fixing your module manifest or build output before changing policy here.
|
|
13
12
|
|
|
14
|
-
##
|
|
13
|
+
## Note
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `node --test test/module-sdk.test.js test/compliance.test.js`
|
|
15
|
+
Only edit this directory when you are intentionally changing the compliance
|
|
16
|
+
standard for all module authors.
|
package/src/host/AGENTS.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
Apply the root and `src/AGENTS.md` files first.
|
|
3
|
+
Apply the root and `src/AGENTS.md` files first. This directory explains the
|
|
4
|
+
host/runtime boundary that compliant modules can rely on.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## What Authors Should Take From This Directory
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
- The portable shared path is standalone WASI, optionally plus `sdn_host`.
|
|
9
|
+
- Browser helpers here are host-side shims and harnesses, not proof that raw
|
|
10
|
+
WasmEdge-native guest imports are browser-portable.
|
|
11
|
+
- The isomorphic loader and browser harness show how the same artifact is meant
|
|
12
|
+
to run in both places.
|
|
10
13
|
|
|
11
|
-
## Host Rules
|
|
14
|
+
## Host Rules Authors Should Follow
|
|
12
15
|
|
|
13
16
|
- Shared browser/WasmEdge artifacts must stay within standalone WASI plus the
|
|
14
17
|
optional `sdn_host` bridge. Raw WasmEdge-native extension imports are not
|
|
@@ -20,7 +23,7 @@ isomorphic loader path.
|
|
|
20
23
|
- Scope host capabilities tightly: filesystem roots, network allowlists, TLS,
|
|
21
24
|
exec, timers, and crypto should stay explicit.
|
|
22
25
|
|
|
23
|
-
## Key Files
|
|
26
|
+
## Key Files To Read
|
|
24
27
|
|
|
25
28
|
- `abi.js`
|
|
26
29
|
- `browserEdgeShims.js`
|
|
@@ -29,11 +32,7 @@ isomorphic loader path.
|
|
|
29
32
|
- `nodeHost.js`
|
|
30
33
|
- `wasiShim.js`
|
|
31
34
|
|
|
32
|
-
##
|
|
35
|
+
## Note
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- `node --test test/process-invoke.test.js`
|
|
37
|
-
- `node --test test/wasmedge-runner-build.test.js`
|
|
38
|
-
- `node --test test/wasmedge-runner-runtime.test.js` when WasmEdge behavior is
|
|
39
|
-
part of the change
|
|
37
|
+
Prefer using the exported host and harness surfaces rather than editing these
|
|
38
|
+
files unless the task is explicitly to change the SDK host contract.
|
package/src/host/abi.js
CHANGED
|
@@ -7,7 +7,7 @@ export const DEFAULT_HOSTCALL_IMPORT_MODULE = "sdn_host";
|
|
|
7
7
|
export const HOSTCALL_STATUS_OK = 0;
|
|
8
8
|
export const HOSTCALL_STATUS_ERROR = 1;
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const SyncHostcallOperations = Object.freeze([
|
|
11
11
|
"host.runtimeTarget",
|
|
12
12
|
"host.listCapabilities",
|
|
13
13
|
"host.listSupportedCapabilities",
|
|
@@ -22,6 +22,7 @@ export const NodeHostSyncHostcallOperations = Object.freeze([
|
|
|
22
22
|
"schedule.next",
|
|
23
23
|
"filesystem.resolvePath",
|
|
24
24
|
]);
|
|
25
|
+
export const NodeHostSyncHostcallOperations = SyncHostcallOperations;
|
|
25
26
|
|
|
26
27
|
function assertNonEmptyString(value, label) {
|
|
27
28
|
const normalized = String(value ?? "").trim();
|
|
@@ -149,7 +150,7 @@ function encodeHostcallValue(value) {
|
|
|
149
150
|
return value;
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
export function
|
|
153
|
+
export function dispatchHostSyncOperation(host, operation, params = null) {
|
|
153
154
|
const normalized = assertNonEmptyString(operation, "Hostcall operation");
|
|
154
155
|
switch (normalized) {
|
|
155
156
|
case "host.runtimeTarget":
|
|
@@ -185,12 +186,39 @@ export function dispatchNodeHostSyncOperation(host, operation, params = null) {
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
export function dispatchNodeHostSyncOperation(host, operation, params = null) {
|
|
190
|
+
return dispatchHostSyncOperation(host, operation, params);
|
|
191
|
+
}
|
|
192
|
+
|
|
188
193
|
export function createNodeHostSyncDispatcher(host) {
|
|
189
194
|
if (!host || typeof host !== "object") {
|
|
190
195
|
throw new TypeError("createNodeHostSyncDispatcher requires a host object.");
|
|
191
196
|
}
|
|
192
197
|
return (operation, params = null) =>
|
|
193
|
-
|
|
198
|
+
dispatchHostSyncOperation(host, operation, params);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createHostSyncDispatcher(host) {
|
|
202
|
+
return createNodeHostSyncDispatcher(host);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function dispatchHostOperation(host, operation, params = null) {
|
|
206
|
+
if (!host || typeof host !== "object") {
|
|
207
|
+
throw new TypeError("dispatchHostOperation requires a host object.");
|
|
208
|
+
}
|
|
209
|
+
const normalized = assertNonEmptyString(operation, "Host operation");
|
|
210
|
+
if (typeof host.invoke === "function") {
|
|
211
|
+
return host.invoke(normalized, params);
|
|
212
|
+
}
|
|
213
|
+
return dispatchHostSyncOperation(host, normalized, params);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createAsyncHostDispatcher(host) {
|
|
217
|
+
if (!host || typeof host !== "object") {
|
|
218
|
+
throw new TypeError("createAsyncHostDispatcher requires a host object.");
|
|
219
|
+
}
|
|
220
|
+
return (operation, params = null) =>
|
|
221
|
+
dispatchHostOperation(host, operation, params);
|
|
194
222
|
}
|
|
195
223
|
|
|
196
224
|
export function createJsonHostcallBridge(options = {}) {
|
|
@@ -346,6 +346,10 @@ export function createBrowserEdgeShims(options = {}) {
|
|
|
346
346
|
WebSocket: options.WebSocket ?? globalThis.WebSocket,
|
|
347
347
|
crypto: options.crypto ?? globalThis.crypto,
|
|
348
348
|
performance: options.performance ?? globalThis.performance,
|
|
349
|
+
network: options.network ?? null,
|
|
350
|
+
ipfs: options.ipfs ?? null,
|
|
351
|
+
protocolHandle: options.protocolHandle ?? null,
|
|
352
|
+
protocolDial: options.protocolDial ?? null,
|
|
349
353
|
filesystem:
|
|
350
354
|
options.filesystem ??
|
|
351
355
|
createMemoryFilesystemEdgeShim({
|
package/src/host/browserHost.js
CHANGED
|
@@ -20,7 +20,11 @@ export const BrowserHostSupportedCapabilities = Object.freeze([
|
|
|
20
20
|
"schedule_cron",
|
|
21
21
|
"http",
|
|
22
22
|
"websocket",
|
|
23
|
+
"network",
|
|
23
24
|
"filesystem",
|
|
25
|
+
"ipfs",
|
|
26
|
+
"protocol_handle",
|
|
27
|
+
"protocol_dial",
|
|
24
28
|
"context_read",
|
|
25
29
|
"context_write",
|
|
26
30
|
"crypto_hash",
|
|
@@ -39,11 +43,13 @@ export const BrowserHostSupportedOperations = Object.freeze([
|
|
|
39
43
|
"clock.monotonicNow",
|
|
40
44
|
"clock.nowIso",
|
|
41
45
|
"random.bytes",
|
|
46
|
+
"timers.delay",
|
|
42
47
|
"schedule.parse",
|
|
43
48
|
"schedule.matches",
|
|
44
49
|
"schedule.next",
|
|
45
50
|
"http.request",
|
|
46
51
|
"websocket.exchange",
|
|
52
|
+
"network.request",
|
|
47
53
|
"filesystem.resolvePath",
|
|
48
54
|
"filesystem.readFile",
|
|
49
55
|
"filesystem.writeFile",
|
|
@@ -53,6 +59,10 @@ export const BrowserHostSupportedOperations = Object.freeze([
|
|
|
53
59
|
"filesystem.readdir",
|
|
54
60
|
"filesystem.stat",
|
|
55
61
|
"filesystem.rename",
|
|
62
|
+
"ipfs.invoke",
|
|
63
|
+
"protocol_handle.register",
|
|
64
|
+
"protocol_handle.unregister",
|
|
65
|
+
"protocol_dial.dial",
|
|
56
66
|
"context.get",
|
|
57
67
|
"context.set",
|
|
58
68
|
"context.delete",
|
|
@@ -73,6 +83,46 @@ export class BrowserHostCapabilityError extends Error {
|
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
function normalizeBrowserNetworkTransport(params = {}) {
|
|
87
|
+
const explicit = String(
|
|
88
|
+
params.transport ?? params.kind ?? params.request?.transport ?? "",
|
|
89
|
+
)
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase();
|
|
92
|
+
if (explicit) {
|
|
93
|
+
return explicit;
|
|
94
|
+
}
|
|
95
|
+
const candidateUrl = params.url ?? params.request?.url ?? null;
|
|
96
|
+
if (candidateUrl) {
|
|
97
|
+
const protocol = new URL(candidateUrl, "https://browser-host.invalid")
|
|
98
|
+
.protocol.toLowerCase();
|
|
99
|
+
if (protocol === "http:" || protocol === "https:") {
|
|
100
|
+
return "http";
|
|
101
|
+
}
|
|
102
|
+
if (protocol === "ws:" || protocol === "wss:") {
|
|
103
|
+
return "websocket";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error(
|
|
107
|
+
'network.request requires a transport value such as "http" or "websocket".',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function invokeAdapterMethod(adapter, methodName, params, label) {
|
|
112
|
+
if (!adapter || typeof adapter !== "object") {
|
|
113
|
+
throw new Error(`${label} adapter is not configured for this host.`);
|
|
114
|
+
}
|
|
115
|
+
if (typeof adapter[methodName] === "function") {
|
|
116
|
+
return adapter[methodName](params);
|
|
117
|
+
}
|
|
118
|
+
if (typeof adapter.invoke === "function") {
|
|
119
|
+
return adapter.invoke(methodName, params);
|
|
120
|
+
}
|
|
121
|
+
throw new Error(
|
|
122
|
+
`${label} adapter does not implement "${methodName}" or invoke().`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
76
126
|
export class BrowserHost {
|
|
77
127
|
constructor(options = {}) {
|
|
78
128
|
this.runtimeTarget = RuntimeTarget.BROWSER;
|
|
@@ -86,6 +136,11 @@ export class BrowserHost {
|
|
|
86
136
|
WebSocket: options.WebSocket ?? options.edgeShims?.WebSocket,
|
|
87
137
|
crypto: options.crypto ?? options.edgeShims?.crypto,
|
|
88
138
|
performance: options.performance ?? options.edgeShims?.performance,
|
|
139
|
+
network: options.network ?? options.edgeShims?.network,
|
|
140
|
+
ipfs: options.ipfs ?? options.edgeShims?.ipfs,
|
|
141
|
+
protocolHandle:
|
|
142
|
+
options.protocolHandle ?? options.edgeShims?.protocolHandle,
|
|
143
|
+
protocolDial: options.protocolDial ?? options.edgeShims?.protocolDial,
|
|
89
144
|
filesystem: options.filesystem ?? options.edgeShims?.filesystem,
|
|
90
145
|
filesystemRoot: options.filesystemRoot ?? options.edgeShims?.filesystemRoot,
|
|
91
146
|
});
|
|
@@ -97,6 +152,10 @@ export class BrowserHost {
|
|
|
97
152
|
const fetchImpl = edgeShims.fetch;
|
|
98
153
|
const WebSocketImpl = edgeShims.WebSocket;
|
|
99
154
|
const filesystem = edgeShims.filesystem;
|
|
155
|
+
const networkAdapter = edgeShims.network;
|
|
156
|
+
const ipfsAdapter = edgeShims.ipfs;
|
|
157
|
+
const protocolHandleAdapter = edgeShims.protocolHandle;
|
|
158
|
+
const protocolDialAdapter = edgeShims.protocolDial;
|
|
100
159
|
|
|
101
160
|
this._grantedCapabilities = granted;
|
|
102
161
|
this._contextStore = options.contextStore ?? new Map();
|
|
@@ -239,6 +298,73 @@ export class BrowserHost {
|
|
|
239
298
|
},
|
|
240
299
|
});
|
|
241
300
|
|
|
301
|
+
this.network = Object.freeze({
|
|
302
|
+
request: async (params = {}) => {
|
|
303
|
+
this.#assertCapability("network", "network.request");
|
|
304
|
+
const transport = normalizeBrowserNetworkTransport(params);
|
|
305
|
+
const request = params.request ?? params;
|
|
306
|
+
if (transport === "http") {
|
|
307
|
+
return this.http.request(request);
|
|
308
|
+
}
|
|
309
|
+
if (transport === "websocket") {
|
|
310
|
+
return this.websocket.exchange(request);
|
|
311
|
+
}
|
|
312
|
+
if (networkAdapter) {
|
|
313
|
+
return invokeAdapterMethod(networkAdapter, "request", {
|
|
314
|
+
...request,
|
|
315
|
+
transport,
|
|
316
|
+
}, "network");
|
|
317
|
+
}
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Browser host does not support network transport "${transport}".`,
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
this.ipfs = Object.freeze({
|
|
325
|
+
invoke: async (params = {}) => {
|
|
326
|
+
this.#assertCapability("ipfs", "ipfs.invoke");
|
|
327
|
+
const operation = String(params.operation ?? "invoke").trim();
|
|
328
|
+
if (!operation) {
|
|
329
|
+
throw new Error("ipfs.invoke requires a non-empty operation.");
|
|
330
|
+
}
|
|
331
|
+
return invokeAdapterMethod(ipfsAdapter, operation, params, "ipfs");
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
this.protocolHandle = Object.freeze({
|
|
336
|
+
register: async (params = {}) => {
|
|
337
|
+
this.#assertCapability("protocol_handle", "protocol_handle.register");
|
|
338
|
+
return invokeAdapterMethod(
|
|
339
|
+
protocolHandleAdapter,
|
|
340
|
+
"register",
|
|
341
|
+
params,
|
|
342
|
+
"protocol_handle",
|
|
343
|
+
);
|
|
344
|
+
},
|
|
345
|
+
unregister: async (params = {}) => {
|
|
346
|
+
this.#assertCapability("protocol_handle", "protocol_handle.unregister");
|
|
347
|
+
return invokeAdapterMethod(
|
|
348
|
+
protocolHandleAdapter,
|
|
349
|
+
"unregister",
|
|
350
|
+
params,
|
|
351
|
+
"protocol_handle",
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
this.protocolDial = Object.freeze({
|
|
357
|
+
dial: async (params = {}) => {
|
|
358
|
+
this.#assertCapability("protocol_dial", "protocol_dial.dial");
|
|
359
|
+
return invokeAdapterMethod(
|
|
360
|
+
protocolDialAdapter,
|
|
361
|
+
"dial",
|
|
362
|
+
params,
|
|
363
|
+
"protocol_dial",
|
|
364
|
+
);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
242
368
|
this.context = Object.freeze({
|
|
243
369
|
get: (scope, key) => {
|
|
244
370
|
this.#assertCapability("context_read", "context.get");
|
|
@@ -378,17 +504,125 @@ export class BrowserHost {
|
|
|
378
504
|
}
|
|
379
505
|
|
|
380
506
|
hasCapability(capability) {
|
|
381
|
-
|
|
507
|
+
const normalized = String(capability ?? "").trim();
|
|
508
|
+
return (
|
|
509
|
+
this._grantedCapabilities.has(normalized) ||
|
|
510
|
+
(this._grantedCapabilities.has("network") &&
|
|
511
|
+
["http", "websocket"].includes(normalized))
|
|
512
|
+
);
|
|
382
513
|
}
|
|
383
514
|
|
|
384
515
|
assertCapability(capability, operation) {
|
|
385
516
|
this.#assertCapability(capability, operation);
|
|
386
517
|
}
|
|
387
518
|
|
|
519
|
+
async invoke(operation, params = {}) {
|
|
520
|
+
const normalized = String(operation ?? "").trim();
|
|
521
|
+
switch (normalized) {
|
|
522
|
+
case "host.runtimeTarget":
|
|
523
|
+
return this.runtimeTarget;
|
|
524
|
+
case "host.listCapabilities":
|
|
525
|
+
return this.listCapabilities();
|
|
526
|
+
case "host.listSupportedCapabilities":
|
|
527
|
+
return this.listSupportedCapabilities();
|
|
528
|
+
case "host.listOperations":
|
|
529
|
+
return this.listOperations();
|
|
530
|
+
case "host.hasCapability":
|
|
531
|
+
return this.hasCapability(params.capability);
|
|
532
|
+
case "clock.now":
|
|
533
|
+
return this.clock.now();
|
|
534
|
+
case "clock.monotonicNow":
|
|
535
|
+
return this.clock.monotonicNow();
|
|
536
|
+
case "clock.nowIso":
|
|
537
|
+
return this.clock.nowIso();
|
|
538
|
+
case "random.bytes":
|
|
539
|
+
return this.random.bytes(params.length);
|
|
540
|
+
case "timers.delay":
|
|
541
|
+
return this.timers.delay(params.ms ?? params.delayMs ?? 0);
|
|
542
|
+
case "schedule.parse":
|
|
543
|
+
return this.schedule.parse(params.expression);
|
|
544
|
+
case "schedule.matches":
|
|
545
|
+
return this.schedule.matches(params.expression, params.date);
|
|
546
|
+
case "schedule.next":
|
|
547
|
+
return this.schedule.next(params.expression, params.from);
|
|
548
|
+
case "http.request":
|
|
549
|
+
return this.http.request(params);
|
|
550
|
+
case "websocket.exchange":
|
|
551
|
+
return this.websocket.exchange(params);
|
|
552
|
+
case "network.request":
|
|
553
|
+
return this.network.request(params);
|
|
554
|
+
case "filesystem.resolvePath":
|
|
555
|
+
return this.filesystem.resolvePath(params.path);
|
|
556
|
+
case "filesystem.readFile":
|
|
557
|
+
return this.filesystem.readFile(params.path, {
|
|
558
|
+
encoding: params.encoding,
|
|
559
|
+
});
|
|
560
|
+
case "filesystem.writeFile":
|
|
561
|
+
return this.filesystem.writeFile(params.path, params.value, {
|
|
562
|
+
encoding: params.encoding,
|
|
563
|
+
});
|
|
564
|
+
case "filesystem.appendFile":
|
|
565
|
+
return this.filesystem.appendFile(params.path, params.value, {
|
|
566
|
+
encoding: params.encoding,
|
|
567
|
+
});
|
|
568
|
+
case "filesystem.deleteFile":
|
|
569
|
+
return this.filesystem.deleteFile(params.path);
|
|
570
|
+
case "filesystem.mkdir":
|
|
571
|
+
return this.filesystem.mkdir(params.path, {
|
|
572
|
+
recursive: params.recursive,
|
|
573
|
+
});
|
|
574
|
+
case "filesystem.readdir":
|
|
575
|
+
return this.filesystem.readdir(params.path);
|
|
576
|
+
case "filesystem.stat":
|
|
577
|
+
return this.filesystem.stat(params.path);
|
|
578
|
+
case "filesystem.rename":
|
|
579
|
+
return this.filesystem.rename(params.fromPath, params.toPath);
|
|
580
|
+
case "ipfs.invoke":
|
|
581
|
+
return this.ipfs.invoke(params);
|
|
582
|
+
case "protocol_handle.register":
|
|
583
|
+
return this.protocolHandle.register(params);
|
|
584
|
+
case "protocol_handle.unregister":
|
|
585
|
+
return this.protocolHandle.unregister(params);
|
|
586
|
+
case "protocol_dial.dial":
|
|
587
|
+
return this.protocolDial.dial(params);
|
|
588
|
+
case "context.get":
|
|
589
|
+
return this.context.get(params.scope, params.key);
|
|
590
|
+
case "context.set":
|
|
591
|
+
return this.context.set(params.scope, params.key, params.value);
|
|
592
|
+
case "context.delete":
|
|
593
|
+
return this.context.delete(params.scope, params.key);
|
|
594
|
+
case "context.listKeys":
|
|
595
|
+
return this.context.listKeys(params.scope);
|
|
596
|
+
case "context.listScopes":
|
|
597
|
+
return this.context.listScopes();
|
|
598
|
+
case "crypto.sha256":
|
|
599
|
+
return this.crypto.sha256(params.value ?? params.bytes);
|
|
600
|
+
case "crypto.sha512":
|
|
601
|
+
return this.crypto.sha512(params.value ?? params.bytes);
|
|
602
|
+
case "crypto.aesGcmEncrypt":
|
|
603
|
+
return this.crypto.aesGcmEncrypt(params);
|
|
604
|
+
case "crypto.aesGcmDecrypt":
|
|
605
|
+
return this.crypto.aesGcmDecrypt(params);
|
|
606
|
+
default:
|
|
607
|
+
throw new Error(`Unknown browser host operation "${normalized}".`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
388
611
|
// --- Private ---
|
|
389
612
|
|
|
390
613
|
#assertCapability(capability, operation) {
|
|
391
|
-
|
|
614
|
+
const normalized = String(capability ?? "").trim();
|
|
615
|
+
const networkBackedCapabilities = new Set([
|
|
616
|
+
"http",
|
|
617
|
+
"websocket",
|
|
618
|
+
]);
|
|
619
|
+
if (
|
|
620
|
+
!this._grantedCapabilities.has(normalized) &&
|
|
621
|
+
!(
|
|
622
|
+
this._grantedCapabilities.has("network") &&
|
|
623
|
+
networkBackedCapabilities.has(normalized)
|
|
624
|
+
)
|
|
625
|
+
) {
|
|
392
626
|
throw new BrowserHostCapabilityError(capability, operation);
|
|
393
627
|
}
|
|
394
628
|
}
|
|
@@ -13,11 +13,25 @@ import {
|
|
|
13
13
|
createBrowserModuleHarness,
|
|
14
14
|
detectArtifactProfile,
|
|
15
15
|
} from "../testing/browserModuleHarness.js";
|
|
16
|
+
import { dispatchHostOperation } from "./abi.js";
|
|
16
17
|
|
|
17
18
|
const isBrowser =
|
|
18
19
|
typeof globalThis.window !== "undefined" &&
|
|
19
20
|
typeof globalThis.document !== "undefined";
|
|
20
21
|
|
|
22
|
+
function attachHostDispatch(harness, host) {
|
|
23
|
+
if (!host || typeof host !== "object") {
|
|
24
|
+
return harness;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
...harness,
|
|
28
|
+
host,
|
|
29
|
+
async callHost(operation, params = {}) {
|
|
30
|
+
return dispatchHostOperation(host, operation, params);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
async function createWasmEdgeCommandHarness(options = {}) {
|
|
22
36
|
const [
|
|
23
37
|
{ spawn },
|
|
@@ -175,14 +189,18 @@ export async function loadModule(options = {}) {
|
|
|
175
189
|
(inspection.profile === "standalone" || inspection.profile === "sdn-abi") &&
|
|
176
190
|
inspection.exports.includes("_start")
|
|
177
191
|
) {
|
|
178
|
-
return
|
|
192
|
+
return attachHostDispatch(
|
|
193
|
+
await createWasmEdgeCommandHarness(options),
|
|
194
|
+
options.host ?? null,
|
|
195
|
+
);
|
|
179
196
|
}
|
|
180
197
|
}
|
|
181
198
|
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
return attachHostDispatch(
|
|
200
|
+
await createModuleHarness({
|
|
201
|
+
runtime: {
|
|
202
|
+
kind: "wasmedge",
|
|
203
|
+
wasmPath: source,
|
|
186
204
|
wasmEdgeBinary: options.wasmEdgeBinary,
|
|
187
205
|
wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
|
|
188
206
|
enableThreads: options.enableThreads,
|
|
@@ -191,23 +209,28 @@ export async function loadModule(options = {}) {
|
|
|
191
209
|
hostProfile: options.hostProfile,
|
|
192
210
|
modules: options.modules,
|
|
193
211
|
defaultModuleId: options.defaultModuleId,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
metadata: options.metadata,
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
options.host ?? null,
|
|
216
|
+
);
|
|
197
217
|
}
|
|
198
218
|
|
|
199
|
-
return
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
return attachHostDispatch(
|
|
220
|
+
await createModuleHarness({
|
|
221
|
+
runtime: {
|
|
222
|
+
kind: runtimeKind,
|
|
223
|
+
command: options.command ?? runtimeKind,
|
|
224
|
+
args: options.args ?? [source],
|
|
225
|
+
env: options.env,
|
|
226
|
+
cwd: options.cwd,
|
|
227
|
+
hostProfile: options.hostProfile,
|
|
228
|
+
modules: options.modules,
|
|
229
|
+
defaultModuleId: options.defaultModuleId,
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
options.host ?? null,
|
|
233
|
+
);
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
/**
|
package/src/host/nodeHost.js
CHANGED
|
@@ -46,10 +46,14 @@ export const NodeHostSupportedCapabilities = Object.freeze([
|
|
|
46
46
|
"http",
|
|
47
47
|
"websocket",
|
|
48
48
|
"mqtt",
|
|
49
|
+
"network",
|
|
49
50
|
"filesystem",
|
|
50
51
|
"tcp",
|
|
51
52
|
"udp",
|
|
52
53
|
"tls",
|
|
54
|
+
"ipfs",
|
|
55
|
+
"protocol_handle",
|
|
56
|
+
"protocol_dial",
|
|
53
57
|
"context_read",
|
|
54
58
|
"context_write",
|
|
55
59
|
"crypto_hash",
|
|
@@ -75,6 +79,7 @@ export const NodeHostSupportedOperations = Object.freeze([
|
|
|
75
79
|
"websocket.exchange",
|
|
76
80
|
"mqtt.publish",
|
|
77
81
|
"mqtt.subscribeOnce",
|
|
82
|
+
"network.request",
|
|
78
83
|
"filesystem.resolvePath",
|
|
79
84
|
"filesystem.readFile",
|
|
80
85
|
"filesystem.writeFile",
|
|
@@ -87,6 +92,10 @@ export const NodeHostSupportedOperations = Object.freeze([
|
|
|
87
92
|
"tcp.request",
|
|
88
93
|
"udp.request",
|
|
89
94
|
"tls.request",
|
|
95
|
+
"ipfs.invoke",
|
|
96
|
+
"protocol_handle.register",
|
|
97
|
+
"protocol_handle.unregister",
|
|
98
|
+
"protocol_dial.dial",
|
|
90
99
|
"exec.execFile",
|
|
91
100
|
"context.get",
|
|
92
101
|
"context.set",
|
|
@@ -229,6 +238,45 @@ function normalizeAllowedPorts(ports, label, { allowZero = false } = {}) {
|
|
|
229
238
|
return normalized;
|
|
230
239
|
}
|
|
231
240
|
|
|
241
|
+
function normalizeNetworkTransport(params = {}) {
|
|
242
|
+
const explicit = String(
|
|
243
|
+
params.transport ?? params.kind ?? params.request?.transport ?? "",
|
|
244
|
+
)
|
|
245
|
+
.trim()
|
|
246
|
+
.toLowerCase();
|
|
247
|
+
if (explicit) {
|
|
248
|
+
return explicit;
|
|
249
|
+
}
|
|
250
|
+
const candidateUrl = params.url ?? params.request?.url ?? null;
|
|
251
|
+
if (candidateUrl) {
|
|
252
|
+
const protocol = new URL(candidateUrl).protocol.toLowerCase();
|
|
253
|
+
if (protocol === "http:" || protocol === "https:") {
|
|
254
|
+
return "http";
|
|
255
|
+
}
|
|
256
|
+
if (protocol === "ws:" || protocol === "wss:") {
|
|
257
|
+
return "websocket";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error(
|
|
261
|
+
'network.request requires a transport value such as "http", "tcp", or "websocket".',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function invokeAdapterMethod(adapter, methodName, params, label) {
|
|
266
|
+
if (!adapter || typeof adapter !== "object") {
|
|
267
|
+
throw new Error(`${label} adapter is not configured for this host.`);
|
|
268
|
+
}
|
|
269
|
+
if (typeof adapter[methodName] === "function") {
|
|
270
|
+
return adapter[methodName](params);
|
|
271
|
+
}
|
|
272
|
+
if (typeof adapter.invoke === "function") {
|
|
273
|
+
return adapter.invoke(methodName, params);
|
|
274
|
+
}
|
|
275
|
+
throw new Error(
|
|
276
|
+
`${label} adapter does not implement "${methodName}" or invoke().`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
232
280
|
function normalizeFilesystemRoot(rootPath) {
|
|
233
281
|
return path.resolve(String(rootPath ?? process.cwd()));
|
|
234
282
|
}
|
|
@@ -1397,6 +1445,10 @@ export class NodeHost {
|
|
|
1397
1445
|
);
|
|
1398
1446
|
this.fetch = options.fetch ?? globalThis.fetch;
|
|
1399
1447
|
this.WebSocket = options.WebSocket ?? globalThis.WebSocket;
|
|
1448
|
+
this._networkAdapter = options.network ?? null;
|
|
1449
|
+
this._ipfsAdapter = options.ipfs ?? null;
|
|
1450
|
+
this._protocolHandleAdapter = options.protocolHandle ?? null;
|
|
1451
|
+
this._protocolDialAdapter = options.protocolDial ?? null;
|
|
1400
1452
|
if (typeof this.fetch !== "function") {
|
|
1401
1453
|
throw new TypeError(
|
|
1402
1454
|
"A fetch implementation is required to create a Node host.",
|
|
@@ -1550,6 +1602,40 @@ export class NodeHost {
|
|
|
1550
1602
|
}),
|
|
1551
1603
|
});
|
|
1552
1604
|
|
|
1605
|
+
this.network = Object.freeze({
|
|
1606
|
+
request: async (params = {}) =>
|
|
1607
|
+
this.#withCapability("network", "network.request", async () => {
|
|
1608
|
+
const transport = normalizeNetworkTransport(params);
|
|
1609
|
+
const request = params.request ?? params;
|
|
1610
|
+
switch (transport) {
|
|
1611
|
+
case "http":
|
|
1612
|
+
return this.http.request(request);
|
|
1613
|
+
case "websocket":
|
|
1614
|
+
return this.websocket.exchange(request);
|
|
1615
|
+
case "mqtt":
|
|
1616
|
+
return request.subscribe === true
|
|
1617
|
+
? this.mqtt.subscribeOnce(request)
|
|
1618
|
+
: this.mqtt.publish(request);
|
|
1619
|
+
case "tcp":
|
|
1620
|
+
return this.tcp.request(request);
|
|
1621
|
+
case "udp":
|
|
1622
|
+
return this.udp.request(request);
|
|
1623
|
+
case "tls":
|
|
1624
|
+
return this.tls.request(request);
|
|
1625
|
+
default:
|
|
1626
|
+
if (this._networkAdapter) {
|
|
1627
|
+
return invokeAdapterMethod(this._networkAdapter, "request", {
|
|
1628
|
+
...request,
|
|
1629
|
+
transport,
|
|
1630
|
+
}, "network");
|
|
1631
|
+
}
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`Node host does not support network transport "${transport}".`,
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
}),
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1553
1639
|
this.mqtt = Object.freeze({
|
|
1554
1640
|
publish: async (mqttOptions = {}) =>
|
|
1555
1641
|
this.#withCapability("mqtt", "mqtt.publish", async () => {
|
|
@@ -1825,6 +1911,56 @@ export class NodeHost {
|
|
|
1825
1911
|
this._contextStore.listScopes(),
|
|
1826
1912
|
),
|
|
1827
1913
|
});
|
|
1914
|
+
|
|
1915
|
+
this.ipfs = Object.freeze({
|
|
1916
|
+
invoke: async (params = {}) =>
|
|
1917
|
+
this.#withCapability("ipfs", "ipfs.invoke", async () => {
|
|
1918
|
+
const operation = String(params.operation ?? "invoke").trim();
|
|
1919
|
+
if (!operation) {
|
|
1920
|
+
throw new Error("ipfs.invoke requires a non-empty operation.");
|
|
1921
|
+
}
|
|
1922
|
+
return invokeAdapterMethod(this._ipfsAdapter, operation, params, "ipfs");
|
|
1923
|
+
}),
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
this.protocolHandle = Object.freeze({
|
|
1927
|
+
register: async (params = {}) =>
|
|
1928
|
+
this.#withCapability(
|
|
1929
|
+
"protocol_handle",
|
|
1930
|
+
"protocol_handle.register",
|
|
1931
|
+
async () =>
|
|
1932
|
+
invokeAdapterMethod(
|
|
1933
|
+
this._protocolHandleAdapter,
|
|
1934
|
+
"register",
|
|
1935
|
+
params,
|
|
1936
|
+
"protocol_handle",
|
|
1937
|
+
),
|
|
1938
|
+
),
|
|
1939
|
+
unregister: async (params = {}) =>
|
|
1940
|
+
this.#withCapability(
|
|
1941
|
+
"protocol_handle",
|
|
1942
|
+
"protocol_handle.unregister",
|
|
1943
|
+
async () =>
|
|
1944
|
+
invokeAdapterMethod(
|
|
1945
|
+
this._protocolHandleAdapter,
|
|
1946
|
+
"unregister",
|
|
1947
|
+
params,
|
|
1948
|
+
"protocol_handle",
|
|
1949
|
+
),
|
|
1950
|
+
),
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
this.protocolDial = Object.freeze({
|
|
1954
|
+
dial: async (params = {}) =>
|
|
1955
|
+
this.#withCapability("protocol_dial", "protocol_dial.dial", async () =>
|
|
1956
|
+
invokeAdapterMethod(
|
|
1957
|
+
this._protocolDialAdapter,
|
|
1958
|
+
"dial",
|
|
1959
|
+
params,
|
|
1960
|
+
"protocol_dial",
|
|
1961
|
+
),
|
|
1962
|
+
),
|
|
1963
|
+
});
|
|
1828
1964
|
}
|
|
1829
1965
|
|
|
1830
1966
|
listCapabilities() {
|
|
@@ -1840,11 +1976,26 @@ export class NodeHost {
|
|
|
1840
1976
|
}
|
|
1841
1977
|
|
|
1842
1978
|
hasCapability(capability) {
|
|
1843
|
-
|
|
1979
|
+
const normalized = String(capability ?? "").trim();
|
|
1980
|
+
return (
|
|
1981
|
+
this._grantedCapabilities.has(normalized) ||
|
|
1982
|
+
(this._grantedCapabilities.has("network") &&
|
|
1983
|
+
["http", "websocket", "mqtt", "tcp", "udp", "tls"].includes(
|
|
1984
|
+
normalized,
|
|
1985
|
+
))
|
|
1986
|
+
);
|
|
1844
1987
|
}
|
|
1845
1988
|
|
|
1846
1989
|
assertCapability(capability, operation = null) {
|
|
1847
1990
|
const normalized = assertNonEmptyString(capability, "Capability id");
|
|
1991
|
+
const networkBackedCapabilities = new Set([
|
|
1992
|
+
"http",
|
|
1993
|
+
"websocket",
|
|
1994
|
+
"mqtt",
|
|
1995
|
+
"tcp",
|
|
1996
|
+
"udp",
|
|
1997
|
+
"tls",
|
|
1998
|
+
]);
|
|
1848
1999
|
if (!this._supportedCapabilities.has(normalized)) {
|
|
1849
2000
|
throw new HostCapabilityError(
|
|
1850
2001
|
`Capability "${normalized}" is not supported by the reference Node host.`,
|
|
@@ -1855,7 +2006,13 @@ export class NodeHost {
|
|
|
1855
2006
|
},
|
|
1856
2007
|
);
|
|
1857
2008
|
}
|
|
1858
|
-
if (
|
|
2009
|
+
if (
|
|
2010
|
+
!this._grantedCapabilities.has(normalized) &&
|
|
2011
|
+
!(
|
|
2012
|
+
this._grantedCapabilities.has("network") &&
|
|
2013
|
+
networkBackedCapabilities.has(normalized)
|
|
2014
|
+
)
|
|
2015
|
+
) {
|
|
1859
2016
|
throw new HostCapabilityError(
|
|
1860
2017
|
`Capability "${normalized}" is not granted for this Node host.`,
|
|
1861
2018
|
{
|
|
@@ -1871,6 +2028,16 @@ export class NodeHost {
|
|
|
1871
2028
|
async invoke(operation, params = {}) {
|
|
1872
2029
|
const normalized = assertNonEmptyString(operation, "Host operation");
|
|
1873
2030
|
switch (normalized) {
|
|
2031
|
+
case "host.runtimeTarget":
|
|
2032
|
+
return this.runtimeTarget;
|
|
2033
|
+
case "host.listCapabilities":
|
|
2034
|
+
return this.listCapabilities();
|
|
2035
|
+
case "host.listSupportedCapabilities":
|
|
2036
|
+
return this.listSupportedCapabilities();
|
|
2037
|
+
case "host.listOperations":
|
|
2038
|
+
return this.listOperations();
|
|
2039
|
+
case "host.hasCapability":
|
|
2040
|
+
return this.hasCapability(params.capability);
|
|
1874
2041
|
case "clock.now":
|
|
1875
2042
|
return this.clock.now();
|
|
1876
2043
|
case "clock.monotonicNow":
|
|
@@ -1897,6 +2064,8 @@ export class NodeHost {
|
|
|
1897
2064
|
return this.mqtt.publish(params);
|
|
1898
2065
|
case "mqtt.subscribeOnce":
|
|
1899
2066
|
return this.mqtt.subscribeOnce(params);
|
|
2067
|
+
case "network.request":
|
|
2068
|
+
return this.network.request(params);
|
|
1900
2069
|
case "filesystem.resolvePath":
|
|
1901
2070
|
return this.filesystem.resolvePath(params.path);
|
|
1902
2071
|
case "filesystem.readFile":
|
|
@@ -1929,6 +2098,14 @@ export class NodeHost {
|
|
|
1929
2098
|
return this.udp.request(params);
|
|
1930
2099
|
case "tls.request":
|
|
1931
2100
|
return this.tls.request(params);
|
|
2101
|
+
case "ipfs.invoke":
|
|
2102
|
+
return this.ipfs.invoke(params);
|
|
2103
|
+
case "protocol_handle.register":
|
|
2104
|
+
return this.protocolHandle.register(params);
|
|
2105
|
+
case "protocol_handle.unregister":
|
|
2106
|
+
return this.protocolHandle.unregister(params);
|
|
2107
|
+
case "protocol_dial.dial":
|
|
2108
|
+
return this.protocolDial.dial(params);
|
|
1932
2109
|
case "exec.execFile":
|
|
1933
2110
|
return this.exec.execFile(params);
|
|
1934
2111
|
case "context.get":
|
package/src/manifest/AGENTS.md
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
normalization
|
|
3
|
+
This directory is the reference implementation for manifest encoding,
|
|
4
|
+
normalization, and round-trip behavior.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## What Authors Should Use It For
|
|
7
7
|
|
|
8
8
|
- Keep manifest encode/decode round-trips stable.
|
|
9
9
|
- Prefer canonical SDS schema names and file identifiers; do not add repo-local
|
|
10
10
|
aliases when a standards name already exists.
|
|
11
|
-
- If you change a manifest field or normalization rule, update the compiler,
|
|
12
|
-
compliance checks, and any affected examples/tests together.
|
|
13
11
|
|
|
14
|
-
##
|
|
12
|
+
## Note
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `node --test test/module-sdk.test.js test/compliance.test.js`
|
|
14
|
+
Use the public manifest helpers from this repo to build manifests. Only edit
|
|
15
|
+
this directory when you are intentionally changing the manifest standard.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
Apply the root and `src/AGENTS.md` files first.
|
|
3
|
+
Apply the root and `src/AGENTS.md` files first. This directory shows the
|
|
4
|
+
host-owned storage and ingest path available to module authors.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## What Authors Should Take From This Directory
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
- Host-owned durable identity is `(schemaFileId, rowId)` for rows and
|
|
9
|
+
`(regionId, recordIndex)` for aligned-binary regions.
|
|
10
|
+
- The canonical ingest path is direct FlatBuffer bytes, not JSON.
|
|
11
|
+
- If the host owns persistence, use these helpers. If a module owns state, keep
|
|
12
|
+
the stream binary and use the resident-module pump path from `src/testing`.
|
|
10
13
|
|
|
11
14
|
## Storage And Streaming Rules
|
|
12
15
|
|
|
@@ -19,14 +22,13 @@ side.
|
|
|
19
22
|
- If a resident module owns state, keep the stream binary and push into the
|
|
20
23
|
module through the harness/pump path rather than inventing JSON wrappers.
|
|
21
24
|
|
|
22
|
-
## Key Files
|
|
25
|
+
## Key Files To Read
|
|
23
26
|
|
|
24
27
|
- `flatbufferStreamIngestor.js`
|
|
25
28
|
- `flatsqlRuntimeStore.js`
|
|
26
29
|
- `index.js`
|
|
27
30
|
|
|
28
|
-
##
|
|
31
|
+
## Note
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `npm run benchmark:stream-1gib` for large-stream changes
|
|
33
|
+
Do not edit this directory just to store data for one module. Use the exported
|
|
34
|
+
helpers unless you are intentionally changing the runtime-host contract.
|
|
@@ -3,15 +3,71 @@ import { createFlatBufferStreamIngestor } from "./flatbufferStreamIngestor.js";
|
|
|
3
3
|
import { createRuntimeRegionStore } from "./runtimeRegionStore.js";
|
|
4
4
|
import { createModuleRegistry } from "./moduleRegistry.js";
|
|
5
5
|
|
|
6
|
+
function normalizeCapabilityId(capability) {
|
|
7
|
+
const normalized = String(capability ?? "").trim();
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
throw new TypeError("Capability id is required.");
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createCapabilityRegistry(capabilities = {}) {
|
|
15
|
+
const entries =
|
|
16
|
+
capabilities instanceof Map
|
|
17
|
+
? capabilities.entries()
|
|
18
|
+
: Object.entries(capabilities ?? {});
|
|
19
|
+
const registry = new Map();
|
|
20
|
+
for (const [capability, adapter] of entries) {
|
|
21
|
+
registry.set(normalizeCapabilityId(capability), adapter);
|
|
22
|
+
}
|
|
23
|
+
return registry;
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
export function createRuntimeHost(options = {}) {
|
|
7
27
|
const rows = options.rows ?? createFlatSqlRuntimeStore();
|
|
8
28
|
const regions = options.regions ?? createRuntimeRegionStore();
|
|
9
29
|
const moduleRegistry = options.moduleRegistry ?? createModuleRegistry();
|
|
30
|
+
const capabilities = createCapabilityRegistry(options.capabilities);
|
|
10
31
|
|
|
11
32
|
return {
|
|
12
33
|
rows,
|
|
13
34
|
regions,
|
|
14
35
|
moduleRegistry,
|
|
36
|
+
listCapabilities() {
|
|
37
|
+
return Array.from(capabilities.keys()).sort();
|
|
38
|
+
},
|
|
39
|
+
getCapability(capability) {
|
|
40
|
+
return capabilities.get(normalizeCapabilityId(capability)) ?? null;
|
|
41
|
+
},
|
|
42
|
+
registerCapability(capability, adapter) {
|
|
43
|
+
const normalized = normalizeCapabilityId(capability);
|
|
44
|
+
capabilities.set(normalized, adapter);
|
|
45
|
+
return adapter;
|
|
46
|
+
},
|
|
47
|
+
unregisterCapability(capability) {
|
|
48
|
+
return capabilities.delete(normalizeCapabilityId(capability));
|
|
49
|
+
},
|
|
50
|
+
async invokeCapability(operation, params = {}) {
|
|
51
|
+
const normalized = normalizeCapabilityId(operation);
|
|
52
|
+
const separator = normalized.indexOf(".");
|
|
53
|
+
const capabilityId =
|
|
54
|
+
separator >= 0 ? normalized.slice(0, separator) : normalized;
|
|
55
|
+
const methodId =
|
|
56
|
+
separator >= 0 ? normalized.slice(separator + 1) : "invoke";
|
|
57
|
+
const adapter = capabilities.get(capabilityId);
|
|
58
|
+
if (!adapter || typeof adapter !== "object") {
|
|
59
|
+
throw new Error(`Runtime host capability "${capabilityId}" is not registered.`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof adapter[methodId] === "function") {
|
|
62
|
+
return adapter[methodId](params);
|
|
63
|
+
}
|
|
64
|
+
if (typeof adapter.invoke === "function") {
|
|
65
|
+
return adapter.invoke(methodId, params);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Runtime host capability "${capabilityId}" does not implement "${methodId}" or invoke().`,
|
|
69
|
+
);
|
|
70
|
+
},
|
|
15
71
|
};
|
|
16
72
|
}
|
|
17
73
|
|
package/src/testing/AGENTS.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
|
-
Apply the root and `src/AGENTS.md` files first.
|
|
3
|
+
Apply the root and `src/AGENTS.md` files first. This directory contains the
|
|
4
|
+
author-facing harnesses and streaming helpers.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## What Authors Should Use From Here
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
- `createBrowserModuleHarness(...)` is the browser-side proof path for shared
|
|
9
|
+
standalone artifacts.
|
|
10
|
+
- `createModuleHarness(...)` is the generic process-side harness.
|
|
11
|
+
- `createModuleFlatBufferStreamPump(...)` is the canonical no-JSON path for
|
|
12
|
+
streaming size-prefixed FlatBuffer frames into a resident module instance.
|
|
10
13
|
|
|
11
14
|
## Harness Rules
|
|
12
15
|
|
|
@@ -19,15 +22,13 @@ clients, runtime-matrix helpers, and resident-module FlatBuffer pumps.
|
|
|
19
22
|
- Avoid hiding stateful behavior inside one-off demos; if a harness contract is
|
|
20
23
|
real, test it here.
|
|
21
24
|
|
|
22
|
-
## Key Files
|
|
25
|
+
## Key Files To Read
|
|
23
26
|
|
|
24
27
|
- `browserModuleHarness.js`
|
|
25
28
|
- `moduleFlatbufferStreamPump.js`
|
|
26
29
|
- `processInvoke.js`
|
|
27
30
|
|
|
28
|
-
##
|
|
31
|
+
## Note
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `npm run test:module-stream`
|
|
33
|
-
- `node --test test/process-invoke.test.js`
|
|
33
|
+
Use these helpers from your module repo or app harnesses. Only edit them when
|
|
34
|
+
you are intentionally changing the SDK testing/runtime contract.
|
|
@@ -16,7 +16,8 @@ import { createBrowserWasiShim, WasiExitError } from "../host/wasiShim.js";
|
|
|
16
16
|
import { createBrowserHost } from "../host/browserHost.js";
|
|
17
17
|
import {
|
|
18
18
|
createJsonHostcallBridge,
|
|
19
|
-
|
|
19
|
+
createHostSyncDispatcher,
|
|
20
|
+
dispatchHostOperation,
|
|
20
21
|
DEFAULT_HOSTCALL_IMPORT_MODULE,
|
|
21
22
|
} from "../host/abi.js";
|
|
22
23
|
import {
|
|
@@ -90,7 +91,7 @@ async function instantiateBrowserModule(options = {}) {
|
|
|
90
91
|
let instance = null;
|
|
91
92
|
let bridge = null;
|
|
92
93
|
if (needsHostBridge) {
|
|
93
|
-
const dispatch =
|
|
94
|
+
const dispatch = createHostSyncDispatcher(options.host);
|
|
94
95
|
bridge = createJsonHostcallBridge({
|
|
95
96
|
dispatch,
|
|
96
97
|
getMemory: () => instance.exports.memory,
|
|
@@ -238,6 +239,10 @@ export async function createBrowserModuleHarness(options = {}) {
|
|
|
238
239
|
return decodePluginInvokeResponse(responseBytes);
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
async function callHost(operation, params = {}) {
|
|
243
|
+
return dispatchHostOperation(host, operation, params);
|
|
244
|
+
}
|
|
245
|
+
|
|
241
246
|
function readManifest() {
|
|
242
247
|
const getBytesExport =
|
|
243
248
|
instance.exports[DefaultManifestExports.pluginBytesSymbol];
|
|
@@ -267,6 +272,7 @@ export async function createBrowserModuleHarness(options = {}) {
|
|
|
267
272
|
host,
|
|
268
273
|
bridge,
|
|
269
274
|
wasi,
|
|
275
|
+
callHost,
|
|
270
276
|
invoke,
|
|
271
277
|
invokeRaw,
|
|
272
278
|
readManifest,
|
package/src/testing/index.js
CHANGED
|
@@ -120,7 +120,8 @@ const CapabilitySurfaceMatrix = Object.freeze({
|
|
|
120
120
|
syncHostcall: false,
|
|
121
121
|
nodeHostApi: true,
|
|
122
122
|
notes: [
|
|
123
|
-
"The coarse network capability maps to host-side HTTP/TCP/UDP/TLS/WebSocket services
|
|
123
|
+
"The coarse network capability maps to async host-side HTTP/TCP/UDP/TLS/WebSocket services.",
|
|
124
|
+
"Browser harnesses can await the same capability surface through BrowserHost.invoke().",
|
|
124
125
|
"Pure WASI guests cannot reach that surface through the current sync hostcall ABI.",
|
|
125
126
|
"WasmEdge provides non-blocking socket-oriented extensions that can serve as the standard server-side max-WASI target.",
|
|
126
127
|
],
|
|
@@ -198,6 +199,42 @@ const CapabilitySurfaceMatrix = Object.freeze({
|
|
|
198
199
|
"WasmEdge TLS support provides a practical no-wrapper server-side target for guest HTTPS/TLS logic.",
|
|
199
200
|
],
|
|
200
201
|
}),
|
|
202
|
+
ipfs: Object.freeze({
|
|
203
|
+
capability: "ipfs",
|
|
204
|
+
wasi: false,
|
|
205
|
+
standaloneWasi: false,
|
|
206
|
+
wasmedge: false,
|
|
207
|
+
syncHostcall: false,
|
|
208
|
+
nodeHostApi: true,
|
|
209
|
+
notes: [
|
|
210
|
+
"IPFS access is available through async host adapters and browser/module harness dispatch.",
|
|
211
|
+
"Pure WASM guests cannot reach that surface through the current sync hostcall ABI.",
|
|
212
|
+
],
|
|
213
|
+
}),
|
|
214
|
+
protocol_handle: Object.freeze({
|
|
215
|
+
capability: "protocol_handle",
|
|
216
|
+
wasi: false,
|
|
217
|
+
standaloneWasi: false,
|
|
218
|
+
wasmedge: false,
|
|
219
|
+
syncHostcall: false,
|
|
220
|
+
nodeHostApi: true,
|
|
221
|
+
notes: [
|
|
222
|
+
"Protocol registration is available through async host adapters and browser/module harness dispatch.",
|
|
223
|
+
"Pure WASM guests cannot reach that surface through the current sync hostcall ABI.",
|
|
224
|
+
],
|
|
225
|
+
}),
|
|
226
|
+
protocol_dial: Object.freeze({
|
|
227
|
+
capability: "protocol_dial",
|
|
228
|
+
wasi: false,
|
|
229
|
+
standaloneWasi: false,
|
|
230
|
+
wasmedge: false,
|
|
231
|
+
syncHostcall: false,
|
|
232
|
+
nodeHostApi: true,
|
|
233
|
+
notes: [
|
|
234
|
+
"Protocol dialing is available through async host adapters and browser/module harness dispatch.",
|
|
235
|
+
"Pure WASM guests cannot reach that surface through the current sync hostcall ABI.",
|
|
236
|
+
],
|
|
237
|
+
}),
|
|
201
238
|
context_read: Object.freeze({
|
|
202
239
|
capability: "context_read",
|
|
203
240
|
wasi: false,
|