@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 +356 -36
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +328 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +333 -4
- package/dist/index.js.map +1 -1
- 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");
|
|
@@ -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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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({
|
|
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() {
|