@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 +373 -39
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +345 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +350 -7
- package/dist/index.js.map +1 -1
- package/dist/migrations/016_normalize_blank_project_path.sql +7 -0
- package/package.json +1 -1
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:
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
45021
|
+
finish2(err);
|
|
45022
45022
|
}
|
|
45023
45023
|
if (!sync && !queued) {
|
|
45024
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45066
|
+
finish2(err);
|
|
45067
45067
|
else if (!queued)
|
|
45068
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
() =>
|
|
123201
|
+
() => finish2({ reachable: false, error: `status timed out after ${timeoutMs}ms` }),
|
|
123202
123202
|
timeoutMs
|
|
123203
123203
|
);
|
|
123204
123204
|
timer.unref?.();
|
|
123205
|
-
transport.onClose(() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123222
|
+
finish2({ reachable: false, error: "pty-host returned an invalid status response" });
|
|
123223
123223
|
return;
|
|
123224
123224
|
}
|
|
123225
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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({
|
|
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() {
|