@threadbase-sh/streamer 1.58.3 → 1.59.1

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");
@@ -144317,7 +144317,7 @@ var ConversationCache = class _ConversationCache {
144317
144317
  "SELECT id, project_path, project_id, last_activity FROM conversation_meta WHERE project_path IS NOT NULL"
144318
144318
  ),
144319
144319
  hasOrphanProjectId: db.prepare(
144320
- "SELECT 1 FROM conversation_meta WHERE project_id IS NULL AND project_path IS NOT NULL LIMIT 1"
144320
+ "SELECT 1 FROM conversation_meta WHERE project_id IS NULL AND project_path IS NOT NULL AND project_path != '' LIMIT 1"
144321
144321
  ),
144322
144322
  popularProjects: db.prepare(
144323
144323
  `SELECT project_path, project_name, COUNT(*) as cnt
@@ -144914,10 +144914,11 @@ var ConversationCache = class _ConversationCache {
144914
144914
  continue;
144915
144915
  }
144916
144916
  const scannerMetaJson = JSON.stringify(m2);
144917
+ const projectPath = m2.projectPath?.trim() || null;
144917
144918
  this.stmts.upsertFull.run({
144918
144919
  id,
144919
144920
  file_path: canonicalPath2,
144920
- project_path: m2.projectPath ?? null,
144921
+ project_path: projectPath,
144921
144922
  project_name: m2.projectName ?? null,
144922
144923
  title: m2.title ?? m2.sessionName ?? m2.projectName ?? null,
144923
144924
  model: m2.model ?? null,
@@ -148910,6 +148911,250 @@ var SessionsRepository = class {
148910
148911
  // src/server.ts
148911
148912
  init_runtime_store();
148912
148913
 
148914
+ // src/e2ee/noise.ts
148915
+ var import_crypto10 = require("crypto");
148916
+ var NOISE_PROTOCOL_NAME = "Noise_IKpsk1_25519_ChaChaPoly_SHA256";
148917
+ var PAIR_PROLOGUE = Buffer.from("threadbase-e2ee/1 pair", "utf-8");
148918
+ var DHLEN = 32;
148919
+ var HASHLEN = 32;
148920
+ var TAGLEN = 16;
148921
+ var NOISE_MESSAGE_1_OVERHEAD = DHLEN + DHLEN + TAGLEN + TAGLEN;
148922
+ var NOISE_MESSAGE_2_OVERHEAD = DHLEN + TAGLEN;
148923
+ var NOISE_MAX_MESSAGE_BYTES = 4096;
148924
+ var NoiseError = class extends Error {
148925
+ constructor(message) {
148926
+ super(message);
148927
+ this.name = "NoiseError";
148928
+ }
148929
+ };
148930
+ function generateKeyPair() {
148931
+ const { publicKey, privateKey } = (0, import_crypto10.generateKeyPairSync)("x25519");
148932
+ return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };
148933
+ }
148934
+ function keyPairFrom(privateKey) {
148935
+ const publicKey = (0, import_crypto10.createPublicKey)(privateKey);
148936
+ return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };
148937
+ }
148938
+ function rawPublicKey(key) {
148939
+ const pub = key.type === "public" ? key : (0, import_crypto10.createPublicKey)(key);
148940
+ const jwk = pub.export({ format: "jwk" });
148941
+ if (jwk.crv !== "X25519" || typeof jwk.x !== "string") {
148942
+ throw new NoiseError("Not an X25519 public key");
148943
+ }
148944
+ return Buffer.from(jwk.x, "base64url");
148945
+ }
148946
+ function publicKeyFromRaw(raw2) {
148947
+ if (raw2.length !== DHLEN) {
148948
+ throw new NoiseError(`X25519 public key must be ${DHLEN} bytes, got ${raw2.length}`);
148949
+ }
148950
+ try {
148951
+ return (0, import_crypto10.createPublicKey)({
148952
+ key: { kty: "OKP", crv: "X25519", x: raw2.toString("base64url") },
148953
+ format: "jwk"
148954
+ });
148955
+ } catch {
148956
+ throw new NoiseError("Invalid X25519 public key");
148957
+ }
148958
+ }
148959
+ function pskFromPairToken(token) {
148960
+ return (0, import_crypto10.createHash)("sha256").update("threadbase-e2ee/1 psk", "utf-8").update(token, "utf-8").digest();
148961
+ }
148962
+ function chachaNonce(n) {
148963
+ const nonce = Buffer.alloc(12);
148964
+ nonce.writeBigUInt64LE(n, 4);
148965
+ return nonce;
148966
+ }
148967
+ var CipherState = class {
148968
+ k = null;
148969
+ n = 0n;
148970
+ initializeKey(key) {
148971
+ this.k = key;
148972
+ this.n = 0n;
148973
+ }
148974
+ hasKey() {
148975
+ return this.k !== null;
148976
+ }
148977
+ encryptWithAd(ad, plaintext) {
148978
+ if (!this.k) return plaintext;
148979
+ const cipher = (0, import_crypto10.createCipheriv)("chacha20-poly1305", this.k, chachaNonce(this.n), {
148980
+ authTagLength: TAGLEN
148981
+ });
148982
+ cipher.setAAD(ad, { plaintextLength: plaintext.length });
148983
+ const out = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
148984
+ this.n += 1n;
148985
+ return out;
148986
+ }
148987
+ decryptWithAd(ad, ciphertext) {
148988
+ if (!this.k) return ciphertext;
148989
+ if (ciphertext.length < TAGLEN) throw new NoiseError("Ciphertext shorter than its tag");
148990
+ const body = ciphertext.subarray(0, ciphertext.length - TAGLEN);
148991
+ const tag = ciphertext.subarray(ciphertext.length - TAGLEN);
148992
+ const decipher = (0, import_crypto10.createDecipheriv)("chacha20-poly1305", this.k, chachaNonce(this.n), {
148993
+ authTagLength: TAGLEN
148994
+ });
148995
+ decipher.setAAD(ad, { plaintextLength: body.length });
148996
+ decipher.setAuthTag(tag);
148997
+ let out;
148998
+ try {
148999
+ out = Buffer.concat([decipher.update(body), decipher.final()]);
149000
+ } catch {
149001
+ throw new NoiseError("Decryption failed");
149002
+ }
149003
+ this.n += 1n;
149004
+ return out;
149005
+ }
149006
+ };
149007
+ var SymmetricState = class {
149008
+ ck;
149009
+ h;
149010
+ cipher = new CipherState();
149011
+ constructor(protocolName) {
149012
+ const name = Buffer.from(protocolName, "utf-8");
149013
+ this.h = name.length <= HASHLEN ? Buffer.concat([name, Buffer.alloc(HASHLEN - name.length)]) : (0, import_crypto10.createHash)("sha256").update(name).digest();
149014
+ this.ck = Buffer.from(this.h);
149015
+ this.cipher.initializeKey(null);
149016
+ }
149017
+ /**
149018
+ * Noise's HKDF (spec §4.3) is RFC 5869 with `salt = chaining_key`,
149019
+ * `ikm = input_key_material` and an EMPTY info, so Node's `hkdfSync` is
149020
+ * exactly it rather than approximately it. Using the platform's HKDF removes
149021
+ * the HMAC chain that would otherwise be the easiest thing here to get
149022
+ * subtly wrong.
149023
+ */
149024
+ hkdf(ikm, outputs) {
149025
+ const raw2 = Buffer.from((0, import_crypto10.hkdfSync)("sha256", ikm, this.ck, Buffer.alloc(0), HASHLEN * outputs));
149026
+ const out = [];
149027
+ for (let i = 0; i < outputs; i++) out.push(raw2.subarray(i * HASHLEN, (i + 1) * HASHLEN));
149028
+ return out;
149029
+ }
149030
+ mixKey(ikm) {
149031
+ const [ck, tempK] = this.hkdf(ikm, 2);
149032
+ this.ck = ck;
149033
+ this.cipher.initializeKey(tempK);
149034
+ }
149035
+ mixHash(data) {
149036
+ this.h = (0, import_crypto10.createHash)("sha256").update(this.h).update(data).digest();
149037
+ }
149038
+ /** Spec §5.2. Used only by the `psk` token. */
149039
+ mixKeyAndHash(ikm) {
149040
+ const [ck, tempH, tempK] = this.hkdf(ikm, 3);
149041
+ this.ck = ck;
149042
+ this.mixHash(tempH);
149043
+ this.cipher.initializeKey(tempK);
149044
+ }
149045
+ encryptAndHash(plaintext) {
149046
+ const ciphertext = this.cipher.encryptWithAd(this.h, plaintext);
149047
+ this.mixHash(ciphertext);
149048
+ return ciphertext;
149049
+ }
149050
+ decryptAndHash(ciphertext) {
149051
+ const plaintext = this.cipher.decryptWithAd(this.h, ciphertext);
149052
+ this.mixHash(ciphertext);
149053
+ return plaintext;
149054
+ }
149055
+ /** The transcript hash. Both sides must arrive at the same value. */
149056
+ handshakeHash() {
149057
+ return Buffer.from(this.h);
149058
+ }
149059
+ /** Spec §5.2 `Split()`: the two directional transport keys. */
149060
+ split() {
149061
+ const [k1, k2] = this.hkdf(Buffer.alloc(0), 2);
149062
+ return { k1: Buffer.from(k1), k2: Buffer.from(k2) };
149063
+ }
149064
+ };
149065
+ function dh(privateKey, publicKey) {
149066
+ return (0, import_crypto10.diffieHellman)({ privateKey, publicKey });
149067
+ }
149068
+ function readMessage1(args) {
149069
+ assertMessageSize(args.message1, NOISE_MESSAGE_1_OVERHEAD);
149070
+ const state = new SymmetricState(NOISE_PROTOCOL_NAME);
149071
+ state.mixHash(args.prologue ?? PAIR_PROLOGUE);
149072
+ state.mixHash(args.staticKeyPair.publicKeyRaw);
149073
+ const reRaw = args.message1.subarray(0, DHLEN);
149074
+ const re2 = publicKeyFromRaw(reRaw);
149075
+ state.mixHash(reRaw);
149076
+ state.mixKey(reRaw);
149077
+ state.mixKey(dh(args.staticKeyPair.privateKey, re2));
149078
+ const encryptedStatic = args.message1.subarray(DHLEN, DHLEN + DHLEN + TAGLEN);
149079
+ const initiatorStaticPub = state.decryptAndHash(encryptedStatic);
149080
+ const rs2 = publicKeyFromRaw(initiatorStaticPub);
149081
+ state.mixKey(dh(args.staticKeyPair.privateKey, rs2));
149082
+ state.mixKeyAndHash(args.psk);
149083
+ const payload = state.decryptAndHash(args.message1.subarray(DHLEN + DHLEN + TAGLEN));
149084
+ return {
149085
+ symmetric: state,
149086
+ initiatorStaticPub,
149087
+ payload,
149088
+ initiatorEphemeral: re2,
149089
+ initiatorStatic: rs2,
149090
+ staticKeyPair: args.staticKeyPair
149091
+ };
149092
+ }
149093
+ function writeMessage2(state, responsePayload, ephemeral) {
149094
+ const e = ephemeral ?? generateKeyPair();
149095
+ state.symmetric.mixHash(e.publicKeyRaw);
149096
+ state.symmetric.mixKey(e.publicKeyRaw);
149097
+ state.symmetric.mixKey(dh(e.privateKey, state.initiatorEphemeral));
149098
+ state.symmetric.mixKey(dh(e.privateKey, state.initiatorStatic));
149099
+ const encryptedPayload = state.symmetric.encryptAndHash(responsePayload);
149100
+ return {
149101
+ message2: Buffer.concat([e.publicKeyRaw, encryptedPayload]),
149102
+ keys: finish(state.symmetric)
149103
+ };
149104
+ }
149105
+ function finish(state) {
149106
+ const { k1, k2 } = state.split();
149107
+ return { handshakeHash: state.handshakeHash(), clientToServer: k1, serverToClient: k2 };
149108
+ }
149109
+ function assertMessageSize(message, minimum) {
149110
+ if (message.length < minimum) {
149111
+ throw new NoiseError(`Handshake message too short: ${message.length} < ${minimum}`);
149112
+ }
149113
+ if (message.length > NOISE_MAX_MESSAGE_BYTES) {
149114
+ throw new NoiseError(`Handshake message too large: ${message.length}`);
149115
+ }
149116
+ }
149117
+
149118
+ // src/e2ee/pair-request.ts
149119
+ var E2EE_EXCHANGE_VERSION = 1;
149120
+ var MAX_BASE64_CHARS = Math.ceil(NOISE_MAX_MESSAGE_BYTES * 4 / 3) + 4;
149121
+ var E2eeRequestError = class extends Error {
149122
+ /** Stable code for the client, so a version mismatch is not a parse failure. */
149123
+ code;
149124
+ constructor(code, message) {
149125
+ super(message);
149126
+ this.name = "E2eeRequestError";
149127
+ this.code = code;
149128
+ }
149129
+ };
149130
+ function parseE2eeRequest(raw2) {
149131
+ if (raw2 === void 0 || raw2 === null) return null;
149132
+ if (typeof raw2 !== "object" || Array.isArray(raw2)) {
149133
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee must be an object");
149134
+ }
149135
+ const { v: v2, noise } = raw2;
149136
+ if (typeof v2 !== "number" || !Number.isInteger(v2)) {
149137
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.v must be an integer");
149138
+ }
149139
+ if (v2 !== E2EE_EXCHANGE_VERSION) {
149140
+ throw new E2eeRequestError(
149141
+ "E2EE_VERSION_UNSUPPORTED",
149142
+ `e2ee.v ${v2} is not supported; this server speaks ${E2EE_EXCHANGE_VERSION}`
149143
+ );
149144
+ }
149145
+ if (typeof noise !== "string") {
149146
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.noise must be a base64 string");
149147
+ }
149148
+ if (noise.length > MAX_BASE64_CHARS) {
149149
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.noise is too large");
149150
+ }
149151
+ const message1 = Buffer.from(noise, "base64");
149152
+ if (message1.length === 0) {
149153
+ throw new E2eeRequestError("E2EE_MALFORMED", "e2ee.noise is not valid base64");
149154
+ }
149155
+ return { version: v2, message1 };
149156
+ }
149157
+
148913
149158
  // src/external-tails.ts
148914
149159
  var import_fs24 = require("fs");
148915
149160
  init_logger();
@@ -149087,7 +149332,7 @@ init_live_session_manager();
149087
149332
  init_logger();
149088
149333
 
149089
149334
  // src/pair-store.ts
149090
- var import_crypto12 = require("crypto");
149335
+ var import_crypto13 = require("crypto");
149091
149336
  var DEFAULT_TTL_SECONDS = 180;
149092
149337
  var SWEEP_INTERVAL_MS = 6e4;
149093
149338
  var PairTokenStore = class {
@@ -149102,7 +149347,7 @@ var PairTokenStore = class {
149102
149347
  }
149103
149348
  }
149104
149349
  mint() {
149105
- const token = `pt_${(0, import_crypto12.randomBytes)(16).toString("hex")}`;
149350
+ const token = `pt_${(0, import_crypto13.randomBytes)(16).toString("hex")}`;
149106
149351
  const expiresAt = Date.now() + this.ttlMs;
149107
149352
  this.current = { token, expiresAt, used: false };
149108
149353
  return {
@@ -149596,7 +149841,7 @@ function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
149596
149841
 
149597
149842
  // src/scanner-manager.ts
149598
149843
  var REFRESH_TTL_MS = 2e3;
149599
- var ScannerManager = class {
149844
+ var ScannerManager = class _ScannerManager {
149600
149845
  constructor(deps) {
149601
149846
  this.deps = deps;
149602
149847
  this.persistenceDisabled = deps.persistenceDisabled;
@@ -149617,6 +149862,12 @@ var ScannerManager = class {
149617
149862
  allScanners = /* @__PURE__ */ new Set();
149618
149863
  stalePaths = /* @__PURE__ */ new Set();
149619
149864
  reconcileInFlight = null;
149865
+ lastAutoFullReconcileAt = 0;
149866
+ // reconcileMode() returns "full" on every poll while a permanent drift
149867
+ // condition holds (e.g. an orphan row) — without a cooldown each poll
149868
+ // re-walks the whole corpus back-to-back. refresh=1 and per-file
149869
+ // reconciles go through other paths and are unaffected.
149870
+ static AUTO_FULL_RECONCILE_COOLDOWN_MS = 6e4;
149620
149871
  refreshInFlight = /* @__PURE__ */ new Map();
149621
149872
  log = getLogger("server");
149622
149873
  /**
@@ -149933,6 +150184,13 @@ var ScannerManager = class {
149933
150184
  // in-flight cache write before shutting the DB.
149934
150185
  startBackgroundReconcile(mode = "full") {
149935
150186
  if (this.reconcileInFlight) return;
150187
+ if (mode === "full") {
150188
+ const now = Date.now();
150189
+ if (now - this.lastAutoFullReconcileAt < _ScannerManager.AUTO_FULL_RECONCILE_COOLDOWN_MS) {
150190
+ return;
150191
+ }
150192
+ this.lastAutoFullReconcileAt = now;
150193
+ }
149936
150194
  const paths = mode === "files" ? this.takeStaleFiles() : [];
149937
150195
  const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileFromDisk()).finally(() => {
149938
150196
  this.reconcileInFlight = null;
@@ -150473,7 +150731,7 @@ function createApiDeps(deps) {
150473
150731
  }
150474
150732
 
150475
150733
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
150476
- var import_crypto13 = require("crypto");
150734
+ var import_crypto14 = require("crypto");
150477
150735
  var import_fs34 = require("fs");
150478
150736
 
150479
150737
  // src/services/cache-integrity/alertStore.ts
@@ -150538,7 +150796,7 @@ function envInt(name, fallback) {
150538
150796
  }
150539
150797
  function fingerprintOf(ids) {
150540
150798
  const sorted = [...ids].sort();
150541
- return `sha256:${(0, import_crypto13.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
150799
+ return `sha256:${(0, import_crypto14.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
150542
150800
  }
150543
150801
  var CacheIntegrityMonitor = class {
150544
150802
  constructor(cache, wsHub, log15, cacheDir, rescan, runDuringReset) {
@@ -153088,7 +153346,7 @@ var StreamerServer = class {
153088
153346
  runtimeStore = null;
153089
153347
  // Identifies this streamer run. A registry row carrying a different id is a
153090
153348
  // session that outlived the process that started it.
153091
- streamerInstanceId = (0, import_crypto14.randomUUID)();
153349
+ streamerInstanceId = (0, import_crypto15.randomUUID)();
153092
153350
  cacheMetadataRepo = null;
153093
153351
  // Push registration + delivery state (C7). Null when the cache DB failed to
153094
153352
  // open — registration then degrades to a no-op rather than 500ing.
@@ -154216,6 +154474,14 @@ var StreamerServer = class {
154216
154474
  json2(res, 400, { error: "Missing token or clientPublicKey" });
154217
154475
  return;
154218
154476
  }
154477
+ let e2eeRequest;
154478
+ try {
154479
+ e2eeRequest = parseE2eeRequest(body?.e2ee);
154480
+ } catch (err) {
154481
+ const e = err;
154482
+ json2(res, 400, { error: e.message, code: e.code });
154483
+ return;
154484
+ }
154219
154485
  const precheck = this.pairTokens.wouldConsume(token);
154220
154486
  if (!precheck.ok) {
154221
154487
  if (precheck.reason === "used") {
@@ -154227,6 +154493,25 @@ var StreamerServer = class {
154227
154493
  json2(res, 401, { error: `Pair token ${precheck.reason}` });
154228
154494
  return;
154229
154495
  }
154496
+ let handshake = null;
154497
+ if (e2eeRequest) {
154498
+ try {
154499
+ handshake = readMessage1({
154500
+ staticKeyPair: keyPairFrom(loadOrCreateServerIdentity().privateKey),
154501
+ // The pair token binds this handshake to the scanned QR. Derivation
154502
+ // is specified in design.md §2.4 and pinned by a committed vector
154503
+ // that tb-mobile checks against independently.
154504
+ psk: pskFromPairToken(token),
154505
+ message1: e2eeRequest.message1
154506
+ });
154507
+ } catch {
154508
+ json2(res, 400, {
154509
+ error: "E2EE handshake failed. Scan a fresh pairing code and try again.",
154510
+ code: "E2EE_HANDSHAKE_FAILED"
154511
+ });
154512
+ return;
154513
+ }
154514
+ }
154230
154515
  let sealed;
154231
154516
  try {
154232
154517
  sealed = seal(this.apiKey, clientPublicKey);
@@ -154250,14 +154535,59 @@ var StreamerServer = class {
154250
154535
  try {
154251
154536
  const name = typeof body?.deviceName === "string" ? body.deviceName.slice(0, 100) : null;
154252
154537
  const preset = body?.readOnly === true ? "read-only" : "full";
154253
- device = this.devicesRepo?.register({ publicKey: clientPublicKey, name, preset }) ?? null;
154538
+ device = this.devicesRepo?.register({
154539
+ publicKey: clientPublicKey,
154540
+ name,
154541
+ preset,
154542
+ // Recorded only when the handshake authenticated it. The static key
154543
+ // comes out of the transcript, never off the wire as a claim — that
154544
+ // is the difference between a device identified by a key it proved
154545
+ // it holds and one identified by a string it sent.
154546
+ //
154547
+ // Setting it also sets `e2ee_required`, so a device that has once
154548
+ // paired encrypted is pinned and never served plaintext again.
154549
+ ...handshake && {
154550
+ e2eeStaticPub: handshake.initiatorStaticPub.toString("base64"),
154551
+ e2eeVersion: E2EE_EXCHANGE_VERSION
154552
+ }
154553
+ }) ?? null;
154254
154554
  } catch (err) {
154255
154555
  this.log.warn("[pair] device registration failed; pairing continues", {
154256
154556
  event: "pair.device_register_failed",
154257
154557
  err
154258
154558
  });
154259
154559
  }
154560
+ let e2eeResponse = null;
154561
+ if (handshake) {
154562
+ try {
154563
+ const { message2 } = writeMessage2(
154564
+ handshake,
154565
+ Buffer.from(
154566
+ JSON.stringify({
154567
+ v: E2EE_EXCHANGE_VERSION,
154568
+ deviceId: device?.deviceId ?? null,
154569
+ serverVersion: getVersion(),
154570
+ // Always true on this path: completing a handshake is what pins
154571
+ // the device, and design.md §6.3 says nothing a client sends
154572
+ // ever clears it.
154573
+ e2eeRequired: true
154574
+ }),
154575
+ "utf-8"
154576
+ )
154577
+ );
154578
+ e2eeResponse = { v: E2EE_EXCHANGE_VERSION, noise: message2.toString("base64") };
154579
+ } catch (err) {
154580
+ this.log.warn("[pair] E2EE response could not be written; pairing continues", {
154581
+ event: "pair.e2ee_response_failed",
154582
+ err
154583
+ });
154584
+ }
154585
+ }
154260
154586
  json2(res, 200, {
154587
+ // Unchanged and still sent on the E2EE path, deliberately. An older app
154588
+ // is the only thing that can read these and it cannot be force-updated
154589
+ // (docs/compatibility/tb-mobile.md); a new app ignores them and uses the
154590
+ // Noise result. The response grows a field, it never loses one.
154261
154591
  ciphertext: sealed.ciphertext,
154262
154592
  nonce: sealed.nonce,
154263
154593
  ephemeralPublicKey: sealed.ephemeralPublicKey,
@@ -154280,7 +154610,11 @@ var StreamerServer = class {
154280
154610
  deviceId: device.deviceId,
154281
154611
  deviceToken: device.deviceToken,
154282
154612
  capabilities: device.capabilities
154283
- }
154613
+ },
154614
+ // Additive. Absent means this pairing is plaintext — either the client
154615
+ // never asked, or writing the reply failed — and a client that asked for
154616
+ // encryption must treat its absence as a refusal, not as consent.
154617
+ ...e2eeResponse && { e2ee: e2eeResponse }
154284
154618
  });
154285
154619
  }
154286
154620
  rotateApiKey() {