@unicity-astrid/sdk 0.1.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 +120 -0
- package/dist/approval.d.ts +23 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +29 -0
- package/dist/approval.js.map +1 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +19 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/capsule.d.ts +39 -0
- package/dist/capsule.d.ts.map +1 -0
- package/dist/capsule.js +67 -0
- package/dist/capsule.js.map +1 -0
- package/dist/contracts.d.ts +1104 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +4 -0
- package/dist/contracts.js.map +1 -0
- package/dist/elicit.d.ts +30 -0
- package/dist/elicit.d.ts.map +1 -0
- package/dist/elicit.js +103 -0
- package/dist/elicit.js.map +1 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +27 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +108 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs.d.ts +135 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +257 -0
- package/dist/fs.js.map +1 -0
- package/dist/http.d.ts +90 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +276 -0
- package/dist/http.js.map +1 -0
- package/dist/identity.d.ts +46 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +69 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +22 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/ipc.d.ts +143 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +261 -0
- package/dist/ipc.js.map +1 -0
- package/dist/kv.d.ts +45 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +91 -0
- package/dist/kv.js.map +1 -0
- package/dist/log.d.ts +17 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +40 -0
- package/dist/log.js.map +1 -0
- package/dist/net.d.ts +154 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +421 -0
- package/dist/net.js.map +1 -0
- package/dist/process.d.ts +77 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +128 -0
- package/dist/process.js.map +1 -0
- package/dist/runtime/bridge.d.ts +34 -0
- package/dist/runtime/bridge.d.ts.map +1 -0
- package/dist/runtime/bridge.js +326 -0
- package/dist/runtime/bridge.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/registry.d.ts +58 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/registry.js +129 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime.d.ts +36 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +50 -0
- package/dist/runtime.js.map +1 -0
- package/dist/time.d.ts +29 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +43 -0
- package/dist/time.js.map +1 -0
- package/dist/tool.d.ts +48 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +86 -0
- package/dist/tool.js.map +1 -0
- package/dist/uplink.d.ts +27 -0
- package/dist/uplink.d.ts.map +1 -0
- package/dist/uplink.js +36 -0
- package/dist/uplink.js.map +1 -0
- package/package.json +38 -0
- package/src/approval.ts +38 -0
- package/src/capabilities.ts +22 -0
- package/src/capsule.ts +90 -0
- package/src/contracts.ts +1189 -0
- package/src/elicit.ts +136 -0
- package/src/env.ts +31 -0
- package/src/errors.ts +122 -0
- package/src/fs.ts +357 -0
- package/src/http.ts +345 -0
- package/src/identity.ts +101 -0
- package/src/index.ts +83 -0
- package/src/interceptors.ts +25 -0
- package/src/ipc.ts +354 -0
- package/src/kv.ts +123 -0
- package/src/log.ts +43 -0
- package/src/net.ts +545 -0
- package/src/process.ts +205 -0
- package/src/runtime/bridge.ts +374 -0
- package/src/runtime/index.ts +11 -0
- package/src/runtime/registry.ts +178 -0
- package/src/runtime.ts +70 -0
- package/src/time.ts +48 -0
- package/src/tool.ts +125 -0
- package/src/uplink.ts +49 -0
- package/src/wit-imports.d.ts +689 -0
- package/wit-contracts/astrid-contracts.wit +1266 -0
package/src/net.ts
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Networking — Unix domain sockets, outbound TCP, inbound TCP listeners,
|
|
3
|
+
* UDP, and DNS. Mirrors `astrid_sdk::net`. The kernel pre-binds a single
|
|
4
|
+
* Unix-domain listener per capsule; outbound TCP and UDP are gated by
|
|
5
|
+
* `net_connect` / `net_udp` allowlists and run through the SSRF airlock.
|
|
6
|
+
*
|
|
7
|
+
* Resource handles ({@link UnixListener}, {@link TcpListener}, {@link TcpStream},
|
|
8
|
+
* {@link UdpSocket}) are Component Model resources with automatic Drop. Use
|
|
9
|
+
* `using` for scope-bound cleanup or call `.close()` explicitly.
|
|
10
|
+
*
|
|
11
|
+
* Per-capsule caps: 8 concurrent TCP streams, 4 UDP sockets, 4 TCP listeners.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
bindUnix as hostBindUnix,
|
|
16
|
+
bindTcp as hostBindTcp,
|
|
17
|
+
connectTcp as hostConnectTcp,
|
|
18
|
+
udpBind as hostUdpBind,
|
|
19
|
+
lookupHost as hostLookupHost,
|
|
20
|
+
type NetReadStatus,
|
|
21
|
+
type ShutdownHow,
|
|
22
|
+
type UdpDatagram,
|
|
23
|
+
type UnixListener as WitUnixListener,
|
|
24
|
+
type TcpListener as WitTcpListener,
|
|
25
|
+
type TcpStream as WitTcpStream,
|
|
26
|
+
type UdpSocket as WitUdpSocket,
|
|
27
|
+
} from "astrid:net/host@1.0.0";
|
|
28
|
+
import { SysError, callHost } from "./errors.js";
|
|
29
|
+
import { sleepMs as hostSleepMs } from "./time.js";
|
|
30
|
+
|
|
31
|
+
export type { ShutdownHow, NetReadStatus, UdpDatagram } from "astrid:net/host@1.0.0";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// mpsc-shaped errors (preserved from pre-migration API)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export class RecvError extends Error {
|
|
38
|
+
override readonly name = "RecvError";
|
|
39
|
+
readonly code = "EPIPE" as const;
|
|
40
|
+
constructor() {
|
|
41
|
+
super("stream closed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type TryRecvError =
|
|
46
|
+
| { kind: "empty"; message: string; code: "EAGAIN" }
|
|
47
|
+
| { kind: "closed"; message: string; code: "EPIPE" };
|
|
48
|
+
|
|
49
|
+
export class SendError extends Error {
|
|
50
|
+
override readonly name = "SendError";
|
|
51
|
+
readonly code = "EPIPE" as const;
|
|
52
|
+
constructor() {
|
|
53
|
+
super("stream closed");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Internal helpers
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function toHostTimeout(timeoutMs: number | undefined): bigint | undefined {
|
|
62
|
+
if (timeoutMs === undefined) return undefined;
|
|
63
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
64
|
+
throw SysError.api(
|
|
65
|
+
`timeout must be a positive integer (got ${timeoutMs}); use undefined to clear`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return BigInt(Math.floor(timeoutMs));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Listener handles
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/** The capsule's pre-bound Unix domain listener. Activated by {@link bindUnix}. */
|
|
76
|
+
export class UnixListener {
|
|
77
|
+
#inner: WitUnixListener | undefined;
|
|
78
|
+
|
|
79
|
+
constructor(inner: WitUnixListener) {
|
|
80
|
+
this.#inner = inner;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Blocking accept. Performs peer-credential verification + session token handshake. */
|
|
84
|
+
accept(): TcpStream {
|
|
85
|
+
const inner = callHost("net.UnixListener.accept", () =>
|
|
86
|
+
this.#requireInner().accept(),
|
|
87
|
+
);
|
|
88
|
+
return new TcpStream(inner);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Polling accept with caller-controlled timeout. Returns `undefined` if none arrived. */
|
|
92
|
+
pollAccept(timeoutMs: number): TcpStream | undefined {
|
|
93
|
+
const ms = toHostTimeout(timeoutMs) ?? 0n;
|
|
94
|
+
const inner = callHost(`net.UnixListener.pollAccept(${timeoutMs}ms)`, () =>
|
|
95
|
+
this.#requireInner().pollAccept(ms),
|
|
96
|
+
);
|
|
97
|
+
return inner === undefined ? undefined : new TcpStream(inner);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
close(): void {
|
|
101
|
+
if (this.#inner === undefined) return;
|
|
102
|
+
const inner = this.#inner;
|
|
103
|
+
this.#inner = undefined;
|
|
104
|
+
try {
|
|
105
|
+
inner[Symbol.dispose]();
|
|
106
|
+
} catch {
|
|
107
|
+
// already released
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[Symbol.dispose](): void {
|
|
112
|
+
this.close();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#requireInner(): WitUnixListener {
|
|
116
|
+
if (this.#inner === undefined) throw SysError.api("UnixListener is closed");
|
|
117
|
+
return this.#inner;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A bound TCP listener accepting inbound network connections. Per-capsule cap: 4. */
|
|
122
|
+
export class TcpListener {
|
|
123
|
+
#inner: WitTcpListener | undefined;
|
|
124
|
+
|
|
125
|
+
constructor(inner: WitTcpListener) {
|
|
126
|
+
this.#inner = inner;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Blocking accept. */
|
|
130
|
+
accept(): TcpStream {
|
|
131
|
+
const inner = callHost("net.TcpListener.accept", () => this.#requireInner().accept());
|
|
132
|
+
return new TcpStream(inner);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Polling accept with caller-controlled timeout. */
|
|
136
|
+
pollAccept(timeoutMs: number): TcpStream | undefined {
|
|
137
|
+
const ms = toHostTimeout(timeoutMs) ?? 0n;
|
|
138
|
+
const inner = callHost(`net.TcpListener.pollAccept(${timeoutMs}ms)`, () =>
|
|
139
|
+
this.#requireInner().pollAccept(ms),
|
|
140
|
+
);
|
|
141
|
+
return inner === undefined ? undefined : new TcpStream(inner);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
localAddr(): string {
|
|
145
|
+
return callHost("net.TcpListener.localAddr", () => this.#requireInner().localAddr());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
close(): void {
|
|
149
|
+
if (this.#inner === undefined) return;
|
|
150
|
+
const inner = this.#inner;
|
|
151
|
+
this.#inner = undefined;
|
|
152
|
+
try {
|
|
153
|
+
inner[Symbol.dispose]();
|
|
154
|
+
} catch {
|
|
155
|
+
// already released
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
[Symbol.dispose](): void {
|
|
160
|
+
this.close();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#requireInner(): WitTcpListener {
|
|
164
|
+
if (this.#inner === undefined) throw SysError.api("TcpListener is closed");
|
|
165
|
+
return this.#inner;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// TCP stream — bidirectional resource used for both Unix-domain and TCP
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
export class TcpStream {
|
|
174
|
+
#inner: WitTcpStream | undefined;
|
|
175
|
+
|
|
176
|
+
constructor(inner: WitTcpStream) {
|
|
177
|
+
this.#inner = inner;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- Length-prefixed framed I/O (uplink-proxy use case) -----------------
|
|
181
|
+
|
|
182
|
+
/** Non-blocking framed read. Returns Data/Closed/Pending. Max frame: 10 MB. */
|
|
183
|
+
tryRecv(): Uint8Array | { kind: "empty" } {
|
|
184
|
+
const status: NetReadStatus = callHost("net.TcpStream.tryRecv", () =>
|
|
185
|
+
this.#requireInner().read(),
|
|
186
|
+
);
|
|
187
|
+
switch (status.tag) {
|
|
188
|
+
case "data":
|
|
189
|
+
return status.val;
|
|
190
|
+
case "pending":
|
|
191
|
+
return { kind: "empty" };
|
|
192
|
+
case "closed":
|
|
193
|
+
this.close();
|
|
194
|
+
throw new RecvError();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Blocking framed receive. Spins on `tryRecv` with a 50 ms sleep between
|
|
200
|
+
* polls; StarlingMonkey's syncify makes this a real blocking call from the
|
|
201
|
+
* host's POV. Mirrors the Rust SDK polling-loop shape.
|
|
202
|
+
*/
|
|
203
|
+
recv(): Uint8Array {
|
|
204
|
+
while (true) {
|
|
205
|
+
const result = this.tryRecv();
|
|
206
|
+
if (result instanceof Uint8Array) return result;
|
|
207
|
+
sleepMs(50);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Write a length-prefixed frame. */
|
|
212
|
+
send(data: Uint8Array): void {
|
|
213
|
+
try {
|
|
214
|
+
callHost("net.TcpStream.send", () => this.#requireInner().write(data));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
// Map host errors on send to SendError to mirror Rust SDK.
|
|
217
|
+
if (err instanceof SysError) throw new SendError();
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- Byte-stream I/O ----------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Read up to `maxBytes` without length-prefix framing. Empty array = EOF.
|
|
226
|
+
* Honours any timeout set via {@link setReadTimeout}.
|
|
227
|
+
*/
|
|
228
|
+
readBytes(maxBytes: number): Uint8Array {
|
|
229
|
+
return callHost(`net.TcpStream.readBytes(${maxBytes})`, () =>
|
|
230
|
+
this.#requireInner().readBytes(maxBytes),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Write `data` without framing. Returns bytes written (may be less than `data.length`). */
|
|
235
|
+
writeBytes(data: Uint8Array): number {
|
|
236
|
+
return callHost("net.TcpStream.writeBytes", () =>
|
|
237
|
+
this.#requireInner().writeBytes(data),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Peek up to `maxBytes` without consuming them. */
|
|
242
|
+
peek(maxBytes: number): Uint8Array {
|
|
243
|
+
return callHost(`net.TcpStream.peek(${maxBytes})`, () =>
|
|
244
|
+
this.#requireInner().peek(maxBytes),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Half-close the read side, write side, or both. */
|
|
249
|
+
shutdown(how: ShutdownHow): void {
|
|
250
|
+
callHost(`net.TcpStream.shutdown(${how})`, () => this.#requireInner().shutdown(how));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---- Address accessors --------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/** Remote peer address as `"ip:port"`. Returns `not-tcp` for Unix-domain streams. */
|
|
256
|
+
peerAddr(): string {
|
|
257
|
+
return callHost("net.TcpStream.peerAddr", () => this.#requireInner().peerAddr());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Local socket address as `"ip:port"`. */
|
|
261
|
+
localAddr(): string {
|
|
262
|
+
return callHost("net.TcpStream.localAddr", () => this.#requireInner().localAddr());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- TCP socket options -------------------------------------------------
|
|
266
|
+
|
|
267
|
+
setNodelay(nodelay: boolean): void {
|
|
268
|
+
callHost(`net.TcpStream.setNodelay(${nodelay})`, () =>
|
|
269
|
+
this.#requireInner().setNodelay(nodelay),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
nodelay(): boolean {
|
|
274
|
+
return callHost("net.TcpStream.nodelay", () => this.#requireInner().nodelay());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
setReadTimeout(timeoutMs: number | undefined): void {
|
|
278
|
+
const ms = toHostTimeout(timeoutMs);
|
|
279
|
+
callHost(`net.TcpStream.setReadTimeout(${timeoutMs})`, () =>
|
|
280
|
+
this.#requireInner().setReadTimeout(ms),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
readTimeout(): number | undefined {
|
|
285
|
+
const v = callHost("net.TcpStream.readTimeout", () => this.#requireInner().readTimeout());
|
|
286
|
+
return v === undefined ? undefined : Number(v);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
setWriteTimeout(timeoutMs: number | undefined): void {
|
|
290
|
+
const ms = toHostTimeout(timeoutMs);
|
|
291
|
+
callHost(`net.TcpStream.setWriteTimeout(${timeoutMs})`, () =>
|
|
292
|
+
this.#requireInner().setWriteTimeout(ms),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
writeTimeout(): number | undefined {
|
|
297
|
+
const v = callHost("net.TcpStream.writeTimeout", () => this.#requireInner().writeTimeout());
|
|
298
|
+
return v === undefined ? undefined : Number(v);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** IPv6 hop limit / IPv4 TTL. */
|
|
302
|
+
setHopLimit(hops: number): void {
|
|
303
|
+
callHost(`net.TcpStream.setHopLimit(${hops})`, () =>
|
|
304
|
+
this.#requireInner().setHopLimit(hops),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
hopLimit(): number {
|
|
309
|
+
return callHost("net.TcpStream.hopLimit", () => this.#requireInner().hopLimit());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** TCP keepalive probe interval in seconds (`undefined` disables). */
|
|
313
|
+
setKeepalive(keepaliveSecs: number | undefined): void {
|
|
314
|
+
const v = keepaliveSecs === undefined ? undefined : BigInt(Math.max(0, Math.floor(keepaliveSecs)));
|
|
315
|
+
callHost(`net.TcpStream.setKeepalive(${keepaliveSecs})`, () =>
|
|
316
|
+
this.#requireInner().setKeepalive(v),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
keepalive(): number | undefined {
|
|
321
|
+
const v = callHost("net.TcpStream.keepalive", () => this.#requireInner().keepalive());
|
|
322
|
+
return v === undefined ? undefined : Number(v);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** SO_LINGER. `undefined` = default; `0` = immediate RST; otherwise drain time in ms. */
|
|
326
|
+
setLinger(lingerMs: number | undefined): void {
|
|
327
|
+
const v = lingerMs === undefined ? undefined : BigInt(Math.max(0, Math.floor(lingerMs)));
|
|
328
|
+
callHost(`net.TcpStream.setLinger(${lingerMs})`, () =>
|
|
329
|
+
this.#requireInner().setLinger(v),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
linger(): number | undefined {
|
|
334
|
+
const v = callHost("net.TcpStream.linger", () => this.#requireInner().linger());
|
|
335
|
+
return v === undefined ? undefined : Number(v);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
setReuseaddr(reuse: boolean): void {
|
|
339
|
+
callHost(`net.TcpStream.setReuseaddr(${reuse})`, () =>
|
|
340
|
+
this.#requireInner().setReuseaddr(reuse),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
reuseaddr(): boolean {
|
|
345
|
+
return callHost("net.TcpStream.reuseaddr", () => this.#requireInner().reuseaddr());
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---- Lifecycle ----------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
close(): void {
|
|
351
|
+
if (this.#inner === undefined) return;
|
|
352
|
+
const inner = this.#inner;
|
|
353
|
+
this.#inner = undefined;
|
|
354
|
+
try {
|
|
355
|
+
inner[Symbol.dispose]();
|
|
356
|
+
} catch {
|
|
357
|
+
// already released
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
[Symbol.dispose](): void {
|
|
362
|
+
this.close();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Async iterator yielding each frame until the peer closes. */
|
|
366
|
+
async *[Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array> {
|
|
367
|
+
try {
|
|
368
|
+
while (this.#inner !== undefined) {
|
|
369
|
+
try {
|
|
370
|
+
yield this.recv();
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err instanceof RecvError) return;
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
this.close();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#requireInner(): WitTcpStream {
|
|
382
|
+
if (this.#inner === undefined) throw SysError.api("TcpStream is closed");
|
|
383
|
+
return this.#inner;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// UDP socket
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* UDP datagram socket. Two modes: unconnected (`sendTo`/`recvFrom`) and
|
|
393
|
+
* connected (`connect` + `send`/`recv`). Per-capsule cap: 4.
|
|
394
|
+
*/
|
|
395
|
+
export class UdpSocket {
|
|
396
|
+
#inner: WitUdpSocket | undefined;
|
|
397
|
+
|
|
398
|
+
constructor(inner: WitUdpSocket) {
|
|
399
|
+
this.#inner = inner;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
sendTo(data: Uint8Array, peerHost: string, peerPort: number): number {
|
|
403
|
+
return callHost(`net.UdpSocket.sendTo(${peerHost}:${peerPort})`, () =>
|
|
404
|
+
this.#requireInner().sendTo(data, peerHost, peerPort),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
recvFrom(maxBytes: number): UdpDatagram | undefined {
|
|
409
|
+
return callHost(`net.UdpSocket.recvFrom(${maxBytes})`, () =>
|
|
410
|
+
this.#requireInner().recvFrom(maxBytes),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
connect(peerHost: string, peerPort: number): void {
|
|
415
|
+
callHost(`net.UdpSocket.connect(${peerHost}:${peerPort})`, () =>
|
|
416
|
+
this.#requireInner().connect(peerHost, peerPort),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
disconnect(): void {
|
|
421
|
+
callHost("net.UdpSocket.disconnect", () => this.#requireInner().disconnect());
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
send(data: Uint8Array): number {
|
|
425
|
+
return callHost("net.UdpSocket.send", () => this.#requireInner().send(data));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
recv(maxBytes: number): Uint8Array | undefined {
|
|
429
|
+
return callHost(`net.UdpSocket.recv(${maxBytes})`, () =>
|
|
430
|
+
this.#requireInner().recv(maxBytes),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
peerAddr(): string | undefined {
|
|
435
|
+
return callHost("net.UdpSocket.peerAddr", () => this.#requireInner().peerAddr());
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
setReadTimeout(timeoutMs: number | undefined): void {
|
|
439
|
+
const ms = toHostTimeout(timeoutMs);
|
|
440
|
+
callHost(`net.UdpSocket.setReadTimeout(${timeoutMs})`, () =>
|
|
441
|
+
this.#requireInner().setReadTimeout(ms),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
localAddr(): string {
|
|
446
|
+
return callHost("net.UdpSocket.localAddr", () => this.#requireInner().localAddr());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
close(): void {
|
|
450
|
+
if (this.#inner === undefined) return;
|
|
451
|
+
const inner = this.#inner;
|
|
452
|
+
this.#inner = undefined;
|
|
453
|
+
try {
|
|
454
|
+
inner[Symbol.dispose]();
|
|
455
|
+
} catch {
|
|
456
|
+
// already released
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
[Symbol.dispose](): void {
|
|
461
|
+
this.close();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
#requireInner(): WitUdpSocket {
|
|
465
|
+
if (this.#inner === undefined) throw SysError.api("UdpSocket is closed");
|
|
466
|
+
return this.#inner;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Pre-migration handle types preserved for source-compatibility — these are
|
|
472
|
+
// now just type aliases over the resource-backed classes above. Callers using
|
|
473
|
+
// the legacy names (`StreamHandle`, `ListenerHandle`) continue to compile.
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
export { TcpStream as StreamHandle, UnixListener as ListenerHandle };
|
|
477
|
+
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Factory functions
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
/** Bind the kernel-pre-provisioned Unix socket and return a listener. */
|
|
483
|
+
export function bindUnix(): UnixListener {
|
|
484
|
+
const inner = callHost("net.bindUnix", () => hostBindUnix());
|
|
485
|
+
return new UnixListener(inner);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Bind a TCP listener for inbound connections. Gated by `net_tcp_bind`. Port
|
|
490
|
+
* 0 selects an ephemeral port.
|
|
491
|
+
*/
|
|
492
|
+
export function bindTcp(host: string, port: number): TcpListener {
|
|
493
|
+
const inner = callHost(`net.bindTcp(${host}:${port})`, () => hostBindTcp(host, port));
|
|
494
|
+
return new TcpListener(inner);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Block until the next incoming connection on the Unix listener. */
|
|
498
|
+
export function accept(listener: UnixListener): TcpStream {
|
|
499
|
+
return listener.accept();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Non-blocking accept. Returns `undefined` if none ready. */
|
|
503
|
+
export function tryAccept(listener: UnixListener): TcpStream | undefined {
|
|
504
|
+
return listener.pollAccept(0);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Open an outbound TCP connection to `host:port`. Goes through the SSRF
|
|
509
|
+
* airlock (rejects private/loopback/etc.) and the `net_connect` allowlist.
|
|
510
|
+
*/
|
|
511
|
+
export function connect(host: string, port: number): TcpStream {
|
|
512
|
+
const inner = callHost(`net.connect(${host}:${port})`, () => hostConnectTcp(host, port));
|
|
513
|
+
return new TcpStream(inner);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Alias for {@link connect} matching the WIT name. */
|
|
517
|
+
export const connectTcp = connect;
|
|
518
|
+
|
|
519
|
+
/** Bind a UDP socket. */
|
|
520
|
+
export function udpBind(host: string, port: number): UdpSocket {
|
|
521
|
+
const inner = callHost(`net.udpBind(${host}:${port})`, () => hostUdpBind(host, port));
|
|
522
|
+
return new UdpSocket(inner);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Resolve a hostname to a list of `"ip:port"` (or `"ip"` if no port in input)
|
|
527
|
+
* strings. SSRF airlock applies — private/loopback/etc. ranges stripped.
|
|
528
|
+
*/
|
|
529
|
+
export function lookupHost(host: string): string[] {
|
|
530
|
+
return callHost(`net.lookupHost(${host})`, () => hostLookupHost(host));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
// Sleep shim — used by the polling recv loop
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 50 ms host-mediated sleep. Routes through `astrid:sys/host.sleep-ns`
|
|
539
|
+
* so the kernel can cancel the wait when the capsule unloads and
|
|
540
|
+
* account for the wait in audit. Mirrors the Rust SDK switch from
|
|
541
|
+
* `std::thread::sleep` to `crate::time::sleep`.
|
|
542
|
+
*/
|
|
543
|
+
function sleepMs(ms: number): void {
|
|
544
|
+
hostSleepMs(ms);
|
|
545
|
+
}
|