@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.cjs
CHANGED
|
@@ -215,7 +215,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
218
|
-
if (!timestampHeader) return
|
|
218
|
+
if (!timestampHeader) return false;
|
|
219
219
|
const t = Number(timestampHeader);
|
|
220
220
|
if (!Number.isFinite(t)) return false;
|
|
221
221
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -2589,7 +2589,7 @@ function isLocalRequest(remoteAddr) {
|
|
|
2589
2589
|
const addr = remoteAddr ?? "";
|
|
2590
2590
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2591
2591
|
}
|
|
2592
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
2592
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
|
|
2593
2593
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2594
2594
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
2595
2595
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -3052,14 +3052,16 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3052
3052
|
const app = new import_hono10.Hono();
|
|
3053
3053
|
app.get(
|
|
3054
3054
|
"/ws",
|
|
3055
|
-
upgradeWebSocket(() => {
|
|
3055
|
+
upgradeWebSocket((c) => {
|
|
3056
|
+
const key = c.req.query("key");
|
|
3057
|
+
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3056
3058
|
let openWs = null;
|
|
3057
3059
|
return {
|
|
3058
3060
|
onOpen(_evt, ws) {
|
|
3059
3061
|
const raw = ws.raw;
|
|
3060
3062
|
if (!raw) return;
|
|
3061
3063
|
openWs = raw;
|
|
3062
|
-
deps.handleWsOpen(raw);
|
|
3064
|
+
deps.handleWsOpen(raw, preAuthed);
|
|
3063
3065
|
},
|
|
3064
3066
|
onMessage(evt, _ws) {
|
|
3065
3067
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -4964,6 +4966,13 @@ function deriveProjectChatTitle(input) {
|
|
|
4964
4966
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
4965
4967
|
}
|
|
4966
4968
|
|
|
4969
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
4970
|
+
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
4971
|
+
function sanitizeAnswerKeys(keys) {
|
|
4972
|
+
if (keys === void 0) return void 0;
|
|
4973
|
+
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4967
4976
|
// src/services/questions/detectAskUserQuestion.ts
|
|
4968
4977
|
function normalizeContent2(raw) {
|
|
4969
4978
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5568,6 +5577,8 @@ var WSHub = class {
|
|
|
5568
5577
|
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.`;
|
|
5569
5578
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5570
5579
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5580
|
+
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5581
|
+
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5571
5582
|
var REFRESH_TTL_MS = 2e3;
|
|
5572
5583
|
var START_READY_TIMEOUT_MS = 15e3;
|
|
5573
5584
|
function parseIncludeAgentsEnv(raw) {
|
|
@@ -5658,6 +5669,14 @@ var StreamerServer = class {
|
|
|
5658
5669
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5659
5670
|
// Reverse map for cleanup on close
|
|
5660
5671
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
5672
|
+
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
5673
|
+
// { type: "auth", token } first message). Only authed sockets are added to
|
|
5674
|
+
// the hub and receive broadcasts.
|
|
5675
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
5676
|
+
wsAuthed = /* @__PURE__ */ new Set();
|
|
5677
|
+
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
5678
|
+
wsAuthPending = /* @__PURE__ */ new Map();
|
|
5679
|
+
wsAuthTimeoutMs;
|
|
5661
5680
|
cache = null;
|
|
5662
5681
|
projectsRepo = null;
|
|
5663
5682
|
conversationsRepo = null;
|
|
@@ -5697,6 +5716,7 @@ var StreamerServer = class {
|
|
|
5697
5716
|
this.scanProfiles = config.scanProfiles;
|
|
5698
5717
|
this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
5699
5718
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
5719
|
+
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5700
5720
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5701
5721
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5702
5722
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
@@ -5954,17 +5974,39 @@ var StreamerServer = class {
|
|
|
5954
5974
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
5955
5975
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
5956
5976
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
5957
|
-
handleWsOpen: (ws) => {
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
if (this.cacheReady) {
|
|
5962
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
5977
|
+
handleWsOpen: (ws, preAuthed) => {
|
|
5978
|
+
if (preAuthed) {
|
|
5979
|
+
this.completeWsAuth(ws);
|
|
5980
|
+
return;
|
|
5963
5981
|
}
|
|
5982
|
+
const timer = setTimeout(() => {
|
|
5983
|
+
this.wsAuthPending.delete(ws);
|
|
5984
|
+
try {
|
|
5985
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
5986
|
+
} catch {
|
|
5987
|
+
}
|
|
5988
|
+
}, this.wsAuthTimeoutMs);
|
|
5989
|
+
this.wsAuthPending.set(ws, timer);
|
|
5964
5990
|
},
|
|
5965
5991
|
handleWsMessage: async (ws, raw) => {
|
|
5966
5992
|
try {
|
|
5967
5993
|
const msg = JSON.parse(String(raw));
|
|
5994
|
+
if (!this.wsAuthed.has(ws)) {
|
|
5995
|
+
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
5996
|
+
const t = this.wsAuthPending.get(ws);
|
|
5997
|
+
if (t) clearTimeout(t);
|
|
5998
|
+
this.wsAuthPending.delete(ws);
|
|
5999
|
+
if (validateApiKey(msg.token, this.apiKey)) {
|
|
6000
|
+
this.completeWsAuth(ws);
|
|
6001
|
+
} else {
|
|
6002
|
+
try {
|
|
6003
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
6004
|
+
} catch {
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
5968
6010
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
5969
6011
|
const oldClientId = this.wsToClientId.get(ws);
|
|
5970
6012
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -5979,12 +6021,20 @@ var StreamerServer = class {
|
|
|
5979
6021
|
}
|
|
5980
6022
|
}
|
|
5981
6023
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5982
|
-
this.
|
|
6024
|
+
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
|
|
6025
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
6026
|
+
}
|
|
5983
6027
|
}
|
|
5984
6028
|
} catch {
|
|
5985
6029
|
}
|
|
5986
6030
|
},
|
|
5987
6031
|
handleWsClose: (ws) => {
|
|
6032
|
+
const pendingTimer = this.wsAuthPending.get(ws);
|
|
6033
|
+
if (pendingTimer) {
|
|
6034
|
+
clearTimeout(pendingTimer);
|
|
6035
|
+
this.wsAuthPending.delete(ws);
|
|
6036
|
+
}
|
|
6037
|
+
this.wsAuthed.delete(ws);
|
|
5988
6038
|
const clientId = this.wsToClientId.get(ws);
|
|
5989
6039
|
if (clientId) {
|
|
5990
6040
|
this.clientIdToWs.delete(clientId);
|
|
@@ -6054,6 +6104,20 @@ var StreamerServer = class {
|
|
|
6054
6104
|
this.wsHub.broadcast(payload);
|
|
6055
6105
|
}
|
|
6056
6106
|
}
|
|
6107
|
+
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6108
|
+
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6109
|
+
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6110
|
+
// broadcast.
|
|
6111
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6112
|
+
completeWsAuth(ws) {
|
|
6113
|
+
this.wsAuthed.add(ws);
|
|
6114
|
+
this.wsHub.addClient(ws);
|
|
6115
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6116
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6117
|
+
if (this.cacheReady) {
|
|
6118
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6057
6121
|
addSessionSubscriber(sessionId, ws) {
|
|
6058
6122
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
6059
6123
|
if (!subs) {
|
|
@@ -6340,6 +6404,9 @@ var StreamerServer = class {
|
|
|
6340
6404
|
this.ptyManager.dispose();
|
|
6341
6405
|
this.fileWatcher.dispose();
|
|
6342
6406
|
this.wsHub.dispose();
|
|
6407
|
+
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6408
|
+
this.wsAuthPending.clear();
|
|
6409
|
+
this.wsAuthed.clear();
|
|
6343
6410
|
this.pairTokens.dispose();
|
|
6344
6411
|
if (this.dbPool) {
|
|
6345
6412
|
await this.dbPool.end();
|
|
@@ -7466,12 +7533,16 @@ var StreamerServer = class {
|
|
|
7466
7533
|
return;
|
|
7467
7534
|
}
|
|
7468
7535
|
this.pendingPermission.set(sessionId, gate);
|
|
7536
|
+
const safeOptions = gate.options.map((o) => {
|
|
7537
|
+
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7538
|
+
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
7539
|
+
});
|
|
7469
7540
|
this.wsHub.broadcast({
|
|
7470
7541
|
type: "permission",
|
|
7471
7542
|
sessionId,
|
|
7472
7543
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7473
7544
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7474
|
-
options:
|
|
7545
|
+
options: safeOptions,
|
|
7475
7546
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7476
7547
|
});
|
|
7477
7548
|
}
|