@vibecook/truffle 0.4.7 → 0.4.9

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.
@@ -1,4 +1,44 @@
1
1
  import { NapiNode, type NapiPeerEvent } from '@vibecook/truffle-native';
2
+ import { type TruffleNet } from './net.js';
3
+ import { type TruffleHttp } from './http.js';
4
+ import { type TruffleQuic } from './quic.js';
5
+ import { type TruffleDgram } from './dgram.js';
6
+ import { type TruffleWs } from './ws.js';
7
+ /**
8
+ * A started mesh node: the full native `NapiNode` API plus protocol
9
+ * namespaces (RFC 021). Namespaces are attached to the same underlying
10
+ * instance, so every `NapiNode` method is available directly and new
11
+ * native methods surface automatically.
12
+ */
13
+ export type MeshNode = NapiNode & {
14
+ /** node:net-shaped raw TCP API over the mesh (RFC 021). */
15
+ net: TruffleNet;
16
+ /**
17
+ * node:http interop over the mesh (RFC 021): a MeshAgent for outbound
18
+ * requests to peers plus `request`/`get`/`fetchText` sugar. Inbound HTTP
19
+ * is served by feeding `mesh.net` connections into an `http.Server` via
20
+ * `httpServer.emit('connection', socket)`.
21
+ */
22
+ http: TruffleHttp;
23
+ /**
24
+ * QUIC over the mesh (RFC 021): multiplexed bidirectional byte streams
25
+ * with no head-of-line blocking, async-iterable connections/streams.
26
+ */
27
+ quic: TruffleQuic;
28
+ /**
29
+ * node:dgram-shaped raw UDP API over the mesh (RFC 021): datagram sockets
30
+ * with `'message'` events and preserved datagram boundaries.
31
+ */
32
+ dgram: TruffleDgram;
33
+ /**
34
+ * WebSocket over the mesh (RFC 021): thin wrappers over the `ws` package —
35
+ * `ws.connect(peer, port)` for a client, `ws.createServer({ port })` for a
36
+ * server. Requires the optional `ws` peer dependency to be installed.
37
+ */
38
+ ws: TruffleWs;
39
+ /** The underlying native handle (escape hatch; the same object). */
40
+ native: NapiNode;
41
+ };
2
42
  /**
3
43
  * Options for {@link createMeshNode}. RFC 017 shape — `appId` is required,
4
44
  * `deviceName` / `deviceId` are optional overrides that the underlying
@@ -36,7 +76,7 @@ export interface CreateMeshNodeOptions {
36
76
  authKey?: string;
37
77
  /** Whether this node is ephemeral (removed from tailnet on stop). */
38
78
  ephemeral?: boolean;
39
- /** WebSocket listener port (0 = auto). */
79
+ /** WebSocket listener port. Defaults to 9417 when omitted. */
40
80
  wsPort?: number;
41
81
  /**
42
82
  * Auto-open the Tailscale auth URL in the default browser.
@@ -62,15 +102,20 @@ export interface CreateMeshNodeOptions {
62
102
  *
63
103
  * @example
64
104
  * ```ts
65
- * const node = await createMeshNode({
105
+ * const mesh = await createMeshNode({
66
106
  * appId: 'playground',
67
107
  * deviceName: 'alice-mbp',
68
108
  * onPeerChange: (event) => console.log('Peer event:', event),
69
109
  * });
70
110
  *
71
- * const peers = await node.getPeers();
72
- * await node.send(peers[0].deviceId, 'chat', Buffer.from(JSON.stringify({ text: 'hello' })));
111
+ * const peers = await mesh.getPeers();
112
+ * await mesh.send(peers[0].deviceId, 'chat', Buffer.from(JSON.stringify({ text: 'hello' })));
113
+ *
114
+ * // Raw TCP over the mesh, node:net-style (RFC 021):
115
+ * const server = mesh.net.createServer((socket) => socket.pipe(socket));
116
+ * server.listen(8080);
117
+ * const client = mesh.net.connect({ host: 'other-machine', port: 8080 });
73
118
  * ```
74
119
  */
