@stim-cli/core 1.20.2 → 1.21.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.
@@ -0,0 +1,164 @@
1
+ import { f as NoiseTransport, l as NoiseKeyPair, s as NoiseDh } from "./noise-DYIbymAJ.mjs";
2
+ import { EventEmitter } from "node:events";
3
+ //#region noise-session.d.ts
4
+ /** The protocol version byte that opens every protocol 2 connection. */
5
+ declare const STIM_PROTOCOL_VERSION = 2;
6
+ /** A client the server already has a record for: `Noise_XX`, nothing after the mode byte. */
7
+ declare const MODE_RETURNING = 0;
8
+ /** A client spending a pairing token or setup ticket: `Noise_XXpsk2`, with the secret's `lookup` after the mode byte. */
9
+ declare const MODE_FIRST_CONTACT = 1;
10
+ type HandshakeMode = typeof MODE_RETURNING | typeof MODE_FIRST_CONTACT;
11
+ declare const SECRET_BYTES = 32;
12
+ declare const LOOKUP_BYTES = 16;
13
+ /** Largest message, plaintext without its type byte, a server accepts from a client. */
14
+ declare const MAX_CLIENT_MESSAGE_BYTES: number;
15
+ /** Largest message, plaintext without its type byte, a server sends. */
16
+ declare const MAX_SERVER_MESSAGE_BYTES: number;
17
+ /** What a transport message adds to its plaintext on the wire: the type byte and the ChaCha20-Poly1305 tag. */
18
+ declare const TRANSPORT_OVERHEAD_BYTES = 17;
19
+ /** The largest first WebSocket message: the cleartext prefix and Noise message 1. */
20
+ declare const MAX_FIRST_MESSAGE_BYTES: number;
21
+ /** The close code a server or client uses for an authentication refusal; the close reason is the error code. */
22
+ declare const CLOSE_REFUSED = 4401;
23
+ /** The close code for a message this side cannot use, such as a protocol 1 hello; the reason is the error code. */
24
+ declare const CLOSE_BAD_REQUEST = 4400;
25
+ /** A pairing token or setup ticket as people and commands carry it: 43 base64url characters of 32 random bytes. */
26
+ declare function newSecret(): string;
27
+ /** The 32 bytes of a pairing token or setup ticket, or null when `text` is not one. */
28
+ declare function decodeSecret(text: string): Buffer | null;
29
+ interface SecretValues {
30
+ /** The Noise PSK: `SHA-256("stim noise psk v1" || t)`. */
31
+ psk: Buffer;
32
+ /** Names the secret in the cleartext prefix: `SHA-256("stim noise lookup v1" || t)[0..16]`. */
33
+ lookup: Buffer;
34
+ /** The setup journal's file name and URL path, lowercase hex of `SHA-256("stim setup journal v1" || t)`. */
35
+ journal: string;
36
+ }
37
+ /** The values both sides derive from a secret's 32 bytes. None reveals `psk` or the secret. */
38
+ declare function secretValues(secret: Uint8Array): SecretValues;
39
+ /** A server key's fingerprint, as the pairing QR code carries it: base64url of the key's SHA-256. */
40
+ declare function keyFingerprint(publicKey: Uint8Array): string;
41
+ /** The cleartext prefix of the first WebSocket message. */
42
+ declare function handshakePrefix(mode: HandshakeMode, lookup?: Uint8Array): Buffer;
43
+ type ParsedFirstMessage = {
44
+ mode: typeof MODE_RETURNING;
45
+ prefix: Buffer;
46
+ message: Buffer;
47
+ } | {
48
+ mode: typeof MODE_FIRST_CONTACT;
49
+ prefix: Buffer;
50
+ lookup: Buffer;
51
+ message: Buffer;
52
+ } | {
53
+ error: "protocol-unsupported" | "handshake-failed";
54
+ };
55
+ /** Splits the first WebSocket message into its prefix and Noise message 1. */
56
+ declare function parseFirstMessage(data: Uint8Array): ParsedFirstMessage;
57
+ /** `"stim noise v1" || prefix`, so the handshake authenticates the version, mode and lookup. */
58
+ declare function handshakePrologue(prefix: Uint8Array): Buffer;
59
+ /**
60
+ * The long-term X25519 key in `file`, created with mode 0600 the first time. A file that exists but does not hold a
61
+ * 32-byte key throws instead of being replaced, because a new key makes every pinned peer refuse this side.
62
+ */
63
+ declare function readOrCreateKeyFile(file: string): NoiseKeyPair;
64
+ /** The subset of a `ws` WebSocket that a Noise session runs on. */
65
+ interface RawSocket {
66
+ readonly readyState: number;
67
+ readonly bufferedAmount: number;
68
+ /** Bytes go as a binary message and a string as a text message, as `ws` sends them. */
69
+ send(data: Buffer | string, callback?: (error?: Error) => void): void;
70
+ close(code?: number, reason?: string): void;
71
+ terminate(): void;
72
+ ping?(): void;
73
+ on(event: "message", listener: (data: unknown, isBinary: boolean) => void): unknown;
74
+ on(event: "close", listener: (code: number, reason: Buffer) => void): unknown;
75
+ on(event: "error", listener: (error: Error) => void): unknown;
76
+ on(event: "pong", listener: () => void): unknown;
77
+ off(event: "message", listener: (data: unknown, isBinary: boolean) => void): unknown;
78
+ off(event: "close", listener: (code: number, reason: Buffer) => void): unknown;
79
+ off(event: "error", listener: (error: Error) => void): unknown;
80
+ }
81
+ /** Why a handshake did not complete. `code` is the server's close reason when it refused this client. */
82
+ declare class HandshakeRefusal extends Error {
83
+ override name: string;
84
+ readonly code: string;
85
+ constructor(code: string, message: string);
86
+ }
87
+ /**
88
+ * One authenticated, encrypted connection over a WebSocket, with the subset of `ws`'s WebSocket API Stim uses.
89
+ * `send` takes a string for a text message and bytes for a binary one; `message` listeners get the plaintext as a
90
+ * Buffer and whether it was binary. A message that fails to decrypt closes the connection. A socket error is emitted
91
+ * only to `error` listeners; without one it is dropped, since the socket also closes and `close` reports that.
92
+ */
93
+ declare class NoiseSocket extends EventEmitter {
94
+ #private;
95
+ readonly CONNECTING = 0;
96
+ readonly OPEN = 1;
97
+ readonly CLOSING = 2;
98
+ readonly CLOSED = 3;
99
+ /** The authenticated static public key of the other side. */
100
+ readonly remoteKey: Buffer;
101
+ constructor(raw: RawSocket, transport: NoiseTransport, remoteKey: Buffer);
102
+ get readyState(): number;
103
+ get bufferedAmount(): number;
104
+ /**
105
+ * Sends a string as a text message or bytes as a binary message. A message over this side's limit closes the
106
+ * connection with 1009, as a WebSocket peer does for an oversized frame.
107
+ */
108
+ send(data: string | Uint8Array, options?: {
109
+ binary?: boolean;
110
+ } | ((error?: Error) => void), callback?: (error?: Error) => void): void;
111
+ close(code?: number, reason?: string): void;
112
+ terminate(): void;
113
+ ping(): void;
114
+ }
115
+ interface InitiateOptions {
116
+ keyPair: NoiseKeyPair;
117
+ /** The server key pinned at pairing; the handshake fails unless the server proves it holds this key. */
118
+ serverKey?: Uint8Array;
119
+ /** A pairing token or setup ticket's 32 bytes, for first contact. Without it, the client must pin `serverKey`. */
120
+ secret?: Uint8Array;
121
+ /** The device name first contact records; sent encrypted in message 3. */
122
+ name?: string;
123
+ /** Accepts a server key on first contact, such as one matching a QR code's fingerprint. */
124
+ acceptServerKey?: (key: Buffer) => boolean;
125
+ timeoutMs: number;
126
+ /** Largest message this client sends and receives; defaults to the client side of the spec's limits. */
127
+ maxSendBytes?: number;
128
+ maxReceiveBytes?: number;
129
+ }
130
+ /**
131
+ * Runs the client side of the protocol 2 handshake on an open WebSocket. It rejects with {@link HandshakeRefusal} and
132
+ * closes the socket when the server refuses or the handshake fails; the server's close reason names why.
133
+ */
134
+ declare function initiateNoise(raw: RawSocket, options: InitiateOptions): Promise<NoiseSocket>;
135
+ /** What the server learned from a completed handshake, before it decides whether to keep the connection. */
136
+ interface AcceptedHandshake {
137
+ socket: NoiseSocket;
138
+ mode: HandshakeMode;
139
+ /** The first-contact secret's lookup, as hex. */
140
+ lookup?: string;
141
+ clientKey: Buffer;
142
+ /** Message 3's payload: the first-contact JSON `{ "name": ... }`, or empty. */
143
+ payload: Buffer;
144
+ }
145
+ interface AcceptOptions {
146
+ keyPair: NoiseKeyPair;
147
+ /** The PSK of the secret `lookup` (hex) names, or null when no valid secret matches. */
148
+ psk: (lookup: string) => Buffer | null;
149
+ timeoutMs: number;
150
+ /** X25519 in place of node:crypto's, as `HandshakeOptions.dh` takes it. */
151
+ dh?: NoiseDh;
152
+ maxSendBytes?: number;
153
+ maxReceiveBytes?: number;
154
+ }
155
+ /** The message a protocol 1 client gets: in cleartext, since it cannot decrypt, then the socket closes. */
156
+ declare const PROTOCOL_UNSUPPORTED_MESSAGE: string;
157
+ /**
158
+ * Runs the server side of the handshake on the first message a client sent. It closes the socket and rejects with
159
+ * {@link HandshakeRefusal} when the client speaks another protocol, names no valid secret (`unknown-secret`), or fails
160
+ * the handshake. A text first message, a protocol 1 hello, gets a cleartext `protocol-unsupported` reply.
161
+ */
162
+ declare function acceptNoise(raw: RawSocket, first: unknown, isBinary: boolean, options: AcceptOptions): Promise<AcceptedHandshake>;
163
+ //#endregion
164
+ export { AcceptOptions, AcceptedHandshake, CLOSE_BAD_REQUEST, CLOSE_REFUSED, HandshakeMode, HandshakeRefusal, InitiateOptions, LOOKUP_BYTES, MAX_CLIENT_MESSAGE_BYTES, MAX_FIRST_MESSAGE_BYTES, MAX_SERVER_MESSAGE_BYTES, MODE_FIRST_CONTACT, MODE_RETURNING, NoiseSocket, PROTOCOL_UNSUPPORTED_MESSAGE, ParsedFirstMessage, RawSocket, SECRET_BYTES, STIM_PROTOCOL_VERSION, SecretValues, TRANSPORT_OVERHEAD_BYTES, acceptNoise, decodeSecret, handshakePrefix, handshakePrologue, initiateNoise, keyFingerprint, newSecret, parseFirstMessage, readOrCreateKeyFile, secretValues };
@@ -0,0 +1,402 @@
1
+ import { HandshakeState, MAX_HANDSHAKE_MESSAGE_BYTES, NOISE_XX, NOISE_XX_PSK2, NoiseError, NoiseTransport, generateNoiseKeyPair, noiseKeyPairFromPrivateKey } from "./noise.mjs";
2
+ import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { EventEmitter } from "node:events";
6
+ //#region noise-session.ts
7
+ /** The protocol version byte that opens every protocol 2 connection. */
8
+ const STIM_PROTOCOL_VERSION = 2;
9
+ /** A client the server already has a record for: `Noise_XX`, nothing after the mode byte. */
10
+ const MODE_RETURNING = 0;
11
+ /** A client spending a pairing token or setup ticket: `Noise_XXpsk2`, with the secret's `lookup` after the mode byte. */
12
+ const MODE_FIRST_CONTACT = 1;
13
+ const SECRET_BYTES = 32;
14
+ const LOOKUP_BYTES = 16;
15
+ /** Largest message, plaintext without its type byte, a server accepts from a client. */
16
+ const MAX_CLIENT_MESSAGE_BYTES = 65536;
17
+ /** Largest message, plaintext without its type byte, a server sends. */
18
+ const MAX_SERVER_MESSAGE_BYTES = 16777216;
19
+ /** What a transport message adds to its plaintext on the wire: the type byte and the ChaCha20-Poly1305 tag. */
20
+ const TRANSPORT_OVERHEAD_BYTES = 17;
21
+ const MAX_PREFIX_BYTES = 18;
22
+ /** The largest first WebSocket message: the cleartext prefix and Noise message 1. */
23
+ const MAX_FIRST_MESSAGE_BYTES = MAX_PREFIX_BYTES + MAX_HANDSHAKE_MESSAGE_BYTES;
24
+ /** How many messages a connection holds before its first `message` listener; a peer that sends more is closed. */
25
+ const MAX_HELD_MESSAGES = 64;
26
+ /** The close code a server or client uses for an authentication refusal; the close reason is the error code. */
27
+ const CLOSE_REFUSED = 4401;
28
+ /** The close code for a message this side cannot use, such as a protocol 1 hello; the reason is the error code. */
29
+ const CLOSE_BAD_REQUEST = 4400;
30
+ const PROLOGUE = Buffer.from("stim noise v1", "ascii");
31
+ const SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/;
32
+ /** A pairing token or setup ticket as people and commands carry it: 43 base64url characters of 32 random bytes. */
33
+ function newSecret() {
34
+ return randomBytes(32).toString("base64url");
35
+ }
36
+ /** The 32 bytes of a pairing token or setup ticket, or null when `text` is not one. */
37
+ function decodeSecret(text) {
38
+ if (!SECRET_PATTERN.test(text)) return null;
39
+ const bytes = Buffer.from(text, "base64url");
40
+ return bytes.length === 32 && bytes.toString("base64url") === text ? bytes : null;
41
+ }
42
+ const sha256 = (...parts) => {
43
+ const hash = createHash("sha256");
44
+ for (const part of parts) hash.update(part);
45
+ return hash.digest();
46
+ };
47
+ /** The values both sides derive from a secret's 32 bytes. None reveals `psk` or the secret. */
48
+ function secretValues(secret) {
49
+ if (secret.length !== 32) throw new RangeError("A pairing token or setup ticket is 32 bytes");
50
+ return {
51
+ psk: sha256(Buffer.from("stim noise psk v1", "ascii"), secret),
52
+ lookup: sha256(Buffer.from("stim noise lookup v1", "ascii"), secret).subarray(0, 16),
53
+ journal: sha256(Buffer.from("stim setup journal v1", "ascii"), secret).toString("hex")
54
+ };
55
+ }
56
+ /** A server key's fingerprint, as the pairing QR code carries it: base64url of the key's SHA-256. */
57
+ function keyFingerprint(publicKey) {
58
+ return sha256(publicKey).toString("base64url");
59
+ }
60
+ /** The cleartext prefix of the first WebSocket message. */
61
+ function handshakePrefix(mode, lookup) {
62
+ if (mode === 1 !== (lookup !== void 0)) throw new TypeError("Only first contact sends lookup");
63
+ if (lookup && lookup.length !== 16) throw new RangeError("lookup is 16 bytes");
64
+ return Buffer.concat([Uint8Array.of(2, mode), lookup ?? Buffer.alloc(0)]);
65
+ }
66
+ /** Splits the first WebSocket message into its prefix and Noise message 1. */
67
+ function parseFirstMessage(data) {
68
+ const bytes = Buffer.from(data);
69
+ if (bytes.length < 1 || bytes[0] !== 2) return { error: "protocol-unsupported" };
70
+ if (bytes.length > MAX_FIRST_MESSAGE_BYTES) return { error: "handshake-failed" };
71
+ if (bytes[1] === 0) return {
72
+ mode: 0,
73
+ prefix: bytes.subarray(0, 2),
74
+ message: bytes.subarray(2)
75
+ };
76
+ if (bytes[1] === 1 && bytes.length >= MAX_PREFIX_BYTES) return {
77
+ mode: 1,
78
+ prefix: bytes.subarray(0, MAX_PREFIX_BYTES),
79
+ lookup: bytes.subarray(2, MAX_PREFIX_BYTES),
80
+ message: bytes.subarray(MAX_PREFIX_BYTES)
81
+ };
82
+ return { error: "handshake-failed" };
83
+ }
84
+ /** `"stim noise v1" || prefix`, so the handshake authenticates the version, mode and lookup. */
85
+ function handshakePrologue(prefix) {
86
+ return Buffer.concat([PROLOGUE, prefix]);
87
+ }
88
+ /**
89
+ * The long-term X25519 key in `file`, created with mode 0600 the first time. A file that exists but does not hold a
90
+ * 32-byte key throws instead of being replaced, because a new key makes every pinned peer refuse this side.
91
+ */
92
+ function readOrCreateKeyFile(file) {
93
+ const read = () => {
94
+ const bytes = readFileSync(file);
95
+ if (bytes.length !== 32) throw new Error(`${file} does not hold a 32-byte X25519 key; move it away to make a new one`);
96
+ return noiseKeyPairFromPrivateKey(bytes);
97
+ };
98
+ try {
99
+ return read();
100
+ } catch (error) {
101
+ if (error.code !== "ENOENT") throw error;
102
+ }
103
+ mkdirSync(dirname(file), {
104
+ recursive: true,
105
+ mode: 448
106
+ });
107
+ const keyPair = generateNoiseKeyPair();
108
+ const temporary = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
109
+ writeFileSync(temporary, keyPair.privateKey, {
110
+ mode: 384,
111
+ flag: "wx"
112
+ });
113
+ try {
114
+ linkSync(temporary, file);
115
+ } catch (error) {
116
+ if (error.code !== "EEXIST") throw error;
117
+ return read();
118
+ } finally {
119
+ rmSync(temporary, { force: true });
120
+ }
121
+ return keyPair;
122
+ }
123
+ function bytesOf(data) {
124
+ if (Buffer.isBuffer(data)) return data;
125
+ if (Array.isArray(data)) return Buffer.concat(data);
126
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
127
+ return Buffer.from(data);
128
+ }
129
+ /** Why a handshake did not complete. `code` is the server's close reason when it refused this client. */
130
+ var HandshakeRefusal = class extends Error {
131
+ name = "HandshakeRefusal";
132
+ code;
133
+ constructor(code, message) {
134
+ super(message);
135
+ this.code = code;
136
+ }
137
+ };
138
+ const REFUSAL_MESSAGES = {
139
+ "unknown-secret": "The machine does not know this pairing token or setup ticket yet, or it expired.",
140
+ unauthorized: "The machine does not recognize this client. Pair again.",
141
+ "protocol-unsupported": "The machine speaks another stim-server protocol. Update it and this client, then pair again.",
142
+ "handshake-failed": "The authentication handshake failed.",
143
+ "server-key-mismatch": "The machine answered with a different server key than the one pinned at pairing. Pair again."
144
+ };
145
+ /**
146
+ * One authenticated, encrypted connection over a WebSocket, with the subset of `ws`'s WebSocket API Stim uses.
147
+ * `send` takes a string for a text message and bytes for a binary one; `message` listeners get the plaintext as a
148
+ * Buffer and whether it was binary. A message that fails to decrypt closes the connection. A socket error is emitted
149
+ * only to `error` listeners; without one it is dropped, since the socket also closes and `close` reports that.
150
+ */
151
+ var NoiseSocket = class extends EventEmitter {
152
+ CONNECTING = 0;
153
+ OPEN = 1;
154
+ CLOSING = 2;
155
+ CLOSED = 3;
156
+ /** The authenticated static public key of the other side. */
157
+ remoteKey;
158
+ #raw;
159
+ #transport;
160
+ /** Messages that arrive before the first `message` listener, such as a hello sent right after message 3. */
161
+ #held = [];
162
+ constructor(raw, transport, remoteKey) {
163
+ super();
164
+ this.#raw = raw;
165
+ this.#transport = transport;
166
+ this.remoteKey = remoteKey;
167
+ raw.on("message", (data, isBinary) => {
168
+ if (!isBinary) return void raw.close(CLOSE_BAD_REQUEST, "handshake-failed");
169
+ let message;
170
+ try {
171
+ message = transport.decrypt(bytesOf(data));
172
+ } catch {
173
+ raw.close(CLOSE_BAD_REQUEST, "decrypt-failed");
174
+ return;
175
+ }
176
+ const event = message.type === "text" ? [Buffer.from(message.data, "utf8"), false] : [message.data, true];
177
+ if (!this.#held) return void this.emit("message", ...event);
178
+ if (this.#held.length >= MAX_HELD_MESSAGES) return void raw.close(1008, "too many messages");
179
+ this.#held.push(event);
180
+ });
181
+ this.on("newListener", (event) => {
182
+ if (event !== "message" || !this.#held) return;
183
+ const held = this.#held;
184
+ this.#held = null;
185
+ queueMicrotask(() => {
186
+ for (const each of held) this.emit("message", ...each);
187
+ });
188
+ });
189
+ raw.on("close", (code, reason) => this.emit("close", code, reason));
190
+ raw.on("error", (error) => {
191
+ if (this.listenerCount("error")) this.emit("error", error);
192
+ });
193
+ raw.on("pong", () => this.emit("pong"));
194
+ }
195
+ get readyState() {
196
+ return this.#raw.readyState;
197
+ }
198
+ get bufferedAmount() {
199
+ return this.#raw.bufferedAmount;
200
+ }
201
+ /**
202
+ * Sends a string as a text message or bytes as a binary message. A message over this side's limit closes the
203
+ * connection with 1009, as a WebSocket peer does for an oversized frame.
204
+ */
205
+ send(data, options, callback) {
206
+ const done = typeof options === "function" ? options : callback;
207
+ const binary = typeof options === "object" ? options.binary : void 0;
208
+ if (this.#raw.readyState !== this.OPEN) return void done?.(/* @__PURE__ */ new Error("The connection is not open"));
209
+ let frame;
210
+ try {
211
+ frame = this.#transport.encrypt(typeof data !== "string" && binary === false ? Buffer.from(data).toString() : data);
212
+ } catch (error) {
213
+ this.#raw.close(error instanceof RangeError ? 1009 : 1011, "message not sent");
214
+ done?.(error);
215
+ return;
216
+ }
217
+ this.#raw.send(frame, done);
218
+ }
219
+ close(code, reason) {
220
+ this.#raw.close(code, reason);
221
+ }
222
+ terminate() {
223
+ this.#raw.terminate();
224
+ }
225
+ ping() {
226
+ this.#raw.ping?.();
227
+ }
228
+ };
229
+ /**
230
+ * Runs the client side of the protocol 2 handshake on an open WebSocket. It rejects with {@link HandshakeRefusal} and
231
+ * closes the socket when the server refuses or the handshake fails; the server's close reason names why.
232
+ */
233
+ function initiateNoise(raw, options) {
234
+ const first = options.secret !== void 0;
235
+ if (!first && !options.serverKey) throw new TypeError("A returning client pins the server key");
236
+ const values = first ? secretValues(options.secret) : null;
237
+ const prefix = values ? handshakePrefix(1, values.lookup) : handshakePrefix(0);
238
+ const handshake = new HandshakeState({
239
+ protocol: values ? NOISE_XX_PSK2 : NOISE_XX,
240
+ initiator: true,
241
+ prologue: handshakePrologue(prefix),
242
+ staticKeyPair: options.keyPair,
243
+ ...values ? { psk: values.psk } : {}
244
+ });
245
+ return new Promise((resolve, reject) => {
246
+ const cleanup = () => {
247
+ clearTimeout(timer);
248
+ raw.off("message", onMessage);
249
+ raw.off("close", onClose);
250
+ raw.off("error", onError);
251
+ };
252
+ const fail = (code, detail) => {
253
+ cleanup();
254
+ if (raw.readyState === 1) raw.close(CLOSE_REFUSED, code);
255
+ reject(new HandshakeRefusal(code, detail ?? REFUSAL_MESSAGES[code] ?? `The machine refused the connection (${code}).`));
256
+ };
257
+ const onClose = (code, reason) => {
258
+ const text = reason.toString();
259
+ fail(text || "closed", text ? void 0 : `The connection closed during the handshake (${code}).`);
260
+ };
261
+ const onError = (error) => fail("closed", error.message);
262
+ const onMessage = (data, isBinary) => {
263
+ if (!isBinary) {
264
+ const refusal = plaintextRefusal(bytesOf(data));
265
+ return fail(refusal?.code ?? "handshake-failed", refusal?.message);
266
+ }
267
+ try {
268
+ if (handshake.readMessage(bytesOf(data)).length) return fail("handshake-failed");
269
+ } catch {
270
+ return fail("handshake-failed");
271
+ }
272
+ const serverKey = handshake.remoteStaticPublicKey;
273
+ if (options.serverKey ? !serverKey.equals(Buffer.from(options.serverKey)) : options.acceptServerKey?.(serverKey) === false) return fail("server-key-mismatch");
274
+ const payload = first ? Buffer.from(JSON.stringify({ name: options.name ?? "" }), "utf8") : void 0;
275
+ let message3;
276
+ try {
277
+ message3 = handshake.writeMessage(payload);
278
+ } catch {
279
+ return fail("handshake-failed");
280
+ }
281
+ cleanup();
282
+ raw.send(message3);
283
+ const { send, receive } = handshake.split();
284
+ resolve(new NoiseSocket(raw, new NoiseTransport({
285
+ send,
286
+ receive,
287
+ maxSendBytes: options.maxSendBytes ?? 65536,
288
+ maxReceiveBytes: options.maxReceiveBytes ?? 16777216
289
+ }), serverKey));
290
+ };
291
+ const timer = setTimeout(() => fail("timeout", "The machine did not finish the handshake in time."), options.timeoutMs);
292
+ raw.on("message", onMessage);
293
+ raw.on("close", onClose);
294
+ raw.on("error", onError);
295
+ try {
296
+ raw.send(Buffer.concat([prefix, handshake.writeMessage()]));
297
+ } catch {
298
+ fail("handshake-failed");
299
+ }
300
+ });
301
+ }
302
+ function plaintextRefusal(data) {
303
+ try {
304
+ const message = JSON.parse(data.toString("utf8"));
305
+ return typeof message.error?.code === "string" && typeof message.error.message === "string" ? {
306
+ code: message.error.code,
307
+ message: message.error.message
308
+ } : null;
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ /** The message a protocol 1 client gets: in cleartext, since it cannot decrypt, then the socket closes. */
314
+ const PROTOCOL_UNSUPPORTED_MESSAGE = "This stim-server speaks protocol 2, with its own authentication. Update this app or Stim, then pair again.";
315
+ /**
316
+ * Runs the server side of the handshake on the first message a client sent. It closes the socket and rejects with
317
+ * {@link HandshakeRefusal} when the client speaks another protocol, names no valid secret (`unknown-secret`), or fails
318
+ * the handshake. A text first message, a protocol 1 hello, gets a cleartext `protocol-unsupported` reply.
319
+ */
320
+ function acceptNoise(raw, first, isBinary, options) {
321
+ return new Promise((resolve, reject) => {
322
+ const refuse = (code, closeCode = CLOSE_REFUSED) => {
323
+ clearTimeout(timer);
324
+ raw.off("message", onMessage);
325
+ raw.off("close", onClose);
326
+ if (raw.readyState === 1) raw.close(closeCode, code);
327
+ reject(new HandshakeRefusal(code, REFUSAL_MESSAGES[code] ?? code));
328
+ };
329
+ const onClose = () => refuse("closed");
330
+ let handshake = null;
331
+ let parsed;
332
+ const onMessage = (data, binary) => {
333
+ if (!binary || !handshake) return refuse("handshake-failed");
334
+ let payload;
335
+ try {
336
+ payload = handshake.readMessage(bytesOf(data));
337
+ } catch {
338
+ return refuse("handshake-failed");
339
+ }
340
+ clearTimeout(timer);
341
+ raw.off("message", onMessage);
342
+ raw.off("close", onClose);
343
+ const { send, receive } = handshake.split();
344
+ const clientKey = handshake.remoteStaticPublicKey;
345
+ resolve({
346
+ socket: new NoiseSocket(raw, new NoiseTransport({
347
+ send,
348
+ receive,
349
+ maxSendBytes: options.maxSendBytes ?? 16777216,
350
+ maxReceiveBytes: options.maxReceiveBytes ?? 65536
351
+ }), clientKey),
352
+ mode: parsed.mode,
353
+ ...parsed.mode === 1 ? { lookup: parsed.lookup.toString("hex") } : {},
354
+ clientKey,
355
+ payload
356
+ });
357
+ };
358
+ const timer = setTimeout(() => refuse("timeout"), options.timeoutMs);
359
+ raw.on("close", onClose);
360
+ if (!isBinary) {
361
+ const bytes = bytesOf(first);
362
+ let id = null;
363
+ try {
364
+ id = JSON.parse(bytes.toString("utf8")).id ?? null;
365
+ } catch {
366
+ id = null;
367
+ }
368
+ const reply = {
369
+ id: typeof id === "number" || typeof id === "string" ? id : null,
370
+ error: {
371
+ code: "protocol-unsupported",
372
+ message: PROTOCOL_UNSUPPORTED_MESSAGE
373
+ }
374
+ };
375
+ if (raw.readyState === 1) raw.send(JSON.stringify(reply));
376
+ return refuse("protocol-unsupported", CLOSE_BAD_REQUEST);
377
+ }
378
+ const message = parseFirstMessage(bytesOf(first));
379
+ if ("error" in message) return refuse(message.error, message.error === "protocol-unsupported" ? CLOSE_BAD_REQUEST : CLOSE_REFUSED);
380
+ parsed = message;
381
+ const psk = message.mode === 1 ? options.psk(message.lookup.toString("hex")) : null;
382
+ if (message.mode === 1 && !psk) return refuse("unknown-secret");
383
+ try {
384
+ handshake = new HandshakeState({
385
+ protocol: psk ? NOISE_XX_PSK2 : NOISE_XX,
386
+ initiator: false,
387
+ prologue: handshakePrologue(message.prefix),
388
+ staticKeyPair: options.keyPair,
389
+ ...psk ? { psk } : {},
390
+ ...options.dh ? { dh: options.dh } : {}
391
+ });
392
+ if (handshake.readMessage(message.message).length) return refuse("handshake-failed");
393
+ raw.send(handshake.writeMessage());
394
+ } catch (error) {
395
+ if (!(error instanceof NoiseError)) throw error;
396
+ return refuse("handshake-failed");
397
+ }
398
+ raw.on("message", onMessage);
399
+ });
400
+ }
401
+ //#endregion
402
+ export { CLOSE_BAD_REQUEST, CLOSE_REFUSED, HandshakeRefusal, LOOKUP_BYTES, MAX_CLIENT_MESSAGE_BYTES, MAX_FIRST_MESSAGE_BYTES, MAX_SERVER_MESSAGE_BYTES, MODE_FIRST_CONTACT, MODE_RETURNING, NoiseSocket, PROTOCOL_UNSUPPORTED_MESSAGE, SECRET_BYTES, STIM_PROTOCOL_VERSION, TRANSPORT_OVERHEAD_BYTES, acceptNoise, decodeSecret, handshakePrefix, handshakePrologue, initiateNoise, keyFingerprint, newSecret, parseFirstMessage, readOrCreateKeyFile, secretValues };
@@ -0,0 +1,2 @@
1
+ import { a as NOISE_XX, c as NoiseError, d as NoiseProtocolName, f as NoiseTransport, h as noiseKeyPairFromPrivateKey, i as MAX_HANDSHAKE_MESSAGE_BYTES, l as NoiseKeyPair, m as generateNoiseKeyPair, n as HandshakeOptions, o as NOISE_XX_PSK2, p as NoiseTransportOptions, r as HandshakeState, s as NoiseDh, t as CipherState, u as NoiseMessage } from "./noise-DYIbymAJ.mjs";
2
+ export { CipherState, HandshakeOptions, HandshakeState, MAX_HANDSHAKE_MESSAGE_BYTES, NOISE_XX, NOISE_XX_PSK2, NoiseDh, NoiseError, NoiseKeyPair, NoiseMessage, NoiseProtocolName, NoiseTransport, NoiseTransportOptions, generateNoiseKeyPair, noiseKeyPairFromPrivateKey };