slack-term 1.32.3 → 1.32.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.32.3",
3
+ "version": "1.32.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -8,6 +8,7 @@ import { join } from "node:path";
8
8
 
9
9
  import yargs, { type Options } from "yargs";
10
10
  import { hideBin } from "yargs/helpers";
11
+ import { guardUrlBoundaries } from "./urlGuard.ts";
11
12
  import { listProfiles, removeProfile, resolveBotToken, resolveCookie, resolveToken, useProfile, type Profile } from "./profiles.ts";
12
13
  import { diagnoseBotMessaging, formatDiagnosis } from "./botdoctor.ts";
13
14
  import { cmdAuthLogin, cmdAuthChrome, cmdAuthFirefox, cmdAuthToken, cmdAuthApp } from "./auth.ts";
@@ -1160,6 +1161,7 @@ function sumCounts(m: Map<string, number>): number {
1160
1161
 
1161
1162
  // --- edit ---
1162
1163
  interface EditArgs {
1164
+ allowUrlAdjacent?: boolean;
1163
1165
  target: string;
1164
1166
  newText: string;
1165
1167
  code?: string;
@@ -1252,6 +1254,8 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
1252
1254
  ? await encodeMentions(args.mentionToken ?? token, args.newText, channelId, mentionCookie ? { cookie: mentionCookie } : {})
1253
1255
  : args.newText;
1254
1256
 
1257
+ guardUrlBoundaries(newText, args.allowUrlAdjacent);
1258
+
1255
1259
  // Identity is part of the hash for the same reason it is on `send`: a code
1256
1260
  // minted while previewing as one identity must not confirm the write as
1257
1261
  // another (Slack only lets you edit your own messages, so a silent profile
@@ -1358,6 +1362,7 @@ async function cmdReact(token: string, args: ReactArgs): Promise<void> {
1358
1362
 
1359
1363
  // --- send ---
1360
1364
  interface SendArgs {
1365
+ allowUrlAdjacent?: boolean;
1361
1366
  target: string;
1362
1367
  message: string;
1363
1368
  code?: string;
@@ -1443,6 +1448,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
1443
1448
  // show the real line breaks so "how many lines is this?" is answered by
1444
1449
  // looking rather than by guessing.
1445
1450
  const rawMessage = unescapeArg(args.message);
1451
+ guardUrlBoundaries(rawMessage, args.allowUrlAdjacent);
1446
1452
  let message = rawMessage;
1447
1453
  let mentionReport: MentionEncodeResult | null = null;
1448
1454
  if (args.mentions) {
@@ -1720,6 +1726,7 @@ const ASK_MAX_CONSECUTIVE_ERRORS = 8;
1720
1726
 
1721
1727
 
1722
1728
  interface AskArgs {
1729
+ allowUrlAdjacent?: boolean;
1723
1730
  target: string;
1724
1731
  question: string;
1725
1732
  choices: string[];
@@ -2219,6 +2226,7 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
2219
2226
  // INTO a thread is thread-scoped for the same reason.
2220
2227
  const threadOnly = !channelId.startsWith("D") || !!threadTs;
2221
2228
  const message = askBuildText(question, body, reactable, overflow, threadOnly);
2229
+ guardUrlBoundaries(message, args.allowUrlAdjacent);
2222
2230
 
2223
2231
  // Preview the destination's last message, exactly as `send` does — the gate's
2224
2232
  // job is to make you look at where this is going before it goes. Fail-soft:
@@ -3621,13 +3629,14 @@ async function main(): Promise<void> {
3621
3629
  .positional("target", { type: "string", demandOption: true, describe: "#chan, @user, #chan:thread_ts, or permalink (a message permalink replies in its thread)" })
3622
3630
  .positional("message", { type: "string", demandOption: true })
3623
3631
  .option("code", { type: "string", describe: "Safety hash to confirm send" })
3632
+ .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
3624
3633
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
3625
3634
  .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
3626
3635
  .option("as-bot", { type: "boolean", default: false, describe: "Send via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient and can be two-way" })
3627
3636
  .option("broadcast", { type: "boolean", default: false, describe: "Also send to channel: broadcast a threaded reply back to the channel (Slack's \"Also send to #channel\" checkbox). Only effective with a thread target." })
3628
3637
  .option("mentions", { type: "boolean", default: true, describe: "Convert @handle tokens to real <@USERID> mentions (on by default; resolves via users.list, then channel members for Slack Connect guests). Unresolved handles stay as plain text. The confirm preview shows the converted message before sending. Disable with --no-mentions for literal @text." }),
3629
3638
  async (argv) => {
3630
- const args: SendArgs = { target: argv.target!, message: argv.message! };
3639
+ const args: SendArgs = { target: argv.target!, message: argv.message!, allowUrlAdjacent: argv["allow-url-adjacent"] };
3631
3640
  if (argv.code) args.code = argv.code;
3632
3641
  if (argv["channel-id"]) args.channelId = argv["channel-id"];
3633
3642
  if (argv["user-id"]) args.userId = argv["user-id"];
@@ -3699,6 +3708,7 @@ async function main(): Promise<void> {
3699
3708
  .positional("question", { type: "string", describe: "The question. Must @tag whoever may answer (@alice), or the whole channel (@here / @channel / @everyone) — only their answer counts. In a 1:1 DM the other party counts automatically." })
3700
3709
  .positional("choices", { type: "string", array: true, describe: "Up to 10 choices, seeded as 1️⃣..🔟 reactions. Beyond 10 they are listed but answerable only by text. With none, the question asks for a free-text reply." })
3701
3710
  .option("code", { type: "string", describe: "Safety hash to confirm the ask" })
3711
+ .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
3702
3712
  .option("body", { type: "string", describe: "Extra context shown under the question" })
3703
3713
  .option("wait", { type: "boolean", default: false, describe: "Block until answered; print ONLY the answer on stdout. Exit 0 = answered, 2 = nobody replied, 3 = transport failure, 4 = somebody replied but picked none of the choices (stdout empty — do not act on it)." })
3704
3714
  .option("waitFor", { type: "string", describe: "Collect the answer to a question already posted: pass its permalink. Nothing is posted. Same stdout/exit contract as --wait; --timeout 0 checks once and exits 2 if still open." })
@@ -3759,6 +3769,7 @@ async function main(): Promise<void> {
3759
3769
  process.exit(ASK_EXIT_ERROR);
3760
3770
  }
3761
3771
  const args: AskArgs = {
3772
+ allowUrlAdjacent: argv["allow-url-adjacent"],
3762
3773
  target: argv.target!,
3763
3774
  question,
3764
3775
  choices: ((argv.choices as string[] | undefined) ?? []).map((s) => String(s).trim()).filter(Boolean),
@@ -4077,11 +4088,12 @@ async function main(): Promise<void> {
4077
4088
  .positional("target", { type: "string", demandOption: true, describe: "#chan:ts, @user:ts, or permalink" })
4078
4089
  .positional("newText", { type: "string", demandOption: true })
4079
4090
  .option("code", { type: "string", describe: "Safety hash to confirm edit" })
4091
+ .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
4080
4092
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
4081
4093
  .option("mentions", { type: "boolean", default: true, describe: "Convert @handle tokens in the new text to real <@USERID> mentions (on by default). Unresolved handles stay as plain text. Disable with --no-mentions for literal @text." })
4082
4094
  .option("as-bot", { type: "boolean", default: false, describe: "Edit as the bot (xoxb / SLACK_BOT_TOKEN). REQUIRED to correct a message the bot posted: Slack only lets a token edit its OWN messages, so a bot-authored message is uneditable by the user token and chat.update returns cant_update_message. Address it by permalink or --channel-id; an @user target resolves against the BOT's DM list, which is where a bot-authored DM actually lives." }),
4083
4095
  async (argv) => {
4084
- const args: EditArgs = { target: argv.target!, newText: argv.newText! };
4096
+ const args: EditArgs = { target: argv.target!, newText: argv.newText!, allowUrlAdjacent: argv["allow-url-adjacent"] };
4085
4097
  if (argv.code) args.code = argv.code;
4086
4098
  if (argv["channel-id"]) args.channelId = argv["channel-id"];
4087
4099
  if (argv.mentions !== false) args.mentions = true;
package/ts/slack.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Slack Web API client (user token, Authorization: Bearer)
2
+ import { resolveCookie } from "./profiles.ts";
2
3
 
3
4
  export class RateLimitError extends Error {
4
5
  retryAfter: number;
@@ -22,6 +23,7 @@ function base(): string {
22
23
  }
23
24
 
24
25
  async function call(token: string, method: string, init: RequestInit, cookie?: string): Promise<Json> {
26
+ cookie = token.startsWith("xoxc-") ? (cookie ?? resolveCookie()) : undefined;
25
27
  const extraHeaders: Record<string, string> = {};
26
28
  if (cookie) extraHeaders["Cookie"] = `d=${cookie}`;
27
29
  const res = await fetch(`${base()}/${method}`, {
@@ -152,6 +154,7 @@ export async function authTest(
152
154
  export async function authScopes(token: string, cookie?: string): Promise<{
153
155
  userId: string; user: string; botId: string; team: string; url: string; scopes: string[];
154
156
  }> {
157
+ cookie = token.startsWith("xoxc-") ? (cookie ?? resolveCookie()) : undefined;
155
158
  const headers: Record<string, string> = { Authorization: `Bearer ${token}` };
156
159
  if (cookie) headers["Cookie"] = `d=${cookie}`;
157
160
  const res = await fetch(`${base()}/auth.test`, { method: "GET", headers });
package/ts/urlGuard.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** Bare Unicode URL text has an ambiguous boundary for Slack's autolinker.
2
+ * Explicit Slack links delimit the URL, including intentional Unicode paths.
3
+ * ASCII whitespace (especially newlines) is a safe boundary; Unicode spaces
4
+ * should be made explicit too. Do not rewrite the message or its newlines. */
5
+ export function adjacentUrls(text: string): string[] {
6
+ const tokens = text.matchAll(/<https?:\/\/[^\s<>|]+(?:\|[^<>\r\n]*)?>|https?:\/\/[^\t\n\v\f\r <]+/gi);
7
+ return [...tokens]
8
+ .map(([token]) => token)
9
+ .filter((token) => !token.startsWith("<") && (/[^\x00-\x7f]/.test(token) || token.includes(">")));
10
+ }
11
+
12
+ export function guardUrlBoundaries(text: string, allow = false, warn: (message: string) => void = console.error): void {
13
+ const urls = adjacentUrls(text);
14
+ if (!urls.length) return;
15
+ const message = `Ambiguous URL boundary: ${urls.map((url) => JSON.stringify(url)).join(", ")}. ` +
16
+ "Put the URL on its own line, or wrap it as <url> / <url|label>. " +
17
+ "Use --allow-url-adjacent to warn only.";
18
+ if (!allow) throw new Error(message);
19
+ warn(`Warning: ${message}`);
20
+ }