@threadbase-sh/streamer 1.58.3 → 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 {
@@ -150473,7 +150717,7 @@ function createApiDeps(deps) {
150473
150717
  }
150474
150718
 
150475
150719
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
150476
- var import_crypto13 = require("crypto");
150720
+ var import_crypto14 = require("crypto");
150477
150721
  var import_fs34 = require("fs");
150478
150722
 
150479
150723
  // src/services/cache-integrity/alertStore.ts
@@ -150538,7 +150782,7 @@ function envInt(name, fallback) {
150538
150782
  }
150539
150783
  function fingerprintOf(ids) {
150540
150784
  const sorted = [...ids].sort();
150541
- 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")}`;
150542
150786
  }
150543
150787
  var CacheIntegrityMonitor = class {
150544
150788
  constructor(cache, wsHub, log15, cacheDir, rescan, runDuringReset) {
@@ -153088,7 +153332,7 @@ var StreamerServer = class {
153088
153332
  runtimeStore = null;
153089
153333
  // Identifies this streamer run. A registry row carrying a different id is a
153090
153334
  // session that outlived the process that started it.
153091
- streamerInstanceId = (0, import_crypto14.randomUUID)();
153335
+ streamerInstanceId = (0, import_crypto15.randomUUID)();
153092
153336
  cacheMetadataRepo = null;
153093
153337
  // Push registration + delivery state (C7). Null when the cache DB failed to
153094
153338
  // open — registration then degrades to a no-op rather than 500ing.
@@ -154216,6 +154460,14 @@ var StreamerServer = class {
154216
154460
  json2(res, 400, { error: "Missing token or clientPublicKey" });
154217
154461
  return;
154218
154462
  }
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
+ }
154219
154471
  const precheck = this.pairTokens.wouldConsume(token);
154220
154472
  if (!precheck.ok) {
154221
154473
  if (precheck.reason === "used") {
@@ -154227,6 +154479,25 @@ var StreamerServer = class {
154227
154479
  json2(res, 401, { error: `Pair token ${precheck.reason}` });
154228
154480
  return;
154229
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
+ }
154230
154501
  let sealed;
154231
154502
  try {
154232
154503
  sealed = seal(this.apiKey, clientPublicKey);
@@ -154250,14 +154521,59 @@ var StreamerServer = class {
154250
154521
  try {
154251
154522
  const name = typeof body?.deviceName === "string" ? body.deviceName.slice(0, 100) : null;
154252
154523
  const preset = body?.readOnly === true ? "read-only" : "full";
154253
- 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;
154254
154540
  } catch (err) {
154255
154541
  this.log.warn("[pair] device registration failed; pairing continues", {
154256
154542
  event: "pair.device_register_failed",
154257
154543
  err
154258
154544
  });
154259
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
+ }
154260
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.
154261
154577
  ciphertext: sealed.ciphertext,
154262
154578
  nonce: sealed.nonce,
154263
154579
  ephemeralPublicKey: sealed.ephemeralPublicKey,
@@ -154280,7 +154596,11 @@ var StreamerServer = class {
154280
154596
  deviceId: device.deviceId,
154281
154597
  deviceToken: device.deviceToken,
154282
154598
  capabilities: device.capabilities
154283
- }
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 }
154284
154604
  });
154285
154605
  }
154286
154606
  rotateApiKey() {