agent-messenger 2.23.5 → 2.24.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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -1
  3. package/bun.lock +10 -1
  4. package/dist/package.json +4 -1
  5. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  6. package/dist/src/platforms/slack/commands/auth.js +56 -0
  7. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  8. package/dist/src/platforms/slack/ensure-auth.d.ts +1 -1
  9. package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -1
  10. package/dist/src/platforms/slack/ensure-auth.js +2 -2
  11. package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
  12. package/dist/src/platforms/slack/index.d.ts +4 -0
  13. package/dist/src/platforms/slack/index.d.ts.map +1 -1
  14. package/dist/src/platforms/slack/index.js +2 -0
  15. package/dist/src/platforms/slack/index.js.map +1 -1
  16. package/dist/src/platforms/slack/qr-http-login.d.ts +14 -0
  17. package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -0
  18. package/dist/src/platforms/slack/qr-http-login.js +90 -0
  19. package/dist/src/platforms/slack/qr-http-login.js.map +1 -0
  20. package/dist/src/platforms/slack/qr-login.d.ts +10 -0
  21. package/dist/src/platforms/slack/qr-login.d.ts.map +1 -0
  22. package/dist/src/platforms/slack/qr-login.js +72 -0
  23. package/dist/src/platforms/slack/qr-login.js.map +1 -0
  24. package/dist/src/platforms/webex/client.d.ts +1 -0
  25. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  26. package/dist/src/platforms/webex/client.js +29 -16
  27. package/dist/src/platforms/webex/client.js.map +1 -1
  28. package/dist/src/vendor/linejs/base/request/mod.js +1 -1
  29. package/dist/src/vendor/linejs/base/request/mod.test.ts +54 -0
  30. package/docs/content/docs/cli/slack.mdx +22 -0
  31. package/docs/content/docs/sdk/slack.mdx +15 -0
  32. package/package.json +4 -1
  33. package/skills/agent-channeltalk/SKILL.md +1 -1
  34. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  35. package/skills/agent-discord/SKILL.md +1 -1
  36. package/skills/agent-discordbot/SKILL.md +1 -1
  37. package/skills/agent-instagram/SKILL.md +1 -1
  38. package/skills/agent-kakaotalk/SKILL.md +1 -1
  39. package/skills/agent-line/SKILL.md +1 -1
  40. package/skills/agent-slack/SKILL.md +45 -1
  41. package/skills/agent-slack/references/authentication.md +29 -0
  42. package/skills/agent-slackbot/SKILL.md +1 -1
  43. package/skills/agent-teams/SKILL.md +1 -1
  44. package/skills/agent-telegram/SKILL.md +1 -1
  45. package/skills/agent-telegrambot/SKILL.md +1 -1
  46. package/skills/agent-webex/SKILL.md +1 -1
  47. package/skills/agent-webexbot/SKILL.md +1 -1
  48. package/skills/agent-wechatbot/SKILL.md +1 -1
  49. package/skills/agent-whatsapp/SKILL.md +1 -1
  50. package/skills/agent-whatsappbot/SKILL.md +1 -1
  51. package/src/platforms/slack/commands/auth.ts +73 -0
  52. package/src/platforms/slack/ensure-auth.ts +6 -2
  53. package/src/platforms/slack/index.test.ts +10 -0
  54. package/src/platforms/slack/index.ts +4 -0
  55. package/src/platforms/slack/qr-http-login.test.ts +157 -0
  56. package/src/platforms/slack/qr-http-login.ts +120 -0
  57. package/src/platforms/slack/qr-login.test.ts +103 -0
  58. package/src/platforms/slack/qr-login.ts +90 -0
  59. package/src/platforms/webex/client.test.ts +63 -1
  60. package/src/platforms/webex/client.ts +35 -17
  61. package/src/vendor/linejs/base/request/mod.js +1 -1
  62. package/src/vendor/linejs/base/request/mod.test.ts +54 -0
@@ -81,6 +81,38 @@ describe('WebexClient', () => {
81
81
  expect((client as any).deviceUrl).toBe('https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1')
82
82
  expect((client as any).tokenType).toBe('extracted')
83
83
  })
