@threadbase-sh/streamer 1.29.1 → 1.29.2
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 +84 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +83 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +83 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -137480,7 +137480,7 @@ function isLocalRequest(remoteAddr) {
|
|
|
137480
137480
|
const addr = remoteAddr ?? "";
|
|
137481
137481
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
137482
137482
|
}
|
|
137483
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
137483
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
|
|
137484
137484
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
137485
137485
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
137486
137486
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -137810,7 +137810,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
137810
137810
|
}
|
|
137811
137811
|
}
|
|
137812
137812
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
137813
|
-
if (!timestampHeader) return
|
|
137813
|
+
if (!timestampHeader) return false;
|
|
137814
137814
|
const t = Number(timestampHeader);
|
|
137815
137815
|
if (!Number.isFinite(t)) return false;
|
|
137816
137816
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -138021,18 +138021,21 @@ var createSessionRoutes = (deps) => {
|
|
|
138021
138021
|
};
|
|
138022
138022
|
|
|
138023
138023
|
// src/api/routes/ws.routes.ts
|
|
138024
|
+
init_auth();
|
|
138024
138025
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
138025
138026
|
const app = new Hono2();
|
|
138026
138027
|
app.get(
|
|
138027
138028
|
"/ws",
|
|
138028
|
-
upgradeWebSocket(() => {
|
|
138029
|
+
upgradeWebSocket((c) => {
|
|
138030
|
+
const key = c.req.query("key");
|
|
138031
|
+
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
138029
138032
|
let openWs = null;
|
|
138030
138033
|
return {
|
|
138031
138034
|
onOpen(_evt, ws2) {
|
|
138032
138035
|
const raw2 = ws2.raw;
|
|
138033
138036
|
if (!raw2) return;
|
|
138034
138037
|
openWs = raw2;
|
|
138035
|
-
deps.handleWsOpen(raw2);
|
|
138038
|
+
deps.handleWsOpen(raw2, preAuthed);
|
|
138036
138039
|
},
|
|
138037
138040
|
onMessage(evt, _ws) {
|
|
138038
138041
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -143492,6 +143495,13 @@ function deriveProjectChatTitle(input) {
|
|
|
143492
143495
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
143493
143496
|
}
|
|
143494
143497
|
|
|
143498
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
143499
|
+
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
143500
|
+
function sanitizeAnswerKeys(keys) {
|
|
143501
|
+
if (keys === void 0) return void 0;
|
|
143502
|
+
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
143503
|
+
}
|
|
143504
|
+
|
|
143495
143505
|
// src/services/questions/detectAskUserQuestion.ts
|
|
143496
143506
|
function normalizeContent2(raw2) {
|
|
143497
143507
|
if (Array.isArray(raw2)) return raw2;
|
|
@@ -144334,6 +144344,8 @@ var WSHub = class {
|
|
|
144334
144344
|
var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
|
|
144335
144345
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
144336
144346
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
144347
|
+
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
144348
|
+
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
144337
144349
|
var REFRESH_TTL_MS = 2e3;
|
|
144338
144350
|
var START_READY_TIMEOUT_MS = 15e3;
|
|
144339
144351
|
function parseIncludeAgentsEnv(raw2) {
|
|
@@ -144424,6 +144436,14 @@ var StreamerServer = class {
|
|
|
144424
144436
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
144425
144437
|
// Reverse map for cleanup on close
|
|
144426
144438
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
144439
|
+
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
144440
|
+
// { type: "auth", token } first message). Only authed sockets are added to
|
|
144441
|
+
// the hub and receive broadcasts.
|
|
144442
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
144443
|
+
wsAuthed = /* @__PURE__ */ new Set();
|
|
144444
|
+
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
144445
|
+
wsAuthPending = /* @__PURE__ */ new Map();
|
|
144446
|
+
wsAuthTimeoutMs;
|
|
144427
144447
|
cache = null;
|
|
144428
144448
|
projectsRepo = null;
|
|
144429
144449
|
conversationsRepo = null;
|
|
@@ -144463,6 +144483,7 @@ var StreamerServer = class {
|
|
|
144463
144483
|
this.scanProfiles = config2.scanProfiles;
|
|
144464
144484
|
this.codexRoots = config2.codexRoots ?? [(0, import_path22.join)((0, import_os10.homedir)(), ".codex", "sessions")];
|
|
144465
144485
|
this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
144486
|
+
this.wsAuthTimeoutMs = config2.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
144466
144487
|
this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
144467
144488
|
this.defaultPermissionMode = config2.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
144468
144489
|
this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path22.join)((0, import_os10.homedir)(), ".threadbase", "cache");
|
|
@@ -144720,17 +144741,39 @@ var StreamerServer = class {
|
|
|
144720
144741
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
144721
144742
|
handleBrowse: (url2, res) => this.handleBrowse(url2, res),
|
|
144722
144743
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
144723
|
-
handleWsOpen: (ws2) => {
|
|
144724
|
-
|
|
144725
|
-
|
|
144726
|
-
|
|
144727
|
-
if (this.cacheReady) {
|
|
144728
|
-
ws2.send(JSON.stringify({ type: "cache_ready" }));
|
|
144744
|
+
handleWsOpen: (ws2, preAuthed) => {
|
|
144745
|
+
if (preAuthed) {
|
|
144746
|
+
this.completeWsAuth(ws2);
|
|
144747
|
+
return;
|
|
144729
144748
|
}
|
|
144749
|
+
const timer = setTimeout(() => {
|
|
144750
|
+
this.wsAuthPending.delete(ws2);
|
|
144751
|
+
try {
|
|
144752
|
+
ws2.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
144753
|
+
} catch {
|
|
144754
|
+
}
|
|
144755
|
+
}, this.wsAuthTimeoutMs);
|
|
144756
|
+
this.wsAuthPending.set(ws2, timer);
|
|
144730
144757
|
},
|
|
144731
144758
|
handleWsMessage: async (ws2, raw2) => {
|
|
144732
144759
|
try {
|
|
144733
144760
|
const msg = JSON.parse(String(raw2));
|
|
144761
|
+
if (!this.wsAuthed.has(ws2)) {
|
|
144762
|
+
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
144763
|
+
const t = this.wsAuthPending.get(ws2);
|
|
144764
|
+
if (t) clearTimeout(t);
|
|
144765
|
+
this.wsAuthPending.delete(ws2);
|
|
144766
|
+
if (validateApiKey(msg.token, this.apiKey)) {
|
|
144767
|
+
this.completeWsAuth(ws2);
|
|
144768
|
+
} else {
|
|
144769
|
+
try {
|
|
144770
|
+
ws2.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
144771
|
+
} catch {
|
|
144772
|
+
}
|
|
144773
|
+
}
|
|
144774
|
+
}
|
|
144775
|
+
return;
|
|
144776
|
+
}
|
|
144734
144777
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
144735
144778
|
const oldClientId = this.wsToClientId.get(ws2);
|
|
144736
144779
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -144745,12 +144788,20 @@ var StreamerServer = class {
|
|
|
144745
144788
|
}
|
|
144746
144789
|
}
|
|
144747
144790
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
144748
|
-
this.
|
|
144791
|
+
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws2)) {
|
|
144792
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
144793
|
+
}
|
|
144749
144794
|
}
|
|
144750
144795
|
} catch {
|
|
144751
144796
|
}
|
|
144752
144797
|
},
|
|
144753
144798
|
handleWsClose: (ws2) => {
|
|
144799
|
+
const pendingTimer = this.wsAuthPending.get(ws2);
|
|
144800
|
+
if (pendingTimer) {
|
|
144801
|
+
clearTimeout(pendingTimer);
|
|
144802
|
+
this.wsAuthPending.delete(ws2);
|
|
144803
|
+
}
|
|
144804
|
+
this.wsAuthed.delete(ws2);
|
|
144754
144805
|
const clientId = this.wsToClientId.get(ws2);
|
|
144755
144806
|
if (clientId) {
|
|
144756
144807
|
this.clientIdToWs.delete(clientId);
|
|
@@ -144820,6 +144871,20 @@ var StreamerServer = class {
|
|
|
144820
144871
|
this.wsHub.broadcast(payload);
|
|
144821
144872
|
}
|
|
144822
144873
|
}
|
|
144874
|
+
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
144875
|
+
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
144876
|
+
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
144877
|
+
// broadcast.
|
|
144878
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
144879
|
+
completeWsAuth(ws2) {
|
|
144880
|
+
this.wsAuthed.add(ws2);
|
|
144881
|
+
this.wsHub.addClient(ws2);
|
|
144882
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
144883
|
+
ws2.send(JSON.stringify({ type: "session_list", sessions }));
|
|
144884
|
+
if (this.cacheReady) {
|
|
144885
|
+
ws2.send(JSON.stringify({ type: "cache_ready" }));
|
|
144886
|
+
}
|
|
144887
|
+
}
|
|
144823
144888
|
addSessionSubscriber(sessionId, ws2) {
|
|
144824
144889
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
144825
144890
|
if (!subs) {
|
|
@@ -145106,6 +145171,9 @@ var StreamerServer = class {
|
|
|
145106
145171
|
this.ptyManager.dispose();
|
|
145107
145172
|
this.fileWatcher.dispose();
|
|
145108
145173
|
this.wsHub.dispose();
|
|
145174
|
+
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
145175
|
+
this.wsAuthPending.clear();
|
|
145176
|
+
this.wsAuthed.clear();
|
|
145109
145177
|
this.pairTokens.dispose();
|
|
145110
145178
|
if (this.dbPool) {
|
|
145111
145179
|
await this.dbPool.end();
|
|
@@ -146232,12 +146300,16 @@ var StreamerServer = class {
|
|
|
146232
146300
|
return;
|
|
146233
146301
|
}
|
|
146234
146302
|
this.pendingPermission.set(sessionId, gate);
|
|
146303
|
+
const safeOptions = gate.options.map((o) => {
|
|
146304
|
+
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
146305
|
+
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
146306
|
+
});
|
|
146235
146307
|
this.wsHub.broadcast({
|
|
146236
146308
|
type: "permission",
|
|
146237
146309
|
sessionId,
|
|
146238
146310
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
146239
146311
|
...gate.detail ? { detail: gate.detail } : {},
|
|
146240
|
-
options:
|
|
146312
|
+
options: safeOptions,
|
|
146241
146313
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
146242
146314
|
});
|
|
146243
146315
|
}
|