@vibecook/truffle 0.4.8 → 0.5.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/dist/quic.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { Duplex } from 'node:stream';
2
+ import type { NapiNode, NapiQuicConnection, NapiQuicListener, NapiQuicStream } from '@vibecook/truffle-native';
3
+ /**
4
+ * A bidirectional byte stream on a QUIC connection, as a `stream.Duplex`.
5
+ *
6
+ * Behaves like an independent TCP connection (ordered, reliable,
7
+ * flow-controlled) without head-of-line blocking against sibling streams.
8
+ * `end()` half-closes (the peer sees clean EOF); `destroy()` also stops
9
+ * the read side.
10
+ */
11
+ export declare class TruffleQuicStream extends Duplex {
12
+ #private;
13
+ constructor(native: NapiQuicStream);
14
+ _read(size: number): void;
15
+ _write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
16
+ _final(callback: (error?: Error | null) => void): void;
17
+ _destroy(error: Error | null, callback: (error: Error | null) => void): void;
18
+ }
19
+ /**
20
+ * A QUIC connection to a peer. Open streams with {@link openStream};
21
+ * iterate peer-opened streams with `for await (const stream of conn)`.
22
+ *
23
+ * Streams are lazy: the peer's iterator does not yield a stream until the
24
+ * opener writes its first bytes. Keep the connection object alive while
25
+ * its streams are in use — closing it closes all of them.
26
+ */
27
+ export declare class TruffleQuicConnection implements AsyncIterable<TruffleQuicStream> {
28
+ #private;
29
+ /** The peer's tailnet address (`100.x.x.x:port`). */
30
+ readonly remoteAddress: string;
31
+ /** The peer's stable device id when known. */
32
+ readonly remotePeerId?: string;
33
+ constructor(native: NapiQuicConnection);
34
+ /** Open a new bidirectional byte stream on this connection. */
35
+ openStream(): Promise<TruffleQuicStream>;
36
+ /**
37
+ * Accept the next stream the peer opens; `null` once the connection has
38
+ * closed. Prefer iteration: `for await (const stream of conn.streams())`.
39
+ */
40
+ acceptStream(): Promise<TruffleQuicStream | null>;
41
+ /** Async iterator over peer-opened streams, ending when the connection closes. */
42
+ streams(): AsyncIterableIterator<TruffleQuicStream>;
43
+ [Symbol.asyncIterator](): AsyncIterableIterator<TruffleQuicStream>;
44
+ /** Close the connection and all its streams. */
45
+ close(): void;
46
+ }
47
+ /**
48
+ * A QUIC server on the mesh. Iterate incoming connections:
49
+ *
50
+ * ```ts
51
+ * const server = await mesh.quic.listen(4433);
52
+ * for await (const conn of server) {
53
+ * for await (const stream of conn.streams()) {
54
+ * stream.pipe(stream); // echo
55
+ * }
56
+ * }
57
+ * ```
58
+ */
59
+ export declare class TruffleQuicServer implements AsyncIterable<TruffleQuicConnection> {
60
+ #private;
61
+ /** The port this server is bound to. */
62
+ readonly port: number;
63
+ constructor(native: NapiQuicListener);
64
+ /** Accept the next incoming connection; `null` once the server is closed. */
65
+ accept(): Promise<TruffleQuicConnection | null>;
66
+ /** Alias of async iteration, mirroring the RFC's `sessions()` sketch. */
67
+ connections(): AsyncIterableIterator<TruffleQuicConnection>;
68
+ [Symbol.asyncIterator](): AsyncIterableIterator<TruffleQuicConnection>;
69
+ /** Close the server and every connection accepted from it. */
70
+ close(): void;
71
+ }
72
+ /** QUIC namespace bound to a mesh node. */
73
+ export interface TruffleQuic {
74
+ /**
75
+ * Open a QUIC connection to a peer. `host` accepts a device id (or
76
+ * unique ≥4-char prefix), device name, Tailscale hostname, or IP. Only
77
+ * same-app peers can complete the handshake (ALPN scoping).
78
+ */
79
+ connect(host: string, port: number): Promise<TruffleQuicConnection>;
80
+ /**
81
+ * Listen for QUIC connections. Ports 443/9417 are reserved; port 0 is
82
+ * not supported over the tsnet relay — choose an explicit port
83
+ * (suggested default: 9420+).
84
+ */
85
+ listen(port: number): Promise<TruffleQuicServer>;
86
+ }
87
+ export declare function createQuicNamespace(node: NapiNode): TruffleQuic;
88
+ //# sourceMappingURL=quic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quic.d.ts","sourceRoot":"","sources":["../src/quic.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACf,MAAM,0BAA0B,CAAC;AAElC;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,MAAM;;gBAI/B,MAAM,EAAE,cAAc;IAKzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAezB,MAAM,CACb,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GACvC,IAAI;IAIE,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI;IAItD,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI;CAItF;AAED;;;;;;;GAOG;AACH,qBAAa,qBAAsB,YAAW,aAAa,CAAC,iBAAiB,CAAC;;IAG5E,qDAAqD;IACrD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,MAAM,EAAE,kBAAkB;IAMtC,+DAA+D;IACzD,UAAU,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAI9C;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAKvD,kFAAkF;IAClF,OAAO,IAAI,qBAAqB,CAAC,iBAAiB,CAAC;IAI5C,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,qBAAqB,CAAC,iBAAiB,CAAC;IAQzE,gDAAgD;IAChD,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,YAAW,aAAa,CAAC,qBAAqB,CAAC;;IAG5E,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,gBAAgB;IAKpC,6EAA6E;IACvE,MAAM,IAAI,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAKrD,yEAAyE;IACzE,WAAW,IAAI,qBAAqB,CAAC,qBAAqB,CAAC;IAIpD,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,qBAAqB,CAAC,qBAAqB,CAAC;IAQ7E,8DAA8D;IAC9D,KAAK,IAAI,IAAI;CAGd;AAED,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpE;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAClD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAS/D"}
package/dist/quic.js ADDED
@@ -0,0 +1,150 @@
1
+ // mesh.quic — modern QUIC API over the mesh (RFC 021 Phase 3).
2
+ //
3
+ // Shape follows the RFC (and iroh's precedent): connect a peer → a
4
+ // connection carrying many concurrent bidirectional byte streams with no
5
+ // head-of-line blocking between them. Connections and streams are async
6
+ // iterable; each stream is a real `stream.Duplex` (same pull-model
7
+ // backpressure as `mesh.net` sockets).
8
+ import { Duplex } from 'node:stream';
9
+ /**
10
+ * A bidirectional byte stream on a QUIC connection, as a `stream.Duplex`.
11
+ *
12
+ * Behaves like an independent TCP connection (ordered, reliable,
13
+ * flow-controlled) without head-of-line blocking against sibling streams.
14
+ * `end()` half-closes (the peer sees clean EOF); `destroy()` also stops
15
+ * the read side.
16
+ */
17
+ export class TruffleQuicStream extends Duplex {
18
+ #native;
19
+ #reading = false;
20
+ constructor(native) {
21
+ super({ allowHalfOpen: true });
22
+ this.#native = native;
23
+ }
24
+ _read(size) {
25
+ if (this.#reading)
26
+ return;
27
+ this.#reading = true;
28
+ this.#native
29
+ .read(size > 0 ? size : undefined)
30
+ .then((chunk) => {
31
+ this.#reading = false;
32
+ this.push(chunk);
33
+ })
34
+ .catch((err) => {
35
+ this.#reading = false;
36
+ this.destroy(err);
37
+ });
38
+ }
39
+ _write(chunk, _encoding, callback) {
40
+ this.#native.write(chunk).then(() => callback(null), callback);
41
+ }
42
+ _final(callback) {
43
+ this.#native.finish().then(() => callback(null), callback);
44
+ }
45
+ _destroy(error, callback) {
46
+ const finish = () => callback(error);
47
+ this.#native.close().then(finish, finish);
48
+ }
49
+ }
50
+ /**
51
+ * A QUIC connection to a peer. Open streams with {@link openStream};
52
+ * iterate peer-opened streams with `for await (const stream of conn)`.
53
+ *
54
+ * Streams are lazy: the peer's iterator does not yield a stream until the
55
+ * opener writes its first bytes. Keep the connection object alive while
56
+ * its streams are in use — closing it closes all of them.
57
+ */
58
+ export class TruffleQuicConnection {
59
+ #native;
60
+ /** The peer's tailnet address (`100.x.x.x:port`). */
61
+ remoteAddress;
62
+ /** The peer's stable device id when known. */
63
+ remotePeerId;
64
+ constructor(native) {
65
+ this.#native = native;
66
+ this.remoteAddress = native.remoteAddress();
67
+ this.remotePeerId = native.remotePeerId() ?? undefined;
68
+ }
69
+ /** Open a new bidirectional byte stream on this connection. */
70
+ async openStream() {
71
+ return new TruffleQuicStream(await this.#native.openStream());
72
+ }
73
+ /**
74
+ * Accept the next stream the peer opens; `null` once the connection has
75
+ * closed. Prefer iteration: `for await (const stream of conn.streams())`.
76
+ */
77
+ async acceptStream() {
78
+ const native = await this.#native.acceptStream();
79
+ return native === null ? null : new TruffleQuicStream(native);
80
+ }
81
+ /** Async iterator over peer-opened streams, ending when the connection closes. */
82
+ streams() {
83
+ return this[Symbol.asyncIterator]();
84
+ }
85
+ async *[Symbol.asyncIterator]() {
86
+ for (;;) {
87
+ const stream = await this.acceptStream();
88
+ if (stream === null)
89
+ return;
90
+ yield stream;
91
+ }
92
+ }
93
+ /** Close the connection and all its streams. */
94
+ close() {
95
+ this.#native.close();
96
+ }
97
+ }
98
+ /**
99
+ * A QUIC server on the mesh. Iterate incoming connections:
100
+ *
101
+ * ```ts
102
+ * const server = await mesh.quic.listen(4433);
103
+ * for await (const conn of server) {
104
+ * for await (const stream of conn.streams()) {
105
+ * stream.pipe(stream); // echo
106
+ * }
107
+ * }
108
+ * ```
109
+ */
110
+ export class TruffleQuicServer {
111
+ #native;
112
+ /** The port this server is bound to. */
113
+ port;
114
+ constructor(native) {
115
+ this.#native = native;
116
+ this.port = native.port();
117
+ }
118
+ /** Accept the next incoming connection; `null` once the server is closed. */
119
+ async accept() {
120
+ const native = await this.#native.accept();
121
+ return native === null ? null : new TruffleQuicConnection(native);
122
+ }
123
+ /** Alias of async iteration, mirroring the RFC's `sessions()` sketch. */
124
+ connections() {
125
+ return this[Symbol.asyncIterator]();
126
+ }
127
+ async *[Symbol.asyncIterator]() {
128
+ for (;;) {
129
+ const conn = await this.accept();
130
+ if (conn === null)
131
+ return;
132
+ yield conn;
133
+ }
134
+ }
135
+ /** Close the server and every connection accepted from it. */
136
+ close() {
137
+ this.#native.close();
138
+ }
139
+ }
140
+ export function createQuicNamespace(node) {
141
+ return {
142
+ async connect(host, port) {
143
+ return new TruffleQuicConnection(await node.connectQuic(host, port));
144
+ },
145
+ async listen(port) {
146
+ return new TruffleQuicServer(await node.listenQuic(port));
147
+ },
148
+ };
149
+ }
150
+ //# sourceMappingURL=quic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quic.js","sourceRoot":"","sources":["../src/quic.ts"],"names":[],"mappings":"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"}
package/dist/ws.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { TruffleNet } from './net.js';
2
+ import type WebSocket from 'ws';
3
+ import type { WebSocketServer as WsServerInstance, ServerOptions as WsServerOptions, ClientOptions as WsClientOptions } from 'ws';
4
+ /** The subset of the `ws` module mesh.ws uses; see {@link WsLoader}. */
5
+ interface WsExports {
6
+ /** The `ws` client constructor. */
7
+ WebSocket: new (address: string, options?: WsClientOptions) => WebSocket;
8
+ /** The `ws` server constructor. */
9
+ WebSocketServer: new (options?: WsServerOptions) => WsServerInstance;
10
+ }
11
+ /**
12
+ * Loads the `ws` module. Defaults to a real `import('ws')`; the seam exists so
13
+ * tests can inject a loader (including one that rejects, to exercise the
14
+ * missing-dependency path) without touching the filesystem.
15
+ */
16
+ export type WsLoader = () => Promise<WsExports>;
17
+ /**
18
+ * A `ws` {@link WebSocket.Server} bound to a mesh listener.
19
+ *
20
+ * Everything about it is the ordinary `ws` server API — most importantly the
21
+ * `'connection'` event still fires with `(ws, request)` — with two additions:
22
+ * `port` (resolved, so an ephemeral `0` request surfaces the real port) and a
23
+ * `close()` that also tears down the underlying mesh listener.
24
+ */
25
+ export interface TruffleWsServer extends WsServerInstance {
26
+ /** The mesh port this server is bound to (resolved when `0` was requested). */
27
+ readonly port: number;
28
+ }
29
+ /** Options for {@link TruffleWs.createServer}. */
30
+ export interface TruffleWsServerOptions {
31
+ /**
32
+ * Mesh port to listen on. `0` binds an ephemeral port (read it back from
33
+ * `server.port`). Ports 443 and 9417 are reserved by the mesh and rejected.
34
+ */
35
+ port: number;
36
+ /**
37
+ * Optional path filter, like `ws`'s own `path` option: only upgrade requests
38
+ * whose URL path matches are accepted; others get a `400`-style socket close.
39
+ */
40
+ path?: string;
41
+ }
42
+ /** WebSocket-over-mesh namespace bound to a mesh node's `net` namespace. */
43
+ export interface TruffleWs {
44
+ /**
45
+ * Open a WebSocket to a peer, returning a connecting `ws` `WebSocket` (await
46
+ * its `'open'` event as usual). `host` accepts any peer reference — device
47
+ * id (or ≥4-char prefix), device name, Tailscale hostname, or `100.x` IP —
48
+ * including names with spaces and case-sensitive ids that aren't valid URL
49
+ * hostnames (they're pinned past the URL, see the module docs). `path`
50
+ * defaults to `/`; `options` are forwarded to the `ws` `WebSocket`
51
+ * constructor (a caller-supplied `agent` is overridden — the mesh agent is
52
+ * how it reaches the peer).
53
+ */
54
+ connect(host: string, port: number, path?: string, options?: WsClientOptions): Promise<WebSocket>;
55
+ /**
56
+ * Start a WebSocket server on the mesh. Resolves once it's listening; the
57
+ * returned server's `'connection'` event delivers `(ws, request)` like any
58
+ * `ws` server. Close it (and release the mesh port) with `server.close()`.
59
+ */
60
+ createServer(options: TruffleWsServerOptions): Promise<TruffleWsServer>;
61
+ }
62
+ /**
63
+ * Build the `mesh.ws` namespace over a node's `net` namespace.
64
+ *
65
+ * @param net The node's {@link TruffleNet} (mesh TCP), used for the client
66
+ * agent and the server's listener.
67
+ * @param load How to obtain the `ws` module; defaults to `import('ws')`.
68
+ * Injectable for tests.
69
+ */
70
+ export declare function createWsNamespace(net: TruffleNet, load?: WsLoader): TruffleWs;
71
+ export {};
72
+ //# sourceMappingURL=ws.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;AAChC,OAAO,KAAK,EACV,eAAe,IAAI,gBAAgB,EACnC,aAAa,IAAI,eAAe,EAChC,aAAa,IAAI,eAAe,EACjC,MAAM,IAAI,CAAC;AAsBZ,wEAAwE;AACxE,UAAU,SAAS;IACjB,mCAAmC;IACnC,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,SAAS,CAAC;IACzE,mCAAmC;IACnC,eAAe,EAAE,KAAK,OAAO,CAAC,EAAE,eAAe,KAAK,gBAAgB,CAAC;CACtE;AAED;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;AAyBhD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAClG;;;;OAIG;IACH,YAAY,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACzE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,GAAE,QAAmB,GAAG,SAAS,CA8FvF"}
package/dist/ws.js ADDED
@@ -0,0 +1,152 @@
1
+ // WebSocket over the mesh via the `ws` package (RFC 021 Phase 4, D3).
2
+ //
3
+ // mesh.ws ships zero new Rust: it runs the battle-tested `ws` npm package over
4
+ // mesh TCP. The client drives `ws`'s HTTP upgrade through a MeshAgent (whose
5
+ // `createConnection` returns a mesh `TruffleSocket`); the server feeds mesh
6
+ // connections into a stock `http.Server` and lets a `noServer` WebSocketServer
7
+ // complete the upgrade — exactly the seams `ws` documents for custom
8
+ // transports. `websocket.rs` stays the internal envelope pipe on 9417; this is
9
+ // a separate, user-facing raw-WebSocket surface.
10
+ //
11
+ // `ws` is an OPTIONAL peer dependency: apps that never touch mesh.ws don't need
12
+ // it installed. Nothing here imports `ws` at module load — the namespace holds
13
+ // lazy async functions that `await import('ws')` on first use and throw a
14
+ // clear, actionable error when it's missing.
15
+ import http from 'node:http';
16
+ import { MeshAgent } from './http.js';
17
+ /**
18
+ * Hostname placed in the `ws://` URL when the caller's peer reference can't be
19
+ * a URL hostname (device names with spaces, case-sensitive device ids, …).
20
+ * It only ever sets the HTTP `Host` header — correctness never depends on it,
21
+ * because the agent dials the real peer reference verbatim (see {@link PIN_KEY}).
22
+ * `.invalid` is reserved by RFC 2606 and never resolves, so it reads as the
23
+ * placeholder it is.
24
+ */
25
+ const PLACEHOLDER_HOST = 'truffle.invalid';
26
+ /**
27
+ * Request-option key carrying the true peer reference past `ws`'s URL
28
+ * rewriting. `ws` overwrites `options.host` with the URL's hostname before the
29
+ * agent sees it, so a non-URL-safe peer reference can't ride `options.host`.
30
+ * It CAN ride an unknown key: `ws` spreads user options into the request and
31
+ * `http` forwards unknown keys to the agent's `createConnection` untouched
32
+ * (verified empirically). {@link MeshWsAgent} reads it back.
33
+ */
34
+ const PIN_KEY = 'truffleWsPeer';
35
+ const importWs = () => import('ws');
36
+ /**
37
+ * A {@link MeshAgent} that honours a pinned peer reference ({@link PIN_KEY}).
38
+ *
39
+ * For a URL-safe peer reference the pin equals the URL hostname and this is a
40
+ * no-op; for a non-URL-safe one the URL carries a placeholder and the real
41
+ * reference arrives only via the pin, so we rewrite `host`/`hostname` before
42
+ * delegating to MeshAgent — which then dials the exact peer over mesh TCP.
43
+ */
44
+ class MeshWsAgent extends MeshAgent {
45
+ createConnection(options, callback) {
46
+ const pinned = options[PIN_KEY];
47
+ if (typeof pinned === 'string' && pinned.length > 0) {
48
+ return super.createConnection({ ...options, host: pinned, hostname: pinned }, callback);
49
+ }
50
+ return super.createConnection(options, callback);
51
+ }
52
+ }
53
+ /**
54
+ * Build the `mesh.ws` namespace over a node's `net` namespace.
55
+ *
56
+ * @param net The node's {@link TruffleNet} (mesh TCP), used for the client
57
+ * agent and the server's listener.
58
+ * @param load How to obtain the `ws` module; defaults to `import('ws')`.
59
+ * Injectable for tests.
60
+ */
61
+ export function createWsNamespace(net, load = importWs) {
62
+ const agent = new MeshWsAgent(net);
63
+ async function loadWs() {
64
+ try {
65
+ return await load();
66
+ }
67
+ catch (err) {
68
+ throw new Error("mesh.ws requires the 'ws' package, which is an optional peer dependency — " +
69
+ 'install it with `npm install ws` (or `pnpm add ws`).', { cause: err });
70
+ }
71
+ }
72
+ async function connect(host, port, path = '/', options = {}) {
73
+ const { WebSocket: WebSocketImpl } = await loadWs();
74
+ const requestPath = path.startsWith('/') ? path : `/${path}`;
75
+ // The URL hostname only sets the HTTP Host header; the agent always dials
76
+ // the pinned peer. Embed `host` when it round-trips as a URL hostname
77
+ // unchanged (a plain IP or lowercase device name), so the Host header is
78
+ // natural; otherwise a placeholder (a name with spaces, or a device id
79
+ // whose case a URL would fold — either would dial the wrong peer).
80
+ const urlHost = urlHostnameFor(host, port) ?? PLACEHOLDER_HOST;
81
+ const url = `ws://${urlHost}:${port}${requestPath}`;
82
+ // `agent` last: the mesh agent is non-negotiable. PIN_KEY is an unknown key
83
+ // ws/http pass through to the agent (see PIN_KEY); cast past the typed
84
+ // ClientOptions to attach it.
85
+ const wsOptions = { ...options, agent, [PIN_KEY]: host };
86
+ return new WebSocketImpl(url, wsOptions);
87
+ }
88
+ async function createServer(options) {
89
+ const { WebSocketServer } = await loadWs();
90
+ // A throwaway http.Server does the HTTP/WebSocket upgrade parsing; a
91
+ // noServer WebSocketServer completes the handshake. We never `.listen()`
92
+ // this http server on a host port — mesh connections are fed in by hand.
93
+ const httpServer = http.createServer();
94
+ const wss = new WebSocketServer({ noServer: true, path: options.path });
95
+ httpServer.on('upgrade', (req, socket, head) => {
96
+ // `ws`'s own path check: true when no `path` was set, else on a match.
97
+ Promise.resolve(wss.shouldHandle(req)).then((ok) => {
98
+ if (!ok) {
99
+ socket.destroy();
100
+ return;
101
+ }
102
+ wss.handleUpgrade(req, socket, head, (client, request) => {
103
+ wss.emit('connection', client, request);
104
+ });
105
+ });
106
+ });
107
+ // Each mesh TCP connection becomes an http.Server 'connection' — exactly
108
+ // what a real listening socket would emit — so http parses it and, on an
109
+ // Upgrade request, fires the 'upgrade' handler above.
110
+ const meshServer = net.createServer((sock) => httpServer.emit('connection', sock));
111
+ await new Promise((resolve, reject) => {
112
+ const onError = (err) => reject(err);
113
+ meshServer.once('error', onError);
114
+ meshServer.listen(options.port, () => {
115
+ meshServer.removeListener('error', onError);
116
+ resolve();
117
+ });
118
+ });
119
+ const server = wss;
120
+ Object.defineProperty(server, 'port', {
121
+ value: meshServer.port ?? options.port,
122
+ enumerable: true,
123
+ });
124
+ // close() also tears down the mesh listener and any live client sockets;
125
+ // the http.Server holds no listening socket, so there's nothing to close
126
+ // there. Keep ws's void-returning signature so the type stays a ws Server.
127
+ const closeWs = wss.close.bind(wss);
128
+ server.close = (callback) => {
129
+ for (const client of wss.clients)
130
+ client.close();
131
+ meshServer.close();
132
+ closeWs(callback);
133
+ };
134
+ return server;
135
+ }
136
+ return { connect, createServer };
137
+ }
138
+ /**
139
+ * Returns `host` when it is a URL hostname that round-trips unchanged (so it's
140
+ * safe to embed in a `ws://` URL and use for the Host header), else `null`.
141
+ * The equality check rejects anything a WHATWG URL would normalise — notably
142
+ * uppercase device ids, which URL parsing lowercases.
143
+ */
144
+ function urlHostnameFor(host, port) {
145
+ try {
146
+ return new URL(`ws://${host}:${port}`).hostname === host ? host : null;
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ //# sourceMappingURL=ws.js.map
package/dist/ws.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecook/truffle",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "Mesh networking for local-first apps, built on Tailscale",
5
5
  "license": "MIT",
6
6
  "author": "James Yong",
@@ -44,22 +44,33 @@
44
44
  "sidecar-checksums.json"
45
45
  ],
46
46
  "dependencies": {
47
- "@vibecook/truffle-native": "0.4.8"
47
+ "@vibecook/truffle-native": "0.5.0"
48
+ },
49
+ "peerDependencies": {
50
+ "ws": ">=8"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "ws": {
54
+ "optional": true
55
+ }
48
56
  },
49
57
  "optionalDependencies": {
50
- "@vibecook/truffle-sidecar-darwin-arm64": "^0.4.6",
51
- "@vibecook/truffle-sidecar-darwin-x64": "^0.4.6",
52
- "@vibecook/truffle-sidecar-linux-x64": "^0.4.5",
53
- "@vibecook/truffle-sidecar-linux-arm64": "^0.4.5",
54
- "@vibecook/truffle-sidecar-win32-x64": "^0.4.5"
58
+ "@vibecook/truffle-sidecar-darwin-arm64": "0.5.0",
59
+ "@vibecook/truffle-sidecar-darwin-x64": "0.5.0",
60
+ "@vibecook/truffle-sidecar-linux-x64": "0.5.0",
61
+ "@vibecook/truffle-sidecar-linux-arm64": "0.5.0",
62
+ "@vibecook/truffle-sidecar-win32-x64": "0.5.0"
55
63
  },
56
64
  "devDependencies": {
57
- "typescript": "^5.7.0"
65
+ "@types/ws": "^8.5.13",
66
+ "typescript": "^5.7.0",
67
+ "ws": "^8.18.0"
58
68
  },
59
69
  "scripts": {
60
70
  "build": "tsc",
61
71
  "clean": "rm -rf dist",
62
72
  "typecheck": "tsc --noEmit",
73
+ "test": "node --test \"test/**/*.test.cjs\"",
63
74
  "postinstall": "node scripts/postinstall.cjs"
64
75
  }
65
76
  }
@@ -103,6 +103,20 @@ function loadExpectedChecksum(version, asset) {
103
103
  }
104
104
  }
105
105
 
106
+ /** Release tags are `truffle-v{version}` (release-please monorepo tag scheme), NOT `v{version}`. */
107
+ function buildDownloadUrl(version, asset) {
108
+ return `https://github.com/${GITHUB_REPO}/releases/download/truffle-v${version}/${asset}`;
109
+ }
110
+
111
+ /** Throw if `actual` does not match `expected` (hex, case-insensitive). */
112
+ function assertChecksum(actual, expected, label) {
113
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
114
+ throw new Error(
115
+ `SECURITY: sidecar checksum mismatch for ${label}. Expected ${expected}, got ${actual}.`,
116
+ );
117
+ }
118
+ }
119
+
106
120
  async function main() {
107
121
  const key = `${process.platform}-${process.arch}`;
108
122
  const pkg = PLATFORM_PACKAGES[key];
@@ -142,7 +156,7 @@ async function main() {
142
156
  if (!asset) return;
143
157
 
144
158
  const version = require('../package.json').version;
145
- const url = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${asset}`;
159
+ const url = buildDownloadUrl(version, asset);
146
160
  const expected = loadExpectedChecksum(version, asset);
147
161
 
148
162
  console.log(`[truffle] Sidecar not found via npm. Downloading from GitHub Releases...`);
@@ -176,21 +190,21 @@ async function main() {
176
190
  // Integrity check.
177
191
  if (expected) {
178
192
  const actual = await sha256File(dest);
179
- if (actual.toLowerCase() !== expected.toLowerCase()) {
193
+ try {
194
+ assertChecksum(actual, expected, `truffle-v${version}/${asset}`);
195
+ } catch (err) {
180
196
  try {
181
197
  unlinkSync(dest);
182
198
  } catch {
183
199
  /* ignore */
184
200
  }
185
- console.error(
186
- `[truffle] SECURITY: sidecar checksum mismatch for v${version}/${asset}. ` +
187
- `Deleted the download and refusing to install it. Expected ${expected}, got ${actual}.`,
188
- );
201
+ console.error(`[truffle] ${err.message} Deleted the download and refusing to install it.`);
202
+ process.exitCode = 1;
189
203
  return;
190
204
  }
191
205
  } else {
192
206
  console.warn(
193
- `[truffle] WARNING: no pinned checksum for v${version}/${asset}; integrity NOT verified. ` +
207
+ `[truffle] WARNING: no pinned checksum for truffle-v${version}/${asset}; integrity NOT verified. ` +
194
208
  `Prefer installing with optionalDependencies enabled.`,
195
209
  );
196
210
  }
@@ -202,4 +216,11 @@ async function main() {
202
216
  console.log(`[truffle] Sidecar downloaded${expected ? ' and verified' : ''}.`);
203
217
  }
204
218
 
205
- main();
219
+ module.exports = { buildDownloadUrl, assertChecksum, loadExpectedChecksum };
220
+
221
+ if (require.main === module) {
222
+ main().catch((err) => {
223
+ console.error(`[truffle] postinstall failed: ${err.message}`);
224
+ process.exitCode = 1;
225
+ });
226
+ }
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "_comment": "SHA-256 of each GitHub Release sidecar asset, keyed by release version then by asset filename. This file ships inside the npm package (which npm integrity-protects), so it is the trust anchor for the postinstall HTTPS download fallback. When an entry for the installing version/asset is present, postinstall FAILS CLOSED on a checksum mismatch; when absent it warns that integrity was not verified. Populate this at release time, e.g. `shasum -a 256 tsnet-sidecar-*` over the built assets. Keys must match GITHUB_ASSETS in scripts/postinstall.cjs.",
3
- "_example": {
4
- "0.4.7": {
5
- "tsnet-sidecar-darwin-arm64": "<sha256-hex>",
6
- "tsnet-sidecar-darwin-amd64": "<sha256-hex>",
7
- "tsnet-sidecar-linux-amd64": "<sha256-hex>",
8
- "tsnet-sidecar-linux-arm64": "<sha256-hex>",
9
- "tsnet-sidecar-windows-amd64.exe": "<sha256-hex>"
10
- }
3
+ "0.4.8": {
4
+ "tsnet-sidecar-darwin-arm64": "acfaed6a2de2cb250adb5d3ce5122f5530005a331ad8eb539af6376fe79fc69a",
5
+ "tsnet-sidecar-darwin-amd64": "68bcbf7bb6e61de57e357f0940ef4db8dd86ab2bd69d34e48264ff5d945a6e6c",
6
+ "tsnet-sidecar-linux-amd64": "a4450bbbf72777b9773207b96a9585040cbf27ea98f0c02a6e186a288b5d85c2",
7
+ "tsnet-sidecar-linux-arm64": "90482a6f6b671873d58c57c466c6bd0390573b6b150a98ec4648d1634a1000b2",
8
+ "tsnet-sidecar-windows-amd64.exe": "62db3a5d3fea8b917851000107cd3ede08dbc0245429693c6baf2f2e0b87c3e7"
9
+ },
10
+ "0.4.9": {
11
+ "tsnet-sidecar-darwin-arm64": "b9ab84a715aa031e6f0689824cafd54195b8ae30edc4697d083ae18bfd77abe4",
12
+ "tsnet-sidecar-darwin-amd64": "a6c796117b9811c69a1a1f6bfc101eba3f1059e7d091021ee278ef651e2682cf",
13
+ "tsnet-sidecar-linux-amd64": "9429bd4b9247292856b3c5d64c6d8aa28d922149199bb679a8c3c123f09f51a3",
14
+ "tsnet-sidecar-linux-arm64": "8739d9f8f70b17c2385578a91b332f3fefeab91b6fd05b0adaf81a1822b3003c",
15
+ "tsnet-sidecar-windows-amd64.exe": "0adb921eba8b3cc193fbc56ef35c713a62c6fcf66d4ee75338993c898adc0edc"
16
+ },
17
+ "0.5.0": {
18
+ "tsnet-sidecar-darwin-arm64": "fc272fb3a02d399c99cb14028da9aafab09b1316ab0115ba31b36ccfd2a63467",
19
+ "tsnet-sidecar-darwin-amd64": "49a9eae78188df8aa16bc602ebc4bd1a5e7f5f36b8075affc4d3a32d2d0ea0b1",
20
+ "tsnet-sidecar-linux-amd64": "42c6c9be8a8a36b8fd873f22d6edc271633dc4bfbd72916909ff5f799c8c97ea",
21
+ "tsnet-sidecar-linux-arm64": "6972149aed545f3370d4ae6032ae185c67e4134b9c6afeffc743fcb1037d9467",
22
+ "tsnet-sidecar-windows-amd64.exe": "914750c30eb2485536d49030b1e317d0b197189d0943817e88f2e55227cfbdc7"
11
23
  }
12
24
  }