agent-messenger 2.25.0 → 2.27.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 (53) hide show
  1. package/.claude-plugin/README.md +16 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +28 -1
  4. package/dist/package.json +1 -1
  5. package/dist/src/platforms/discord/commands/auth.d.ts +4 -0
  6. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  7. package/dist/src/platforms/discord/commands/auth.js +61 -1
  8. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  9. package/dist/src/platforms/discord/index.d.ts +2 -0
  10. package/dist/src/platforms/discord/index.d.ts.map +1 -1
  11. package/dist/src/platforms/discord/index.js +1 -0
  12. package/dist/src/platforms/discord/index.js.map +1 -1
  13. package/dist/src/platforms/discord/listener.d.ts +1 -4
  14. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  15. package/dist/src/platforms/discord/listener.js +33 -15
  16. package/dist/src/platforms/discord/listener.js.map +1 -1
  17. package/dist/src/platforms/discord/remote-auth.d.ts +42 -0
  18. package/dist/src/platforms/discord/remote-auth.d.ts.map +1 -0
  19. package/dist/src/platforms/discord/remote-auth.js +208 -0
  20. package/dist/src/platforms/discord/remote-auth.js.map +1 -0
  21. package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -1
  22. package/dist/src/platforms/slack/qr-http-login.js +105 -6
  23. package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
  24. package/docs/content/docs/sdk/discord.mdx +23 -9
  25. package/e2e/README.md +3 -1
  26. package/package.json +1 -1
  27. package/skills/agent-channeltalk/SKILL.md +1 -1
  28. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  29. package/skills/agent-discord/SKILL.md +43 -7
  30. package/skills/agent-discord/references/authentication.md +46 -8
  31. package/skills/agent-discordbot/SKILL.md +1 -1
  32. package/skills/agent-instagram/SKILL.md +1 -1
  33. package/skills/agent-kakaotalk/SKILL.md +1 -1
  34. package/skills/agent-line/SKILL.md +1 -1
  35. package/skills/agent-slack/SKILL.md +7 -4
  36. package/skills/agent-slack/references/authentication.md +8 -3
  37. package/skills/agent-slackbot/SKILL.md +1 -1
  38. package/skills/agent-teams/SKILL.md +1 -1
  39. package/skills/agent-telegram/SKILL.md +1 -1
  40. package/skills/agent-telegrambot/SKILL.md +1 -1
  41. package/skills/agent-webex/SKILL.md +1 -1
  42. package/skills/agent-webexbot/SKILL.md +1 -1
  43. package/skills/agent-wechatbot/SKILL.md +1 -1
  44. package/skills/agent-whatsapp/SKILL.md +1 -1
  45. package/skills/agent-whatsappbot/SKILL.md +1 -1
  46. package/src/platforms/discord/commands/auth.ts +81 -1
  47. package/src/platforms/discord/index.ts +2 -0
  48. package/src/platforms/discord/listener.test.ts +12 -4
  49. package/src/platforms/discord/listener.ts +36 -16
  50. package/src/platforms/discord/remote-auth.test.ts +261 -0
  51. package/src/platforms/discord/remote-auth.ts +317 -0
  52. package/src/platforms/slack/qr-http-login.test.ts +47 -0
  53. package/src/platforms/slack/qr-http-login.ts +112 -7
@@ -23,10 +23,10 @@ export async function loginWithQr(dataUrl: string, options: QrLoginOptions = {})
23
23
  const debug = options.debug
24
24
  debug?.(`Decoded QR for workspace ${login.workspace}`)
25
25
 
26
- const cookie = await captureDCookie(login.url, options)
26
+ const { cookie, denialReason, ssoProvider } = await captureDCookie(login.url, options)
27
27
  if (!cookie) {
28
28
  throw new SlackError(
29
- 'Could not establish a Slack session from the QR code. The link may have expired — generate a new QR code and try again.',
29
+ qrSessionFailureMessage(denialReason, { workspace: login.workspace, ssoProvider }),
30
30
  'qr_session_failed',
31
31
  )
32
32
  }
@@ -44,13 +44,74 @@ export async function loginWithQr(dataUrl: string, options: QrLoginOptions = {})
44
44
  return { token, cookie, workspace: login.workspace }
45
45
  }
46
46
 
