@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/index.d.cts
CHANGED
|
@@ -321,6 +321,7 @@ interface ServerConfig {
|
|
|
321
321
|
codexRoots?: string[];
|
|
322
322
|
scannerPersistent?: boolean;
|
|
323
323
|
ptyGracePeriodMs?: number;
|
|
324
|
+
wsAuthTimeoutMs?: number;
|
|
324
325
|
cacheDir?: string;
|
|
325
326
|
tailSize?: number;
|
|
326
327
|
directoryScanDebounceMs?: number;
|
|
@@ -859,7 +860,7 @@ type ApiDeps = {
|
|
|
859
860
|
handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
860
861
|
handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
|
|
861
862
|
handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
862
|
-
handleWsOpen: (ws: WebSocket) => void;
|
|
863
|
+
handleWsOpen: (ws: WebSocket, preAuthed: boolean) => void;
|
|
863
864
|
handleWsMessage: (ws: WebSocket, raw: unknown) => void;
|
|
864
865
|
handleWsClose: (ws: WebSocket) => void;
|
|
865
866
|
agentClient: AgentClient | null;
|
|
@@ -1015,6 +1016,9 @@ declare class StreamerServer {
|
|
|
1015
1016
|
private sessionSubscribers;
|
|
1016
1017
|
private clientIdToWs;
|
|
1017
1018
|
private wsToClientId;
|
|
1019
|
+
private wsAuthed;
|
|
1020
|
+
private wsAuthPending;
|
|
1021
|
+
private wsAuthTimeoutMs;
|
|
1018
1022
|
private cache;
|
|
1019
1023
|
private projectsRepo;
|
|
1020
1024
|
private conversationsRepo;
|
|
@@ -1043,6 +1047,7 @@ declare class StreamerServer {
|
|
|
1043
1047
|
* a full broadcast if no match exists (old clients, or no WS registered yet).
|
|
1044
1048
|
*/
|
|
1045
1049
|
private broadcastOrUnicastSessionList;
|
|
1050
|
+
private completeWsAuth;
|
|
1046
1051
|
private addSessionSubscriber;
|
|
1047
1052
|
private startGraceTimer;
|
|
1048
1053
|
get port(): number;
|
package/dist/index.d.ts
CHANGED
|
@@ -321,6 +321,7 @@ interface ServerConfig {
|
|
|
321
321
|
codexRoots?: string[];
|
|
322
322
|
scannerPersistent?: boolean;
|
|
323
323
|
ptyGracePeriodMs?: number;
|
|
324
|
+
wsAuthTimeoutMs?: number;
|
|
324
325
|
cacheDir?: string;
|
|
325
326
|
tailSize?: number;
|
|
326
327
|
directoryScanDebounceMs?: number;
|
|
@@ -859,7 +860,7 @@ type ApiDeps = {
|
|
|
859
860
|
handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
860
861
|
handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
|
|
861
862
|
handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
862
|
-
handleWsOpen: (ws: WebSocket) => void;
|
|
863
|
+
handleWsOpen: (ws: WebSocket, preAuthed: boolean) => void;
|
|
863
864
|
handleWsMessage: (ws: WebSocket, raw: unknown) => void;
|
|
864
865
|
handleWsClose: (ws: WebSocket) => void;
|
|
865
866
|
agentClient: AgentClient | null;
|
|
@@ -1015,6 +1016,9 @@ declare class StreamerServer {
|
|
|
1015
1016
|
private sessionSubscribers;
|
|
1016
1017
|
private clientIdToWs;
|
|
1017
1018
|
private wsToClientId;
|
|
1019
|
+
private wsAuthed;
|
|
1020
|
+
private wsAuthPending;
|
|
1021
|
+
private wsAuthTimeoutMs;
|
|
1018
1022
|
private cache;
|
|
1019
1023
|
private projectsRepo;
|
|
1020
1024
|
private conversationsRepo;
|
|
@@ -1043,6 +1047,7 @@ declare class StreamerServer {
|
|
|
1043
1047
|
* a full broadcast if no match exists (old clients, or no WS registered yet).
|
|
1044
1048
|
*/
|
|
1045
1049
|
private broadcastOrUnicastSessionList;
|
|
1050
|
+
private completeWsAuth;
|
|
1046
1051
|
private addSessionSubscriber;
|
|
1047
1052
|
private startGraceTimer;
|
|
1048
1053
|
get port(): number;
|
package/dist/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
167
|
-
if (!timestampHeader) return
|
|
167
|
+
if (!timestampHeader) return false;
|
|
168
168
|
const t = Number(timestampHeader);
|
|
169
169
|
if (!Number.isFinite(t)) return false;
|
|
170
170
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -2551,7 +2551,7 @@ function isLocalRequest(remoteAddr) {
|
|
|
2551
2551
|
const addr = remoteAddr ?? "";
|
|
2552
2552
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2553
2553
|
}
|
|
2554
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
2554
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
|
|
2555
2555
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2556
2556
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
2557
2557
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -3014,14 +3014,16 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3014
3014
|
const app = new Hono10();
|
|
3015
3015
|
app.get(
|
|
3016
3016
|
"/ws",
|
|
3017
|
-
upgradeWebSocket(() => {
|
|
3017
|
+
upgradeWebSocket((c) => {
|
|
3018
|
+
const key = c.req.query("key");
|
|
3019
|
+
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3018
3020
|
let openWs = null;
|
|
3019
3021
|
return {
|
|
3020
3022
|
onOpen(_evt, ws) {
|
|
3021
3023
|
const raw = ws.raw;
|
|
3022
3024
|
if (!raw) return;
|
|
3023
3025
|
openWs = raw;
|
|
3024
|
-
deps.handleWsOpen(raw);
|
|
3026
|
+
deps.handleWsOpen(raw, preAuthed);
|
|
3025
3027
|
},
|
|
3026
3028
|
onMessage(evt, _ws) {
|
|
3027
3029
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -4928,6 +4930,13 @@ function deriveProjectChatTitle(input) {
|
|
|
4928
4930
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
4929
4931
|
}
|
|
4930
4932
|
|
|
4933
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
4934
|
+
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
4935
|
+
function sanitizeAnswerKeys(keys) {
|
|
4936
|
+
if (keys === void 0) return void 0;
|
|
4937
|
+
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4931
4940
|
// src/services/questions/detectAskUserQuestion.ts
|
|
4932
4941
|
function normalizeContent2(raw) {
|
|
4933
4942
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5532,6 +5541,8 @@ var WSHub = class {
|
|
|
5532
5541
|
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.`;
|
|
5533
5542
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5534
5543
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5544
|
+
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5545
|
+
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5535
5546
|
var REFRESH_TTL_MS = 2e3;
|
|
5536
5547
|
var START_READY_TIMEOUT_MS = 15e3;
|
|
5537
5548
|
function parseIncludeAgentsEnv(raw) {
|
|
@@ -5622,6 +5633,14 @@ var StreamerServer = class {
|
|
|
5622
5633
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5623
5634
|
// Reverse map for cleanup on close
|
|
5624
5635
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
5636
|
+
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
5637
|
+
// { type: "auth", token } first message). Only authed sockets are added to
|
|
5638
|
+
// the hub and receive broadcasts.
|
|
5639
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
5640
|
+
wsAuthed = /* @__PURE__ */ new Set();
|
|
5641
|
+
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
5642
|
+
wsAuthPending = /* @__PURE__ */ new Map();
|
|
5643
|
+
wsAuthTimeoutMs;
|
|
5625
5644
|
cache = null;
|
|
5626
5645
|
projectsRepo = null;
|
|
5627
5646
|
conversationsRepo = null;
|
|
@@ -5661,6 +5680,7 @@ var StreamerServer = class {
|
|
|
5661
5680
|
this.scanProfiles = config.scanProfiles;
|
|
5662
5681
|
this.codexRoots = config.codexRoots ?? [join12(homedir5(), ".codex", "sessions")];
|
|
5663
5682
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
5683
|
+
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5664
5684
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5665
5685
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5666
5686
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join12(homedir5(), ".threadbase", "cache");
|
|
@@ -5918,17 +5938,39 @@ var StreamerServer = class {
|
|
|
5918
5938
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
5919
5939
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
5920
5940
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
5921
|
-
handleWsOpen: (ws) => {
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
if (this.cacheReady) {
|
|
5926
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
5941
|
+
handleWsOpen: (ws, preAuthed) => {
|
|
5942
|
+
if (preAuthed) {
|
|
5943
|
+
this.completeWsAuth(ws);
|
|
5944
|
+
return;
|
|
5927
5945
|
}
|
|
5946
|
+
const timer = setTimeout(() => {
|
|
5947
|
+
this.wsAuthPending.delete(ws);
|
|
5948
|
+
try {
|
|
5949
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
5950
|
+
} catch {
|
|
5951
|
+
}
|
|
5952
|
+
}, this.wsAuthTimeoutMs);
|
|
5953
|
+
this.wsAuthPending.set(ws, timer);
|
|
5928
5954
|
},
|
|
5929
5955
|
handleWsMessage: async (ws, raw) => {
|
|
5930
5956
|
try {
|
|
5931
5957
|
const msg = JSON.parse(String(raw));
|
|
5958
|
+
if (!this.wsAuthed.has(ws)) {
|
|
5959
|
+
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
5960
|
+
const t = this.wsAuthPending.get(ws);
|
|
5961
|
+
if (t) clearTimeout(t);
|
|
5962
|
+
this.wsAuthPending.delete(ws);
|
|
5963
|
+
if (validateApiKey(msg.token, this.apiKey)) {
|
|
5964
|
+
this.completeWsAuth(ws);
|
|
5965
|
+
} else {
|
|
5966
|
+
try {
|
|
5967
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
5968
|
+
} catch {
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
return;
|
|
5973
|
+
}
|
|
5932
5974
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
5933
5975
|
const oldClientId = this.wsToClientId.get(ws);
|
|
5934
5976
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -5943,12 +5985,20 @@ var StreamerServer = class {
|
|
|
5943
5985
|
}
|
|
5944
5986
|
}
|
|
5945
5987
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5946
|
-
this.
|
|
5988
|
+
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
|
|
5989
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
5990
|
+
}
|
|
5947
5991
|
}
|
|
5948
5992
|
} catch {
|
|
5949
5993
|
}
|
|
5950
5994
|
},
|
|
5951
5995
|
handleWsClose: (ws) => {
|
|
5996
|
+
const pendingTimer = this.wsAuthPending.get(ws);
|
|
5997
|
+
if (pendingTimer) {
|
|
5998
|
+
clearTimeout(pendingTimer);
|
|
5999
|
+
this.wsAuthPending.delete(ws);
|
|
6000
|
+
}
|
|
6001
|
+
this.wsAuthed.delete(ws);
|
|
5952
6002
|
const clientId = this.wsToClientId.get(ws);
|
|
5953
6003
|
if (clientId) {
|
|
5954
6004
|
this.clientIdToWs.delete(clientId);
|
|
@@ -6018,6 +6068,20 @@ var StreamerServer = class {
|
|
|
6018
6068
|
this.wsHub.broadcast(payload);
|
|
6019
6069
|
}
|
|
6020
6070
|
}
|
|
6071
|
+
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6072
|
+
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6073
|
+
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6074
|
+
// broadcast.
|
|
6075
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6076
|
+
completeWsAuth(ws) {
|
|
6077
|
+
this.wsAuthed.add(ws);
|
|
6078
|
+
this.wsHub.addClient(ws);
|
|
6079
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6080
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6081
|
+
if (this.cacheReady) {
|
|
6082
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6021
6085
|
addSessionSubscriber(sessionId, ws) {
|
|
6022
6086
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
6023
6087
|
if (!subs) {
|
|
@@ -6304,6 +6368,9 @@ var StreamerServer = class {
|
|
|
6304
6368
|
this.ptyManager.dispose();
|
|
6305
6369
|
this.fileWatcher.dispose();
|
|
6306
6370
|
this.wsHub.dispose();
|
|
6371
|
+
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6372
|
+
this.wsAuthPending.clear();
|
|
6373
|
+
this.wsAuthed.clear();
|
|
6307
6374
|
this.pairTokens.dispose();
|
|
6308
6375
|
if (this.dbPool) {
|
|
6309
6376
|
await this.dbPool.end();
|
|
@@ -7430,12 +7497,16 @@ var StreamerServer = class {
|
|
|
7430
7497
|
return;
|
|
7431
7498
|
}
|
|
7432
7499
|
this.pendingPermission.set(sessionId, gate);
|
|
7500
|
+
const safeOptions = gate.options.map((o) => {
|
|
7501
|
+
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7502
|
+
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
7503
|
+
});
|
|
7433
7504
|
this.wsHub.broadcast({
|
|
7434
7505
|
type: "permission",
|
|
7435
7506
|
sessionId,
|
|
7436
7507
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7437
7508
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7438
|
-
options:
|
|
7509
|
+
options: safeOptions,
|
|
7439
7510
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7440
7511
|
});
|
|
7441
7512
|
}
|