84
+
85
+ const DEVICE_URL = 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1'
86
+ const encryptionOf = (client: WebexClient) =>
87
+ (client as unknown as { encryption: WebexEncryptionService | null }).encryption
88
+
89
+ it('initializes encryption for explicit extracted credentials with a device URL', async () => {
90
+ const client = await new WebexClient().login({
91
+ token: 'extracted-token',
92
+ deviceUrl: DEVICE_URL,
93
+ tokenType: 'extracted',
94
+ })
95
+ expect(encryptionOf(client)).toBeInstanceOf(WebexEncryptionService)
96
+ })
97
+
98
+ it('initializes encryption for explicit password credentials with a device URL', async () => {
99
+ const client = await new WebexClient().login({
100
+ token: 'password-token',
101
+ deviceUrl: DEVICE_URL,
102
+ tokenType: 'password',
103
+ })
104
+ expect(encryptionOf(client)).toBeInstanceOf(WebexEncryptionService)
105
+ })
106
+
107
+ it('does not initialize encryption for a plain token without device URL', async () => {
108
+ const client = await new WebexClient().login({ token: 'plain-token' })
109
+ expect(encryptionOf(client)).toBeNull()
110
+ })
111
+
112
+ it('does not initialize encryption when device URL is absent', async () => {
113
+ const client = await new WebexClient().login({ token: 'extracted-token', tokenType: 'extracted' })
114
+ expect(encryptionOf(client)).toBeNull()
115
+ })
84
116
  })
85
117
 