47
- async function captureDCookie(startUrl: string, options: QrLoginOptions): Promise<string | null> {
47
+ interface QrFailureContext {
48
+ workspace?: string
49
+ ssoProvider?: string | null
50
+ }
51
+
52
+ function qrSessionFailureMessage(denialReason: string | null, context: QrFailureContext = {}): string {
53
+ const workspace = context.workspace ? `"${context.workspace}"` : 'this workspace'
54
+
55
+ if (denialReason === 'sso_required') {
56
+ const via = context.ssoProvider ? ` (via ${context.ssoProvider})` : ''
57
+ return [
58
+ `Slack workspace ${workspace} enforces SSO${via}, so QR sign-in cannot complete here.`,
59
+ '',
60
+ `Why: scanning the QR redirects the login to your identity provider${via}. agent-slack follows the`,
61
+ 'redirect chain over plain HTTP and never sends your session cookie off Slack-owned hosts, so it',
62
+ 'stops at the IdP and no Slack session (the "d" cookie) is ever issued. Completing SSO requires a',
63
+ 'real browser, which a headless environment does not have.',
64
+ '',
65
+ 'What to do instead:',
66
+ ' On a computer where you are already signed in to Slack (desktop app or a Chromium browser),',
67
+ ' run: agent-slack auth extract',
68
+ ' This reads your existing session locally — no SSO re-login, no DevTools.',
69
+ '',
70
+ 'Background: https://github.com/agent-messenger/agent-messenger/issues/260',
71
+ ].join('\n')
72
+ }
73
+ if (denialReason === 'link_expired') {
74
+ return [
75
+ `The Slack QR code for ${workspace} has expired or was already used.`,
76
+ 'The z-app sign-in link is single-use and short-lived: it is consumed the first time it is opened',
77
+ '(including previewing or scanning it), and re-using it returns "Link Expired".',
78
+ '',
79
+ 'Generate a fresh QR (your name → "Sign in on mobile") and pipe it in immediately, without',
80
+ 'previewing or scanning it first.',
81
+ ].join('\n')
82
+ }
83
+ if (denialReason === 'awaiting_device_confirmation') {
84
+ return [
85
+ `Slack requires this sign-in to ${workspace} to be confirmed on an already-authenticated device.`,
86
+ 'QR sign-in over HTTP cannot complete that approval step.',
87
+ '',
88
+ 'Use agent-slack auth extract on a computer already signed in to Slack instead.',
89
+ ].join('\n')
90
+ }
91
+ return [
92
+ `Could not establish a Slack session for ${workspace} from the QR code.`,
93
+ 'The link may have expired, or the workspace requires a sign-in step that QR-over-HTTP cannot complete.',
94
+ '',
95
+ 'Generate a fresh QR and try again, or run agent-slack auth extract on a computer already signed in',
96
+ 'to Slack.',
97
+ ].join('\n')
98
+ }
99
+
100
+ interface CookieCapture {
101
+ cookie: string | null
102
+ denialReason: string | null
103
+ ssoProvider: string | null
104
+ }
105
+
106
+ async function captureDCookie(startUrl: string, options: QrLoginOptions): Promise<CookieCapture> {
48
107
  const doFetch = options.fetchImpl ?? fetch
49
108
  const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS
50
109
  const debug = options.debug
51
110
 
52
111
  let url = startUrl
53
112
  let dCookie: string | null = null
113
+ let sessionDenialReason: string | null = null
114
+ let ssoProvider: string | null = null
54
115
  const cookieJar: string[] = []
55
116
 
56
117
  for (let hop = 0; hop < maxRedirects; hop++) {
@@ -72,27 +133,71 @@ async function captureDCookie(startUrl: string, options: QrLoginOptions): Promis
72
133
  },
73
134
  })
74
135
 
136
+ const cookieNames: string[] = []
75
137
  for (const setCookie of getSetCookies(response)) {
76
138
  const value = parseDCookie(setCookie)
77
139
  if (value) dCookie = value
140
+ const name = setCookie.split('=')[0]?.trim()
141
+ if (name) cookieNames.push(name)
78
142
  const pair = setCookie.split(';')[0]?.trim()
79
143
  if (pair) cookieJar.push(pair)
80
144
  }
81
145
 
82
- debug?.(`hop ${hop}: ${response.status}`)
146
+ debug?.(`hop ${hop}: ${response.status} set-cookie=[${cookieNames.join(',') || 'none'}]`)
83
147
 
84
148
  const location = response.status >= 300 && response.status < 400 ? response.headers.get('location') : null
85
- if (!location) break
149
+ if (!location) {
150
+ if (!dCookie) sessionDenialReason = await classifySessionDenial(response)
151
+ break
152
+ }
86
153
 
87
154
  const next = new URL(location, url).toString()
88
155
  if (!isSlackHost(next)) {
89
- debug?.(`hop ${hop}: redirect target is not a Slack host, stopping`)
156
+ // A no-cookie login QR that redirects off Slack is an SSO/IdP hand-off the
157
+ // HTTP flow cannot complete — classify it regardless of whether the IdP
158
+ // host is a recognized provider (custom/vanity SAML domains included).
159
+ if (!dCookie) {
160
+ sessionDenialReason = 'sso_required'
161
+ ssoProvider = ssoProviderFromUrl(next)
162
+ }
163
+ debug?.(`hop ${hop}: redirect target is not a Slack host (${new URL(next).hostname}), stopping`)
90
164
  break
91
165
  }
92
166
  url = next
93
167
  }
94
168
 
95
- return dCookie
169
+ return { cookie: dCookie, denialReason: sessionDenialReason, ssoProvider }
170
+ }
171
+
172
+ function ssoProviderFromUrl(rawUrl: string): string | null {
173
+ let hostname: string
174
+ try {
175
+ hostname = new URL(rawUrl).hostname
176
+ } catch {
177
+ return null
178
+ }
179
+ if (hostname === 'accounts.google.com') return 'Google'
180
+ if (hostname.endsWith('.okta.com') || hostname.endsWith('.oktapreview.com')) return 'Okta'
181
+ if (hostname === 'login.microsoftonline.com') return 'Microsoft'
182
+ if (hostname.endsWith('.onelogin.com')) return 'OneLogin'
183
+ if (hostname.endsWith('.pingidentity.com') || hostname.endsWith('.pingone.com')) return 'Ping Identity'
184
+ if (hostname.endsWith('.auth0.com')) return 'Auth0'
185
+ return null
186
+ }
187
+
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
200
+ }
96
201
  }
97
202
 
98
203
  export function isSlackHost(rawUrl: string): boolean {