agent-messenger 2.32.0 → 2.32.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.
Files changed (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bun.lock +7 -0
  3. package/dist/package.json +2 -1
  4. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  5. package/dist/src/platforms/channeltalk/token-extractor.js +67 -18
  6. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  7. package/dist/src/platforms/discord/commands/channel.d.ts.map +1 -1
  8. package/dist/src/platforms/discord/commands/channel.js +3 -2
  9. package/dist/src/platforms/discord/commands/channel.js.map +1 -1
  10. package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
  11. package/dist/src/platforms/discord/commands/snapshot.js +7 -6
  12. package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
  13. package/dist/src/platforms/discord/types.d.ts +17 -0
  14. package/dist/src/platforms/discord/types.d.ts.map +1 -1
  15. package/dist/src/platforms/discord/types.js +36 -0
  16. package/dist/src/platforms/discord/types.js.map +1 -1
  17. package/dist/src/platforms/slack/index.d.ts +1 -0
  18. package/dist/src/platforms/slack/index.d.ts.map +1 -1
  19. package/dist/src/platforms/slack/index.js.map +1 -1
  20. package/dist/src/platforms/slack/qr-confirmation.d.ts +19 -0
  21. package/dist/src/platforms/slack/qr-confirmation.d.ts.map +1 -0
  22. package/dist/src/platforms/slack/qr-confirmation.js +68 -0
  23. package/dist/src/platforms/slack/qr-confirmation.js.map +1 -0
  24. package/dist/src/platforms/slack/qr-cookie-jar.d.ts +7 -0
  25. package/dist/src/platforms/slack/qr-cookie-jar.d.ts.map +1 -0
  26. package/dist/src/platforms/slack/qr-cookie-jar.js +48 -0
  27. package/dist/src/platforms/slack/qr-cookie-jar.js.map +1 -0
  28. package/dist/src/platforms/slack/qr-http-login.d.ts +2 -1
  29. package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -1
  30. package/dist/src/platforms/slack/qr-http-login.js +88 -53
  31. package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
  32. package/docs/content/docs/cli/channeltalk.mdx +4 -4
  33. package/package.json +2 -1
  34. package/skills/agent-channeltalk/SKILL.md +5 -5
  35. package/skills/agent-channeltalk/references/authentication.md +9 -5
  36. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  37. package/skills/agent-discord/SKILL.md +8 -4
  38. package/skills/agent-discord/references/common-patterns.md +7 -3
  39. package/skills/agent-discordbot/SKILL.md +1 -1
  40. package/skills/agent-imessage/SKILL.md +1 -1
  41. package/skills/agent-instagram/SKILL.md +1 -1
  42. package/skills/agent-kakaotalk/SKILL.md +1 -1
  43. package/skills/agent-line/SKILL.md +1 -1
  44. package/skills/agent-slack/SKILL.md +25 -2
  45. package/skills/agent-slackbot/SKILL.md +1 -1
  46. package/skills/agent-teams/SKILL.md +1 -1
  47. package/skills/agent-telegram/SKILL.md +1 -1
  48. package/skills/agent-telegrambot/SKILL.md +1 -1
  49. package/skills/agent-webex/SKILL.md +1 -1
  50. package/skills/agent-webexbot/SKILL.md +1 -1
  51. package/skills/agent-wechatbot/SKILL.md +1 -1
  52. package/skills/agent-whatsapp/SKILL.md +1 -1
  53. package/skills/agent-whatsappbot/SKILL.md +1 -1
  54. package/src/platforms/channeltalk/token-extractor.test.ts +235 -0
  55. package/src/platforms/channeltalk/token-extractor.ts +81 -22
  56. package/src/platforms/discord/commands/channel.test.ts +33 -23
  57. package/src/platforms/discord/commands/channel.ts +3 -2
  58. package/src/platforms/discord/commands/snapshot.test.ts +80 -2
  59. package/src/platforms/discord/commands/snapshot.ts +7 -6
  60. package/src/platforms/discord/types.test.ts +45 -0
  61. package/src/platforms/discord/types.ts +41 -0
  62. package/src/platforms/slack/index.ts +1 -0
  63. package/src/platforms/slack/qr-confirmation.ts +86 -0
  64. package/src/platforms/slack/qr-cookie-jar.ts +54 -0
  65. package/src/platforms/slack/qr-http-login-2fa.test.ts +398 -0
  66. package/src/platforms/slack/qr-http-login.test.ts +10 -18
  67. package/src/platforms/slack/qr-http-login.ts +109 -52
@@ -1,5 +1,12 @@
1
1
  import { SlackError } from './client'
2
2
  import { refreshTokenFromWeb } from './ensure-auth'
3
+ import {
4
+ buildConfirmationRequest,
5
+ expectedConfirmationPath,
6
+ parseConfirmationPage,
7
+ type SlackConfirmationCodeRequest,
8
+ } from './qr-confirmation'
9
+ import { setCookieNames, SlackCookieJar } from './qr-cookie-jar'
3
10
  import { decodeSlackQr } from './qr-login'
4
11
 
5
12
  export interface QrSession {
@@ -12,10 +19,9 @@ export interface QrLoginOptions {
12
19
  fetchImpl?: typeof fetch
13
20
  maxRedirects?: number
14
21
  debug?: (message: string) => void
22
+ requestConfirmationCode?: SlackConfirmationCodeRequest
15
23
  }
16
24
 
17
- const BROWSER_USER_AGENT =
18
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'
19
25
  const DEFAULT_MAX_REDIRECTS = 10
20
26
 
21
27
  export async function loginWithQr(dataUrl: string, options: QrLoginOptions = {}): Promise<QrSession> {
@@ -23,7 +29,7 @@ export async function loginWithQr(dataUrl: string, options: QrLoginOptions = {})
23
29
  const debug = options.debug
24
30
  debug?.(`Decoded QR for workspace ${login.workspace}`)
25
31
 
26
- const { cookie, denialReason, ssoProvider } = await captureDCookie(login.url, options)
32
+ const { cookie, denialReason, ssoProvider } = await captureDCookie(login, options)
27
33
  if (!cookie) {
28
34
  throw new SlackError(
29
35
  qrSessionFailureMessage(denialReason, { workspace: login.workspace, ssoProvider }),
@@ -103,16 +109,20 @@ interface CookieCapture {
103
109
  ssoProvider: string | null
104
110
  }
105
111
 
106
- async function captureDCookie(startUrl: string, options: QrLoginOptions): Promise<CookieCapture> {
112
+ async function captureDCookie(
113
+ login: ReturnType<typeof decodeSlackQr>,
114
+ options: QrLoginOptions,
115
+ ): Promise<CookieCapture> {
107
116
  const doFetch = options.fetchImpl ?? fetch
108
117
  const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS
109
118
  const debug = options.debug
110
119
 
111
- let url = startUrl
120
+ let url = login.url
112
121
  let dCookie: string | null = null
113
122
  let sessionDenialReason: string | null = null
114
123
  let ssoProvider: string | null = null
115
- const cookieJar: string[] = []
124
+ const cookieJar = new SlackCookieJar()
125
+ const sessionCookieUrl = tokenUrl(login.workspace)
116
126
 
117
127
  for (let hop = 0; hop < maxRedirects; hop++) {
118
128
  // Only ever send captured cookies (including the d session cookie) to Slack
@@ -123,31 +133,56 @@ async function captureDCookie(startUrl: string, options: QrLoginOptions): Promis
123
133
  break
124
134
  }
125
135
 
126
- const response = await doFetch(url, {
127
- redirect: 'manual',
128
- headers: {
129
- 'User-Agent': BROWSER_USER_AGENT,
130
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
131
- 'Accept-Language': 'en-US,en;q=0.9',
132
- ...(cookieJar.length ? { Cookie: cookieJar.join('; ') } : {}),
133
- },
134
- })
135
-
136
- const cookieNames: string[] = []
137
- for (const setCookie of getSetCookies(response)) {
138
- const value = parseDCookie(setCookie)
139
- if (value) dCookie = value
140
- const name = setCookie.split('=')[0]?.trim()
141
- if (name) cookieNames.push(name)
142
- const pair = setCookie.split(';')[0]?.trim()
143
- if (pair) cookieJar.push(pair)
144
- }
136
+ const response = await cookieJar.fetch(doFetch, url)
137
+ dCookie = await cookieJar.sessionCookie(sessionCookieUrl)
145
138
 
139
+ const cookieNames = setCookieNames(response)
146
140
  debug?.(`hop ${hop}: ${response.status} set-cookie=[${cookieNames.join(',') || 'none'}]`)
147
141
 
148
142
  const location = response.status >= 300 && response.status < 400 ? response.headers.get('location') : null
149
143
  if (!location) {
150
- if (!dCookie) sessionDenialReason = await classifySessionDenial(response)
144
+ if (!dCookie) {
145
+ const body = await response.text()
146
+ const props = parseConfirmationPage(body)
147
+ if (props && options.requestConfirmationCode) {
148
+ const request = buildConfirmationRequest(login, url, props, '')
149
+ if (!request) {
150
+ sessionDenialReason = 'invalid_confirmation_origin'
151
+ break
152
+ }
153
+ const code = await options.requestConfirmationCode({
154
+ email: props.emailAddress,
155
+ type: props.twoFactorType,
156
+ })
157
+ if (code.trim() === '') {
158
+ sessionDenialReason = 'confirmation_failed'
159
+ break
160
+ }
161
+ const confirmedRequest = buildConfirmationRequest(login, url, props, code)
162
+ if (!confirmedRequest) {
163
+ sessionDenialReason = 'invalid_confirmation_origin'
164
+ break
165
+ }
166
+ const confirmed = await cookieJar.fetch(doFetch, confirmedRequest.url, {
167
+ method: 'POST',
168
+ body: confirmedRequest.body,
169
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
170
+ })
171
+ debug?.(`confirmation submit: ${confirmed.status}`)
172
+ dCookie = await followConfirmationRedirects(
173
+ confirmed,
174
+ confirmedRequest.url,
175
+ sessionCookieUrl,
176
+ cookieJar,
177
+ doFetch,
178
+ maxRedirects,
179
+ debug,
180
+ )
181
+ if (!dCookie) sessionDenialReason = 'confirmation_failed'
182
+ } else {
183
+ sessionDenialReason = classifySessionDenialBody(body)
184
+ }
185
+ }
151
186
  break
152
187
  }
153
188
 
@@ -163,12 +198,55 @@ async function captureDCookie(startUrl: string, options: QrLoginOptions): Promis
163
198
  debug?.(`hop ${hop}: redirect target is not a Slack host (${new URL(next).hostname}), stopping`)
164
199
  break
165
200
  }
201
+ if (!dCookie && options.requestConfirmationCode && isWorkspaceConfirmationUrl(next, login) === false) {
202
+ sessionDenialReason = 'invalid_confirmation_origin'
203
+ break
204
+ }
166
205
  url = next
167
206
  }
168
207
 
169
208
  return { cookie: dCookie, denialReason: sessionDenialReason, ssoProvider }
170
209
  }
171
210
 
211
+ function isWorkspaceConfirmationUrl(rawUrl: string, login: ReturnType<typeof decodeSlackQr>): boolean | null {
212
+ const expected = expectedConfirmationPath(login)
213
+ if (!expected) return null
214
+ const url = new URL(rawUrl)
215
+ if (url.pathname !== expected) return null
216
+ return url.hostname === `${login.workspace}.slack.com`
217
+ }
218
+
219
+ async function followConfirmationRedirects(
220
+ response: Response,
221
+ responseUrl: string,
222
+ sessionCookieUrl: string,
223
+ cookieJar: SlackCookieJar,
224
+ doFetch: typeof fetch,
225
+ maxRedirects: number,
226
+ debug?: (message: string) => void,
227
+ ): Promise<string | null> {
228
+ let currentResponse = response
229
+ let currentUrl = responseUrl
230
+ let currentSessionCookie = await cookieJar.sessionCookie(sessionCookieUrl)
231
+ for (let hop = 0; hop < maxRedirects; hop++) {
232
+ if (currentSessionCookie) return currentSessionCookie
233
+ const location =
234
+ currentResponse.status >= 300 && currentResponse.status < 400 ? currentResponse.headers.get('location') : null
235
+ if (!location) return null
236
+ const next = new URL(location, currentUrl).toString()
237
+ if (!isSlackHost(next)) return null
238
+ currentUrl = next
239
+ currentResponse = await cookieJar.fetch(doFetch, currentUrl)
240
+ currentSessionCookie = await cookieJar.sessionCookie(sessionCookieUrl)
241
+ debug?.(`confirmation hop ${hop}: ${currentResponse.status}`)
242
+ }
243
+ return currentSessionCookie
244
+ }
245
+
246
+ function tokenUrl(workspace: string): string {
247
+ return `https://${workspace}.slack.com/ssb/redirect`
248
+ }
249
+
172
250
  function ssoProviderFromUrl(rawUrl: string): string | null {
173
251
  let hostname: string
174
252
  try {
@@ -185,19 +263,12 @@ function ssoProviderFromUrl(rawUrl: string): string | null {
185
263
  return null
186
264
  }
187
265
 
188
- async function classifySessionDenial(response: Response): Promise<string | null> {
189
- try {
190
- const body = await response.text()
191
- if (/Link Expired/i.test(body) || /trouble signing you in/i.test(body)) {
192
- return 'link_expired'
193
- }
194
- if (/sign in on your other device/i.test(body) || /open Slack/i.test(body) || /confirm/i.test(body)) {
195
- return 'awaiting_device_confirmation'
196
- }
197
- return null
198
- } catch {
199
- return null
266
+ function classifySessionDenialBody(body: string): string | null {
267
+ if (/Link Expired/i.test(body) || /trouble signing you in/i.test(body)) return 'link_expired'
268
+ if (/sign in on your other device/i.test(body) || /open Slack/i.test(body) || /confirm/i.test(body)) {
269
+ return 'awaiting_device_confirmation'
200
270
  }
271
+ return null
201
272
  }
202
273
 
203
274
  export function isSlackHost(rawUrl: string): boolean {
@@ -209,17 +280,3 @@ export function isSlackHost(rawUrl: string): boolean {
209
280
  return false
210
281
  }
211
282
  }
212
-
213
- export function parseDCookie(setCookieHeader: string): string | null {
214
- const match = setCookieHeader.match(/(?:^|,\s*)d=(xoxd-[^;]+)/)
215
- return match ? match[1] : null
216
- }
217
-
218
- function getSetCookies(response: Response): string[] {
219
- const withGetter = response.headers as Headers & { getSetCookie?: () => string[] }
220
- if (typeof withGetter.getSetCookie === 'function') {
221
- return withGetter.getSetCookie()
222
- }
223
- const single = response.headers.get('set-cookie')
224
- return single ? [single] : []
225
- }