@polyengine/wasi 0.1.0-pre.g633468a
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 +202 -0
- package/README.md +23 -0
- package/esm/cli.js +160 -0
- package/esm/cli_stdio.js +203 -0
- package/esm/clocks.js +84 -0
- package/esm/filesystem.js +83 -0
- package/esm/filesystem_node.js +463 -0
- package/esm/filesystem_web.js +315 -0
- package/esm/http.js +663 -0
- package/esm/internal/cli_shared.js +26 -0
- package/esm/internal/fs_provider.js +795 -0
- package/esm/internal/sockets_02.js +992 -0
- package/esm/internal/sockets_03.js +969 -0
- package/esm/internal/sockets_platform.js +530 -0
- package/esm/internal/sockets_shared.js +268 -0
- package/esm/io.js +643 -0
- package/esm/mod.js +80 -0
- package/esm/package.json +3 -0
- package/esm/random.js +96 -0
- package/esm/sockets.js +172 -0
- package/package.json +98 -0
- package/types/cli.d.ts +44 -0
- package/types/cli_stdio.d.ts +31 -0
- package/types/clocks.d.ts +8 -0
- package/types/filesystem.d.ts +30 -0
- package/types/filesystem_node.d.ts +16 -0
- package/types/filesystem_web.d.ts +61 -0
- package/types/http.d.ts +169 -0
- package/types/internal/cli_shared.d.ts +26 -0
- package/types/internal/fs_provider.d.ts +174 -0
- package/types/internal/sockets_02.d.ts +37 -0
- package/types/internal/sockets_03.d.ts +12 -0
- package/types/internal/sockets_platform.d.ts +112 -0
- package/types/internal/sockets_shared.d.ts +256 -0
- package/types/io.d.ts +177 -0
- package/types/mod.d.ts +31 -0
- package/types/random.d.ts +17 -0
- package/types/sockets.d.ts +22 -0
package/esm/mod.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// `@polyengine/wasi` — the WASI providers for polyengine hosts, and the
|
|
2
|
+
// executable check that the embedder conventions
|
|
3
|
+
// (`@polyengine/runtime/embedder`) serve WASI (contracts/embedder-api.md C2
|
|
4
|
+
// checklist item 7; docs/architecture.md §2 keeps implementations out of
|
|
5
|
+
// the RUNTIME — this package is where they live). Scope: p2
|
|
6
|
+
// baseline + p3 clocks + à la carte sockets on BOTH tracks (the
|
|
7
|
+
// poll-shaped `@0.2` surface std::net links, and `@0.3` UDP + TCP
|
|
8
|
+
// client/listener; one node-builtins backend serving Deno and Node;
|
|
9
|
+
// `@polyengine/wasi/sockets`, issue #4, server-JS hosts only). Sockets is
|
|
10
|
+
// deliberately not merged here: this root module stays host-agnostic
|
|
11
|
+
// web-platform code, and `wasi()` merges only AMBIENT, side-effect-
|
|
12
|
+
// benign capabilities (time, entropy, stdio capture, an empty
|
|
13
|
+
// filesystem). Anything granting network egress or host storage is
|
|
14
|
+
// opt-in regardless of how portable it is — sockets, the fetch-backed
|
|
15
|
+
// http fragment, the host-stdio cli impl (`./cli-stdio` — real
|
|
16
|
+
// stdin/stdout/terminal access; the unqualified `./cli` stays the
|
|
17
|
+
// capture impl), and the real filesystem impls (`./filesystem-node` =
|
|
18
|
+
// node:fs, `./filesystem-web` = OPFS; the unqualified `./filesystem`
|
|
19
|
+
// stays the empty-preopens stub).
|
|
20
|
+
//
|
|
21
|
+
// COMPOSITION — three forms, coarsest to finest (virtualization scenarios
|
|
22
|
+
// pinned by tests/version_resolution_test.ts):
|
|
23
|
+
//
|
|
24
|
+
// 1. Batteries: `instantiate(a, { ...wasi() })`.
|
|
25
|
+
// 2. À la carte fragments: every IMPL is its own subpath export
|
|
26
|
+
// (`@polyengine/wasi/{cli,cli-stdio,clocks,filesystem,filesystem-node,`
|
|
27
|
+
// `filesystem-web,http,io,random,sockets}`) and
|
|
28
|
+
// a plain `{ imports }` record — hand-merge exactly the set you mean:
|
|
29
|
+
// `{ ...io().imports, ...clocks().imports }`. Fragment dependencies:
|
|
30
|
+
// io.ts is the package's shared vocabulary (the parking kernel and
|
|
31
|
+
// the stream classes) — cli, clocks, and the real filesystem and
|
|
32
|
+
// sockets impls all ride it; random and http stand alone. Impl
|
|
33
|
+
// machinery that is not itself a fragment lives under src/internal/
|
|
34
|
+
// (never exported). Naming convention as impls multiply: one
|
|
35
|
+
// subpath per impl; the unqualified name is the batteries impl
|
|
36
|
+
// (`./filesystem` = empty preopens), alternatives carry their backend
|
|
37
|
+
// (`./filesystem-node` = node:fs with explicit preopens,
|
|
38
|
+
// `./filesystem-web` = OPFS).
|
|
39
|
+
// 3. Per-interface override: the merged record is a plain object keyed by
|
|
40
|
+
// TRACK keys (`wasi:random/random@0.2`), so later spreads replace
|
|
41
|
+
// single interfaces wholesale:
|
|
42
|
+
// `{ ...wasi(), "wasi:random/random@0.2": myStub }`.
|
|
43
|
+
// Replace the track key, don't add an exact-versioned sibling — the
|
|
44
|
+
// resolver refuses track+exact coexistence on one track as ambiguous
|
|
45
|
+
// (contracts/embedder-api.md §"Version canonicalization"). Per-guest
|
|
46
|
+
// virtualization needs no version tricks anyway: compose a different
|
|
47
|
+
// record per `instantiate` call. Some fragments also take the finer
|
|
48
|
+
// knob directly — `random({ source })` swaps the CSPRNG while keeping
|
|
49
|
+
// the WIT shapes and the no-short-reads rule.
|
|
50
|
+
//
|
|
51
|
+
// `wasi(options)` returns one flat imports-record fragment, keyed by
|
|
52
|
+
// compatibility-**track** keys per contracts/embedder-api.md §"Version
|
|
53
|
+
// canonicalization" (`@0.2`, `@0.3`) — this package is the flagship
|
|
54
|
+
// track-key-registration consumer: one `@0.2` provider serves every p2
|
|
55
|
+
// leaf regardless of whether the guest's binary says `0.2.6`, `0.2.9` or
|
|
56
|
+
// `0.2.12` (C0 finding D-2), and one `@0.3` union provider serves both
|
|
57
|
+
// divergent `monotonic-clock@0.3.0` drafts the corpus actually links
|
|
58
|
+
// (C0 finding D-1).
|
|
59
|
+
import { cli } from "./cli.js";
|
|
60
|
+
import { clocks } from "./clocks.js";
|
|
61
|
+
import { filesystem } from "./filesystem.js";
|
|
62
|
+
import { io } from "./io.js";
|
|
63
|
+
import { random } from "./random.js";
|
|
64
|
+
export { ExitError } from "./internal/cli_shared.js";
|
|
65
|
+
/**
|
|
66
|
+
* Build the merged `wasi:*` imports fragment for `instantiate`.
|
|
67
|
+
*
|
|
68
|
+
* Usage: `instantiate(artifacts, { ...wasi(), ...moreImports })`.
|
|
69
|
+
*/
|
|
70
|
+
export function wasi(options = {}) {
|
|
71
|
+
const c = cli(options.cli);
|
|
72
|
+
const merged = {
|
|
73
|
+
...c.imports,
|
|
74
|
+
...io().imports,
|
|
75
|
+
...clocks(options.clocks).imports,
|
|
76
|
+
...random(options.random).imports,
|
|
77
|
+
...filesystem().imports,
|
|
78
|
+
};
|
|
79
|
+
return Object.assign(merged, { captured: c.captured });
|
|
80
|
+
}
|
package/esm/package.json
ADDED
package/esm/random.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// `wasi:random@0.2` + `wasi:random@0.3` — random, insecure, insecure-seed
|
|
2
|
+
// (contracts/embedder-api.md §"WASI examination"; 0.2 leaves confirmed
|
|
3
|
+
// against `engine-go/main.wasm`'s import surface: `get-random-bytes`; 0.3
|
|
4
|
+
// shapes from the WASI 0.3.1 release — same three interfaces, same
|
|
5
|
+
// function names, `len` renamed `max-len` with short reads permitted; see
|
|
6
|
+
// the divergence note on `GET_RANDOM_VALUES_MAX`). One impl serves both
|
|
7
|
+
// tracks: chunk-to-full is conforming on each.
|
|
8
|
+
/**
|
|
9
|
+
* `crypto.getRandomValues` rejects requests over 65536 bytes
|
|
10
|
+
* (QuotaExceededError), while the 0.2 WIT this fragment serves requires
|
|
11
|
+
* exactly `len` bytes back ("Return `len` cryptographically-secure random
|
|
12
|
+
* or pseudo-random bytes", random.wit @0.2.x) — no shorter-return
|
|
13
|
+
* latitude, and callers (Rust `getrandom`, the Go runtime) fill fixed-size
|
|
14
|
+
* buffers trusting the length. So: chunk the fill, never clamp it.
|
|
15
|
+
*
|
|
16
|
+
* TRACK DIVERGENCE, for a future @0.3 fragment: `wasi:random@0.3.0`
|
|
17
|
+
* renames the parameter to `max-len` and PERMITS short reads
|
|
18
|
+
* ("Implementations MAY return fewer bytes than requested"; callers must
|
|
19
|
+
* loop; ≥1 byte required for max-len > 0). Authority moved with the WASI
|
|
20
|
+
* consolidation: `WebAssembly/WASI proposals/random/wit/random.wit` — the
|
|
21
|
+
* archived wasi-random repo's 0.3 rc still shows the old exact-len text.
|
|
22
|
+
* Chunk-to-full remains conforming there too ("up to max-len" includes
|
|
23
|
+
* exactly max-len) and makes conforming callers' mandatory loops terminate
|
|
24
|
+
* in one pass, so this helper serves both tracks unchanged.
|
|
25
|
+
*
|
|
26
|
+
* Either way the fill stays synchronous, satisfying both tracks' "must not
|
|
27
|
+
* block ... including on requests for [large] numbers of bytes".
|
|
28
|
+
*/
|
|
29
|
+
const GET_RANDOM_VALUES_MAX = 65536;
|
|
30
|
+
function cryptoBytes(len) {
|
|
31
|
+
const out = new Uint8Array(Number(len));
|
|
32
|
+
for (let i = 0; i < out.length; i += GET_RANDOM_VALUES_MAX) {
|
|
33
|
+
crypto.getRandomValues(out.subarray(i, Math.min(i + GET_RANDOM_VALUES_MAX, out.length)));
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** The fill, honoring a virtualized `source` and its exact-length contract. */
|
|
38
|
+
function makeRandomBytes(source) {
|
|
39
|
+
if (source === undefined)
|
|
40
|
+
return cryptoBytes;
|
|
41
|
+
return (len) => {
|
|
42
|
+
const out = source(Number(len));
|
|
43
|
+
if (out.length !== Number(len)) {
|
|
44
|
+
throw new TypeError(`random source returned ${out.length} bytes, need exactly ${len} ` +
|
|
45
|
+
`(the @0.2 WIT permits no short reads)`);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function makeRandomU64(bytes) {
|
|
51
|
+
return () => {
|
|
52
|
+
const out = bytes(8n);
|
|
53
|
+
return new DataView(out.buffer, out.byteOffset, 8).getBigUint64(0, true);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A fixed default: `wasi:random/insecure-seed` is explicitly documented (WIT
|
|
58
|
+
* doc comment, io.wit deps) as allowed to be entirely deterministic — it
|
|
59
|
+
* exists to seed hash-map DoS resistance, not for cryptographic use. This
|
|
60
|
+
* shim defaults to a fixed, obviously-synthetic pair (documented here as
|
|
61
|
+
* exactly that) so runs are reproducible; pass `insecureSeed` to override.
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_INSECURE_SEED = [0n, 1n];
|
|
64
|
+
/** `wasi:random@0.2` + `@0.3` provider fragment (two track keys). */
|
|
65
|
+
export function random(options = {}) {
|
|
66
|
+
const seed = options.insecureSeed ?? DEFAULT_INSECURE_SEED;
|
|
67
|
+
const randomBytes = makeRandomBytes(options.source);
|
|
68
|
+
const randomU64 = makeRandomU64(randomBytes);
|
|
69
|
+
const randomIface = {
|
|
70
|
+
getRandomBytes: randomBytes,
|
|
71
|
+
getRandomU64: randomU64,
|
|
72
|
+
};
|
|
73
|
+
// "insecure" only means "not required to be a CSPRNG" — it is still
|
|
74
|
+
// wired to the real CSPRNG here for simplicity; only `insecure-seed`
|
|
75
|
+
// is deliberately, documentedly predictable.
|
|
76
|
+
const insecureIface = {
|
|
77
|
+
getInsecureRandomBytes: randomBytes,
|
|
78
|
+
getInsecureRandomU64: randomU64,
|
|
79
|
+
};
|
|
80
|
+
const insecureSeedIface = {
|
|
81
|
+
insecureSeed: () => seed,
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
imports: {
|
|
85
|
+
"wasi:random/random@0.2": randomIface,
|
|
86
|
+
"wasi:random/insecure@0.2": insecureIface,
|
|
87
|
+
"wasi:random/insecure-seed@0.2": insecureSeedIface,
|
|
88
|
+
// The @0.3 track: identical function names; `max-len` PERMITS short
|
|
89
|
+
// reads but chunk-to-full returns exactly max-len, which "up to
|
|
90
|
+
// max-len" includes (module header) — one impl, both tracks.
|
|
91
|
+
"wasi:random/random@0.3": randomIface,
|
|
92
|
+
"wasi:random/insecure@0.3": insecureIface,
|
|
93
|
+
"wasi:random/insecure-seed@0.3": insecureSeedIface,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/esm/sockets.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `wasi:sockets` — grants this process's NETWORK REACH to the guest,
|
|
2
|
+
// unscoped: there is no allowlist, address check, or TCP/UDP toggle, so
|
|
3
|
+
// a guest can reach anything this process can, loopback and instance
|
|
4
|
+
// metadata endpoints included (docs/security.md). Never rides the
|
|
5
|
+
// default `wasi()` merge.
|
|
6
|
+
//
|
|
7
|
+
// BOTH tracks: `types@0.3` + `ip-name-lookup@0.3` (UDP
|
|
8
|
+
// and TCP, client + listener; internal/sockets_03.ts) and the poll-shaped
|
|
9
|
+
// `@0.2` surface (internal/sockets_02.ts). THIS module is the public
|
|
10
|
+
// face: the `sockets()` fragment factory and the vocabulary re-exports.
|
|
11
|
+
// One backend serves both tracks: the
|
|
12
|
+
// node builtins (`node:dgram` / `node:net`), which
|
|
13
|
+
// real Node provides natively, Deno serves as STABLE node-compat (no
|
|
14
|
+
// `--unstable-net` needed — that flag gates only the native API's shape,
|
|
15
|
+
// not the capability), and Bun reaches through its compat (findings-only;
|
|
16
|
+
// JSC lacks multi-memory, so polyengine guests cannot run there regardless).
|
|
17
|
+
// Backend rationale, adapters, and measured costs: internal/sockets_platform.ts.
|
|
18
|
+
//
|
|
19
|
+
// À la carte (issue #4): this module is a separate export
|
|
20
|
+
// (`@polyengine/wasi/sockets`), never merged into `wasi()` — the
|
|
21
|
+
// baseline package stays host-agnostic web-platform code, while this
|
|
22
|
+
// fragment is server-JS-native by nature (browsers have no sockets;
|
|
23
|
+
// wasmtime owns the native story). Consumers that want it spread it in:
|
|
24
|
+
//
|
|
25
|
+
// instantiate(artifacts, { ...wasi(), ...sockets().imports })
|
|
26
|
+
//
|
|
27
|
+
// The UDP provider is adopted from polymorph-components/polymorph-iroh#69
|
|
28
|
+
// (that host's exam drives it over loopback QUIC); divergences from the
|
|
29
|
+
// adopted code are the track key (`@0.3`, per this package's conventions —
|
|
30
|
+
// one provider serves every 0.3.x), the fragment-scoped `onCall` hook
|
|
31
|
+
// replacing a module-global call log (a published provider must not grow a
|
|
32
|
+
// string per datagram by default), and `globalThis`-based feature detection
|
|
33
|
+
// (the module evaluates and answers honestly on any host). The TCP client
|
|
34
|
+
// surface is what the wosh listener bridges through (its
|
|
35
|
+
// `listener-core/src/tcp.rs` — issue #4's prospective consumer) and the
|
|
36
|
+
// smoke-c0 leg-4 composed-websocket shopping list names.
|
|
37
|
+
//
|
|
38
|
+
// The implemented resource shapes (0.3.x WIT — the full 0.3.1 release
|
|
39
|
+
// surface, minus the recorded not-supported options below):
|
|
40
|
+
//
|
|
41
|
+
// resource udp-socket {
|
|
42
|
+
// create/bind/connect/disconnect/send/receive,
|
|
43
|
+
// get-local-address/get-remote-address/get-address-family,
|
|
44
|
+
// get+set unicast-hop-limit, get+set receive/send-buffer-size
|
|
45
|
+
// }
|
|
46
|
+
// resource tcp-socket {
|
|
47
|
+
// create/bind/connect/listen/send/receive,
|
|
48
|
+
// get-local-address/get-remote-address/get-address-family/get-is-listening,
|
|
49
|
+
// set-listen-backlog-size, get+set keep-alive-enabled,
|
|
50
|
+
// get+set keep-alive-idle-time
|
|
51
|
+
// }
|
|
52
|
+
// ip-name-lookup { resolve-addresses } (system resolver via node:dns)
|
|
53
|
+
//
|
|
54
|
+
// OPTIONS HONESTY (the node option surface is narrow; nothing is
|
|
55
|
+
// emulated silently):
|
|
56
|
+
//
|
|
57
|
+
// * udp connect/disconnect are OS-level (node dgram connect: kernel
|
|
58
|
+
// filtering and default destination), not adapter filtering.
|
|
59
|
+
// * udp unicast-hop-limit: setter is real (dgram setTTL); the getter
|
|
60
|
+
// reports the cached value (default 64, documented) — node has no
|
|
61
|
+
// getter. Buffer sizes are real both ways once bound (SO_RCVBUF/
|
|
62
|
+
// SO_SNDBUF); before bind, gets report the cached request or fail
|
|
63
|
+
// `not-supported`.
|
|
64
|
+
// * tcp keep-alive: enabled + idle-time are real (node setKeepAlive);
|
|
65
|
+
// gets report cached values (idle default 7200 s, documented).
|
|
66
|
+
// keep-alive-interval/count, tcp hop-limit, and tcp buffer sizes
|
|
67
|
+
// have NO node:net API and fail `not-supported`.
|
|
68
|
+
// * set-listen-backlog-size: applied as listen()'s backlog hint;
|
|
69
|
+
// changing it while listening is `not-supported` (node cannot
|
|
70
|
+
// re-listen; wasmtime re-listens).
|
|
71
|
+
// * accepted sockets do NOT inherit the listener's options (wasmtime
|
|
72
|
+
// inherits; recorded divergence).
|
|
73
|
+
//
|
|
74
|
+
// Anything else a future guest links fails loudly with a trap naming the
|
|
75
|
+
// missing method rather than riding an untested emulation.
|
|
76
|
+
//
|
|
77
|
+
// The behavioral yardstick is wasmtime-wasi's p3 provider (the consumers'
|
|
78
|
+
// wasmtime hosts serve the same guests through it).
|
|
79
|
+
//
|
|
80
|
+
// UDP: the same 64 KiB datagram ceiling, the same state machine (`bind`
|
|
81
|
+
// once from unbound; `receive` and `get-local-address` demand a bound
|
|
82
|
+
// socket; `send` to a remote implicitly binds an unbound socket to a
|
|
83
|
+
// wildcard address; an omitted `send` remote requires connected mode, and
|
|
84
|
+
// an explicit remote on a connected socket is `invalid-argument`), and the same address-family validation (an
|
|
85
|
+
// IPv4-mapped or deprecated IPv4-compatible IPv6 address never crosses a
|
|
86
|
+
// family boundary). Recorded divergences, rooted in the platform exposing
|
|
87
|
+
// no socket options:
|
|
88
|
+
//
|
|
89
|
+
// * scope-id: a non-zero IPv6 `scope-id` fails `not-supported` (node
|
|
90
|
+
// hostnames cannot carry a zone; wasmtime binds it).
|
|
91
|
+
// * v6-only: wasmtime sets IPV6_V6ONLY on IPv6 sockets; node leaves the
|
|
92
|
+
// OS default, so an `::` wildcard bind on Linux is dual-stack and may
|
|
93
|
+
// receive IPv4 traffic, surfaced as IPv4-mapped sender addresses —
|
|
94
|
+
// which is also why the address codec parses the `::ffff:a.b.c.d`
|
|
95
|
+
// spelling.
|
|
96
|
+
// * unread datagrams queue in the adapter (node's receive path is
|
|
97
|
+
// push-shaped) and tail-drop past a bound — the kernel-buffer
|
|
98
|
+
// analogue; see sockets_platform.ts `MAX_QUEUED_DATAGRAMS`.
|
|
99
|
+
//
|
|
100
|
+
// TCP (the TcpSocketOperationalSemantics-0.3.0 state machine): `connect`
|
|
101
|
+
// once from `unbound` (a failed attempt closes the socket); `listen` once
|
|
102
|
+
// from `unbound` (implicit wildcard-ephemeral bind) or `bound`;
|
|
103
|
+
// `send`/`receive` once each, only when `connected`, and their failures
|
|
104
|
+
// NEVER throw — `send`'s error channel is its returned future (amendment
|
|
105
|
+
// A12: the async method's promise IS the future source) and `receive`'s
|
|
106
|
+
// is the future half of its tuple, resolved as result values. `listen`
|
|
107
|
+
// returns the perpetual accept stream, whose elements are connected
|
|
108
|
+
// `tcp-socket` resources (amendment A13: un-taken elements are destroyed
|
|
109
|
+
// at teardown, closing their connections); per-connection accept failures
|
|
110
|
+
// are skipped, listener-fatal ones end the stream. Stream teardown
|
|
111
|
+
// follows the WIT's shared-ownership note: the OS socket closes only when
|
|
112
|
+
// the resource handle AND every derived stream (pumps, accept stream) are
|
|
113
|
+
// done, so they all remain functional after the guest drops the
|
|
114
|
+
// `tcp-socket` handle. The receive stream ends (cleanly, no fake data) on
|
|
115
|
+
// BOTH graceful FIN and abnormal close; the two are distinguished by the
|
|
116
|
+
// future (`ok` vs `err`), exactly as the WIT documents. Guest-side
|
|
117
|
+
// failures while consuming `send`'s stream (a peer trap) are NOT socket
|
|
118
|
+
// errors: they propagate as producer failures on the host-failure channel.
|
|
119
|
+
//
|
|
120
|
+
// `listen` is SUSPENDING (embedder-api A1/A2 — the wasi:io `block`
|
|
121
|
+
// kernel): node defers the OS bind one event-loop turn, so `listen` parks
|
|
122
|
+
// the calling guest frame for that tick and returns fully settled — real
|
|
123
|
+
// ephemeral addresses from `get-local-address`, real error codes
|
|
124
|
+
// (`address-in-use`) from a failed bind. Guests that link `listen`
|
|
125
|
+
// auto-select jspi mode on JSPI engines (V8: Deno, Node, Chromium);
|
|
126
|
+
// client-shaped guests are untouched.
|
|
127
|
+
//
|
|
128
|
+
// Recorded TCP divergences:
|
|
129
|
+
//
|
|
130
|
+
// * `bind` records the address; the OS bind is DEFERRED to `listen` or
|
|
131
|
+
// `connect` (node cannot bind an unconnected socket), so bind errors
|
|
132
|
+
// (`address-in-use`, `address-not-bindable`) surface at those calls —
|
|
133
|
+
// with their real codes — not at `bind`.
|
|
134
|
+
//
|
|
135
|
+
// When the node builtins are absent (`process.getBuiltinModule` missing —
|
|
136
|
+
// a browser), `create` fails `error-code.not-supported` — the honest
|
|
137
|
+
// capability answer. On Deno the providers need `--allow-net`; a denied
|
|
138
|
+
// permission arrives as `Deno.errors.NotCapable` through the compat layer
|
|
139
|
+
// and maps to `access-denied`.
|
|
140
|
+
import { sockets02 } from "./internal/sockets_02.js";
|
|
141
|
+
import { sockets03 } from "./internal/sockets_03.js";
|
|
142
|
+
// The public vocabulary (types, address codec, validators, the platform
|
|
143
|
+
// error mapper) lives in internal/sockets_shared.ts; THIS module is its
|
|
144
|
+
// public home. The tracks live in internal/sockets_03.ts and
|
|
145
|
+
// internal/sockets_02.ts and share one node-builtins backend
|
|
146
|
+
// (internal/sockets_platform.ts).
|
|
147
|
+
export { ipHostname, isUnspecified, isValidAddressFamily, mapPlatformError, MAX_UDP_DATAGRAM_SIZE, parseNetAddr, sameSocketAddress, wildcardAddress, } from "./internal/sockets_shared.js";
|
|
148
|
+
// The 0.2 track's public pieces: the enum error vocabulary, the io-error
|
|
149
|
+
// resource `network-error-code` downcasts, and the opaque network.
|
|
150
|
+
export { Network, SocketIoError, } from "./internal/sockets_02.js";
|
|
151
|
+
export const SOCKETS_TYPES_INTERFACE = "wasi:sockets/types@0.3";
|
|
152
|
+
/**
|
|
153
|
+
* The `wasi:sockets` provider fragment, BOTH tracks (module header):
|
|
154
|
+
* `types@0.3` + `ip-name-lookup@0.3` (internal/sockets_03.ts) and the
|
|
155
|
+
* seven poll-shaped `@0.2` interfaces (internal/sockets_02.ts). Track
|
|
156
|
+
* keys — one provider serves every 0.2.x / 0.3.x the resolver folds onto
|
|
157
|
+
* its track. Resource classes are built per fragment so the `onCall`
|
|
158
|
+
* observer is scoped to it.
|
|
159
|
+
*/
|
|
160
|
+
export function sockets(options = {}) {
|
|
161
|
+
const onCall = options.onCall ?? (() => { });
|
|
162
|
+
const track03 = sockets03(onCall);
|
|
163
|
+
return {
|
|
164
|
+
imports: {
|
|
165
|
+
...track03.imports,
|
|
166
|
+
...sockets02(onCall).imports,
|
|
167
|
+
},
|
|
168
|
+
UdpSocket: track03.UdpSocket,
|
|
169
|
+
TcpSocket: track03.TcpSocket,
|
|
170
|
+
resolveAddresses: track03.resolveAddresses,
|
|
171
|
+
};
|
|
172
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polyengine/wasi",
|
|
3
|
+
"version": "0.1.0-pre.g633468a",
|
|
4
|
+
"description": "WASI providers for polyengine hosts: the p2 baseline and p3 clocks, one module per semver track.",
|
|
5
|
+
"homepage": "https://github.com/polymorph-components/polyengine#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/polymorph-components/polyengine.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/polymorph-components/polyengine/issues"
|
|
13
|
+
},
|
|
14
|
+
"module": "./esm/mod.js",
|
|
15
|
+
"types": "./types/mod.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./types/mod.d.ts",
|
|
20
|
+
"default": "./esm/mod.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"./cli": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./types/cli.d.ts",
|
|
26
|
+
"default": "./esm/cli.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"./cli-stdio": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./types/cli_stdio.d.ts",
|
|
32
|
+
"default": "./esm/cli_stdio.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./clocks": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./types/clocks.d.ts",
|
|
38
|
+
"default": "./esm/clocks.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"./filesystem": {
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./types/filesystem.d.ts",
|
|
44
|
+
"default": "./esm/filesystem.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"./filesystem-node": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./types/filesystem_node.d.ts",
|
|
50
|
+
"default": "./esm/filesystem_node.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./filesystem-web": {
|
|
54
|
+
"import": {
|
|
55
|
+
"types": "./types/filesystem_web.d.ts",
|
|
56
|
+
"default": "./esm/filesystem_web.js"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"./http": {
|
|
60
|
+
"import": {
|
|
61
|
+
"types": "./types/http.d.ts",
|
|
62
|
+
"default": "./esm/http.js"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"./io": {
|
|
66
|
+
"import": {
|
|
67
|
+
"types": "./types/io.d.ts",
|
|
68
|
+
"default": "./esm/io.js"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"./random": {
|
|
72
|
+
"import": {
|
|
73
|
+
"types": "./types/random.d.ts",
|
|
74
|
+
"default": "./esm/random.js"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"./sockets": {
|
|
78
|
+
"import": {
|
|
79
|
+
"types": "./types/sockets.d.ts",
|
|
80
|
+
"default": "./esm/sockets.js"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"./package.json": "./package.json"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {},
|
|
86
|
+
"type": "module",
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=22.14.0"
|
|
89
|
+
},
|
|
90
|
+
"publishConfig": {
|
|
91
|
+
"access": "public"
|
|
92
|
+
},
|
|
93
|
+
"dependencies": {
|
|
94
|
+
"@polyengine/protocol": "0.1.0-pre.g633468a",
|
|
95
|
+
"@polyengine/runtime": "0.1.0-pre.g633468a"
|
|
96
|
+
},
|
|
97
|
+
"_generatedBy": "dnt@0.43.2"
|
|
98
|
+
}
|
package/types/cli.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export { type CliByteSource, type CliErrorCode, type CliIoResult, ExitError, TerminalInput, TerminalOutput, } from "./internal/cli_shared.js";
|
|
2
|
+
export interface CliOptions {
|
|
3
|
+
/** `get-arguments`; default `[]`. */
|
|
4
|
+
args?: string[];
|
|
5
|
+
/** `get-environment`; default `{}`. */
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
/** `initial-cwd`; default `undefined` (none). */
|
|
8
|
+
cwd?: string;
|
|
9
|
+
/** `get-stdin`'s buffer contents; default empty (matches contract: "stdin (empty)"). */
|
|
10
|
+
stdinBuffer?: Uint8Array;
|
|
11
|
+
/** Also `console.log`/`console.error` captured stdout/stderr writes. Default false. */
|
|
12
|
+
passthrough?: boolean;
|
|
13
|
+
/** `exit()` throws `ExitError` instead of merely recording. Default false. */
|
|
14
|
+
throwOnExit?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Captured host-observable state exposed on the returned handle (contract wording). */
|
|
17
|
+
export interface CliCaptured {
|
|
18
|
+
stdout(): Uint8Array;
|
|
19
|
+
stderr(): Uint8Array;
|
|
20
|
+
stdoutText(): string;
|
|
21
|
+
stderrText(): string;
|
|
22
|
+
/** Whether `wasi:cli/exit#exit` has been called. */
|
|
23
|
+
exited(): boolean;
|
|
24
|
+
/** The `result` kind of the last `exit()` call's `status`, or `undefined` if never called. */
|
|
25
|
+
exitOk(): boolean | undefined;
|
|
26
|
+
/** The last `exit-with-code` status (0.3), or `undefined` if never called. */
|
|
27
|
+
exitCode(): number | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface CliResult {
|
|
30
|
+
imports: Record<string, unknown>;
|
|
31
|
+
captured: CliCaptured;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `wasi:cli@0.2` provider fragment (track key).
|
|
35
|
+
*
|
|
36
|
+
* `exit`'s WIT signature is `exit: func(status: result)` — `result` with no
|
|
37
|
+
* type parameters, i.e. `result<_, _>`. Per contracts/embedder-api.md's value
|
|
38
|
+
* table, a `result` in **parameter** (non-return) position is plain nested
|
|
39
|
+
* data: `{ kind: "ok" } | { kind: "err" }` (the A10 family — this comment
|
|
40
|
+
* and the impl carried the pre-A10 `tag` spelling until 2026-08-14, a
|
|
41
|
+
* latent bug the direct-call unit tests masked), never a throw. Only a
|
|
42
|
+
* function's own *return*-position result throws/rejects.
|
|
43
|
+
*/
|
|
44
|
+
export declare function cli(options?: CliOptions): CliResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ByteSink } from "./io.js";
|
|
2
|
+
export { type ByteSink } from "./io.js";
|
|
3
|
+
export interface CliStdioOptions {
|
|
4
|
+
/** stdin bytes; default: the host process's stdin. */
|
|
5
|
+
stdin?: AsyncIterable<Uint8Array>;
|
|
6
|
+
/** stdout sink; default: the host process's stdout. */
|
|
7
|
+
stdout?: ByteSink;
|
|
8
|
+
/** stderr sink; default: the host process's stderr. */
|
|
9
|
+
stderr?: ByteSink;
|
|
10
|
+
/** Terminal-ness per stream; default: the real streams' `isTTY`. */
|
|
11
|
+
isTty?: {
|
|
12
|
+
stdin?: boolean;
|
|
13
|
+
stdout?: boolean;
|
|
14
|
+
stderr?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** `get-arguments`; default: the host process's argv (script-relative). */
|
|
17
|
+
args?: string[];
|
|
18
|
+
/** `get-environment`; default: the host process's env. */
|
|
19
|
+
env?: Record<string, string>;
|
|
20
|
+
/** `initial-cwd`/`get-initial-cwd`; default: the host process's cwd. */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
/** `exit` terminates the host process instead of throwing `ExitError`. */
|
|
23
|
+
exitProcess?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface CliStdio {
|
|
26
|
+
imports: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `wasi:cli` over the host process's stdio (both tracks — module header).
|
|
30
|
+
*/
|
|
31
|
+
export declare function cliStdio(options?: CliStdioOptions): CliStdio;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ClocksOptions {
|
|
2
|
+
/** Override the monotonic clock's `now()` (nanoseconds); default `performance.now()`-derived. */
|
|
3
|
+
now?: () => bigint;
|
|
4
|
+
}
|
|
5
|
+
/** `wasi:clocks@0.2` + `wasi:clocks@0.3` provider fragment (two track keys). */
|
|
6
|
+
export declare function clocks(options?: ClocksOptions): {
|
|
7
|
+
imports: Record<string, unknown>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wasi:filesystem/types.descriptor` — constructible-never.
|
|
3
|
+
*
|
|
4
|
+
* `preopens#get-directories` always returns `[]` (below), so no `descriptor`
|
|
5
|
+
* instance can ever reach a guest through this shim; the class exists only
|
|
6
|
+
* so the resource *type* is a legal, structurally-typeable import target.
|
|
7
|
+
* Every method throws if somehow reached — loud, not a silent wrong answer —
|
|
8
|
+
* because reaching one would mean a caller found a descriptor this shim never
|
|
9
|
+
* handed out.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Descriptor {
|
|
12
|
+
#private;
|
|
13
|
+
constructor();
|
|
14
|
+
getFlags(): never;
|
|
15
|
+
getType(): never;
|
|
16
|
+
stat(): never;
|
|
17
|
+
statAt(): never;
|
|
18
|
+
openAt(): never;
|
|
19
|
+
readViaStream(): never;
|
|
20
|
+
writeViaStream(): never;
|
|
21
|
+
appendViaStream(): never;
|
|
22
|
+
metadataHashAt(): never;
|
|
23
|
+
}
|
|
24
|
+
/** `wasi:filesystem/types.directory-entry-stream` — same rationale as `Descriptor`. */
|
|
25
|
+
export declare class DirectoryEntryStream {
|
|
26
|
+
}
|
|
27
|
+
/** `wasi:filesystem@0.2` provider fragment (track key). */
|
|
28
|
+
export declare function filesystem(): {
|
|
29
|
+
imports: Record<string, unknown>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type FilesystemAccessOptions, type FilesystemFragment, type MaybeAsync } from "./internal/fs_provider.js";
|
|
2
|
+
export interface FilesystemNodeOptions extends FilesystemAccessOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Guest name → host directory path. Each entry becomes a preopen
|
|
5
|
+
* (`preopens#get-directories`). No default: filesystem access is an
|
|
6
|
+
* explicit grant. Host paths are resolved (realpath) at construction
|
|
7
|
+
* and must name directories. Read-only unless `writable` is set.
|
|
8
|
+
*/
|
|
9
|
+
preopens: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `wasi:filesystem` over node's `node:fs` builtin (module header).
|
|
13
|
+
* Serves both the `@0.2` and `@0.3` tracks.
|
|
14
|
+
*/
|
|
15
|
+
export declare function filesystemNode(options: FilesystemNodeOptions): FilesystemFragment;
|
|
16
|
+
export type { FilesystemFragment, MaybeAsync };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type FilesystemAccessOptions, type FilesystemFragment } from "./internal/fs_provider.js";
|
|
2
|
+
export interface OpfsFileLike {
|
|
3
|
+
readonly size: number;
|
|
4
|
+
readonly lastModified: number;
|
|
5
|
+
slice(start: number, end: number): {
|
|
6
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
7
|
+
};
|
|
8
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
9
|
+
}
|
|
10
|
+
export interface OpfsWritable {
|
|
11
|
+
write(params: {
|
|
12
|
+
type: "write";
|
|
13
|
+
position: number;
|
|
14
|
+
data: Uint8Array;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
truncate(size: number): Promise<void>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface OpfsFileHandle {
|
|
20
|
+
readonly kind: "file";
|
|
21
|
+
readonly name: string;
|
|
22
|
+
getFile(): Promise<OpfsFileLike>;
|
|
23
|
+
createWritable(opts?: {
|
|
24
|
+
keepExistingData?: boolean;
|
|
25
|
+
}): Promise<OpfsWritable>;
|
|
26
|
+
isSameEntry(other: OpfsFileHandle | OpfsDirectoryHandle): Promise<boolean>;
|
|
27
|
+
/** Chromium's FileSystemHandle.move; absent on Firefox/Safari. */
|
|
28
|
+
move?(parent: OpfsDirectoryHandle, name: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface OpfsDirectoryHandle {
|
|
31
|
+
readonly kind: "directory";
|
|
32
|
+
readonly name: string;
|
|
33
|
+
getDirectoryHandle(name: string, opts?: {
|
|
34
|
+
create?: boolean;
|
|
35
|
+
}): Promise<OpfsDirectoryHandle>;
|
|
36
|
+
getFileHandle(name: string, opts?: {
|
|
37
|
+
create?: boolean;
|
|
38
|
+
}): Promise<OpfsFileHandle>;
|
|
39
|
+
removeEntry(name: string, opts?: {
|
|
40
|
+
recursive?: boolean;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
entries(): AsyncIterable<[string, OpfsDirectoryHandle | OpfsFileHandle]>;
|
|
43
|
+
isSameEntry(other: OpfsFileHandle | OpfsDirectoryHandle): Promise<boolean>;
|
|
44
|
+
move?(parent: OpfsDirectoryHandle, name: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export interface FilesystemWebOptions extends FilesystemAccessOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Guest name → OPFS directory handle. Each entry becomes a preopen.
|
|
49
|
+
* No default: filesystem access is an explicit grant — pass
|
|
50
|
+
* `await navigator.storage.getDirectory()` (or any structural
|
|
51
|
+
* equivalent, e.g. an in-memory fake) yourself. Read-only unless
|
|
52
|
+
* `writable` is set.
|
|
53
|
+
*/
|
|
54
|
+
preopens: Record<string, OpfsDirectoryHandle>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `wasi:filesystem` over the Origin Private File System (module header).
|
|
58
|
+
* Serves both the `@0.2` (parking, JSPI) and `@0.3` tracks.
|
|
59
|
+
*/
|
|
60
|
+
export declare function filesystemWeb(options: FilesystemWebOptions): FilesystemFragment;
|
|
61
|
+
export type { FilesystemFragment };
|