75
- export declare function createMeshNode(options: CreateMeshNodeOptions): Promise<NapiNode>;
120
+ export declare function createMeshNode(options: CreateMeshNodeOptions): Promise<MeshNode>;
76
121
  //# sourceMappingURL=create-mesh-node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-mesh-node.d.ts","sourceRoot":"","sources":["../src/create-mesh-node.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAuB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG7F;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC/C;AAmBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuDtF"}
1
+ {"version":3,"file":"create-mesh-node.d.ts","sourceRoot":"","sources":["../src/create-mesh-node.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAuB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC7F,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAG5D;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG;IAChC,2DAA2D;IAC3D,GAAG,EAAE,UAAU,CAAC;IAChB;;;;;OAKG;IACH,IAAI,EAAE,WAAW,CAAC;IAClB;;;OAGG;IACH,IAAI,EAAE,WAAW,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;;OAIG;IACH,EAAE,EAAE,SAAS,CAAC;IACd,oEAAoE;IACpE,MAAM,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC/C;AAsBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoFtF"}
@@ -1,24 +1,29 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execFile } from 'node:child_process';
2
2
  import { NapiNode } from '@vibecook/truffle-native';
3
+ import { createNetNamespace } from './net.js';
4
+ import { createHttpNamespace } from './http.js';
5
+ import { createQuicNamespace } from './quic.js';
6
+ import { createDgramNamespace } from './dgram.js';
7
+ import { createWsNamespace } from './ws.js';
3
8
  import { resolveSidecarPath } from './sidecar.js';
4
9
  /**
5
10
  * Open a URL in the system's default browser.
6
11
  */