86
118
  describe('testAuth', () => {
@@ -623,12 +655,17 @@ describe('WebexClient', () => {
623
655
  activities: { items: activities },
624
656
  })
625
657
 
658
+ // These tests exercise the cleartext internal-API shape, so the KMS-backed
659
+ // encryption service is cleared after login; the encrypted path has its own
660
+ // createEncryptedClient that re-attaches a stub service.
626
661
  const createExtractedClient = async () => {
627
- return new WebexClient().login({
662
+ const client = await new WebexClient().login({
628
663
  token: 'extracted-token',
629
664
  deviceUrl: TEST_DEVICE_URL,
630
665
  tokenType: 'extracted',
631
666
  })
667
+ ;(client as unknown as { encryption: null }).encryption = null
668
+ return client
632
669
  }
633
670
 
634
671
  describe('sendMessage', () => {
@@ -988,6 +1025,31 @@ describe('WebexClient', () => {
988
1025
  expect(decodeJweHeader(body.object.displayName).kid).toBe(TEST_KEY_URI)
989
1026
  expect(decodeJweHeader(body.object.content).kid).toBe(TEST_KEY_URI)
990
1027
  })
1028
+
1029
+ it('explicit-credential login encrypts the send without manually attaching a service', async () => {
1030
+ const keystore = jose.JWK.createKeyStore()
1031
+ const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
1032
+ const serializedKey = JSON.stringify({ uri: TEST_KEY_URI, jwk: key.toJSON(true) })
1033
+
1034
+ const client = await new WebexClient().login({
1035
+ token: 'extracted-token',
1036
+ deviceUrl: TEST_DEVICE_URL,
1037
+ tokenType: 'extracted',
1038
+ })
1039
+ const service = (client as unknown as { encryption: WebexEncryptionService | null }).encryption
1040
+ expect(service).toBeInstanceOf(WebexEncryptionService)
1041
+ service?.setKeyProvider({ fetchKey: async () => serializedKey })
1042
+
1043
+ mockResponse({ id: TEST_CONV_UUID, defaultActivityEncryptionKeyUrl: TEST_KEY_URI })
1044
+ mockResponse(mockActivity('Hello world'))
1045
+
1046
+ await client.sendMessage(TEST_ROOM_ID, 'Hello world')
1047
+
1048
+ const body = JSON.parse(fetchCalls[1].options?.body as string)
1049
+ expect(body.object.displayName.startsWith('eyJ')).toBe(true)
1050
+ expect(body.encryptionKeyUrl).toBe(TEST_KEY_URI)
1051
+ expect(decodeJweHeader(body.object.displayName).kid).toBe(TEST_KEY_URI)
1052
+ })
991
1053
  })
992
1054
 
993
1055
  describe('sendDirectMessage', () => {
@@ -52,6 +52,7 @@ export class WebexClient {
52
52
  this.token = credentials.token
53
53
  if (credentials.deviceUrl !== undefined) this.deviceUrl = credentials.deviceUrl
54
54
  if (credentials.tokenType !== undefined) this.tokenType = credentials.tokenType
55
+ this.initializeEncryption(credentials.token)
55
56
  return this
56
57
  }
57
58
 
@@ -63,29 +64,46 @@ export class WebexClient {
63
64
  if (!token) {
64
65
  throw new WebexError('No Webex credentials found. Run "auth login" to authenticate.', 'no_credentials')
65
66
  }
67
+ this.token = token
66
68
  this.deviceUrl = config?.deviceUrl ?? null
67
69
  this.tokenType = config?.tokenType ?? null
68
- await this.login({ token })
69
-
70
- if (this.tokenType === 'extracted' || this.tokenType === 'password') {
71
- const keysMap = new Map(Object.entries(config?.encryptionKeys ?? {}))
72
- this.encryption = new WebexEncryptionService(keysMap)
73
- const kmsProvider = new KmsKeyProvider({ token })
74
- this.encryption.setKeyProvider({
75
- fetchKey: async (keyUri: string) => {
76
- const serializedKey = await kmsProvider.fetchKey(keyUri)
77
- if (serializedKey) {
78
- await this.persistEncryptionKey(credManager, keyUri, serializedKey)
79
- }
80
- return serializedKey
81
- },
82
- close: () => kmsProvider.close(),
83
- })
84
- }
70
+ this.initializeEncryption(token, {
71
+ cachedKeys: config?.encryptionKeys,
72
+ persist: (keyUri, serializedKey) => this.persistEncryptionKey(credManager, keyUri, serializedKey),
73
+ })
85
74
 
86
75
  return this
87
76
  }
88
77
 
78
+ // Both login paths must run this. Explicit-credential callers would otherwise
79
+ // skip encryption setup and send cleartext on the internal conversation API,
80
+ // which Webex flags as "not encrypted before it was sent". Gated on a
81
+ // device-backed extracted/password session, the only case E2E applies to.
82
+ private initializeEncryption(
83
+ token: string,
84
+ options?: {
85
+ cachedKeys?: Record<string, string>
86
+ persist?: (keyUri: string, serializedKey: string) => Promise<void>
87
+ },
88
+ ): void {
89
+ if (this.tokenType !== 'extracted' && this.tokenType !== 'password') return
90
+ if (this.deviceUrl === null) return
91
+
92
+ const keysMap = new Map(Object.entries(options?.cachedKeys ?? {}))
93
+ this.encryption = new WebexEncryptionService(keysMap)
94
+ const kmsProvider = new KmsKeyProvider({ token })
95
+ this.encryption.setKeyProvider({
96
+ fetchKey: async (keyUri: string) => {
97
+ const serializedKey = await kmsProvider.fetchKey(keyUri)
98
+ if (serializedKey) {
99
+ await options?.persist?.(keyUri, serializedKey)
100
+ }
101
+ return serializedKey
102
+ },
103
+ close: () => kmsProvider.close(),
104
+ })
105
+ }
106
+
89
107
  async dispose(): Promise<void> {
90
108
  await this.encryption?.close()
91
109
  }
@@ -158,7 +158,7 @@ const square = [
158
158
  throw new InternalError("RequestError", `Request internal failed, ${methodName}(${path}) -> ` + JSON.stringify(res.data.e), res.data.e);
159
159
  }
160
160
  if (hasError && !isRefresh) {
161
- if (res.data.e.code === "NOT_AUTHORIZED_DEVICE") {
161
+ if (res.data.e && res.data.e.code === "NOT_AUTHORIZED_DEVICE") {
162
162
  delete this.client.authToken;
163
163
  this.client.emit("end", this.client.profile);
164
164
  }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { InternalError } from '../core/mod.js'
4
+ import { RequestClient } from './mod.js'
5
+
6
+ // Regression: a thrift error response can set hasError (empty res.data[0]) while
7
+ // omitting the exception struct (res.data[1] absent), leaving res.data.e undefined.
8
+ function createClient(readThriftResult: { data: Record<string, unknown> }) {
9
+ const deviceDetails = {
10
+ device: 'TEST',
11
+ appVersion: '0.0.0',
12
+ systemName: 'TEST',
13
+ systemVersion: '0.0.0',
14
+ }
15
+ const stubClient = {
16
+ deviceDetails,
17
+ endpoint: 'legy.line-apps.test',
18
+ authToken: 'expired-token',
19
+ config: { timeout: 1000 },
20
+ storage: { get: async () => undefined },
21
+ log: () => {},
22
+ emit: () => {},
23
+ fetch: async () => ({
24
+ headers: { get: () => null },
25
+ arrayBuffer: async () => new ArrayBuffer(0),
26
+ }),
27
+ thrift: {
28
+ writeThrift: () => new Uint8Array(),
29
+ readThrift: () => readThriftResult,
30
+ rename_data: () => {},
31
+ },
32
+ }
33
+
34
+ return new RequestClient(stubClient as never)
35
+ }
36
+
37
+ describe('RequestClient.requestCore error handling', () => {
38
+ it('throws a clean RequestError when hasError is set but no exception struct is present', async () => {
39
+ // given: an error response with an empty success slot and no exception struct
40
+ const client = createClient({ data: { 0: undefined, someField: 1 } })
41
+
42
+ // when / then: the error branch must throw InternalError, not a TypeError
43
+ let thrown: unknown
44
+ try {
45
+ await client.requestCore('/S3', [], 'testMethod', 3)
46
+ } catch (error) {
47
+ thrown = error
48
+ }
49
+
50
+ expect(thrown).toBeInstanceOf(InternalError)
51
+ expect((thrown as InternalError).type).toBe('RequestError')
52
+ expect(thrown).not.toBeInstanceOf(TypeError)
53
+ })
54
+ })