agent-messenger 2.1.0 → 2.2.0

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 (207) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +35 -17
  3. package/README.md +7 -7
  4. package/bun.lock +6 -6
  5. package/dist/package.json +2 -2
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
  8. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  9. package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
  10. package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  13. package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
  14. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  15. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  16. package/dist/src/platforms/discord/commands/auth.js +57 -49
  17. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  18. package/dist/src/platforms/discord/ensure-auth.js +3 -3
  19. package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
  20. package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
  21. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  22. package/dist/src/platforms/discord/token-extractor.js +167 -14
  23. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  24. package/dist/src/platforms/instagram/client.d.ts +2 -0
  25. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  26. package/dist/src/platforms/instagram/client.js +2 -2
  27. package/dist/src/platforms/instagram/client.js.map +1 -1
  28. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  29. package/dist/src/platforms/instagram/commands/auth.js +107 -14
  30. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  31. package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
  32. package/dist/src/platforms/instagram/ensure-auth.js +57 -11
  33. package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
  34. package/dist/src/platforms/instagram/index.d.ts +1 -0
  35. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  36. package/dist/src/platforms/instagram/index.js +1 -0
  37. package/dist/src/platforms/instagram/index.js.map +1 -1
  38. package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
  39. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
  40. package/dist/src/platforms/instagram/token-extractor.js +407 -0
  41. package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
  42. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  43. package/dist/src/platforms/kakaotalk/client.js +2 -1
  44. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  45. package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
  46. package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
  47. package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
  48. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  49. package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
  50. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  51. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  52. package/dist/src/platforms/line/commands/auth.js +6 -5
  53. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  54. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  55. package/dist/src/platforms/slack/commands/auth.js +11 -10
  56. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  57. package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
  58. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  59. package/dist/src/platforms/slack/token-extractor.js +300 -23
  60. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  61. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/teams/commands/auth.js +9 -8
  63. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  65. package/dist/src/platforms/teams/ensure-auth.js +2 -1
  66. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  67. package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
  68. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  69. package/dist/src/platforms/teams/token-extractor.js +161 -29
  70. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  71. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  72. package/dist/src/platforms/telegram/client.js +25 -7
  73. package/dist/src/platforms/telegram/client.js.map +1 -1
  74. package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
  75. package/dist/src/platforms/telegram/commands/auth.js +6 -5
  76. package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
  77. package/dist/src/platforms/webex/client.d.ts +10 -0
  78. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  79. package/dist/src/platforms/webex/client.js +124 -0
  80. package/dist/src/platforms/webex/client.js.map +1 -1
  81. package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
  82. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  83. package/dist/src/platforms/webex/commands/auth.js +46 -4
  84. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  85. package/dist/src/platforms/webex/credential-manager.js +1 -1
  86. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  87. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  88. package/dist/src/platforms/webex/ensure-auth.js +21 -5
  89. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  90. package/dist/src/platforms/webex/index.d.ts +2 -0
  91. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  92. package/dist/src/platforms/webex/index.js +1 -0
  93. package/dist/src/platforms/webex/index.js.map +1 -1
  94. package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
  95. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
  96. package/dist/src/platforms/webex/token-extractor.js +344 -0
  97. package/dist/src/platforms/webex/token-extractor.js.map +1 -0
  98. package/dist/src/platforms/webex/types.d.ts +4 -1
  99. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  100. package/dist/src/platforms/webex/types.js +2 -1
  101. package/dist/src/platforms/webex/types.js.map +1 -1
  102. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  103. package/dist/src/platforms/whatsapp/client.js +6 -2
  104. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  105. package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
  106. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  107. package/dist/src/shared/utils/error-handler.d.ts +1 -1
  108. package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
  109. package/dist/src/shared/utils/error-handler.js +3 -2
  110. package/dist/src/shared/utils/error-handler.js.map +1 -1
  111. package/dist/src/shared/utils/stderr.d.ts +5 -0
  112. package/dist/src/shared/utils/stderr.d.ts.map +1 -0
  113. package/dist/src/shared/utils/stderr.js +18 -0
  114. package/dist/src/shared/utils/stderr.js.map +1 -0
  115. package/docs/content/docs/cli/channeltalk.mdx +7 -7
  116. package/docs/content/docs/cli/discord.mdx +3 -3
  117. package/docs/content/docs/cli/instagram.mdx +28 -6
  118. package/docs/content/docs/cli/slack.mdx +2 -2
  119. package/docs/content/docs/cli/teams.mdx +6 -4
  120. package/docs/content/docs/cli/webex.mdx +30 -11
  121. package/e2e/README.md +132 -8
  122. package/e2e/channeltalk.e2e.test.ts +2 -7
  123. package/e2e/channeltalkbot.e2e.test.ts +2 -6
  124. package/e2e/config.ts +172 -10
  125. package/e2e/helpers.ts +7 -0
  126. package/e2e/instagram.e2e.test.ts +97 -0
  127. package/e2e/kakaotalk.e2e.test.ts +74 -0
  128. package/e2e/line.e2e.test.ts +92 -0
  129. package/e2e/teams.e2e.test.ts +46 -1
  130. package/e2e/telegram.e2e.test.ts +84 -0
  131. package/e2e/webex.e2e.test.ts +190 -0
  132. package/e2e/whatsapp.e2e.test.ts +90 -0
  133. package/e2e/whatsappbot.e2e.test.ts +78 -0
  134. package/package.json +2 -2
  135. package/skills/agent-channeltalk/SKILL.md +9 -9
  136. package/skills/agent-channeltalk/references/authentication.md +21 -18
  137. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  138. package/skills/agent-discord/SKILL.md +5 -5
  139. package/skills/agent-discord/references/authentication.md +8 -8
  140. package/skills/agent-discordbot/SKILL.md +1 -1
  141. package/skills/agent-instagram/SKILL.md +51 -9
  142. package/skills/agent-instagram/references/authentication.md +35 -3
  143. package/skills/agent-kakaotalk/SKILL.md +1 -1
  144. package/skills/agent-line/SKILL.md +1 -1
  145. package/skills/agent-slack/SKILL.md +5 -5
  146. package/skills/agent-slack/references/authentication.md +8 -8
  147. package/skills/agent-slackbot/SKILL.md +1 -1
  148. package/skills/agent-teams/SKILL.md +6 -6
  149. package/skills/agent-teams/references/authentication.md +8 -8
  150. package/skills/agent-telegram/SKILL.md +1 -1
  151. package/skills/agent-webex/SKILL.md +35 -15
  152. package/skills/agent-webex/references/authentication.md +62 -9
  153. package/skills/agent-webex/references/common-patterns.md +6 -3
  154. package/skills/agent-whatsapp/SKILL.md +1 -1
  155. package/skills/agent-whatsappbot/SKILL.md +1 -1
  156. package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
  157. package/src/platforms/channeltalk/commands/auth.ts +38 -32
  158. package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
  159. package/src/platforms/channeltalk/ensure-auth.ts +6 -6
  160. package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
  161. package/src/platforms/channeltalk/token-extractor.ts +344 -30
  162. package/src/platforms/discord/commands/auth.test.ts +3 -3
  163. package/src/platforms/discord/commands/auth.ts +58 -54
  164. package/src/platforms/discord/ensure-auth.test.ts +3 -3
  165. package/src/platforms/discord/ensure-auth.ts +3 -3
  166. package/src/platforms/discord/token-extractor.test.ts +199 -27
  167. package/src/platforms/discord/token-extractor.ts +190 -17
  168. package/src/platforms/instagram/client.ts +2 -2
  169. package/src/platforms/instagram/commands/auth.ts +133 -14
  170. package/src/platforms/instagram/ensure-auth.ts +63 -12
  171. package/src/platforms/instagram/index.ts +1 -0
  172. package/src/platforms/instagram/token-extractor.test.ts +424 -0
  173. package/src/platforms/instagram/token-extractor.ts +478 -0
  174. package/src/platforms/kakaotalk/client.ts +3 -1
  175. package/src/platforms/kakaotalk/commands/auth.ts +14 -13
  176. package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
  177. package/src/platforms/line/commands/auth.ts +7 -6
  178. package/src/platforms/slack/cli.test.ts +6 -5
  179. package/src/platforms/slack/commands/auth.test.ts +11 -7
  180. package/src/platforms/slack/commands/auth.ts +11 -10
  181. package/src/platforms/slack/token-extractor.test.ts +98 -1
  182. package/src/platforms/slack/token-extractor.ts +338 -26
  183. package/src/platforms/teams/commands/auth.ts +9 -8
  184. package/src/platforms/teams/ensure-auth.ts +3 -1
  185. package/src/platforms/teams/token-extractor.test.ts +136 -17
  186. package/src/platforms/teams/token-extractor.ts +182 -31
  187. package/src/platforms/telegram/client.test.ts +134 -0
  188. package/src/platforms/telegram/client.ts +27 -6
  189. package/src/platforms/telegram/commands/auth.ts +6 -5
  190. package/src/platforms/webex/client.test.ts +314 -0
  191. package/src/platforms/webex/client.ts +158 -0
  192. package/src/platforms/webex/commands/auth.ts +67 -4
  193. package/src/platforms/webex/commands/member.test.ts +10 -1
  194. package/src/platforms/webex/commands/message.test.ts +9 -5
  195. package/src/platforms/webex/commands/snapshot.test.ts +13 -4
  196. package/src/platforms/webex/commands/space.test.ts +12 -2
  197. package/src/platforms/webex/credential-manager.ts +1 -1
  198. package/src/platforms/webex/ensure-auth.test.ts +4 -0
  199. package/src/platforms/webex/ensure-auth.ts +23 -4
  200. package/src/platforms/webex/index.ts +2 -0
  201. package/src/platforms/webex/token-extractor.test.ts +327 -0
  202. package/src/platforms/webex/token-extractor.ts +393 -0
  203. package/src/platforms/webex/types.ts +4 -2
  204. package/src/platforms/whatsapp/client.ts +11 -7
  205. package/src/shared/utils/derived-key-cache.ts +1 -1
  206. package/src/shared/utils/error-handler.ts +4 -2
  207. package/src/shared/utils/stderr.ts +22 -0