7
12
  function defaultOpenUrl(url) {
8
- try {
9
- if (process.platform === 'darwin') {
10
- execSync(`open "${url}"`);
11
- }
12
- else if (process.platform === 'win32') {
13
- execSync(`start "" "${url}"`);
14
- }
15
- else {
16
- execSync(`xdg-open "${url}"`);
17
- }
18
- }
19
- catch {
20
- // Silently fail
21
- }
13
+ // Only ever open http(s) URLs, and pass the URL as an argv element (never
14
+ // through a shell) so it cannot be interpreted as a command. The previous
15
+ // `execSync(`open "${url}"`)` form let shell metacharacters ($(), backticks)
16
+ // in the URL execute arbitrary commands.
17
+ if (!/^https?:\/\//i.test(url))
18
+ return;
19
+ const [cmd, args] = process.platform === 'darwin'
20
+ ? ['open', [url]]
21
+ : process.platform === 'win32'
22
+ ? ['explorer', [url]]
23
+ : ['xdg-open', [url]];
24
+ execFile(cmd, args, () => {
25
+ // Best-effort; ignore failures (no browser, headless environment, etc.).
26
+ });
22
27
  }
23
28
  /**
24
29
  * Create and start a Truffle node with sensible defaults.
@@ -29,18 +34,29 @@ function defaultOpenUrl(url) {
29
34
  *
30
35
  * @example
31
36
  * ```ts
32
- * const node = await createMeshNode({
37
+ * const mesh = await createMeshNode({
33
38
  * appId: 'playground',
34
39
  * deviceName: 'alice-mbp',
35
40
  * onPeerChange: (event) => console.log('Peer event:', event),
36
41
  * });
37
42
  *
38
- * const peers = await node.getPeers();
39
- * await node.send(peers[0].deviceId, 'chat', Buffer.from(JSON.stringify({ text: 'hello' })));
43
+ * const peers = await mesh.getPeers();
44
+ * await mesh.send(peers[0].deviceId, 'chat', Buffer.from(JSON.stringify({ text: 'hello' })));
45
+ *
46
+ * // Raw TCP over the mesh, node:net-style (RFC 021):
47
+ * const server = mesh.net.createServer((socket) => socket.pipe(socket));
48
+ * server.listen(8080);
49
+ * const client = mesh.net.connect({ host: 'other-machine', port: 8080 });
40
50
  * ```
41
51
  */
42
52
  export async function createMeshNode(options) {
43
53
  const { appId, deviceName, deviceId, autoAuth = true, openUrl: customOpenUrl, onAuthRequired, onPeerChange, sidecarPath, stateDir, authKey, ephemeral, wsPort, } = options;
54
+ // Validate appId synchronously (mirrors the Rust-side rule) so a bad value
55
+ // fails here with a clear error instead of asynchronously across the FFI.
56
+ if (!/^[a-z][a-z0-9-]{1,31}$/.test(appId)) {
57
+ throw new Error(`[truffle] Invalid appId ${JSON.stringify(appId)}: must match ^[a-z][a-z0-9-]{1,31}$ ` +
58
+ `(2–32 chars; lowercase letters, digits, hyphens; must start with a letter).`);
59
+ }
44
60
  const resolvedSidecarPath = sidecarPath ?? resolveSidecarPath();
45
61
  const node = new NapiNode();
46
62
  // IMPORTANT: install the auth-required callback BEFORE calling `start()`.
@@ -68,11 +84,32 @@ export async function createMeshNode(options) {
68
84
  ephemeral,
69
85
  wsPort,
70
86
  };
71
- await node.start(config);
87
+ try {
88
+ await node.start(config);
89
+ }
90
+ catch (err) {
91
+ // Best-effort cleanup so a failed start doesn't leak the spawned sidecar
92
+ // process. `stop()` is safe to call even if start() failed early.
93
+ try {
94
+ await node.stop();
95
+ }
96
+ catch {
97
+ // ignore — nothing to clean up, or already torn down
98
+ }
99
+ throw err;
100
+ }
72
101
  // Post-start: forward ongoing peer lifecycle events to the user callback.
73
102
  if (onPeerChange) {
74
103
  node.onPeerChange(onPeerChange);
75
104
  }
76
- return node;
105
+ // Attach the protocol namespaces (RFC 021) to the native instance.
106
+ const mesh = node;
107
+ mesh.net = createNetNamespace(node);
108
+ mesh.http = createHttpNamespace(mesh.net);
109
+ mesh.quic = createQuicNamespace(node);
110
+ mesh.dgram = createDgramNamespace(node);
111
+ mesh.ws = createWsNamespace(mesh.net);
112
+ mesh.native = node;
113
+ return mesh;
77
114
  }
78
115
  //# sourceMappingURL=create-mesh-node.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-mesh-node.js","sourceRoot":"","sources":["../src/create-mesh-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAA2C,MAAM,0BAA0B,CAAC;AAC7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAyDlD;;GAEG;AACH,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAClC,QAAQ,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC;QAC5B,CAAC;aAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACxC,QAAQ,CAAC,aAAa,GAAG,GAAG,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,aAAa,GAAG,GAAG,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,gBAAgB;IAClB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAA8B;IACjE,MAAM,EACJ,KAAK,EACL,UAAU,EACV,QAAQ,EACR,QAAQ,GAAG,IAAI,EACf,OAAO,EAAE,aAAa,EACtB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,OAAO,EACP,SAAS,EACT,MAAM,GACP,GAAG,OAAO,CAAC;IAEZ,MAAM,mBAAmB,GAAG,WAAW,IAAI,kBAAkB,EAAE,CAAC;IAEhE,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAE5B,0EAA0E;IAC1E,wEAAwE;IACxE,iEAAiE;IACjE,wEAAwE;IACxE,gEAAgE;IAChE,EAAE;IACF,0EAA0E;IAC1E,4DAA4D;IAC5D,IAAI,CAAC,cAAc,CAAC,CAAC,GAAW,EAAE,EAAE;QAClC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,aAAa,IAAI,cAAc,CAAC;YAC/C,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC;QACD,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAmB;QAC7B,KAAK;QACL,UAAU;QACV,QAAQ;QACR,WAAW,EAAE,mBAAmB;QAChC,QAAQ;QACR,OAAO;QACP,SAAS;QACT,MAAM;KACP,CAAC;IAEF,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEzB,0EAA0E;IAC1E,IAAI,YAAY,EAAE,CAAC;QACjB,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"create-mesh-node.js","sourceRoot":"","sources":["../src/create-mesh-node.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { NapiNode } from '@vibecook/truffle-native';
3
+ /**
4
+ * Sender metadata delivered with each `'message'` event, mirroring
5
+ * node:dgram's `RemoteInfo` and extending it with mesh peer identity.
6
+ *
7
+ * `address`/`port` are the datagram's WireGuard-authenticated tailnet
8
+ * source (`100.x` IPv4). `peerId`/`peerName` are best-effort enrichments
9
+ * resolved from the local netmap — they may be `undefined` for a sender
10
+ * that isn't a known peer yet, and message delivery never blocks on them.
11
+ */
12
+ export interface TruffleRemoteInfo {
13
+ /** Sender's tailnet IP (`100.x.x.x`). */
14
+ address: string;
15
+ /** Sender's source port. */
16
+ port: number;
17
+ /** Always `'IPv4'`: datagram addressing is IPv4-only in v1 (RFC 021 §3). */
18
+ family: 'IPv4';
19
+ /** Datagram payload length in bytes. */
20
+ size: number;
21
+ /** Sender's stable device id, when the source IP maps to a known peer. */
22
+ peerId?: string;
23
+ /** Sender's human-readable device name, when known. */
24
+ peerName?: string;
25
+ }
26
+ /**
27
+ * A UDP socket over the mesh, mimicking `dgram.Socket`.
28
+ *
29
+ * Events:
30
+ * - `'listening'` — after {@link bind} resolves; `this.port` is set.
31
+ * - `'message'` — `(msg: Buffer, rinfo: TruffleRemoteInfo)` per datagram.
32
+ * - `'error'` — a terminal receive error, or a {@link send} failure with no
33
+ * callback (node:dgram contract).
34
+ * - `'close'` — the socket has shut down (via {@link close} or a closed
35
+ * relay); fires exactly once.
36
+ *
37
+ * ```ts
38
+ * const sock = mesh.dgram.createSocket();
39
+ * sock.on('message', (msg, rinfo) => {
40
+ * console.log(`${rinfo.peerName ?? rinfo.address}: ${msg}`);
41
+ * });
42
+ * await sock.bind(8081);
43
+ * sock.send(Buffer.from('ping'), 8081, 'other-machine');
44
+ * ```
45
+ */
46
+ export declare class TruffleDgramSocket extends EventEmitter {
47
+ #private;
48
+ /**
49
+ * The mesh port this socket is bound to; `0` until {@link bind} resolves.
50
+ * Note it reflects the *requested* tsnet port — an ephemeral client
51
+ * socket bound with port `0` keeps `0` here (its real relay port isn't
52
+ * surfaced), so read `port` only after `'listening'` and only when you
53
+ * bound an explicit port.
54
+ */
55
+ port: number;
56
+ constructor(node: NapiNode);
57
+ /**
58
+ * Bind the socket to `port` (default `0` = ephemeral relay port) and start
59
+ * receiving. Resolves with the socket once bound; `'listening'` fires at
60
+ * the same point, and `callback` (if given) is invoked as a one-shot
61
+ * `'listening'` listener — so both `await sock.bind(port)` and
62
+ * `sock.bind(port, () => …)` work.
63
+ *
64
+ * Deviates from node:dgram, which returns the socket synchronously and
65
+ * signals readiness only via the event; here bind is genuinely async
66
+ * (it round-trips to the sidecar), so the promise is the primary channel.
67
+ * Failure semantics follow the calling style: promise style (`await
68
+ * sock.bind(p)`) **rejects**; callback style (`sock.bind(p, cb)`,
69
+ * typically fire-and-forget) emits **`'error'`** like node:dgram and
70
+ * never leaves an unhandled rejection.
71
+ */
72
+ bind(port?: number, callback?: () => void): Promise<this>;
73
+ /**
74
+ * Send a datagram to `address:port`. `address` accepts any peer reference
75
+ * (device id or ≥4-char prefix, device name, Tailscale hostname, or `100.x`
76
+ * IP), matching node:dgram's `(msg, port, address, callback?)` arg order.
77
+ *
78
+ * Must be called after {@link bind}: unlike node:dgram this does **not**
79
+ * auto-bind (that would silently pick an ephemeral port), so sending before
80
+ * bind throws synchronously. Async send failures go to `callback` if given,
81
+ * otherwise the `'error'` event (node:dgram contract).
82
+ */
83
+ send(msg: Buffer | string | Uint8Array, port: number, address: string, callback?: (error: Error | null) => void): void;
84
+ /**
85
+ * Close the socket. In-flight `recv()` resolves `null`, ending the receive
86
+ * pump and emitting `'close'` once. Idempotent; `callback` (if given) runs
87
+ * on `'close'`, even if the socket has already closed.
88
+ */
89
+ close(callback?: () => void): void;
90
+ /**
91
+ * node:dgram `address()` compat. Mesh sockets have no local IP, so
92
+ * `address` is empty; `port` is the bound port (see the {@link port} note).
93
+ */
94
+ address(): {
95
+ address: string;
96
+ family: 'IPv4';
97
+ port: number;
98
+ };
99
+ }
100
+ /** node:dgram-shaped namespace bound to a mesh node. */
101
+ export interface TruffleDgram {
102
+ /** Create an unbound UDP socket, like `dgram.createSocket('udp4')`. */
103
+ createSocket(): TruffleDgramSocket;
104
+ }
105
+ export declare function createDgramNamespace(node: NapiNode): TruffleDgram;
106
+ //# sourceMappingURL=dgram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dgram.d.ts","sourceRoot":"","sources":["../src/dgram.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAgB,QAAQ,EAAiB,MAAM,0BAA0B,CAAC;AAUtF;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;;IAYlD;;;;;;OAMG;IACH,IAAI,SAAK;gBAEG,IAAI,EAAE,QAAQ;IAK1B;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,IAAI,SAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCpD;;;;;;;;;OASG;IACH,IAAI,CACF,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,EACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GACvC,IAAI;IAaP;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IAkBlC;;;OAGG;IACH,OAAO,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAwE7D;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,YAAY,IAAI,kBAAkB,CAAC;CACpC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,YAAY,CAIjE"}
package/dist/dgram.js ADDED
@@ -0,0 +1,248 @@
1
+ // node:dgram-shaped API over truffle's raw UDP transport (RFC 021 Phase 3).
2
+ //
3
+ // TruffleDgramSocket mirrors `dgram.Socket` closely enough that existing
4
+ // node:dgram code ports by swapping the module: `createSocket()`, `bind()`,
5
+ // `send()`, `close()`, and the `'listening'` / `'message'` / `'error'` /
6
+ // `'close'` events all behave as a Node developer expects.
7
+ //
8
+ // Two deliberate deviations from node:dgram, both documented at their call
9
+ // sites: (1) `bind()` returns a Promise<this> (node:dgram returns the socket
10
+ // synchronously and signals readiness only via `'listening'`); the event
11
+ // still fires, so callback-style code keeps working. (2) `send()` before
12
+ // `bind()` throws instead of auto-binding — auto-bind would silently pick an
13
+ // ephemeral relay port, and explicitness beats a surprise here.
14
+ //
15
+ // Datagram semantics are preserved by the receive pump: it awaits one
16
+ // `recv()` at a time, so a slow `'message'` handler backpressures the read
17
+ // loop and the relay/OS buffer drops the overflow — correct UDP behaviour
18
+ // with bounded memory, never an unbounded in-process queue.
19
+ import { EventEmitter } from 'node:events';
20
+ /**
21
+ * Minimum spacing between `getPeers()` calls made to enrich datagram
22
+ * `rinfo` with peer identity. A cache miss from an unknown sender triggers
23
+ * at most one refresh per window, so a burst of datagrams from unmapped IPs
24
+ * can't turn into a `getPeers()` storm.
25
+ */
26
+ const PEER_CACHE_TTL_MS = 5_000;
27
+ /**
28
+ * A UDP socket over the mesh, mimicking `dgram.Socket`.
29
+ *
30
+ * Events:
31
+ * - `'listening'` — after {@link bind} resolves; `this.port` is set.
32
+ * - `'message'` — `(msg: Buffer, rinfo: TruffleRemoteInfo)` per datagram.
33
+ * - `'error'` — a terminal receive error, or a {@link send} failure with no
34
+ * callback (node:dgram contract).
35
+ * - `'close'` — the socket has shut down (via {@link close} or a closed
36
+ * relay); fires exactly once.
37
+ *
38
+ * ```ts
39
+ * const sock = mesh.dgram.createSocket();
40
+ * sock.on('message', (msg, rinfo) => {
41
+ * console.log(`${rinfo.peerName ?? rinfo.address}: ${msg}`);
42
+ * });
43
+ * await sock.bind(8081);
44
+ * sock.send(Buffer.from('ping'), 8081, 'other-machine');
45
+ * ```
46
+ */
47
+ export class TruffleDgramSocket extends EventEmitter {
48
+ #node;
49
+ #native = null;
50
+ #binding = false;
51
+ #closed = false;
52
+ #closeEmitted = false;
53
+ // Lazy ip→peer map for rinfo enrichment, rebuilt wholesale on refresh so
54
+ // departed peers don't linger. Refreshes are rate-limited (see below).
55
+ #peerCache = new Map();
56
+ #peerCacheRefreshedAt = 0;
57
+ /**
58
+ * The mesh port this socket is bound to; `0` until {@link bind} resolves.
59
+ * Note it reflects the *requested* tsnet port — an ephemeral client
60
+ * socket bound with port `0` keeps `0` here (its real relay port isn't
61
+ * surfaced), so read `port` only after `'listening'` and only when you
62
+ * bound an explicit port.
63
+ */
64
+ port = 0;
65
+ constructor(node) {
66
+ super();
67
+ this.#node = node;
68
+ }
69
+ /**
70
+ * Bind the socket to `port` (default `0` = ephemeral relay port) and start
71
+ * receiving. Resolves with the socket once bound; `'listening'` fires at
72
+ * the same point, and `callback` (if given) is invoked as a one-shot
73
+ * `'listening'` listener — so both `await sock.bind(port)` and
74
+ * `sock.bind(port, () => …)` work.
75
+ *
76
+ * Deviates from node:dgram, which returns the socket synchronously and
77
+ * signals readiness only via the event; here bind is genuinely async
78
+ * (it round-trips to the sidecar), so the promise is the primary channel.
79
+ * Failure semantics follow the calling style: promise style (`await
80
+ * sock.bind(p)`) **rejects**; callback style (`sock.bind(p, cb)`,
81
+ * typically fire-and-forget) emits **`'error'`** like node:dgram and
82
+ * never leaves an unhandled rejection.
83
+ */
84
+ bind(port = 0, callback) {
85
+ const attempt = this.#bind(port);
86
+ if (!callback)
87
+ return attempt;
88
+ this.once('listening', callback);
89
+ return attempt.catch((err) => {
90
+ this.removeListener('listening', callback);
91
+ this.emit('error', err);
92
+ return this;
93
+ });
94
+ }
95
+ async #bind(port) {
96
+ if (this.#closed)
97
+ throw new Error('dgram: socket is closed');
98
+ if (this.#native || this.#binding)
99
+ throw new Error('dgram: bind() already called');
100
+ this.#binding = true;
101
+ let native;
102
+ try {
103
+ native = await this.#node.bindUdp(port);
104
+ }
105
+ finally {
106
+ this.#binding = false;
107
+ }
108
+ // close() may have raced in while bindUdp was in flight; honour it and
109
+ // don't start the pump or announce 'listening'.
110
+ if (this.#closed) {
111
+ native.close();
112
+ return this;
113
+ }
114
+ this.#native = native;
115
+ this.port = native.port();
116
+ this.emit('listening');
117
+ void this.#recvPump(native);
118
+ return this;
119
+ }
120
+ /**
121
+ * Send a datagram to `address:port`. `address` accepts any peer reference
122
+ * (device id or ≥4-char prefix, device name, Tailscale hostname, or `100.x`
123
+ * IP), matching node:dgram's `(msg, port, address, callback?)` arg order.
124
+ *
125
+ * Must be called after {@link bind}: unlike node:dgram this does **not**
126
+ * auto-bind (that would silently pick an ephemeral port), so sending before
127
+ * bind throws synchronously. Async send failures go to `callback` if given,
128
+ * otherwise the `'error'` event (node:dgram contract).
129
+ */
130
+ send(msg, port, address, callback) {
131
+ const native = this.#native;
132
+ if (!native) {
133
+ throw new Error('dgram: call bind() before send() (auto-bind is not supported)');
134
+ }
135
+ const data = typeof msg === 'string' ? Buffer.from(msg) : Buffer.isBuffer(msg) ? msg : Buffer.from(msg);
136
+ native.send(data, address, port).then(() => callback?.(null), (err) => this.#failSend(err, callback));
137
+ }
138
+ /**
139
+ * Close the socket. In-flight `recv()` resolves `null`, ending the receive
140
+ * pump and emitting `'close'` once. Idempotent; `callback` (if given) runs
141
+ * on `'close'`, even if the socket has already closed.
142
+ */
143
+ close(callback) {
144
+ if (callback) {
145
+ if (this.#closeEmitted)
146
+ queueMicrotask(callback);
147
+ else
148
+ this.once('close', callback);
149
+ }
150
+ if (this.#closed)
151
+ return;
152
+ this.#closed = true;
153
+ const native = this.#native;
154
+ if (native) {
155
+ // recv() resolves null → the pump exits and emits 'close'.
156
+ native.close();
157
+ }
158
+ else {
159
+ // Never bound (or bind still in flight): no pump to unwind.
160
+ this.#emitClose();
161
+ }
162
+ }
163
+ /**
164
+ * node:dgram `address()` compat. Mesh sockets have no local IP, so
165
+ * `address` is empty; `port` is the bound port (see the {@link port} note).
166
+ */
167
+ address() {
168
+ return { address: '', family: 'IPv4', port: this.port };
169
+ }
170
+ // ─── internals ─────────────────────────────────────────────────────────
171
+ async #recvPump(native) {
172
+ try {
173
+ for (;;) {
174
+ const datagram = await native.recv();
175
+ if (datagram === null)
176
+ break; // socket closed
177
+ // Enrichment may await one getPeers() on a cold-cache miss (rate-
178
+ // limited); it's the only place the pump awaits beyond recv itself.
179
+ const rinfo = await this.#rinfoFor(datagram);
180
+ this.emit('message', datagram.data, rinfo);
181
+ }
182
+ }
183
+ catch (err) {
184
+ // A recv error on this socket is terminal — surface it and stop (unless
185
+ // we're already tearing down, where the null-on-close path handles it).
186
+ if (!this.#closed)
187
+ this.emit('error', err);
188
+ }
189
+ this.#emitClose();
190
+ }
191
+ async #rinfoFor(datagram) {
192
+ let peer = this.#peerCache.get(datagram.address);
193
+ if (!peer) {
194
+ await this.#maybeRefreshPeerCache();
195
+ peer = this.#peerCache.get(datagram.address);
196
+ }
197
+ return {
198
+ address: datagram.address,
199
+ port: datagram.port,
200
+ family: 'IPv4',
201
+ size: datagram.data.length,
202
+ peerId: peer?.peerId,
203
+ peerName: peer?.peerName,
204
+ };
205
+ }
206
+ /**
207
+ * Rebuild the ip→peer cache from `getPeers()`, at most once per
208
+ * {@link PEER_CACHE_TTL_MS}. Best-effort: a failed lookup leaves the prior
209
+ * cache in place and never propagates, so enrichment never blocks or breaks
210
+ * message delivery. The pump is sequential, so no in-flight guard is needed.
211
+ */
212
+ async #maybeRefreshPeerCache() {
213
+ const now = Date.now();
214
+ if (now - this.#peerCacheRefreshedAt < PEER_CACHE_TTL_MS)
215
+ return;
216
+ this.#peerCacheRefreshedAt = now; // rate-limit even if the call fails
217
+ try {
218
+ const peers = await this.#node.getPeers();
219
+ const next = new Map();
220
+ for (const peer of peers) {
221
+ if (peer.ip)
222
+ next.set(peer.ip, { peerId: peer.deviceId, peerName: peer.deviceName });
223
+ }
224
+ this.#peerCache = next;
225
+ }
226
+ catch {
227
+ // Enrichment is best-effort; keep the prior cache.
228
+ }
229
+ }
230
+ #failSend(err, callback) {
231
+ if (callback)
232
+ callback(err);
233
+ else
234
+ this.emit('error', err);
235
+ }
236
+ #emitClose() {
237
+ if (this.#closeEmitted)
238
+ return;
239
+ this.#closeEmitted = true;
240
+ this.emit('close');
241
+ }
242
+ }
243
+ export function createDgramNamespace(node) {
244
+ return {
245
+ createSocket: () => new TruffleDgramSocket(node),
246
+ };
247
+ }
248
+ //# sourceMappingURL=dgram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dgram.js","sourceRoot":"","sources":["../src/dgram.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import http from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ import type { TruffleNet } from './net.js';
4
+ /**
5
+ * An `http.Agent` that dials its connections over the mesh instead of the
6
+ * host network. Pass it as `{ agent }` to any `node:http` request and the
7
+ * request travels to the peer named by the URL hostname (or `options.host`).
8
+ *
9
+ * Connection reuse behaves exactly like the stock agent. The default
10
+ * `keepAlive: false` opens a fresh mesh connection per request and closes it
11
+ * on response end; `new MeshAgent(net, { keepAlive: true })` pools and reuses
12
+ * idle sockets like any http.Agent (but see the idle-reaping caveat on
13
+ * `TruffleSocket.setTimeout` — quiet pooled sockets aren't reaped on a timer).
14
+ */
15
+ export declare class MeshAgent extends http.Agent {
16
+ #private;
17
+ constructor(net: TruffleNet, opts?: http.AgentOptions);
18
+ /**
19
+ * Called by node:http to obtain a socket for a request. Returns a mesh
20
+ * TruffleSocket (a `stream.Duplex`) synchronously — it connects in the
21
+ * background and emits `'connect'` later, which is exactly the net.Socket
22
+ * contract http expects. The callback form is also honoured for robustness
23
+ * (node uses whichever settles first, once).
24
+ */
25
+ createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: Duplex) => void): Duplex;
26
+ }
27
+ /** node:http-shaped namespace bound to a mesh node's `net` namespace. */
28
+ export interface TruffleHttp {
29
+ /**
30
+ * Shared MeshAgent instance. Pass as `{ agent: mesh.http.agent }` to a raw
31
+ * `node:http` / `node:https`-less request to route it over the mesh.
32
+ */
33
+ agent: MeshAgent;
34
+ /** The MeshAgent class, for constructing an agent with custom options. */
35
+ Agent: typeof MeshAgent;
36
+ /**
37
+ * `http.request` with the mesh agent injected. URL form:
38
+ * `http://<peer>:<port>/path` (Tailscale IPs work as hostnames). Device
39
+ * names containing spaces aren't valid URL hostnames — use the options-
40
+ * object form, which takes any peer reference via `host`:
41
+ * `mesh.http.request({ host: 'living room pi', port: 8080, path: '/x' })`.
42
+ * A user-supplied `agent` (including `agent: false`) wins over the default.
43
+ */
44
+ request(options: http.RequestOptions | string | URL, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
45
+ request(url: string | URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
46
+ /** As {@link request}, but calls `req.end()` for you (http.get shape). */
47
+ get(options: http.RequestOptions | string | URL, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
48
+ get(url: string | URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
49
+ /** GET a path on a peer and buffer the response body as UTF-8 text. */
50
+ fetchText(peer: string, port: number, path?: string): Promise<{
51
+ status: number;
52
+ headers: http.IncomingHttpHeaders;
53
+ body: string;
54
+ }>;
55
+ }
56
+ export declare function createHttpNamespace(net: TruffleNet): TruffleHttp;
57
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAmBA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,qBAAa,SAAU,SAAQ,IAAI,CAAC,KAAK;;gBAG3B,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,YAAY;IAKrD;;;;;;OAMG;IACM,gBAAgB,CACvB,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAC/B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GACrD,MAAM;CAiBV;AAgBD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,KAAK,EAAE,SAAS,CAAC;IACjB,0EAA0E;IAC1E,KAAK,EAAE,OAAO,SAAS,CAAC;IACxB;;;;;;;OAOG;IACH,OAAO,CACL,OAAO,EAAE,IAAI,CAAC,cAAc,GAAG,MAAM,GAAG,GAAG,EAC3C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAC7C,IAAI,CAAC,aAAa,CAAC;IACtB,OAAO,CACL,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,EAAE,IAAI,CAAC,cAAc,EAC5B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAC7C,IAAI,CAAC,aAAa,CAAC;IACtB,0EAA0E;IAC1E,GAAG,CACD,OAAO,EAAE,IAAI,CAAC,cAAc,GAAG,MAAM,GAAG,GAAG,EAC3C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAC7C,IAAI,CAAC,aAAa,CAAC;IACtB,GAAG,CACD,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,EAAE,IAAI,CAAC,cAAc,EAC5B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAC7C,IAAI,CAAC,aAAa,CAAC;IACtB,uEAAuE;IACvE,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjF;AAeD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,CAoFhE"}