@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 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 true;
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
- this.wsHub.addClient(ws2);
144725
- const sessions = this.sessionStore.list(this.ptyAttachedIds());
144726
- ws2.send(JSON.stringify({ type: "session_list", sessions }));
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.startGraceTimer(msg.sessionId, 0);
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: gate.options,
146312
+ options: safeOptions,
146241
146313
  ...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
146242
146314
  });
146243
146315
  }