slack-term 1.41.0 → 1.41.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/SKILL.md +1 -0
- package/dist/cli.js +160 -159
- package/package.json +1 -1
- package/ts/botIntent.ts +68 -0
- package/ts/cli.ts +34 -4
package/package.json
CHANGED
package/ts/botIntent.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Refuse to act as the USER when the caller clearly meant the BOT.
|
|
2
|
+
//
|
|
3
|
+
// SLACK_BOT_TOKEN is only ever used by `--as-bot`: its documented home is the
|
|
4
|
+
// global ~/.config/slack-cli/.env, where it sits for `slack ask` and friends,
|
|
5
|
+
// and it must not turn every post into a bot post. But a lane that EXPORTS it
|
|
6
|
+
// (or puts it in its project's .env) without SLACK_TOKEN is saying "I am the
|
|
7
|
+
// bot" — and without --as-bot every write still went out under the logged-in
|
|
8
|
+
// person's session, silently (reported 2026-10-26: a lane posted a pinlog as
|
|
9
|
+
// the user). In that case writes are refused until the caller picks a side.
|
|
10
|
+
|
|
11
|
+
/** Write commands, as `argv._` spells them (aliases included). Read-only
|
|
12
|
+
* commands are never refused — reading as the user impersonates no one. */
|
|
13
|
+
const WRITES = new Set([
|
|
14
|
+
"send", "ask", "poll", "vote", "edit", "delete", "react", "upload",
|
|
15
|
+
"schedule send", "schedule rm",
|
|
16
|
+
"pinlog new", "pinlog set", "pinlog note", "pinlog pin",
|
|
17
|
+
"todo set", "todo flag",
|
|
18
|
+
"lists create", "lists add", "lists set", "lists share",
|
|
19
|
+
"drafts new", "drafts save", "drafts edit", "drafts update", "drafts delete", "drafts rm",
|
|
20
|
+
"channel create", "channel new", "channel rename", "channel mv",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** `argv._` holds what was TYPED, so top-level aliases must be folded first. */
|
|
24
|
+
const TOP_ALIAS: Record<string, string> = { channels: "channel", ch: "channel" };
|
|
25
|
+
|
|
26
|
+
export function isWriteCommand(path: (string | number)[]): boolean {
|
|
27
|
+
const [raw = "", b = ""] = path.map(String);
|
|
28
|
+
const a = TOP_ALIAS[raw] ?? raw;
|
|
29
|
+
return WRITES.has(a) || WRITES.has(`${a} ${b}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read-only modes of a write command. `ask --ls` only reads the local
|
|
33
|
+
* sent-log and the questions' live state. (`ask --waitFor` and
|
|
34
|
+
* `poll --results` are NOT here: both can edit the posted message.) */
|
|
35
|
+
function isReadMode(path: (string | number)[], argv: Record<string, unknown>): boolean {
|
|
36
|
+
return String(path[0] ?? "") === "ask" && argv.ls === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type BotIntent = {
|
|
40
|
+
/** Where SLACK_BOT_TOKEN (an xoxb-) came from: "shell", a dotenv path, or undefined when unset. */
|
|
41
|
+
botTokenSource: string | undefined;
|
|
42
|
+
/** dotenv paths that are the bot token's GLOBAL home — not a statement of intent. */
|
|
43
|
+
globalPaths: string[];
|
|
44
|
+
slackTokenSet: boolean;
|
|
45
|
+
/** The token this command would act with is itself the bot token (no profiles). */
|
|
46
|
+
activeTokenIsBot: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A refusal message, or null when the write may proceed. */
|
|
50
|
+
export function botIntentRefusal(
|
|
51
|
+
cmd: (string | number)[],
|
|
52
|
+
argv: Record<string, unknown>,
|
|
53
|
+
intent: BotIntent,
|
|
54
|
+
): string | null {
|
|
55
|
+
const src = intent.botTokenSource;
|
|
56
|
+
if (!src || intent.globalPaths.includes(src)) return null;
|
|
57
|
+
if (intent.slackTokenSet || intent.activeTokenIsBot) return null;
|
|
58
|
+
if (!isWriteCommand(cmd) || isReadMode(cmd, argv) || argv["as-bot"] === true) return null;
|
|
59
|
+
const from = src === "shell" ? "exported in the environment" : `set in ${src}`;
|
|
60
|
+
const canBot = "as-bot" in argv;
|
|
61
|
+
return [
|
|
62
|
+
`Error: refusing to act as YOU — SLACK_BOT_TOKEN is ${from}, but \`${cmd.slice(0, 2).join(" ")}\` would use your user session, not the bot.`,
|
|
63
|
+
canBot
|
|
64
|
+
? " As the app: add --as-bot"
|
|
65
|
+
: " This command has no --as-bot: it can only act as a user.",
|
|
66
|
+
" As yourself: set SLACK_TOKEN, or clear the bot token for this run (SLACK_BOT_TOKEN= slack ...)",
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
package/ts/cli.ts
CHANGED
|
@@ -130,11 +130,21 @@ import {
|
|
|
130
130
|
import { setCacheEnabled } from "./cache.ts";
|
|
131
131
|
import { escapeArg, unescapeArg } from "./escapes.ts";
|
|
132
132
|
import { attachmentLines, parseAttachments } from "./attachments.ts";
|
|
133
|
+
import { botIntentRefusal } from "./botIntent.ts";
|
|
133
134
|
import { dayLabel, encodeMentions, encodeMentionsDetailed, findUntaggedMentions, formatYmdHm, maskSecrets, mentionWarnings, resolveDateMarkup, resolveMentions, type MentionEncodeResult } from "./format.ts";
|
|
134
135
|
import { quietHoursNotice } from "./quietHours.ts";
|
|
135
136
|
import { installSyncConsole } from "./stdout.ts";
|
|
136
137
|
import { LockBusyError, PINLOG_MARKER, acquireLock, composeHead, formatJst, headUpdatedAt, isPinlogHead, parsePinlogId, pinlogId, stripPinlogFooter } from "./pinlog.ts";
|
|
137
138
|
|
|
139
|
+
// Which dotenv file supplied each variable (absent = the shell's own env).
|
|
140
|
+
const dotenvSource = new Map<string, string>();
|
|
141
|
+
// Every value a GLOBAL dotenv holds, even when the shell already had the key.
|
|
142
|
+
// A child of `slack agent` (or any process the CLI spawned) inherits the
|
|
143
|
+
// global SLACK_BOT_TOKEN as a plain env var; matching it back to its global
|
|
144
|
+
// file keeps that inherited copy from reading as an explicit export.
|
|
145
|
+
const globalDotenvValue = new Map<string, { path: string; val: string }[]>();
|
|
146
|
+
const GLOBAL_DOTENVS = [join(homedir(), ".slack-cli", ".env.local"), join(homedir(), ".config/slack-cli", ".env")];
|
|
147
|
+
|
|
138
148
|
function loadDotenv(path: string): void {
|
|
139
149
|
if (!existsSync(path)) return;
|
|
140
150
|
const text = readFileSync(path, "utf8");
|
|
@@ -148,7 +158,13 @@ function loadDotenv(path: string): void {
|
|
|
148
158
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
149
159
|
val = val.slice(1, -1);
|
|
150
160
|
}
|
|
151
|
-
if (
|
|
161
|
+
if (GLOBAL_DOTENVS.includes(path)) {
|
|
162
|
+
globalDotenvValue.set(key, [...(globalDotenvValue.get(key) ?? []), { path, val }]);
|
|
163
|
+
}
|
|
164
|
+
if (!(key in process.env)) {
|
|
165
|
+
process.env[key] = val;
|
|
166
|
+
dotenvSource.set(key, path);
|
|
167
|
+
}
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
170
|
|
|
@@ -160,8 +176,7 @@ function ensureSlackCliDir(dir: string): void {
|
|
|
160
176
|
|
|
161
177
|
function loadDotenvFiles(): void {
|
|
162
178
|
// Global home config
|
|
163
|
-
|
|
164
|
-
loadDotenv(join(homedir(), ".config/slack-cli", ".env"));
|
|
179
|
+
for (const p of GLOBAL_DOTENVS) loadDotenv(p);
|
|
165
180
|
// Local project overrides (highest priority — loaded last so they win)
|
|
166
181
|
loadDotenv(join(process.cwd(), ".slack-cli", ".env.local"));
|
|
167
182
|
loadDotenv(join(process.cwd(), ".env.local"));
|
|
@@ -4965,7 +4980,22 @@ async function main(): Promise<void> {
|
|
|
4965
4980
|
// `sent` reads only the local log — no token, no network.
|
|
4966
4981
|
if (!cmd || cmd === "auth" || cmd === "login" || cmd === "agent" || cmd === "sent") return;
|
|
4967
4982
|
try {
|
|
4968
|
-
resolveToken((argv as W).workspace);
|
|
4983
|
+
const active = resolveToken((argv as W).workspace);
|
|
4984
|
+
const bot = resolveBotToken();
|
|
4985
|
+
const refusal = botIntentRefusal(argv._ ?? [], argv as Record<string, unknown>, {
|
|
4986
|
+
botTokenSource: bot
|
|
4987
|
+
? dotenvSource.get("SLACK_BOT_TOKEN")
|
|
4988
|
+
?? globalDotenvValue.get("SLACK_BOT_TOKEN")?.find((g) => g.val === bot)?.path
|
|
4989
|
+
?? "shell"
|
|
4990
|
+
: undefined,
|
|
4991
|
+
globalPaths: GLOBAL_DOTENVS,
|
|
4992
|
+
slackTokenSet: !!process.env.SLACK_TOKEN,
|
|
4993
|
+
activeTokenIsBot: !!bot && active === bot,
|
|
4994
|
+
});
|
|
4995
|
+
if (refusal) {
|
|
4996
|
+
console.error(refusal);
|
|
4997
|
+
process.exit(2);
|
|
4998
|
+
}
|
|
4969
4999
|
} catch (e) {
|
|
4970
5000
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4971
5001
|
if (msg.startsWith("No profiles configured")) {
|