fix: restrict package publishing and improve Slack delivery (#65)

- Set `publishConfig` to "restricted" and update the release script to enforce restricted access.
- Refactor Slack token handling to introduce a `withSlackTeamBotToken` helper.
- Improve delivery robustness by detecting `invalid_thread_ts` errors and automatically clearing stale Slack thread bindings.
- Add unit tests for Slack token routing and error detection logic.
This commit is contained in:
Bee
2026-03-31 23:37:36 -07:00
committed by GitHub
parent 891e2068ef
commit 94008708f4
3 changed files with 234 additions and 100 deletions
+4 -1
View File
@@ -4,6 +4,9 @@
"version": "0.0.0",
"description": "[EXPERIMENTAL] A lightweight Cline CLI built with the Cline SDKs",
"type": "module",
"publishConfig": {
"access": "restricted"
},
"bin": {
"clite": "dist/index.js"
},
@@ -27,7 +30,7 @@
"test:e2e:interactive": "vitest run --config vitest.interactive.e2e.config.ts",
"test:watch": "vitest --config vitest.config.ts",
"test:e2e:cli:tui": "cd src/tests && tui-test",
"release": "bun run build && bun publish --access public",
"release": "bun run build && bun publish --access restricted",
"link": "bun unlink && bun link"
},
"dependencies": {
@@ -121,4 +121,41 @@ describe("slack binding lookup", () => {
label: "alice",
});
});
it("routes Slack posts through the installation bot token for a team", async () => {
const calls: string[] = [];
const result = await __test__.withSlackTeamBotToken({
slack: {
getInstallation: async (teamId: string) => {
calls.push(`get:${teamId}`);
return { botToken: "xoxb-team-token" };
},
withBotToken: <T>(token: string, work: () => T): T => {
calls.push(`token:${token}`);
return work();
},
},
teamId: "T123",
work: async () => {
calls.push("work");
return "ok";
},
});
expect(result).toBe("ok");
expect(calls).toEqual(["get:T123", "token:xoxb-team-token", "work"]);
});
it("detects Slack invalid_thread_ts errors", () => {
expect(
__test__.isSlackInvalidThreadTsError(
new Error("An API error occurred: invalid_thread_ts"),
),
).toBe(true);
expect(
__test__.isSlackInvalidThreadTsError(
new Error("An API error occurred: channel_not_found"),
),
).toBe(false);
});
});
+193 -99
View File
@@ -52,6 +52,7 @@ import {
loadThreadState,
persistMergedThreadState,
readBindings,
writeBindings,
} from "../thread-bindings";
import type {
ConnectCommandDefinition,
@@ -170,11 +171,23 @@ function extractSlackTeamId(raw: unknown): string | undefined {
}
async function withSlackBindingBotToken<T>(input: {
slack: SlackAdapter;
slack: Pick<SlackAdapter, "getInstallation" | "withBotToken">;
binding: ConnectorThreadBinding<SlackThreadState>;
work: () => Promise<T>;
}): Promise<T> {
const teamId = input.binding.state?.teamId?.trim();
return withSlackTeamBotToken({
slack: input.slack,
teamId: input.binding.state?.teamId,
work: input.work,
});
}
async function withSlackTeamBotToken<T>(input: {
slack: Pick<SlackAdapter, "getInstallation" | "withBotToken">;
teamId?: string;
work: () => Promise<T>;
}): Promise<T> {
const teamId = input.teamId?.trim();
if (!teamId) {
return input.work();
}
@@ -185,6 +198,33 @@ async function withSlackBindingBotToken<T>(input: {
return input.slack.withBotToken(installation.botToken, input.work);
}
function isSlackInvalidThreadTsError(error: unknown): boolean {
const message =
error instanceof Error
? error.message
: typeof error === "string"
? error
: "";
return /\binvalid_thread_ts\b/i.test(message);
}
function clearSlackBinding(
bindingsPath: string,
bindingKey: string | undefined,
): boolean {
const key = bindingKey?.trim();
if (!key) {
return false;
}
const bindings = readBindings<SlackThreadState>(bindingsPath);
if (!bindings[key]) {
return false;
}
delete bindings[key];
writeBindings(bindingsPath, bindings);
return true;
}
async function persistSlackThreadContext(input: {
thread: Thread<SlackThreadState>;
bindingsPath: string;
@@ -266,6 +306,7 @@ async function deliverScheduledResult(input: {
? { key: threadId, binding: bindings[threadId] }
: undefined;
const binding = match?.binding;
const deliveryThreadId = match?.key || threadId || bindingKey;
if (!binding?.serializedThread) {
return;
}
@@ -282,11 +323,29 @@ async function deliverScheduledResult(input: {
} else {
body = `Schedule "${schedule?.name ?? input.scheduleId}" ${input.status}.${input.errorMessage ? `\n\n${input.errorMessage}` : ""}`;
}
await withSlackBindingBotToken({
slack: input.slack,
binding,
work: () => thread.post(body).then(() => undefined),
});
try {
await withSlackBindingBotToken({
slack: input.slack,
binding,
work: () => thread.post(body).then(() => undefined),
});
} catch (error) {
if (
isSlackInvalidThreadTsError(error) &&
clearSlackBinding(input.bindingsPath, deliveryThreadId)
) {
input.logger.core.warn?.(
"Cleared stale Slack binding after invalid_thread_ts",
{
transport: "slack",
threadId: deliveryThreadId,
scheduleId: input.scheduleId,
executionId: input.executionId,
},
);
}
throw error;
}
}
class SlackConnector extends ConnectorBase<
@@ -627,101 +686,118 @@ class SlackConnector extends ConnectorBase<
thread: Thread<SlackThreadState>,
text: string,
) => {
const queueKey =
(await loadThreadState(thread, bindingsPath, startRequest))
.participantKey || thread.id;
const currentState = await loadThreadState(
thread,
bindingsPath,
startRequest,
);
const queueKey = currentState.participantKey || thread.id;
const runTurn = async () => {
try {
await handleConnectorUserTurn({
thread,
text,
client,
pendingApprovals,
baseStartRequest: startRequest,
explicitSystemPrompt:
options.systemPrompt?.trim() || getConnectorSystemPrompt("slack"),
clientId,
logger: loggerAdapter,
transport: "slack",
botUserName: options.userName,
requestStop,
bindingsPath,
hookCommand: options.hookCommand,
systemRules: SLACK_SYSTEM_RULES,
errorLabel: "Slack",
firstContactMessage: SLACK_FIRST_CONTACT_MESSAGE,
userInstructionWatcher,
chatCommandHost,
activeTurns,
turnKey: queueKey,
getSessionMetadata: (currentThread, _clientId, currentState) => ({
userName: options.userName,
slackThreadId: currentThread.id,
slackChannelId: currentThread.channelId,
...(currentState.participantKey
? { slackParticipantKey: currentState.participantKey }
: {}),
...(currentState.participantLabel
? { slackParticipantLabel: currentState.participantLabel }
: {}),
}),
reusedLogMessage: "Slack thread reusing RPC session",
startedLogMessage: "Slack thread started RPC session",
onMessageReceived: async (details) => {
await dispatchConnectorHook(
options.hookCommand,
{
adapter: "slack",
botUserName: options.userName,
event: "message.received",
payload: details,
ts: new Date().toISOString(),
await withSlackTeamBotToken({
slack,
teamId: currentState.teamId,
work: async () =>
handleConnectorUserTurn({
thread,
text,
client,
pendingApprovals,
baseStartRequest: startRequest,
explicitSystemPrompt:
options.systemPrompt?.trim() ||
getConnectorSystemPrompt("slack"),
clientId,
logger: loggerAdapter,
transport: "slack",
botUserName: options.userName,
requestStop,
bindingsPath,
hookCommand: options.hookCommand,
systemRules: SLACK_SYSTEM_RULES,
errorLabel: "Slack",
firstContactMessage: SLACK_FIRST_CONTACT_MESSAGE,
userInstructionWatcher,
chatCommandHost,
activeTurns,
turnKey: queueKey,
getSessionMetadata: (
currentThread,
_clientId,
currentState,
) => ({
userName: options.userName,
slackThreadId: currentThread.id,
slackChannelId: currentThread.channelId,
...(currentState.participantKey
? { slackParticipantKey: currentState.participantKey }
: {}),
...(currentState.participantLabel
? { slackParticipantLabel: currentState.participantLabel }
: {}),
}),
reusedLogMessage: "Slack thread reusing RPC session",
startedLogMessage: "Slack thread started RPC session",
onMessageReceived: async (details) => {
await dispatchConnectorHook(
options.hookCommand,
{
adapter: "slack",
botUserName: options.userName,
event: "message.received",
payload: details,
ts: new Date().toISOString(),
},
loggerAdapter,
);
},
loggerAdapter,
);
},
onReplyCompleted: async (result) => {
await dispatchConnectorHook(
options.hookCommand,
{
adapter: "slack",
botUserName: options.userName,
event: "message.completed",
payload: {
threadId: result.threadId,
sessionId: result.sessionId,
finishReason: result.finishReason,
iterations: result.iterations,
outputPreview: truncateText(result.text),
outputLength: result.text.length,
},
ts: new Date().toISOString(),
onReplyCompleted: async (result) => {
await dispatchConnectorHook(
options.hookCommand,
{
adapter: "slack",
botUserName: options.userName,
event: "message.completed",
payload: {
threadId: result.threadId,
sessionId: result.sessionId,
finishReason: result.finishReason,
iterations: result.iterations,
outputPreview: truncateText(result.text),
outputLength: result.text.length,
},
ts: new Date().toISOString(),
},
loggerAdapter,
);
},
loggerAdapter,
);
},
onReplyFailed: async (details) => {
await dispatchConnectorHook(
options.hookCommand,
{
adapter: "slack",
botUserName: options.userName,
event: "message.failed",
payload: {
threadId: details.threadId,
sessionId: details.sessionId,
error: details.error.message,
},
ts: new Date().toISOString(),
onReplyFailed: async (details) => {
await dispatchConnectorHook(
options.hookCommand,
{
adapter: "slack",
botUserName: options.userName,
event: "message.failed",
payload: {
threadId: details.threadId,
sessionId: details.sessionId,
error: details.error.message,
},
ts: new Date().toISOString(),
},
loggerAdapter,
);
},
loggerAdapter,
);
},
}),
});
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
await thread.post(`Slack bridge error: ${message}`);
await withSlackTeamBotToken({
slack,
teamId: currentState.teamId,
work: () => thread.post(`Slack bridge error: ${message}`),
});
}
};
if (activeTurns.has(queueKey)) {
@@ -814,12 +890,28 @@ class SlackConnector extends ConnectorBase<
logger: loggerAdapter,
bindingsPath,
transport: "slack",
postToThread: async ({ thread, binding, body }) => {
await withSlackBindingBotToken({
slack,
binding,
work: () => thread.post(body).then(() => undefined),
});
postToThread: async ({ thread, binding, body, threadId }) => {
try {
await withSlackBindingBotToken({
slack,
binding,
work: () => thread.post(body).then(() => undefined),
});
} catch (error) {
if (
isSlackInvalidThreadTsError(error) &&
clearSlackBinding(bindingsPath, threadId)
) {
loggerAdapter.core.warn?.(
"Cleared stale Slack binding after invalid_thread_ts",
{
transport: "slack",
threadId,
},
);
}
throw error;
}
},
});
@@ -945,6 +1037,8 @@ export const slackConnector: ConnectCommandDefinition = new SlackConnector();
export const __test__ = {
buildSlackParticipantKey,
resolveSlackParticipant,
withSlackTeamBotToken,
isSlackInvalidThreadTsError,
findBindingForThread: (
bindings: ConnectorBindingStore<SlackThreadState>,
thread: Pick<Thread<SlackThreadState>, "id" | "channelId" | "isDM"> & {