claude-threads 1.16.0 → 1.16.1
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/CHANGELOG.md +14 -0
- package/dist/index.js +63 -21
- package/dist/mcp/mcp-server.js +1 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.16.1] - 2026-05-22
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- **Fail-closed authorization on the user-driven paths that invoke Claude.** A confirmed bypass let an unauthorized Slack user (not in `allowedUsers`, non-empty allowlist) reach Claude: the bot posted both the "not authorized" warning and a real answer in the same thread. Rather than chase the one leaking caller, authorization is now deny-by-default at the functions that spawn or message Claude on a user's behalf: `startSession`, `sendFollowUp`, and `resumePausedSession`, plus the resume-from-reaction path. A single helper, `isAuthorizedForSession`, encodes every tier (global allowlist, empty-allowlist allow-all, per-session owner and invited users) and refuses a missing, empty, or `unknown` username. Every user-facing path now routes through that one helper instead of duplicating the check. The biggest gap was message-driven resume, which ran purely from persisted state with no identity check; it now takes a `username` and verifies it against the persisted session allowlist before resuming. The two system-triggered resume paths (bulk restore after a bot restart) stay ungated by design, since they carry no user identity. Legitimate username-less follow-ups (passthrough slash commands like `/context`, already authorized upstream) pass an explicit `system` flag so they are not caught. The mid-session approval flow is untouched: it still adds approved users to the session allowlist, so the check passes once approval is granted. (#388)
|
|
14
|
+
- **hono 4.12.18 → 4.12.21** picks up four upstream security fixes: GHSA-2gcr-mfcq-wcc3 (`app.mount()` stripped the mount prefix from the raw, undecoded pathname, so percent-encoded paths could be cut at the wrong offset and reach a sub-app with the wrong path), GHSA-xrhx-7g5j-rcj5 (`hono/ip-restriction` compared IPs by string equality, so non-canonical IPv6 forms such as compressed or hex IPv4-mapped addresses slipped past static deny rules), GHSA-3hrh-pfw6-9m5x (the cookie helper didn't sanitize `sameSite`/`priority`, allowing Set-Cookie injection via `;`, `\r`, `\n`), and GHSA-f577-qrjj-4474 (`hono/jwt` accepted any two-part Authorization header regardless of scheme, not just `Bearer`). claude-threads reaches hono through `@hono/node-server` on the inbound webhook surface, so the mount-prefix and Set-Cookie fixes are the relevant ones. (#386)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Production deps** bumped: `@hono/node-server` 2.0.2 → 2.0.3 (preserves headers mutated after raw Response construction), `express-rate-limit` 8.5.1 → 8.5.2 (simplified IPv6 key generation). Lockfile-only. (#386)
|
|
18
|
+
- **Dev deps** bumped: `@types/bun` 1.3.13 → 1.3.14, `@types/node` 25.7.0 → 25.9.1, `@types/react` 19.2.14 → 19.2.15, `eslint` 10.3.0 → 10.4.0 (adds `includeIgnoreFile()` and a `for-direction` sequence-expression check, both additive), `lint-staged` 17.0.4 → 17.0.5, `typescript-eslint` 8.59.3 → 8.59.4 (fixes a `no-floating-promises` stack overflow on recursive types). Lockfile-only. (#385)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Multiple pasted screenshots no longer silently vanish.** When you paste several clipboard images into one chat message, the platforms hand them all the same filename (`image.png`). The save path wrote each one with the `wx` (`O_EXCL`) flag, so the second and later files hit `EEXIST`, got caught, and were reported as a download failure, so they never reached Claude. Saves now dedupe filenames within a single message, inserting a numeric suffix before the extension (`image.png`, `image_1.png`, `image_2.png`); files without an extension get `report`, `report_1`, and so on. The unique name is resolved before the write, so the `wx` flag still does its job against symlink races. (#387)
|
|
22
|
+
- **Inbound attachments are no longer capped at 100 MB.** A hard 100 MB ceiling on incoming files meant anything larger was skipped with a "too large" warning, which surprised people sharing big assets. The cap is gone; an attachment of any reported size is downloaded and written to disk. One tradeoff worth knowing: `downloadFile` still buffers the whole file in memory via `arrayBuffer()`, so a very large attachment is held in RAM while it's written. Streaming that download is a separate change. (#387)
|
|
23
|
+
|
|
10
24
|
## [1.16.0] - 2026-05-14
|
|
11
25
|
|
|
12
26
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -50355,6 +50355,24 @@ function sanitizeFilename(name) {
|
|
|
50355
50355
|
}
|
|
50356
50356
|
return cleaned;
|
|
50357
50357
|
}
|
|
50358
|
+
function dedupeFilename(name, used) {
|
|
50359
|
+
if (!used.has(name)) {
|
|
50360
|
+
used.add(name);
|
|
50361
|
+
return name;
|
|
50362
|
+
}
|
|
50363
|
+
const lastDot = name.lastIndexOf(".");
|
|
50364
|
+
const hasExt = lastDot > 0;
|
|
50365
|
+
const stem = hasExt ? name.slice(0, lastDot) : name;
|
|
50366
|
+
const ext = hasExt ? name.slice(lastDot) : "";
|
|
50367
|
+
let counter = 1;
|
|
50368
|
+
let candidate = `${stem}_${counter}${ext}`;
|
|
50369
|
+
while (used.has(candidate)) {
|
|
50370
|
+
counter += 1;
|
|
50371
|
+
candidate = `${stem}_${counter}${ext}`;
|
|
50372
|
+
}
|
|
50373
|
+
used.add(candidate);
|
|
50374
|
+
return candidate;
|
|
50375
|
+
}
|
|
50358
50376
|
function formatBytes(bytes) {
|
|
50359
50377
|
if (bytes < 1024)
|
|
50360
50378
|
return `${bytes} B`;
|
|
@@ -53585,6 +53603,18 @@ function getSessionStatus(session) {
|
|
|
53585
53603
|
return "idle";
|
|
53586
53604
|
}
|
|
53587
53605
|
|
|
53606
|
+
// src/session/authorization.ts
|
|
53607
|
+
function isAuthorizedForSession(check) {
|
|
53608
|
+
const { username, platform, sessionAllowedUsers } = check;
|
|
53609
|
+
if (!username || username === "unknown") {
|
|
53610
|
+
return false;
|
|
53611
|
+
}
|
|
53612
|
+
if (platform.isUserAllowed(username)) {
|
|
53613
|
+
return true;
|
|
53614
|
+
}
|
|
53615
|
+
return sessionAllowedUsers?.has(username) ?? false;
|
|
53616
|
+
}
|
|
53617
|
+
|
|
53588
53618
|
// src/claude/cli.ts
|
|
53589
53619
|
init_spawn();
|
|
53590
53620
|
init_logger();
|
|
@@ -54818,7 +54848,7 @@ function createPassthroughHandler(slashCommand) {
|
|
|
54818
54848
|
return { handled: false };
|
|
54819
54849
|
}
|
|
54820
54850
|
if (ctx.isAllowed) {
|
|
54821
|
-
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}
|
|
54851
|
+
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}`, undefined, undefined, undefined, { system: true });
|
|
54822
54852
|
}
|
|
54823
54853
|
return { handled: true };
|
|
54824
54854
|
};
|
|
@@ -54866,7 +54896,7 @@ async function handleDynamicSlashCommand(command, args, ctx) {
|
|
|
54866
54896
|
}
|
|
54867
54897
|
if (ctx.isAllowed) {
|
|
54868
54898
|
const fullCommand = args ? `/${command} ${args}` : `/${command}`;
|
|
54869
|
-
await ctx.sessionManager.sendFollowUp(ctx.threadId, fullCommand);
|
|
54899
|
+
await ctx.sessionManager.sendFollowUp(ctx.threadId, fullCommand, undefined, undefined, undefined, { system: true });
|
|
54870
54900
|
}
|
|
54871
54901
|
return { handled: true };
|
|
54872
54902
|
}
|
|
@@ -55387,7 +55417,6 @@ import { lstat, mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } f
|
|
|
55387
55417
|
import { tmpdir as tmpdir2 } from "os";
|
|
55388
55418
|
import { join as join9 } from "path";
|
|
55389
55419
|
var log18 = createLogger("streaming");
|
|
55390
|
-
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
55391
55420
|
var UPLOAD_ROOT_DIR = "claude-threads-uploads";
|
|
55392
55421
|
function safeIdSegment(id) {
|
|
55393
55422
|
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -55427,18 +55456,11 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
|
|
|
55427
55456
|
return { saved, skipped };
|
|
55428
55457
|
}
|
|
55429
55458
|
const messageDir = await mkdtemp(join9(uploadDir, `${Date.now().toString(36)}-`));
|
|
55459
|
+
const usedNames = new Set;
|
|
55430
55460
|
for (const file of files) {
|
|
55431
|
-
if (file.size > MAX_UPLOAD_SIZE) {
|
|
55432
|
-
skipped.push({
|
|
55433
|
-
name: file.name,
|
|
55434
|
-
reason: `File too large (${formatBytes(file.size)} > ${formatBytes(MAX_UPLOAD_SIZE)} limit)`,
|
|
55435
|
-
suggestion: "Split the file or share it via an external link"
|
|
55436
|
-
});
|
|
55437
|
-
continue;
|
|
55438
|
-
}
|
|
55439
55461
|
try {
|
|
55440
55462
|
const buffer = await platform.downloadFile(file.id);
|
|
55441
|
-
const safeName = sanitizeFilename(file.name);
|
|
55463
|
+
const safeName = dedupeFilename(sanitizeFilename(file.name), usedNames);
|
|
55442
55464
|
const absolutePath = join9(messageDir, safeName);
|
|
55443
55465
|
await writeFile2(absolutePath, buffer, { mode: 384, flag: "wx" });
|
|
55444
55466
|
saved.push({
|
|
@@ -66728,6 +66750,10 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66728
66750
|
if (!platform) {
|
|
66729
66751
|
throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
|
|
66730
66752
|
}
|
|
66753
|
+
if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers: undefined })) {
|
|
66754
|
+
log30.warn(`auth.denied.startSession: @${username || "unknown"} not authorized to start session in ${threadId.substring(0, 8)}...`);
|
|
66755
|
+
return;
|
|
66756
|
+
}
|
|
66731
66757
|
const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
|
|
66732
66758
|
if (activeOrPending >= ctx.config.maxSessions) {
|
|
66733
66759
|
const formatter2 = platform.getFormatter();
|
|
@@ -67095,9 +67121,15 @@ ${failFormatter.formatItalic("Your previous conversation context is preserved, b
|
|
|
67095
67121
|
await ctx.ops.updateStickyMessage();
|
|
67096
67122
|
}
|
|
67097
67123
|
}
|
|
67098
|
-
async function sendFollowUp(session, message, files, ctx, username, displayName) {
|
|
67124
|
+
async function sendFollowUp(session, message, files, ctx, username, displayName, options) {
|
|
67099
67125
|
if (!session.claude.isRunning())
|
|
67100
67126
|
return;
|
|
67127
|
+
if (!options?.system) {
|
|
67128
|
+
if (!isAuthorizedForSession({ username, platform: session.platform, sessionAllowedUsers: session.sessionAllowedUsers })) {
|
|
67129
|
+
sessionLog6(session).warn(`auth.denied.sendFollowUp: @${username || "unknown"} not authorized`);
|
|
67130
|
+
return;
|
|
67131
|
+
}
|
|
67132
|
+
}
|
|
67101
67133
|
if (session.needsContextPromptOnNextMessage) {
|
|
67102
67134
|
session.needsContextPromptOnNextMessage = false;
|
|
67103
67135
|
await session.messageManager?.prepareForUserMessage();
|
|
@@ -67120,7 +67152,7 @@ async function sendFollowUp(session, message, files, ctx, username, displayName)
|
|
|
67120
67152
|
session.messageCount++;
|
|
67121
67153
|
await session.messageManager.handleUserMessage(messageToSend, files, username, displayName);
|
|
67122
67154
|
}
|
|
67123
|
-
async function resumePausedSession(threadId, message, files, ctx) {
|
|
67155
|
+
async function resumePausedSession(threadId, message, files, ctx, username) {
|
|
67124
67156
|
const persisted = ctx.state.sessionStore.load();
|
|
67125
67157
|
const state = findPersistedByThreadId(persisted, threadId);
|
|
67126
67158
|
if (!state) {
|
|
@@ -67128,6 +67160,16 @@ async function resumePausedSession(threadId, message, files, ctx) {
|
|
|
67128
67160
|
return;
|
|
67129
67161
|
}
|
|
67130
67162
|
const shortId = threadId.substring(0, 8);
|
|
67163
|
+
const platform = ctx.state.platforms.get(state.platformId);
|
|
67164
|
+
if (!platform) {
|
|
67165
|
+
log30.warn(`auth.denied.resume: platform '${state.platformId}' not found for ${shortId}...`);
|
|
67166
|
+
return;
|
|
67167
|
+
}
|
|
67168
|
+
const sessionAllowedUsers = new Set(state.sessionAllowedUsers || [state.startedBy].filter(Boolean));
|
|
67169
|
+
if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
|
|
67170
|
+
log30.warn(`auth.denied.resume: @${username || "unknown"} not authorized to resume ${shortId}...`);
|
|
67171
|
+
return;
|
|
67172
|
+
}
|
|
67131
67173
|
log30.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
|
|
67132
67174
|
await resumeSession(state, ctx);
|
|
67133
67175
|
const session = ctx.ops.findSessionByThreadId(threadId);
|
|
@@ -67645,9 +67687,9 @@ async function tryResumeFromReaction(deps, platformId, postId, username) {
|
|
|
67645
67687
|
const sessionId = `${platformId}:${persistedSession.threadId}`;
|
|
67646
67688
|
if (deps.registry.hasById(sessionId))
|
|
67647
67689
|
return false;
|
|
67648
|
-
const allowedUsers = new Set(persistedSession.sessionAllowedUsers);
|
|
67649
67690
|
const platform = deps.platforms.get(platformId);
|
|
67650
|
-
|
|
67691
|
+
const sessionAllowedUsers = new Set(persistedSession.sessionAllowedUsers || [persistedSession.startedBy].filter(Boolean));
|
|
67692
|
+
if (!platform || !isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
|
|
67651
67693
|
if (platform) {
|
|
67652
67694
|
await platform.createPost(`⚠️ @${username} is not authorized to resume this session`, persistedSession.threadId);
|
|
67653
67695
|
}
|
|
@@ -68233,11 +68275,11 @@ class SessionManager extends EventEmitter4 {
|
|
|
68233
68275
|
}
|
|
68234
68276
|
return;
|
|
68235
68277
|
}
|
|
68236
|
-
async sendFollowUp(threadId, message, files, username, displayName) {
|
|
68278
|
+
async sendFollowUp(threadId, message, files, username, displayName, options) {
|
|
68237
68279
|
const session = this.findSessionByThreadId(threadId);
|
|
68238
68280
|
if (!session || !session.claude.isRunning())
|
|
68239
68281
|
return;
|
|
68240
|
-
await sendFollowUp(session, message, files, this.getContext(), username, displayName);
|
|
68282
|
+
await sendFollowUp(session, message, files, this.getContext(), username, displayName, options);
|
|
68241
68283
|
}
|
|
68242
68284
|
isSessionActive() {
|
|
68243
68285
|
return this.registry.size > 0;
|
|
@@ -68251,8 +68293,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
68251
68293
|
return false;
|
|
68252
68294
|
return this.registry.getPersistedByThreadId(threadId) !== undefined;
|
|
68253
68295
|
}
|
|
68254
|
-
async resumePausedSession(threadId, message, files) {
|
|
68255
|
-
await resumePausedSession(threadId, message, files, this.getContext());
|
|
68296
|
+
async resumePausedSession(threadId, message, files, username) {
|
|
68297
|
+
await resumePausedSession(threadId, message, files, this.getContext(), username);
|
|
68256
68298
|
}
|
|
68257
68299
|
getPersistedSession(threadId) {
|
|
68258
68300
|
return this.registry.getPersistedByThreadId(threadId);
|
|
@@ -78305,7 +78347,7 @@ async function handleMessage(client, session, post2, user, options) {
|
|
|
78305
78347
|
}
|
|
78306
78348
|
const files2 = post2.metadata?.files;
|
|
78307
78349
|
if (content || files2?.length) {
|
|
78308
|
-
await session.resumePausedSession(threadRoot, content, files2);
|
|
78350
|
+
await session.resumePausedSession(threadRoot, content, files2, username);
|
|
78309
78351
|
}
|
|
78310
78352
|
return;
|
|
78311
78353
|
}
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -51043,7 +51043,6 @@ function formatBytes(bytes) {
|
|
|
51043
51043
|
|
|
51044
51044
|
// src/operations/streaming/handler.ts
|
|
51045
51045
|
var log2 = createLogger("streaming");
|
|
51046
|
-
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
51047
51046
|
async function postSkippedFilesFeedback(platform, threadId, skipped) {
|
|
51048
51047
|
if (skipped.length === 0)
|
|
51049
51048
|
return;
|
|
@@ -55810,7 +55809,7 @@ function createPassthroughHandler(slashCommand) {
|
|
|
55810
55809
|
return { handled: false };
|
|
55811
55810
|
}
|
|
55812
55811
|
if (ctx.isAllowed) {
|
|
55813
|
-
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}
|
|
55812
|
+
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}`, undefined, undefined, undefined, { system: true });
|
|
55814
55813
|
}
|
|
55815
55814
|
return { handled: true };
|
|
55816
55815
|
};
|