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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -1
- package/bun.lock +10 -1
- package/dist/package.json +4 -1
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +56 -0
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/ensure-auth.d.ts +1 -1
- package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/ensure-auth.js +2 -2
- package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
- package/dist/src/platforms/slack/index.d.ts +4 -0
- package/dist/src/platforms/slack/index.d.ts.map +1 -1
- package/dist/src/platforms/slack/index.js +2 -0
- package/dist/src/platforms/slack/index.js.map +1 -1
- package/dist/src/platforms/slack/qr-http-login.d.ts +14 -0
- package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -0
- package/dist/src/platforms/slack/qr-http-login.js +90 -0
- package/dist/src/platforms/slack/qr-http-login.js.map +1 -0
- package/dist/src/platforms/slack/qr-login.d.ts +10 -0
- package/dist/src/platforms/slack/qr-login.d.ts.map +1 -0
- package/dist/src/platforms/slack/qr-login.js +72 -0
- package/dist/src/platforms/slack/qr-login.js.map +1 -0
- package/dist/src/platforms/webex/client.d.ts +1 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +29 -16
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/vendor/linejs/base/request/mod.js +1 -1
- package/dist/src/vendor/linejs/base/request/mod.test.ts +54 -0
- package/docs/content/docs/cli/slack.mdx +22 -0
- package/docs/content/docs/sdk/slack.mdx +15 -0
- package/package.json +4 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +45 -1
- package/skills/agent-slack/references/authentication.md +29 -0
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/slack/commands/auth.ts +73 -0
- package/src/platforms/slack/ensure-auth.ts +6 -2
- package/src/platforms/slack/index.test.ts +10 -0
- package/src/platforms/slack/index.ts +4 -0
- package/src/platforms/slack/qr-http-login.test.ts +157 -0
- package/src/platforms/slack/qr-http-login.ts +120 -0
- package/src/platforms/slack/qr-login.test.ts +103 -0
- package/src/platforms/slack/qr-login.ts +90 -0
- package/src/platforms/webex/client.test.ts +63 -1
- package/src/platforms/webex/client.ts +35 -17
- package/src/vendor/linejs/base/request/mod.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
})
|