slkcli 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/src/auth.js +40 -7
package/README.md CHANGED
@@ -145,9 +145,9 @@ On first run, macOS will show a Keychain dialog asking whether to allow access t
145
145
 
146
146
  ### How it works
147
147
 
148
- 1. **Cookie decryption** — Reads the encrypted `d` cookie from Slack's SQLite cookie store (`~/Library/Application Support/Slack/Cookies`). Decrypts it using the "Slack Safe Storage" key from the macOS Keychain via PBKDF2 + AES-128-CBC.
148
+ 1. **Cookie decryption** — Reads the encrypted `d` cookie from Slack's SQLite cookie store (`Cookies` file). Decrypts it using the "Slack Safe Storage" key from the macOS Keychain via PBKDF2 + AES-128-CBC. Supports both direct-download and Mac App Store keychain account names.
149
149
 
150
- 2. **Token extraction** — Scans Slack's LevelDB storage (`~/Library/Application Support/Slack/Local Storage/leveldb/`) for `xoxc-` session tokens. Uses both direct regex scanning and a Python fallback for Snappy-compressed entries.
150
+ 2. **Token extraction** — Scans Slack's LevelDB storage (`Local Storage/leveldb/`) for `xoxc-` session tokens. Uses both direct regex scanning and a Python fallback for Snappy-compressed entries. The Slack data directory is auto-detected (direct download or App Store sandbox).
151
151
 
152
152
  3. **Validation** — Tests each candidate token against `auth.test` with the decrypted cookie. The first valid pair is used.
153
153
 
@@ -177,8 +177,8 @@ Validated tokens are cached to avoid re-extracting on every invocation:
177
177
  | Data | Source | Purpose |
178
178
  |------|--------|---------|
179
179
  | Keychain password | `security find-generic-password -s "Slack Safe Storage"` | Derive AES key for cookie decryption |
180
- | Encrypted cookie | `~/Library/Application Support/Slack/Cookies` (SQLite) | Decrypt the `d` session cookie (`xoxd-`) |
181
- | Session token | `~/Library/Application Support/Slack/Local Storage/leveldb/` | Extract `xoxc-` token |
180
+ | Encrypted cookie | `<slack-data-dir>/Cookies` (SQLite) | Decrypt the `d` session cookie (`xoxd-`) |
181
+ | Session token | `<slack-data-dir>/Local Storage/leveldb/` | Extract `xoxc-` token |
182
182
 
183
183
  ## Agent usage patterns
184
184
 
@@ -241,6 +241,7 @@ npm link # symlink globally for development
241
241
  ## Notes
242
242
 
243
243
  - **macOS only** — uses Keychain and Electron storage paths specific to macOS.
244
+ - **Both Slack variants supported** — works with the direct download (`~/Library/Application Support/Slack/`) and the Mac App Store version (`~/Library/Containers/com.tinyspeck.slackmacgap/.../Slack/`). The correct path is auto-detected at runtime.
244
245
  - **Slack desktop app required** — must be installed and logged in. The app does not need to be running for cached tokens.
245
246
  - **Zero dependencies** — uses only Node.js built-in modules (`crypto`, `fs`, `child_process`, `fetch`).
246
247
  - **Session-based** — uses `xoxc-` tokens (user session), not bot tokens. This means you act as yourself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slkcli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Slack CLI for macOS, so your agents can read and send messages",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/auth.js CHANGED
@@ -14,7 +14,27 @@ import { pbkdf2Sync } from "crypto";
14
14
 
15
15
  import { existsSync, mkdirSync } from "fs";
16
16
 
17
- const SLACK_DIR = join(homedir(), "Library", "Application Support", "Slack");
17
+ const SLACK_DIR_DIRECT = join(homedir(), "Library", "Application Support", "Slack");
18
+ const SLACK_DIR_APPSTORE = join(
19
+ homedir(),
20
+ "Library", "Containers", "com.tinyspeck.slackmacgap",
21
+ "Data", "Library", "Application Support", "Slack"
22
+ );
23
+
24
+ function resolveSlackDir() {
25
+ if (existsSync(SLACK_DIR_DIRECT)) return SLACK_DIR_DIRECT;
26
+ if (existsSync(SLACK_DIR_APPSTORE)) return SLACK_DIR_APPSTORE;
27
+ console.error(
28
+ "Could not find Slack data directory.\n" +
29
+ "Checked:\n" +
30
+ ` ${SLACK_DIR_DIRECT}\n` +
31
+ ` ${SLACK_DIR_APPSTORE}\n` +
32
+ "Is Slack installed?"
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ const SLACK_DIR = resolveSlackDir();
18
38
  const LEVELDB_DIR = join(SLACK_DIR, "Local Storage", "leveldb");
19
39
  const COOKIES_DB = join(SLACK_DIR, "Cookies");
20
40
  const CACHE_DIR = join(homedir(), ".local", "slk");
@@ -23,11 +43,24 @@ const TOKEN_CACHE = join(CACHE_DIR, "token-cache.json");
23
43
  let cachedCreds = null;
24
44
 
25
45
  function getKeychainKey() {
26
- return Buffer.from(
27
- execSync('security find-generic-password -s "Slack Safe Storage" -w', {
28
- encoding: "utf-8",
29
- }).trim()
30
- );
46
+ // Mac App Store Slack uses account "Slack App Store Key", direct download uses "Slack"
47
+ const accounts = SLACK_DIR === SLACK_DIR_APPSTORE
48
+ ? ["Slack App Store Key", "Slack"]
49
+ : ["Slack", "Slack App Store Key"];
50
+
51
+ for (const account of accounts) {
52
+ try {
53
+ return Buffer.from(
54
+ execSync(
55
+ `security find-generic-password -s "Slack Safe Storage" -a "${account}" -w`,
56
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
57
+ ).trim()
58
+ );
59
+ } catch {}
60
+ }
61
+
62
+ console.error("Could not find Slack Safe Storage key in Keychain.");
63
+ process.exit(1);
31
64
  }
32
65
 
33
66
  function decryptCookie() {
@@ -111,7 +144,7 @@ function extractToken() {
111
144
  try {
112
145
  const pyResult = spawnSync("python3", ["-c", `
113
146
  import os, re
114
- path = os.path.expanduser("~/Library/Application Support/Slack/Local Storage/leveldb")
147
+ path = ${JSON.stringify(LEVELDB_DIR)}
115
148
  for f in os.listdir(path):
116
149
  if not (f.endswith(".ldb") or f.endswith(".log")): continue
117
150
  data = open(os.path.join(path, f), "rb").read()