@threadbase-sh/streamer 1.58.2 → 1.59.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/cli.cjs CHANGED
@@ -31270,7 +31270,7 @@ var require_websocket = __commonJS({
31270
31270
  var http = require("http");
31271
31271
  var net = require("net");
31272
31272
  var tls = require("tls");
31273
- var { randomBytes: randomBytes5, createHash: createHash7 } = require("crypto");
31273
+ var { randomBytes: randomBytes5, createHash: createHash8 } = require("crypto");
31274
31274
  var { Duplex, Readable: Readable2 } = require("stream");
31275
31275
  var { URL: URL2 } = require("url");
31276
31276
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -31938,7 +31938,7 @@ var require_websocket = __commonJS({
31938
31938
  abortHandshake(websocket, socket, "Invalid Upgrade header");
31939
31939
  return;
31940
31940
  }
31941
- const digest = createHash7("sha1").update(key + GUID).digest("base64");
31941
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
31942
31942
  if (res.headers["sec-websocket-accept"] !== digest) {
31943
31943
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
31944
31944
  return;
@@ -32229,7 +32229,7 @@ var require_stream = __commonJS({
32229
32229
  callback();
32230
32230
  if (duplex._readableState.endEmitted) duplex.destroy();
32231
32231
  } else {
32232
- ws2._socket.once("finish", function finish() {
32232
+ ws2._socket.once("finish", function finish2() {
32233
32233
  callback();
32234
32234
  });
32235
32235
  ws2.close();
@@ -32307,7 +32307,7 @@ var require_websocket_server = __commonJS({
32307
32307
  var EventEmitter5 = require("events");
32308
32308
  var http = require("http");
32309
32309
  var { Duplex } = require("stream");
32310
- var { createHash: createHash7 } = require("crypto");
32310
+ var { createHash: createHash8 } = require("crypto");
32311
32311
  var extension2 = require_extension();
32312
32312
  var PerMessageDeflate2 = require_permessage_deflate();
32313
32313
  var subprotocol2 = require_subprotocol();
@@ -32614,7 +32614,7 @@ var require_websocket_server = __commonJS({
32614
32614
  );
32615
32615
  }
32616
32616
  if (this._state > RUNNING) return abortHandshake(socket, 503);
32617
- const digest = createHash7("sha1").update(key + GUID).digest("base64");
32617
+ const digest = createHash8("sha1").update(key + GUID).digest("base64");
32618
32618
  const headers = [
32619
32619
  "HTTP/1.1 101 Switching Protocols",
32620
32620
  "Upgrade: websocket",
@@ -42935,7 +42935,7 @@ var require_writer = __commonJS({
42935
42935
  }
42936
42936
  return this;
42937
42937
  };
42938
- Writer.prototype.finish = function finish() {
42938
+ Writer.prototype.finish = function finish2() {
42939
42939
  var head = this.head.next, buf = this.constructor.alloc(this.len), pos = 0;
42940
42940
  while (head) {
42941
42941
  head.fn(head.val, buf, pos);
@@ -44971,7 +44971,7 @@ var require_root = __commonJS({
44971
44971
  return util.asPromise(load, self2, filename, options);
44972
44972
  }
44973
44973
  var sync = callback === SYNC;
44974
- function finish(err, root) {
44974
+ function finish2(err, root) {
44975
44975
  if (!callback) {
44976
44976
  return;
44977
44977
  }
@@ -45018,10 +45018,10 @@ var require_root = __commonJS({
45018
45018
  }
45019
45019
  }
45020
45020
  } catch (err) {
45021
- finish(err);
45021
+ finish2(err);
45022
45022
  }
45023
45023
  if (!sync && !queued) {
45024
- finish(null, self2);
45024
+ finish2(null, self2);
45025
45025
  }
45026
45026
  }
45027
45027
  function fetch2(filename2, weak, depth) {
@@ -45050,7 +45050,7 @@ var require_root = __commonJS({
45050
45050
  source = util.fs.readFileSync(filename2).toString("utf8");
45051
45051
  } catch (err) {
45052
45052
  if (!weak)
45053
- finish(err);
45053
+ finish2(err);
45054
45054
  return;
45055
45055
  }
45056
45056
  process4(filename2, source, depth);
@@ -45063,9 +45063,9 @@ var require_root = __commonJS({
45063
45063
  }
45064
45064
  if (err) {
45065
45065
  if (!weak)
45066
- finish(err);
45066
+ finish2(err);
45067
45067
  else if (!queued)
45068
- finish(null, self2);
45068
+ finish2(null, self2);
45069
45069
  return;
45070
45070
  }
45071
45071
  process4(filename2, source2, depth);
@@ -45084,7 +45084,7 @@ var require_root = __commonJS({
45084
45084
  return self2;
45085
45085
  }
45086
45086
  if (!queued) {
45087
- finish(null, self2);
45087
+ finish2(null, self2);
45088
45088
  }
45089
45089
  return self2;
45090
45090
  };
@@ -120687,11 +120687,11 @@ function toPublicSession(s3) {
120687
120687
  subStatus: s3.subStatus ?? null
120688
120688
  };
120689
120689
  }
120690
- var import_crypto10, import_fs26, import_path26, OUTPUT_BUFFER_MAX, INPUT_HISTORY_MAX, QUIET_DETECT_MS, CODEX_READY_FALLBACK_MS, SUBMIT_BYTES, CODEX_SUBMIT_DELAY_MS, CODEX_SUBMIT_MAX_WAIT_MS, CODEX_SUBMIT_STALE_MS, CodexPtyRunner;
120690
+ var import_crypto11, import_fs26, import_path26, OUTPUT_BUFFER_MAX, INPUT_HISTORY_MAX, QUIET_DETECT_MS, CODEX_READY_FALLBACK_MS, SUBMIT_BYTES, CODEX_SUBMIT_DELAY_MS, CODEX_SUBMIT_MAX_WAIT_MS, CODEX_SUBMIT_STALE_MS, CodexPtyRunner;
120691
120691
  var init_codex_pty_runner = __esm({
120692
120692
  "src/codex-pty-runner.ts"() {
120693
120693
  "use strict";
120694
- import_crypto10 = require("crypto");
120694
+ import_crypto11 = require("crypto");
120695
120695
  import_fs26 = require("fs");
120696
120696
  import_path26 = require("path");
120697
120697
  init_logger();
@@ -120856,7 +120856,7 @@ var init_codex_pty_runner = __esm({
120856
120856
  // binding logic). This runner generates a local placeholder id for the
120857
120857
  // ManagedSession handle only.
120858
120858
  async startFresh(options) {
120859
- const sessionId = (0, import_crypto10.randomUUID)();
120859
+ const sessionId = (0, import_crypto11.randomUUID)();
120860
120860
  const args = ["--cd", options.projectPath, "--no-alt-screen"];
120861
120861
  if (options.systemPrompt) {
120862
120862
  args.push(options.systemPrompt);
@@ -120875,7 +120875,7 @@ var init_codex_pty_runner = __esm({
120875
120875
  * until watchForCodexRollout binds the real id.
120876
120876
  */
120877
120877
  async startFork(options) {
120878
- const sessionId = (0, import_crypto10.randomUUID)();
120878
+ const sessionId = (0, import_crypto11.randomUUID)();
120879
120879
  return this.launch(
120880
120880
  sessionId,
120881
120881
  ["fork", options.forkFromId, "--cd", options.projectPath, "--no-alt-screen"],
@@ -122102,11 +122102,11 @@ function toPublicSession2(s3) {
122102
122102
  subStatus: s3.subStatus ?? null
122103
122103
  };
122104
122104
  }
122105
- var import_crypto11, import_fs27, import_path27, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, PTYManager;
122105
+ var import_crypto12, import_fs27, import_path27, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, PTYManager;
122106
122106
  var init_pty_manager = __esm({
122107
122107
  "src/pty-manager.ts"() {
122108
122108
  "use strict";
122109
- import_crypto11 = require("crypto");
122109
+ import_crypto12 = require("crypto");
122110
122110
  import_fs27 = require("fs");
122111
122111
  import_path27 = require("path");
122112
122112
  init_claude_flags();
@@ -122296,7 +122296,7 @@ var init_pty_manager = __esm({
122296
122296
  // onReady fires once Claude reaches its first prompt (waiting_input).
122297
122297
  async startFresh(options) {
122298
122298
  const nodePty = await loadPty();
122299
- const sessionId = (0, import_crypto11.randomUUID)();
122299
+ const sessionId = (0, import_crypto12.randomUUID)();
122300
122300
  const projectName = options.projectName ?? (0, import_path27.basename)(options.projectPath);
122301
122301
  const permissionMode = options.permissionMode ?? "acceptEdits";
122302
122302
  const args = [
@@ -123190,7 +123190,7 @@ async function probePtyHostStatus(socketPath, timeoutMs = 1e3) {
123190
123190
  return await new Promise((resolve4) => {
123191
123191
  const decoder = new LineDecoder();
123192
123192
  let settled = false;
123193
- const finish = (result) => {
123193
+ const finish2 = (result) => {
123194
123194
  if (settled) return;
123195
123195
  settled = true;
123196
123196
  clearTimeout(timer);
@@ -123198,31 +123198,31 @@ async function probePtyHostStatus(socketPath, timeoutMs = 1e3) {
123198
123198
  resolve4(result);
123199
123199
  };
123200
123200
  const timer = setTimeout(
123201
- () => finish({ reachable: false, error: `status timed out after ${timeoutMs}ms` }),
123201
+ () => finish2({ reachable: false, error: `status timed out after ${timeoutMs}ms` }),
123202
123202
  timeoutMs
123203
123203
  );
123204
123204
  timer.unref?.();
123205
- transport.onClose(() => finish({ reachable: false, error: "pty-host connection closed" }));
123205
+ transport.onClose(() => finish2({ reachable: false, error: "pty-host connection closed" }));
123206
123206
  transport.onLine((chunk) => {
123207
123207
  for (const line of decoder.push(chunk)) {
123208
123208
  let message;
123209
123209
  try {
123210
123210
  message = JSON.parse(line);
123211
123211
  } catch {
123212
- finish({ reachable: false, error: "pty-host returned an invalid status response" });
123212
+ finish2({ reachable: false, error: "pty-host returned an invalid status response" });
123213
123213
  return;
123214
123214
  }
123215
123215
  if (isHostEvent(message) || message.id !== 1) continue;
123216
123216
  if (!message.ok) {
123217
- finish({ reachable: false, error: message.error });
123217
+ finish2({ reachable: false, error: message.error });
123218
123218
  return;
123219
123219
  }
123220
123220
  const status = message.result;
123221
123221
  if (typeof status?.protocolVersion !== "number" || !Array.isArray(status.sessions)) {
123222
- finish({ reachable: false, error: "pty-host returned an invalid status response" });
123222
+ finish2({ reachable: false, error: "pty-host returned an invalid status response" });
123223
123223
  return;
123224
123224
  }
123225
- finish({
123225
+ finish2({
123226
123226
  reachable: true,
123227
123227
  protocolVersion: status.protocolVersion,
123228
123228
  sessionCount: status.sessions.length
@@ -123232,7 +123232,7 @@ async function probePtyHostStatus(socketPath, timeoutMs = 1e3) {
123232
123232
  try {
123233
123233
  transport.send(encodeMessage({ id: 1, type: "status" }));
123234
123234
  } catch (err) {
123235
- finish({ reachable: false, error: err instanceof Error ? err.message : String(err) });
123235
+ finish2({ reachable: false, error: err instanceof Error ? err.message : String(err) });
123236
123236
  }
123237
123237
  });
123238
123238
  }
@@ -132212,7 +132212,7 @@ Content-Length: 0\r
132212
132212
 
132213
132213
  // src/server.ts
132214
132214
  var import_client = __toESM(require_lib3(), 1);
132215
- var import_crypto14 = require("crypto");
132215
+ var import_crypto15 = require("crypto");
132216
132216
  var import_events2 = require("events");
132217
132217
  var import_fs39 = require("fs");
132218
132218
  var import_promises16 = require("fs/promises");
@@ -148910,6 +148910,250 @@ var SessionsRepository = class {
148910
148910
  // src/server.ts
148911
148911
  init_runtime_store();
148912
148912
 
148913
+ // src/e2ee/noise.ts
148914
+ var import_crypto10 = require("crypto");
148915
+ var NOISE_PROTOCOL_NAME = "Noise_IKpsk1_25519_ChaChaPoly_SHA256";
148916
+ var PAIR_PROLOGUE = Buffer.from("threadbase-e2ee/1 pair", "utf-8");
148917
+ var DHLEN = 32;
148918
+ var HASHLEN = 32;
148919
+ var TAGLEN = 16;
148920
+ var NOISE_MESSAGE_1_OVERHEAD = DHLEN + DHLEN + TAGLEN + TAGLEN;
148921
+ var NOISE_MESSAGE_2_OVERHEAD = DHLEN + TAGLEN;
148922
+ var NOISE_MAX_MESSAGE_BYTES = 4096;
148923
+ var NoiseError = class extends Error {
148924
+ constructor(message) {
148925
+ super(message);
148926
+ this.name = "NoiseError";
148927
+ }
148928
+ };
148929
+ function generateKeyPair() {
148930
+ const { publicKey, privateKey } = (0, import_crypto10.generateKeyPairSync)("x25519");
148931
+ return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };
148932
+ }
148933
+ function keyPairFrom(privateKey) {
148934
+ const publicKey = (0, import_crypto10.createPublicKey)(privateKey);
148935
+ return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };
148936
+ }
148937
+ function rawPublicKey(key) {
148938
+ const pub = key.type === "public" ? key : (0, import_crypto10.createPublicKey)(key);
148939
+ const jwk = pub.export({ format: "jwk" });
148940
+ if (jwk.crv !== "X25519" || typeof jwk.x !== "string") {
148941
+ throw new NoiseError("Not an X25519 public key");
148942
+ }
148943
+ return Buffer.from(jwk.x, "base64url");
148944
+ }
148945
+ function publicKeyFromRaw(raw2) {
148946
+ if (raw2.length !== DHLEN) {
148947
+ throw new NoiseError(`X25519 public key must be ${DHLEN} bytes, got ${raw2.length}`);
148948
+ }
148949
+ try {
148950
+ return (0, import_crypto10.createPublicKey)({
148951
+ key: { kty: "OKP", crv: "X25519", x: raw2.toString("base64url") },
148952
+ format: "jwk"
148953
+ });
148954
+ } catch {
148955
+ throw new NoiseError("Invalid X25519 public key");
148956
+ }
148957
+ }
148958
+ function pskFromPairToken(token) {
148959
+ return (0, import_crypto10.createHash)("sha256").update("threadbase-e2ee/1 psk", "utf-8").update(token, "utf-8").digest();
148960
+ }
148961
+ function chachaNonce(n) {
148962
+ const nonce = Buffer.alloc(12);
148963
+ nonce.writeBigUInt64LE(n, 4);
148964
+ return nonce;
148965
+ }
148966
+ var CipherState = class {
148967
+ k = null;
148968
+ n = 0n;
148969
+ initializeKey(key) {
148970
+ this.k = key;
148971
+ this.n = 0n;
148972
+ }
148973
+ hasKey() {
148974
+ return this.k !== null;
148975
+ }
148976
+ encryptWithAd(ad, plaintext) {
148977
+ if (!this.k) return plaintext;
148978
+ const cipher = (0, import_crypto10.createCipheriv)("chacha20-poly1305", this.k, chachaNonce(this.n), {
148979
+ authTagLength: TAGLEN
148980
+ });
148981
+ cipher.setAAD(ad, { plaintextLength: plaintext.length });
148982
+ const out = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
148983
+ this.n += 1n;
148984
+ return out;
148985
+ }
148986
+ decryptWithAd(ad, ciphertext) {
148987
+ if (!this.k) return ciphertext;
148988
+ if (ciphertext.length < TAGLEN) throw new NoiseError("Ciphertext shorter than its tag");
148989
+ const body = ciphertext.subarray(0, ciphertext.length - TAGLEN);
148990
+ const tag = ciphertext.subarray(ciphertext.length - TAGLEN);
148991
+ const decipher = (0, import_crypto10.createDecipheriv)("chacha20-poly1305", this.k, chachaNonce(this.n), {
148992
+ authTagLength: TAGLEN
148993
+ });
148994
+ decipher.setAAD(ad, { plaintextLength: body.length });
148995
+ decipher.setAuthTag(tag);
148996
+ let out;
148997
+ try {
148998
+ out = Buffer.concat([decipher.update(body), decipher.final()]);
148999
+ } catch {
149000
+ throw new NoiseError("Decryption failed");
149001
+ }
149002
+ this.n += 1n;
149003
+ return out;
149004
+ }
149005
+ };
149006
+ var SymmetricState = class {
149007
+ ck;
149008
+ h;
149009
+ cipher = new CipherState();
149010
+ constructor(protocolName) {
149011
+ const name = Buffer.from(protocolName, "utf-8");
149012
+ this.h = name.length <= HASHLEN ? Buffer.concat([name, Buffer.alloc(HASHLEN - name.length)]) : (0, import_crypto10.createHash)("sha256").update(name).digest();
149013
+ this.ck = Buffer.from(this.h);
149014
+ this.cipher.initializeKey(null);
149015
+ }
149016
+ /**
149017
+ * Noise's HKDF (spec §4.3) is RFC 5869 with `salt = chaining_key`,
149018
+ * `ikm = input_key_material` and an EMPTY info, so Node's `hkdfSync` is
149019
+ * exactly it rather than approximately it. Using the platform's HKDF removes
149020
+ * the HMAC chain that would otherwise be the easiest thing here to get
149021
+ * subtly wrong.
149022
+ */
149023
+ hkdf(ikm, outputs) {
149024
+ const raw2 = Buffer.from((0, import_crypto10.hkdfSync)("sha256", ikm, this.ck, Buffer.alloc(0), HASHLEN * outputs));
149025
+ const out = [];
149026
+ for (let i = 0; i < outputs; i++) out.push(raw2.subarray(i * HASHLEN, (i + 1) * HASHLEN));
149027
+ return out;
149028
+ }
149029
+ mixKey(ikm) {
149030
+ const [ck, tempK] = this.hkdf(ikm, 2);
149031
+ this.ck = ck;
149032
+ this.cipher.initializeKey(tempK);
149033
+ }
149034
+ mixHash(data) {
149035
+ this.h = (0, import_crypto10.createHash)("sha256").update(this.h).update(data).digest();
149036
+ }
149037
+ /** Spec §5.2. Used only by the `psk` token. */
149038
+ mixKeyAndHash(ikm) {
149039
+ const [ck, tempH, tempK] = this.hkdf(ikm, 3);
149040
+ this.ck = ck;
149041
+ this.mixHash(tempH);
149042
+ this.cipher.initializeKey(tempK);
149043
+ }
149044
+ encryptAndHash(plaintext) {
149045
+ const ciphertext = this.cipher.encryptWithAd(this.h, plaintext);
149046
+ this.mixHash(ciphertext);
149047
+ return ciphertext;
149048
+ }
149049
+ decryptAndHash(ciphertext) {
149050
+ const plaintext = this.cipher.decryptWithAd(this.h, ciphertext);
149051
+ this.mixHash(ciphertext);
149052
+ return plaintext;
149053
+ }
149054
+ /** The transcript hash. Both sides must arrive at the same value. */
149055
+ handshakeHash() {
149056
+ return Buffer.from(this.h);
149057
+ }
149058
+ /** Spec §5.2 `Split()`: the two directional transport keys. */
149059
+ split() {
149060
+ const [k1, k2] = this.hkdf(Buffer.alloc(0), 2);
149061
+ return { k1: Buffer.from(k1), k2: Buffer.from(k2) };
149062
+ }
149063
+ };
149064
+ function dh(privateKey, publicKey) {
149065
+ return (0, import_crypto10.diffieHellman)({ privateKey, publicKey });
149066
+ }
149067
+ function readMessage1(args) {
149068
+ assertMessageSize(args.message1, NOISE_MESSAGE_1_OVERHEAD);
149069
+ const state = new SymmetricState(NOISE_PROTOCOL_NAME);
149070
+ state.mixHash(args.prologue ?? PAIR_PROLOGUE);
149071
+ state.mixHash(args.staticKeyPair.publicKeyRaw);
149072
+ const reRaw = args.message1.subarray(0, DHLEN);
149073
+ const re2 = publicKeyFromRaw(reRaw);
149074
+ state.mixHash(reRaw);
149075
+ state.mixKey(reRaw);
149076
+ state.mixKey(dh(args.staticKeyPair.privateKey, re2));
149077
+ const encryptedStatic = args.message1.subarray(DHLEN, DHLEN + DHLEN + TAGLEN);
149078
+ const initiatorStaticPub = state.decryptAndHash(encryptedStatic);
149079
+ const rs2 = publicKeyFromRaw(initiatorStaticPub);
149080
+ state.mixKey(dh(args.staticKeyPair.privateKey, rs2));
149081
+ state.mixKeyAndHash(args.psk);
149082
+ const payload = state.decryptAndHash(args.message1.subarray(DHLEN + DHLEN + TAGLEN));
149083
+ return {
149084
+ symmetric: state,
149085
+ initiatorStaticPub,
149086
+ payload,
149087
+ initiatorEphemeral: re2,
149088
+ initiatorStatic: rs2,
149089
+ staticKeyPair: args.staticKeyPair
149090
+ };
149091
+ }
149092
+ function writeMessage2(state, responsePayload, ephemeral) {
149093
+ const e = ephemeral ?? generateKeyPair();
149094
+ state.symmetric.mixHash(e.publicKeyRaw);
149095
+ state.symmetric.mixKey(e.publicKeyRaw);
149096
+ state.symmetric.mixKey(dh(e.privateKey, state.initiatorEphemeral));
149097
+ state.symmetric.mixKey(dh(e.privateKey, state.initiatorStatic));
149098
+ const encryptedPayload = state.symmetric.encryptAndHash(responsePayload);
149099
+ return {
149100
+ message2: Buffer.concat([e.publicKeyRaw, encryptedPayload]),
149101
+ keys: finish(state.symmetric)
149102
+ };
149103
+ }
149104
+ function finish(state) {
149105
+ const { k1, k2 } = state.split();
149106
+ return { handshakeHash: state.handshakeHash(), clientToServer: k1, serverToClient: k2 };
149107
+ }
149108
+ function assertMessageSize(message, minimum) {
149109
+ if (message.length < minimum) {
149110
+ throw new NoiseError(`Handshake message too short: ${message.length} < ${minimum}`);
149111
+ }
149112
+ if (message.length > NOISE_MAX_MESSAGE_BYTES) {
149113
+ throw new NoiseError(`Handshake message too large: ${message.length}`);
149114
+ }
149115
+ }
149116
+
149117
+ // src/e2ee/pair-request.ts
149118
+ var E2EE_EXCHANGE_VERSION = 1;
149119
+ var MAX_BASE64_CHARS = Math.ceil(NOISE_MAX_MESSAGE_BYTES * 4 / 3) + 4;
149120
+ var E2eeRequestError = class extends Error {
149121
+ /** Stable code for the client, so a version mismatch is not a parse failure. */
149122
+ code;
149123
+ constructor(code, message) {
149124
+ super(message);
149125
+ this.name = "E2eeRequestError";
149126
+ this.code = code;
149127
+ }
149128
+ };
149129
+ function parseE2eeRequest(raw2) {
149130
+ if (raw2 === void 0 || raw2 === null) return null;
149131
+ if (typeof raw2 !== "object" || Array.isArray(raw2)) {
149132
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee must be an object");
149133
+ }
149134
+ const { v: v2, noise } = raw2;
149135
+ if (typeof v2 !== "number" || !Number.isInteger(v2)) {
149136
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.v must be an integer");
149137
+ }
149138
+ if (v2 !== E2EE_EXCHANGE_VERSION) {
149139
+ throw new E2eeRequestError(
149140
+ "E2EE_VERSION_UNSUPPORTED",
149141
+ `e2ee.v ${v2} is not supported; this server speaks ${E2EE_EXCHANGE_VERSION}`
149142
+ );
149143
+ }
149144
+ if (typeof noise !== "string") {
149145
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.noise must be a base64 string");
149146
+ }
149147
+ if (noise.length > MAX_BASE64_CHARS) {
149148
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.noise is too large");
149149
+ }
149150
+ const message1 = Buffer.from(noise, "base64");
149151
+ if (message1.length === 0) {
149152
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.noise is not valid base64");
149153
+ }
149154
+ return { version: v2, message1 };
149155
+ }
149156
+
148913
149157
  // src/external-tails.ts
148914
149158
  var import_fs24 = require("fs");
148915
149159
  init_logger();
@@ -149087,7 +149331,7 @@ init_live_session_manager();
149087
149331
  init_logger();
149088
149332
 
149089
149333
  // src/pair-store.ts
149090
- var import_crypto12 = require("crypto");
149334
+ var import_crypto13 = require("crypto");
149091
149335
  var DEFAULT_TTL_SECONDS = 180;
149092
149336
  var SWEEP_INTERVAL_MS = 6e4;
149093
149337
  var PairTokenStore = class {
@@ -149102,7 +149346,7 @@ var PairTokenStore = class {
149102
149346
  }
149103
149347
  }
149104
149348
  mint() {
149105
- const token = `pt_${(0, import_crypto12.randomBytes)(16).toString("hex")}`;
149349
+ const token = `pt_${(0, import_crypto13.randomBytes)(16).toString("hex")}`;
149106
149350
  const expiresAt = Date.now() + this.ttlMs;
149107
149351
  this.current = { token, expiresAt, used: false };
149108
149352
  return {
@@ -149112,7 +149356,7 @@ var PairTokenStore = class {
149112
149356
  };
149113
149357
  }
149114
149358
  /**
149115
- * Whether `consume` would succeed right now, WITHOUT spending the token.
149359
+ * What `consume` would answer right now, WITHOUT spending the token.
149116
149360
  *
149117
149361
  * Exists so a caller can reject a bad token before doing any work, and still
149118
149362
  * spend the token only once the work has succeeded. A pair token is
@@ -149121,10 +149365,14 @@ var PairTokenStore = class {
149121
149365
  * indistinguishable from an attacker replaying a photographed code, which is
149122
149366
  * the one signal `design.md` §2.6 designates as replay detection.
149123
149367
  *
149124
- * Advisory, not a reservation: it takes no lock and holds nothing. The
149125
- * authoritative answer is still `consume`'s.
149368
+ * **Named for its relationship to `consume`, deliberately.** It reserves
149369
+ * nothing and takes no lock, so two callers can both be told `{ ok: true }`
149370
+ * for the same token. A name like `verify` would read as a gate that had
149371
+ * decided something, and inviting a caller to act on this alone is precisely
149372
+ * the misuse the ordering it enables exists to prevent. A reader who sees
149373
+ * `wouldConsume` asks where the `consume` is, which is the right question.
149126
149374
  */
149127
- verify(token) {
149375
+ wouldConsume(token) {
149128
149376
  const result = this.check(token);
149129
149377
  return result.ok ? { ok: true } : result;
149130
149378
  }
@@ -150469,7 +150717,7 @@ function createApiDeps(deps) {
150469
150717
  }
150470
150718
 
150471
150719
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
150472
- var import_crypto13 = require("crypto");
150720
+ var import_crypto14 = require("crypto");
150473
150721
  var import_fs34 = require("fs");
150474
150722
 
150475
150723
  // src/services/cache-integrity/alertStore.ts
@@ -150534,7 +150782,7 @@ function envInt(name, fallback) {
150534
150782
  }
150535
150783
  function fingerprintOf(ids) {
150536
150784
  const sorted = [...ids].sort();
150537
- return `sha256:${(0, import_crypto13.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
150785
+ return `sha256:${(0, import_crypto14.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
150538
150786
  }
150539
150787
  var CacheIntegrityMonitor = class {
150540
150788
  constructor(cache, wsHub, log15, cacheDir, rescan, runDuringReset) {
@@ -153084,7 +153332,7 @@ var StreamerServer = class {
153084
153332
  runtimeStore = null;
153085
153333
  // Identifies this streamer run. A registry row carrying a different id is a
153086
153334
  // session that outlived the process that started it.
153087
- streamerInstanceId = (0, import_crypto14.randomUUID)();
153335
+ streamerInstanceId = (0, import_crypto15.randomUUID)();
153088
153336
  cacheMetadataRepo = null;
153089
153337
  // Push registration + delivery state (C7). Null when the cache DB failed to
153090
153338
  // open — registration then degrades to a no-op rather than 500ing.
@@ -154212,11 +154460,44 @@ var StreamerServer = class {
154212
154460
  json2(res, 400, { error: "Missing token or clientPublicKey" });
154213
154461
  return;
154214
154462
  }
154215
- const precheck = this.pairTokens.verify(token);
154463
+ let e2eeRequest;
154464
+ try {
154465
+ e2eeRequest = parseE2eeRequest(body?.e2ee);
154466
+ } catch (err) {
154467
+ const e = err;
154468
+ json2(res, 400, { error: e.message, code: e.code });
154469
+ return;
154470
+ }
154471
+ const precheck = this.pairTokens.wouldConsume(token);
154216
154472
  if (!precheck.ok) {
154473
+ if (precheck.reason === "used") {
154474
+ this.log.warn(
154475
+ "[pair] a pair token was replayed. If you did not just pair a device, check the paired-devices list and revoke anything you do not recognise.",
154476
+ { event: "pair.token_replayed", ip }
154477
+ );
154478
+ }
154217
154479
  json2(res, 401, { error: `Pair token ${precheck.reason}` });
154218
154480
  return;
154219
154481
  }
154482
+ let handshake = null;
154483
+ if (e2eeRequest) {
154484
+ try {
154485
+ handshake = readMessage1({
154486
+ staticKeyPair: keyPairFrom(loadOrCreateServerIdentity().privateKey),
154487
+ // The pair token binds this handshake to the scanned QR. Derivation
154488
+ // is specified in design.md §2.4 and pinned by a committed vector
154489
+ // that tb-mobile checks against independently.
154490
+ psk: pskFromPairToken(token),
154491
+ message1: e2eeRequest.message1
154492
+ });
154493
+ } catch {
154494
+ json2(res, 400, {
154495
+ error: "E2EE handshake failed. Scan a fresh pairing code and try again.",
154496
+ code: "E2EE_HANDSHAKE_FAILED"
154497
+ });
154498
+ return;
154499
+ }
154500
+ }
154220
154501
  let sealed;
154221
154502
  try {
154222
154503
  sealed = seal(this.apiKey, clientPublicKey);
@@ -154240,14 +154521,59 @@ var StreamerServer = class {
154240
154521
  try {
154241
154522
  const name = typeof body?.deviceName === "string" ? body.deviceName.slice(0, 100) : null;
154242
154523
  const preset = body?.readOnly === true ? "read-only" : "full";
154243
- device = this.devicesRepo?.register({ publicKey: clientPublicKey, name, preset }) ?? null;
154524
+ device = this.devicesRepo?.register({
154525
+ publicKey: clientPublicKey,
154526
+ name,
154527
+ preset,
154528
+ // Recorded only when the handshake authenticated it. The static key
154529
+ // comes out of the transcript, never off the wire as a claim — that
154530
+ // is the difference between a device identified by a key it proved
154531
+ // it holds and one identified by a string it sent.
154532
+ //
154533
+ // Setting it also sets `e2ee_required`, so a device that has once
154534
+ // paired encrypted is pinned and never served plaintext again.
154535
+ ...handshake && {
154536
+ e2eeStaticPub: handshake.initiatorStaticPub.toString("base64"),
154537
+ e2eeVersion: E2EE_EXCHANGE_VERSION
154538
+ }
154539
+ }) ?? null;
154244
154540
  } catch (err) {
154245
154541
  this.log.warn("[pair] device registration failed; pairing continues", {
154246
154542
  event: "pair.device_register_failed",
154247
154543
  err
154248
154544
  });
154249
154545
  }
154546
+ let e2eeResponse = null;
154547
+ if (handshake) {
154548
+ try {
154549
+ const { message2 } = writeMessage2(
154550
+ handshake,
154551
+ Buffer.from(
154552
+ JSON.stringify({
154553
+ v: E2EE_EXCHANGE_VERSION,
154554
+ deviceId: device?.deviceId ?? null,
154555
+ serverVersion: getVersion(),
154556
+ // Always true on this path: completing a handshake is what pins
154557
+ // the device, and design.md §6.3 says nothing a client sends
154558
+ // ever clears it.
154559
+ e2eeRequired: true
154560
+ }),
154561
+ "utf-8"
154562
+ )
154563
+ );
154564
+ e2eeResponse = { v: E2EE_EXCHANGE_VERSION, noise: message2.toString("base64") };
154565
+ } catch (err) {
154566
+ this.log.warn("[pair] E2EE response could not be written; pairing continues", {
154567
+ event: "pair.e2ee_response_failed",
154568
+ err
154569
+ });
154570
+ }
154571
+ }
154250
154572
  json2(res, 200, {
154573
+ // Unchanged and still sent on the E2EE path, deliberately. An older app
154574
+ // is the only thing that can read these and it cannot be force-updated
154575
+ // (docs/compatibility/tb-mobile.md); a new app ignores them and uses the
154576
+ // Noise result. The response grows a field, it never loses one.
154251
154577
  ciphertext: sealed.ciphertext,
154252
154578
  nonce: sealed.nonce,
154253
154579
  ephemeralPublicKey: sealed.ephemeralPublicKey,
@@ -154270,7 +154596,11 @@ var StreamerServer = class {
154270
154596
  deviceId: device.deviceId,
154271
154597
  deviceToken: device.deviceToken,
154272
154598
  capabilities: device.capabilities
154273
- }
154599
+ },
154600
+ // Additive. Absent means this pairing is plaintext — either the client
154601
+ // never asked, or writing the reply failed — and a client that asked for
154602
+ // encryption must treat its absence as a refusal, not as consent.
154603
+ ...e2eeResponse && { e2ee: e2eeResponse }
154274
154604
  });
154275
154605
  }
154276
154606
  rotateApiKey() {