@@ -0,0 +1,478 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
3
+ import { copyFileSync, existsSync, readFileSync, readdirSync, unlinkSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import { homedir, tmpdir } from 'node:os'
6
+ import { join } from 'node:path'
7
+
8
+ import { DerivedKeyCache } from '@/shared/utils/derived-key-cache'
9
+
10
+ const require = createRequire(import.meta.url)
11
+
12
+ export interface ExtractedInstagramCookies {
13
+ sessionid: string
14
+ ds_user_id: string
15
+ csrftoken: string
16
+ mid?: string
17
+ ig_did?: string
18
+ rur?: string
19
+ }
20
+
21
+ interface BrowserConfig {
22
+ name: string
23
+ darwin: string
24
+ linux: string
25
+ win32: string
26
+ localStateDarwin?: string
27
+ localStateLinux?: string
28
+ localStateWin32?: string
29
+ }
30
+
31
+ interface KeychainVariant {
32
+ service: string
33
+ account: string
34
+ }
35
+
36
+ const BROWSERS: BrowserConfig[] = [
37
+ {
38
+ name: 'Chrome',
39
+ darwin: join('Google', 'Chrome'),
40
+ linux: 'google-chrome',
41
+ win32: join('Google', 'Chrome', 'User Data'),
42
+ },
43
+ {
44
+ name: 'Chrome Canary',
45
+ darwin: join('Google', 'Chrome Canary'),
46
+ linux: 'google-chrome-unstable',
47
+ win32: join('Google', 'Chrome SxS', 'User Data'),
48
+ },
49
+ {
50
+ name: 'Edge',
51
+ darwin: 'Microsoft Edge',
52
+ linux: 'microsoft-edge',
53
+ win32: join('Microsoft', 'Edge', 'User Data'),
54
+ },
55
+ {
56
+ name: 'Arc',
57
+ darwin: join('Arc', 'User Data'),
58
+ linux: '',
59
+ win32: join('Arc', 'User Data'),
60
+ },
61
+ {
62
+ name: 'Brave',
63
+ darwin: join('BraveSoftware', 'Brave-Browser'),
64
+ linux: join('BraveSoftware', 'Brave-Browser'),
65
+ win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
66
+ },
67
+ {
68
+ name: 'Vivaldi',
69
+ darwin: 'Vivaldi',
70
+ linux: 'vivaldi',
71
+ win32: join('Vivaldi', 'User Data'),
72
+ },
73
+ {
74
+ name: 'Chromium',
75
+ darwin: 'Chromium',
76
+ linux: 'chromium',
77
+ win32: join('Chromium', 'User Data'),
78
+ },
79
+ ]
80
+
81
+ const INSTAGRAM_HOST_KEYS = ['.instagram.com', 'www.instagram.com', 'i.instagram.com']
82
+ const INSTAGRAM_COOKIE_NAMES = ['sessionid', 'ds_user_id', 'csrftoken', 'mid', 'ig_did', 'rur']
83
+
84
+ export class InstagramTokenExtractor {
85
+ private platform: NodeJS.Platform
86
+ private debugLog: ((message: string) => void) | null
87
+ private cachedKey: Buffer | null = null
88
+
89
+ constructor(
90
+ platform?: NodeJS.Platform,
91
+ debugLog?: (message: string) => void,
92
+ _keyCache?: DerivedKeyCache,
93
+ ) {
94
+ this.platform = platform ?? process.platform
95
+ this.debugLog = debugLog ?? null
96
+ }
97
+
98
+ private debug(message: string): void {
99
+ this.debugLog?.(message)
100
+ }
101
+
102
+ getBrowserCookiesPaths(): string[] {
103
+ const paths: string[] = []
104
+
105
+ for (const browser of BROWSERS) {
106
+ const browserBase = this.getBrowserBasePath(browser)
107
+ if (!browserBase) continue
108
+
109
+ const profileDirs = this.discoverProfileDirs(browserBase)
110
+ for (const profileDir of profileDirs) {
111
+ paths.push(join(profileDir, 'Cookies'))
112
+ paths.push(join(profileDir, 'Network', 'Cookies'))
113
+ }
114
+ }
115
+
116
+ return paths
117
+ }
118
+
119
+ getLocalStatePaths(): string[] {
120
+ const paths: string[] = []
121
+
122
+ for (const browser of BROWSERS) {
123
+ const browserBase = this.getBrowserBasePath(browser)
124
+ if (!browserBase) continue
125
+
126
+ paths.push(join(browserBase, 'Local State'))
127
+ }
128
+
129
+ return paths
130
+ }
131
+
132
+ private getBrowserBasePath(browser: BrowserConfig): string | null {
133
+ let relative: string
134
+
135
+ switch (this.platform) {
136
+ case 'darwin':
137
+ relative = browser.darwin
138
+ if (!relative) return null
139
+ return join(homedir(), 'Library', 'Application Support', relative)
140
+ case 'linux':
141
+ relative = browser.linux
142
+ if (!relative) return null
143
+ return join(homedir(), '.config', relative)
144
+ case 'win32':
145
+ relative = browser.win32
146
+ if (!relative) return null
147
+ return join(
148
+ process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
149
+ relative,
150
+ )
151
+ default:
152
+ return null
153
+ }
154
+ }
155
+
156
+ private discoverProfileDirs(browserBase: string): string[] {
157
+ const dirs: string[] = []
158
+
159
+ dirs.push(join(browserBase, 'Default'))
160
+
161
+ if (!existsSync(browserBase)) return dirs
162
+
163
+ try {
164
+ const entries = readdirSync(browserBase, { withFileTypes: true })
165
+ for (const entry of entries) {
166
+ if (!entry.isDirectory()) continue
167
+ if (!/^Profile \d+$/i.test(entry.name)) continue
168
+ dirs.push(join(browserBase, entry.name))
169
+ }
170
+ } catch {}
171
+
172
+ return dirs
173
+ }
174
+
175
+ getKeychainVariants(): KeychainVariant[] {
176
+ return [
177
+ { service: 'Chrome Safe Storage', account: 'Chrome' },
178
+ { service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' },
179
+ { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
180
+ { service: 'Arc Safe Storage', account: 'Arc' },
181
+ { service: 'Brave Safe Storage', account: 'Brave' },
182
+ { service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
183
+ { service: 'Chromium Safe Storage', account: 'Chromium' },
184
+ ]
185
+ }
186
+
187
+ isEncryptedValue(value: Buffer): boolean {
188
+ if (!value || value.length < 4) return false
189
+ const prefix = value.subarray(0, 3).toString('utf8')
190
+ return prefix === 'v10' || prefix === 'v11'
191
+ }
192
+
193
+ isValidSessionId(sessionid: string): boolean {
194
+ if (!sessionid || sessionid.length === 0) return false
195
+ return sessionid.length >= 20
196
+ }
197
+
198
+ async extract(): Promise<ExtractedInstagramCookies[]> {
199
+ const results: ExtractedInstagramCookies[] = []
200
+ const seenUsers = new Set<string>()
201
+ const cookiePaths = this.getBrowserCookiesPaths()
202
+
203
+ for (const cookiePath of cookiePaths) {
204
+ if (!existsSync(cookiePath)) continue
205
+
206
+ this.debug(`Scanning: ${cookiePath}`)
207
+ const cookies = await this.copyAndExtract(cookiePath)
208
+ if (cookies && !seenUsers.has(cookies.ds_user_id)) {
209
+ this.debug(`Found Instagram cookies in: ${cookiePath}`)
210
+ seenUsers.add(cookies.ds_user_id)
211
+ results.push(cookies)
212
+ }
213
+ }
214
+
215
+ if (results.length === 0) {
216
+ this.debug('No Instagram cookies found in any browser profile')
217
+ }
218
+
219
+ return results
220
+ }
221
+
222
+ private async copyAndExtract(dbPath: string): Promise<ExtractedInstagramCookies | null> {
223
+ const tempPath = join(tmpdir(), `instagram-cookies-${Date.now()}`)
224
+
225
+ try {
226
+ this.copyDatabaseToTemp(dbPath, tempPath)
227
+ const cookies = await this.extractFromSQLite(tempPath, dbPath)
228
+ this.cleanupTempFile(tempPath)
229
+ return cookies
230
+ } catch {
231
+ this.cleanupTempFile(tempPath)
232
+ return null
233
+ }
234
+ }
235
+
236
+ private copyDatabaseToTemp(sourcePath: string, destPath: string): string {
237
+ copyFileSync(sourcePath, destPath)
238
+ return destPath
239
+ }
240
+
241
+ private cleanupTempFile(tempPath: string): void {
242
+ try {
243
+ if (existsSync(tempPath)) {
244
+ unlinkSync(tempPath)
245
+ }
246
+ } catch {
247
+ // Ignore cleanup errors
248
+ }
249
+ }
250
+
251
+ private async extractFromSQLite(
252
+ dbPath: string,
253
+ originalPath: string,
254
+ ): Promise<ExtractedInstagramCookies | null> {
255
+ try {
256
+ const placeholders = INSTAGRAM_HOST_KEYS.map(() => '?').join(', ')
257
+ const sql = `
258
+ SELECT name, value, encrypted_value
259
+ FROM cookies
260
+ WHERE host_key IN (${placeholders})
261
+ `
262
+
263
+ type CookieRow = { name: string; value?: string; encrypted_value?: Uint8Array | Buffer }
264
+
265
+ let rows: CookieRow[]
266
+ if (typeof globalThis.Bun !== 'undefined') {
267
+ const { Database } = require('bun:sqlite')
268
+ const db = new Database(dbPath, { readonly: true })
269
+ rows = db.query(sql).all(...INSTAGRAM_HOST_KEYS) as CookieRow[]
270
+ db.close()
271
+ } else {
272
+ const Database = require('better-sqlite3')
273
+ const db = new Database(dbPath, { readonly: true })
274
+ rows = db.prepare(sql).all(...INSTAGRAM_HOST_KEYS) as CookieRow[]
275
+ db.close()
276
+ }
277
+
278
+ const cookieMap: Record<string, string> = {}
279
+ for (const row of rows) {
280
+ if (!INSTAGRAM_COOKIE_NAMES.includes(row.name)) continue
281
+
282
+ let value = ''
283
+ if (row.encrypted_value && row.encrypted_value.length > 0) {
284
+ const encBuf = Buffer.from(row.encrypted_value)
285
+ if (this.isEncryptedValue(encBuf)) {
286
+ const decrypted = this.decryptCookie(encBuf, originalPath)
287
+ if (decrypted) {
288
+ value = decrypted
289
+ }
290
+ } else {
291
+ value = encBuf.toString('utf8')
292
+ }
293
+ } else if (row.value) {
294
+ value = row.value
295
+ }
296
+
297
+ if (value && !cookieMap[row.name]) {
298
+ cookieMap[row.name] = value
299
+ }
300
+ }
301
+
302
+ if (!cookieMap['sessionid'] || !cookieMap['ds_user_id'] || !cookieMap['csrftoken']) {
303
+ return null
304
+ }
305
+
306
+ if (!this.isValidSessionId(cookieMap['sessionid'])) {
307
+ return null
308
+ }
309
+
310
+ const result: ExtractedInstagramCookies = {
311
+ sessionid: cookieMap['sessionid'],
312
+ ds_user_id: cookieMap['ds_user_id'],
313
+ csrftoken: cookieMap['csrftoken'],
314
+ }
315
+
316
+ if (cookieMap['mid']) result.mid = cookieMap['mid']
317
+ if (cookieMap['ig_did']) result.ig_did = cookieMap['ig_did']
318
+ if (cookieMap['rur']) result.rur = cookieMap['rur']
319
+
320
+ return result
321
+ } catch {
322
+ return null
323
+ }
324
+ }
325
+
326
+ private decryptCookie(encryptedValue: Buffer, dbPath: string): string | null {
327
+ if (!this.isEncryptedValue(encryptedValue)) {
328
+ return encryptedValue.toString('utf8')
329
+ }
330
+
331
+ if (this.platform === 'win32') {
332
+ return this.decryptWindowsCookie(encryptedValue, dbPath)
333
+ } else if (this.platform === 'darwin') {
334
+ return this.decryptMacCookie(encryptedValue)
335
+ } else if (this.platform === 'linux') {
336
+ return this.decryptLinuxCookie(encryptedValue)
337
+ }
338
+
339
+ return null
340
+ }
341
+
342
+ private decryptWindowsCookie(encryptedData: Buffer, dbPath: string): string | null {
343
+ try {
344
+ const localStatePath = this.findLocalStateForCookiePath(dbPath)
345
+ if (!localStatePath || !existsSync(localStatePath)) return null
346
+
347
+ const localState = JSON.parse(readFileSync(localStatePath, 'utf8'))
348
+ const encryptedKey = Buffer.from(localState.os_crypt.encrypted_key, 'base64')
349
+ const dpapiBlobKey = encryptedKey.subarray(5)
350
+ const masterKey = this.decryptDPAPI(dpapiBlobKey)
351
+ if (!masterKey) return null
352
+
353
+ return this.decryptAESGCM(encryptedData, masterKey)
354
+ } catch {
355
+ return null
356
+ }
357
+ }
358
+
359
+ private findLocalStateForCookiePath(cookiePath: string): string | null {
360
+ const parts = cookiePath.split(/[/\\]/)
361
+ for (let levels = 2; levels <= 4; levels++) {
362
+ if (parts.length < levels) break
363
+ const base = parts.slice(0, parts.length - levels).join('/')
364
+ const candidate = join(base, 'Local State')
365
+ if (existsSync(candidate)) return candidate
366
+ }
367
+ return null
368
+ }
369
+
370
+ private decryptDPAPI(encryptedBlob: Buffer): Buffer | null {
371
+ try {
372
+ const b64 = encryptedBlob.toString('base64')
373
+ const psScript = `
374
+ Add-Type -AssemblyName System.Security
375
+ $bytes = [Convert]::FromBase64String('${b64}')
376
+ $decrypted = [Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, 'CurrentUser')
377
+ [Convert]::ToBase64String($decrypted)
378
+ `.replace(/\n/g, ' ')
379
+
380
+ const result = execSync(`powershell -Command "${psScript}"`, { encoding: 'utf8' })
381
+ return Buffer.from(result.trim(), 'base64')
382
+ } catch {
383
+ return null
384
+ }
385
+ }
386
+
387
+ private decryptMacCookie(encryptedData: Buffer): string | null {
388
+ if (this.cachedKey) {
389
+ const decrypted = this.decryptAESCBC(encryptedData, this.cachedKey)
390
+ if (decrypted) return decrypted
391
+ }
392
+
393
+ for (const variant of this.getKeychainVariants()) {
394
+ const password = this.execSecurityCommand(variant.service, variant.account)
395
+ if (!password) continue
396
+
397
+ const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
398
+ const decrypted = this.decryptAESCBC(encryptedData, key)
399
+ if (decrypted) {
400
+ this.cachedKey = key
401
+ return decrypted
402
+ }
403
+ }
404
+
405
+ return null
406
+ }
407
+
408
+ private decryptLinuxCookie(encryptedData: Buffer): string | null {
409
+ const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1')
410
+ return this.decryptAESCBC(encryptedData, key)
411
+ }
412
+
413
+ private getKeychainPassword(): string | null {
414
+ for (const variant of this.getKeychainVariants()) {
415
+ const password = this.execSecurityCommand(variant.service, variant.account)
416
+ if (password) return password
417
+ }
418
+ return null
419
+ }
420
+
421
+ private execSecurityCommand(service: string, account: string): string | null {
422
+ try {
423
+ const safeService = service.replace(/"/g, '\\"')
424
+ const safeAccount = account.replace(/"/g, '\\"')
425
+ const result = execSync(
426
+ `security find-generic-password -s "${safeService}" -a "${safeAccount}" -w 2>/dev/null`,
427
+ { encoding: 'utf8' },
428
+ )
429
+ return result.trim()
430
+ } catch {
431
+ return null
432
+ }
433
+ }
434
+
435
+ private decryptAESCBC(encryptedData: Buffer, key: Buffer): string | null {
436
+ try {
437
+ const ciphertext = encryptedData.subarray(3)
438
+ const iv = Buffer.alloc(16, 0x20)
439
+
440
+ const decipher = createDecipheriv('aes-128-cbc', key, iv)
441
+ decipher.setAutoPadding(true)
442
+
443
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
444
+
445
+ // Chromium v130+ prepends a 32-byte integrity hash before the actual cookie value.
446
+ // Detect by checking if the first bytes contain non-printable characters.
447
+ if (decrypted.length > 32) {
448
+ const hasNonPrintablePrefix = decrypted.subarray(0, 32).some((b) => b < 0x20 || b > 0x7e)
449
+ if (hasNonPrintablePrefix) {
450
+ return decrypted.subarray(32).toString('utf8')
451
+ }
452
+ }
453
+
454
+ return decrypted.toString('utf8')
455
+ } catch {
456
+ return null
457
+ }
458
+ }
459
+
460
+ private decryptAESGCM(encryptedData: Buffer, key: Buffer): string | null {
461
+ try {
462
+ // Format: v10 (3 bytes) + IV (12 bytes) + ciphertext + auth tag (16 bytes)
463
+ if (encryptedData.length < 3 + 12 + 16) return null
464
+
465
+ const iv = encryptedData.subarray(3, 15)
466
+ const authTag = encryptedData.subarray(-16)
467
+ const ciphertext = encryptedData.subarray(15, -16)
468
+
469
+ const decipher = createDecipheriv('aes-256-gcm', key, iv)
470
+ decipher.setAuthTag(authTag)
471
+
472
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
473
+ return decrypted.toString('utf8')
474
+ } catch {
475
+ return null
476
+ }
477
+ }
478
+ }
@@ -1,5 +1,7 @@
1
1
  import { Long } from 'bson'
2
2
 
3
+ import { warn } from '@/shared/utils/stderr'
4
+
3
5
  import { LocoSession } from './protocol/session'
4
6
  import type { ChatListResponse, LoginListResponse } from './protocol/types'
5
7
  import type { KakaoChat, KakaoMessage, KakaoSendResult } from './types'
@@ -279,7 +281,7 @@ export class KakaoTalkClient {
279
281
  cur = maxLog
280
282
  }
281
283
  if (!reachedEnd) {
282
- console.error(`[agent-kakaotalk] Warning: message fetch capped at ${MAX_PAGES} pages. Results may be incomplete.`)
284
+ warn(`[agent-kakaotalk] Warning: message fetch capped at ${MAX_PAGES} pages. Results may be incomplete.`)
283
285
  }
284
286
 
285
287
  allMessages.sort((a, b) => (a.sendAt as number) - (b.sendAt as number))
@@ -4,6 +4,7 @@ import { Command } from 'commander'
4
4
 
5
5
  import { handleError } from '@/shared/utils/error-handler'
6
6
  import { formatOutput } from '@/shared/utils/output'
7
+ import { info, error, debug } from '@/shared/utils/stderr'
7
8
 
8
9
  import { generateDeviceUuid, loginFlow } from '../auth/kakao-login'
9
10
  import { CredentialManager } from '../credential-manager'
@@ -231,15 +232,15 @@ async function loginAction(options: KakaoAuthOptions): Promise<void> {
231
232
  }
232
233
 
233
234
  if (email && interactive) {
234
- console.error(` Using cached credentials for ${email}`)
235
+ info(` Using cached credentials for ${email}`)
235
236
  }
236
237
  if (isHashedPassword && !password) {
237
238
  const passwordPrompt = email ? `Password for ${email}` : 'Password'
238
239
  if (interactive) {
239
- console.error(` One-time setup: password is needed to register this device.`)
240
+ info(` One-time setup: password is needed to register this device.`)
240
241
  password = await promptHidden(passwordPrompt)
241
242
  } else if (hasTTY()) {
242
- console.error(` One-time setup: password is needed to register this device.`)
243
+ info(` One-time setup: password is needed to register this device.`)
243
244
  try { password = await promptHiddenTTY(passwordPrompt) } catch { /* /dev/tty open failed */ }
244
245
  }
245
246
  if (!password) {
@@ -262,7 +263,7 @@ async function loginAction(options: KakaoAuthOptions): Promise<void> {
262
263
  return
263
264
  }
264
265
  email = await promptText('KakaoTalk email')
265
- if (!email) { console.error('Email is required.'); process.exit(1) }
266
+ if (!email) { error('Email is required.'); process.exit(1) }
266
267
  }
267
268
 
268
269
  if (!password) {
@@ -271,7 +272,7 @@ async function loginAction(options: KakaoAuthOptions): Promise<void> {
271
272
  return
272
273
  }
273
274
  password = await promptHidden('Password')
274
- if (!password) { console.error('Password is required.'); process.exit(1) }
275
+ if (!password) { error('Password is required.'); process.exit(1) }
275
276
  }
276
277
 
277
278
  const existing = await credManager.getAccount()
@@ -280,14 +281,14 @@ async function loginAction(options: KakaoAuthOptions): Promise<void> {
280
281
 
281
282
  const onPasscodeDisplay = (code: string) => {
282
283
  if (interactive) {
283
- console.error('')
284
- console.error(` Enter this code on your phone: ${code}`)
285
- console.error(' Waiting for confirmation...')
286
- console.error('')
284
+ info('')
285
+ info(` Enter this code on your phone: ${code}`)
286
+ info(' Waiting for confirmation...')
287
+ info('')
287
288
  }
288
289
  }
289
290
 
290
- const debugLog = options.debug ? (msg: string) => console.error(`[debug] ${msg}`) : undefined
291
+ const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
291
292
 
292
293
  const result = await loginFlow({
293
294
  email,
@@ -380,7 +381,7 @@ async function extractAction(options: {
380
381
  if (options.unsafelyShowSecrets) {
381
382
  options.debug = true
382
383
  }
383
- const debugLog = options.debug ? (msg: string) => console.error(`[debug] ${msg}`) : undefined
384
+ const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
384
385
  const extractor = new KakaoTokenExtractor(undefined, debugLog)
385
386
 
386
387
  const token = await extractor.extract()
@@ -402,8 +403,8 @@ async function extractAction(options: {
402
403
  const display = options.unsafelyShowSecrets
403
404
  ? token.oauth_token
404
405
  : `${token.oauth_token.substring(0, 12)}...`
405
- console.error(`[debug] oauth_token: ${display}`)
406
- console.error(`[debug] user_id: ${token.user_id}`)
406
+ debug(`[debug] oauth_token: ${display}`)
407
+ debug(`[debug] user_id: ${token.user_id}`)
407
408
  }
408
409
 
409
410
  const credManager = new CredentialManager()
@@ -1,6 +1,8 @@
1
1
  import { type Socket, connect as netConnect } from 'node:net'
2
2
  import { connect as tlsConnect } from 'node:tls'
3
3
 
4
+ import { debug } from '@/shared/utils/stderr'
5
+
4
6
  import { LocoCrypto } from './crypto'
5
7
  import { decodePacket, encodePacket } from './packet'
6
8
  import type { LocoPacket } from './types'
@@ -114,7 +116,7 @@ export class LocoConnection {
114
116
  try {
115
117
  decrypted = this.crypto.decrypt(encryptedBody)
116
118
  } catch (err) {
117
- console.error(`[loco] decrypt failed: ${(err as Error).message}`)
119
+ debug(`[loco] decrypt failed: ${(err as Error).message}`)
118
120
  continue
119
121
  }
120
122
 
@@ -8,6 +8,7 @@ import QRCode from 'qrcode'
8
8
 
9
9
  import { handleError } from '@/shared/utils/error-handler'
10
10
  import { formatOutput } from '@/shared/utils/output'
11
+ import { info } from '@/shared/utils/stderr'
11
12
 
12
13
  import { LineClient } from '../client'
13
14
  import { LineCredentialManager } from '../credential-manager'
@@ -85,9 +86,9 @@ async function loginAction(options: {
85
86
  email: options.email,
86
87
  password: options.password,
87
88
  device,
88
- onPincode: (pin) => {
89
+ onPincode: (pin) => {
89
90
  if (interactive) {
90
- console.error(`\nEnter this PIN in the LINE mobile app: ${pin}\n`)
91
+ info(`\nEnter this PIN in the LINE mobile app: ${pin}\n`)
91
92
  }
92
93
  },
93
94
  })
@@ -104,14 +105,14 @@ async function loginAction(options: {
104
105
  await openQRInBrowser(url).catch(() => {})
105
106
  try {
106
107
  const qrAscii = await QRCode.toString(url, { type: 'terminal', small: true })
107
- console.error('\nScan this QR code with the LINE mobile app:\n')
108
- console.error(qrAscii)
108
+ info('\nScan this QR code with the LINE mobile app:\n')
109
+ info(qrAscii)
109
110
  } catch {
110
- console.error(`\nOpen the QR code in the browser window, or scan this URL:\n${url}\n`)
111
+ info(`\nOpen the QR code in the browser window, or scan this URL:\n${url}\n`)
111
112
  }
112
113
  },
113
114
  onPincode: (pin) => {
114
- console.error(`\nEnter this PIN in the LINE mobile app: ${pin}\n`)
115
+ info(`\nEnter this PIN in the LINE mobile app: ${pin}\n`)
115
116
  },
116
117
  })
117
118
  console.log(formatOutput(result, options.pretty))
@@ -39,12 +39,13 @@ describe('CLI Framework', () => {
39
39
  describe('handleError utility', () => {
40
40
  test('logs error as JSON and exits', () => {
41
41
  const originalExit = process.exit
42
- const originalError = console.error
42
+ const originalWrite = process.stderr.write
43
43
  let capturedOutput = ''
44
44
 
45
- console.error = (msg: string) => {
46
- capturedOutput = msg
47
- }
45
+ process.stderr.write = ((chunk: string | Uint8Array) => {
46
+ capturedOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
47
+ return true
48
+ }) as typeof process.stderr.write
48
49
  process.exit = (() => {
49
50
  throw new Error('EXIT_CALLED')
50
51
  }) as never
@@ -58,7 +59,7 @@ describe('CLI Framework', () => {
58
59
  }
59
60
  }
60
61
 
61
- console.error = originalError
62
+ process.stderr.write = originalWrite
62
63
  process.exit = originalExit
63
64
  })
64
65
  })