agent-messenger 2.2.0 → 2.4.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/README.md +16 -16
- package/.claude-plugin/marketplace.json +29 -29
- package/.claude-plugin/plugin.json +5 -5
- package/CONTRIBUTING.md +1 -1
- package/README.md +9 -6
- package/bun.lock +89 -105
- package/bunfig.toml +3 -0
- package/dist/package.json +13 -3
- package/dist/src/platforms/discordbot/client.js +2 -2
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +2 -1
- package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +2 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +52 -2
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/profile.d.ts +3 -0
- package/dist/src/platforms/kakaotalk/commands/profile.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/commands/profile.js +19 -0
- package/dist/src/platforms/kakaotalk/commands/profile.js.map +1 -0
- package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +1 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +2 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +16 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +8 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +32 -20
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.js +2 -0
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +2 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +66 -23
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +4 -0
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +10 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
- package/dist/src/platforms/webex/encryption.js +49 -0
- package/dist/src/platforms/webex/encryption.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +4 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +92 -43
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +4 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +2 -0
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/dist/src/platforms/wechatbot/cli.d.ts +5 -0
- package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/cli.js +18 -0
- package/dist/src/platforms/wechatbot/cli.js.map +1 -0
- package/dist/src/platforms/wechatbot/client.d.ts +36 -0
- package/dist/src/platforms/wechatbot/client.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/client.js +208 -0
- package/dist/src/platforms/wechatbot/client.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/auth.d.ts +28 -0
- package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/auth.js +164 -0
- package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/index.d.ts +5 -0
- package/dist/src/platforms/wechatbot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/index.js +5 -0
- package/dist/src/platforms/wechatbot/commands/index.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/message.d.ts +18 -0
- package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/message.js +80 -0
- package/dist/src/platforms/wechatbot/commands/message.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/shared.d.ts +9 -0
- package/dist/src/platforms/wechatbot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/shared.js +13 -0
- package/dist/src/platforms/wechatbot/commands/shared.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/template.d.ts +19 -0
- package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/template.js +76 -0
- package/dist/src/platforms/wechatbot/commands/template.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/user.d.ts +20 -0
- package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/user.js +53 -0
- package/dist/src/platforms/wechatbot/commands/user.js.map +1 -0
- package/dist/src/platforms/wechatbot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/credential-manager.js +121 -0
- package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -0
- package/dist/src/platforms/wechatbot/index.d.ts +5 -0
- package/dist/src/platforms/wechatbot/index.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/index.js +4 -0
- package/dist/src/platforms/wechatbot/index.js.map +1 -0
- package/dist/src/platforms/wechatbot/types.d.ts +94 -0
- package/dist/src/platforms/wechatbot/types.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/types.js +54 -0
- package/dist/src/platforms/wechatbot/types.js.map +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +27 -13
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.js +21 -18
- package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.js +14 -8
- package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +4 -4
- package/docs/content/docs/cli/channeltalk.mdx +1 -1
- package/docs/content/docs/cli/channeltalkbot.mdx +1 -1
- package/docs/content/docs/cli/discord.mdx +1 -1
- package/docs/content/docs/cli/discordbot.mdx +1 -1
- package/docs/content/docs/cli/instagram.mdx +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +1 -1
- package/docs/content/docs/cli/line.mdx +1 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/slack.mdx +1 -1
- package/docs/content/docs/cli/slackbot.mdx +1 -1
- package/docs/content/docs/cli/teams.mdx +1 -1
- package/docs/content/docs/cli/webex.mdx +5 -3
- package/docs/content/docs/cli/wechatbot.mdx +179 -0
- package/docs/content/docs/cli/whatsapp.mdx +1 -1
- package/docs/content/docs/cli/whatsappbot.mdx +1 -1
- package/docs/content/docs/sdk/meta.json +1 -1
- package/docs/content/docs/sdk/wechatbot.mdx +282 -0
- package/docs/content/docs/tui.mdx +1 -1
- package/docs/src/app/page.tsx +5 -5
- package/package.json +13 -3
- 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 +24 -1
- package/skills/agent-line/SKILL.md +7 -11
- package/skills/agent-line/references/authentication.md +13 -4
- package/skills/agent-slack/SKILL.md +1 -1
- 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-webex/SKILL.md +1 -1
- package/skills/agent-webex/references/authentication.md +4 -3
- package/skills/agent-webex/references/common-patterns.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +385 -0
- package/skills/agent-whatsapp/SKILL.md +12 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/credential-manager.test.ts +18 -1
- package/src/platforms/discordbot/client.ts +2 -2
- package/src/platforms/instagram/commands/auth.test.ts +216 -0
- package/src/platforms/instagram/commands/chat.test.ts +127 -0
- package/src/platforms/instagram/commands/message.test.ts +178 -0
- package/src/platforms/kakaotalk/cli.ts +2 -1
- package/src/platforms/kakaotalk/client.test.ts +157 -0
- package/src/platforms/kakaotalk/client.ts +57 -3
- package/src/platforms/kakaotalk/commands/auth.test.ts +299 -0
- package/src/platforms/kakaotalk/commands/chat.test.ts +97 -0
- package/src/platforms/kakaotalk/commands/index.ts +1 -0
- package/src/platforms/kakaotalk/commands/message.test.ts +113 -0
- package/src/platforms/kakaotalk/commands/profile.test.ts +84 -0
- package/src/platforms/kakaotalk/commands/profile.ts +21 -0
- package/src/platforms/kakaotalk/index.test.ts +5 -0
- package/src/platforms/kakaotalk/index.ts +2 -0
- package/src/platforms/kakaotalk/protocol/session.ts +2 -0
- package/src/platforms/kakaotalk/types.ts +18 -0
- package/src/platforms/line/commands/auth.test.ts +141 -0
- package/src/platforms/line/commands/auth.ts +28 -19
- package/src/platforms/line/commands/chat.test.ts +110 -0
- package/src/platforms/line/commands/friend.test.ts +98 -0
- package/src/platforms/line/commands/message.test.ts +119 -0
- package/src/platforms/line/commands/profile.test.ts +85 -0
- package/src/platforms/slackbot/commands/channel.test.ts +139 -0
- package/src/platforms/slackbot/commands/message.test.ts +226 -0
- package/src/platforms/slackbot/commands/reaction.test.ts +90 -0
- package/src/platforms/slackbot/commands/user.test.ts +143 -0
- package/src/platforms/teams/commands/reaction.test.ts +45 -61
- package/src/platforms/teams/commands/reaction.ts +2 -0
- package/src/platforms/telegram/commands/chat.test.ts +125 -0
- package/src/platforms/telegram/commands/message.test.ts +92 -0
- package/src/platforms/webex/client.ts +98 -26
- package/src/platforms/webex/commands/auth.ts +4 -0
- package/src/platforms/webex/commands/member.test.ts +65 -58
- package/src/platforms/webex/commands/message.test.ts +78 -121
- package/src/platforms/webex/commands/snapshot.test.ts +59 -46
- package/src/platforms/webex/commands/space.test.ts +49 -48
- package/src/platforms/webex/encryption.ts +53 -0
- package/src/platforms/webex/ensure-auth.ts +4 -0
- package/src/platforms/webex/token-extractor.ts +107 -40
- package/src/platforms/webex/types.ts +4 -0
- package/src/platforms/webex/typings/node-jose.d.ts +27 -0
- package/src/platforms/wechatbot/cli.ts +24 -0
- package/src/platforms/wechatbot/client.test.ts +497 -0
- package/src/platforms/wechatbot/client.ts +268 -0
- package/src/platforms/wechatbot/commands/auth.test.ts +211 -0
- package/src/platforms/wechatbot/commands/auth.ts +203 -0
- package/src/platforms/wechatbot/commands/index.ts +4 -0
- package/src/platforms/wechatbot/commands/message.test.ts +155 -0
- package/src/platforms/wechatbot/commands/message.ts +104 -0
- package/src/platforms/wechatbot/commands/shared.ts +22 -0
- package/src/platforms/wechatbot/commands/template.test.ts +199 -0
- package/src/platforms/wechatbot/commands/template.ts +102 -0
- package/src/platforms/wechatbot/commands/user.test.ts +165 -0
- package/src/platforms/wechatbot/commands/user.ts +75 -0
- package/src/platforms/wechatbot/credential-manager.test.ts +255 -0
- package/src/platforms/wechatbot/credential-manager.ts +148 -0
- package/src/platforms/wechatbot/index.test.ts +49 -0
- package/src/platforms/wechatbot/index.ts +19 -0
- package/src/platforms/wechatbot/types.test.ts +223 -0
- package/src/platforms/wechatbot/types.ts +107 -0
- package/src/platforms/whatsapp/client.ts +24 -13
- package/src/platforms/whatsapp/commands/auth.test.ts +311 -0
- package/src/platforms/whatsapp/commands/auth.ts +21 -17
- package/src/platforms/whatsapp/commands/chat.test.ts +198 -0
- package/src/platforms/whatsapp/commands/message.test.ts +231 -0
- package/src/platforms/whatsapp/credential-manager.test.ts +20 -0
- package/src/platforms/whatsapp/credential-manager.ts +17 -8
- package/src/platforms/whatsappbot/commands/auth.test.ts +217 -0
- package/src/platforms/whatsappbot/commands/message.test.ts +198 -0
- package/src/platforms/whatsappbot/commands/template.test.ts +112 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
1
|
+
import { afterAll, afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { WebexClient } from '../client'
|
|
4
3
|
import { WebexError } from '../types'
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
const mockHandleError = mock((err: Error) => {
|
|
6
|
+
throw err
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
mock.module('@/shared/utils/error-handler', () => ({
|
|
10
|
+
handleError: mockHandleError,
|
|
11
|
+
}))
|
|
6
12
|
|
|
7
13
|
const mockSpaces = [
|
|
8
14
|
{
|
|
@@ -36,54 +42,46 @@ const mockSpace = {
|
|
|
36
42
|
creatorId: 'person-1',
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
43
|
-
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
44
|
-
let processExitSpy: ReturnType<typeof spyOn>
|
|
45
|
-
let stderrOutput: string
|
|
46
|
-
let origStderrWrite: typeof process.stderr.write
|
|
45
|
+
const mockListSpaces = mock(() => Promise.resolve(mockSpaces))
|
|
46
|
+
const mockGetSpace = mock(() => Promise.resolve(mockSpace))
|
|
47
|
+
const mockLogin = mock(() => Promise.resolve({ listSpaces: mockListSpaces, getSpace: mockGetSpace }))
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
mock.module('../client', () => ({
|
|
50
|
+
WebexClient: class {
|
|
51
|
+
login = mockLogin
|
|
52
|
+
},
|
|
53
|
+
}))
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
import { infoAction, listAction } from './space'
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
mock.restore()
|
|
59
|
+
})
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
61
|
+
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
mockListSpaces.mockReset().mockImplementation(() => Promise.resolve(mockSpaces))
|
|
65
|
+
mockGetSpace.mockReset().mockImplementation(() => Promise.resolve(mockSpace))
|
|
66
|
+
mockLogin.mockReset().mockImplementation(() =>
|
|
67
|
+
Promise.resolve({ listSpaces: mockListSpaces, getSpace: mockGetSpace }),
|
|
68
|
+
)
|
|
69
|
+
mockHandleError.mockReset().mockImplementation((err: Error) => {
|
|
70
|
+
throw err
|
|
62
71
|
})
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
origStderrWrite = process.stderr.write
|
|
66
|
-
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
67
|
-
stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
|
|
68
|
-
return true
|
|
69
|
-
}) as typeof process.stderr.write
|
|
73
|
+
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
70
74
|
})
|
|
71
75
|
|
|
72
76
|
afterEach(() => {
|
|
73
|
-
|
|
74
|
-
clientLoginSpy?.mockRestore()
|
|
75
|
-
clientListSpacesSpy?.mockRestore()
|
|
76
|
-
clientGetSpaceSpy?.mockRestore()
|
|
77
|
-
consoleLogSpy?.mockRestore()
|
|
78
|
-
consoleErrorSpy?.mockRestore()
|
|
79
|
-
processExitSpy?.mockRestore()
|
|
77
|
+
consoleLogSpy.mockRestore()
|
|
80
78
|
})
|
|
81
79
|
|
|
82
80
|
describe('listAction', () => {
|
|
83
81
|
test('calls listSpaces and outputs mapped array', async () => {
|
|
84
82
|
await listAction({})
|
|
85
83
|
|
|
86
|
-
expect(
|
|
84
|
+
expect(mockListSpaces).toHaveBeenCalled()
|
|
87
85
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
88
86
|
JSON.stringify([
|
|
89
87
|
{
|
|
@@ -107,13 +105,13 @@ describe('listAction', () => {
|
|
|
107
105
|
test('passes type and limit options to listSpaces', async () => {
|
|
108
106
|
await listAction({ type: 'group', limit: 10 })
|
|
109
107
|
|
|
110
|
-
expect(
|
|
108
|
+
expect(mockListSpaces).toHaveBeenCalledWith({ type: 'group', max: 10 })
|
|
111
109
|
})
|
|
112
110
|
|
|
113
111
|
test('passes undefined type and limit when not provided', async () => {
|
|
114
112
|
await listAction({})
|
|
115
113
|
|
|
116
|
-
expect(
|
|
114
|
+
expect(mockListSpaces).toHaveBeenCalledWith({ type: undefined, max: undefined })
|
|
117
115
|
})
|
|
118
116
|
|
|
119
117
|
test('outputs pretty-printed JSON when pretty is true', async () => {
|
|
@@ -144,12 +142,14 @@ describe('listAction', () => {
|
|
|
144
142
|
})
|
|
145
143
|
|
|
146
144
|
test('not authenticated: outputs error and exits', async () => {
|
|
147
|
-
|
|
145
|
+
mockLogin.mockImplementation(async () => {
|
|
146
|
+
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
147
|
+
})
|
|
148
148
|
|
|
149
|
-
await expect(listAction({})).rejects.toThrow('
|
|
149
|
+
await expect(listAction({})).rejects.toThrow('No Webex credentials found.')
|
|
150
150
|
|
|
151
|
-
expect(
|
|
152
|
-
expect(
|
|
151
|
+
expect(mockListSpaces).not.toHaveBeenCalled()
|
|
152
|
+
expect(mockHandleError).toHaveBeenCalledWith(expect.any(WebexError))
|
|
153
153
|
})
|
|
154
154
|
})
|
|
155
155
|
|
|
@@ -157,7 +157,7 @@ describe('infoAction', () => {
|
|
|
157
157
|
test('calls getSpace with spaceId and outputs space details', async () => {
|
|
158
158
|
await infoAction('space-1', {})
|
|
159
159
|
|
|
160
|
-
expect(
|
|
160
|
+
expect(mockGetSpace).toHaveBeenCalledWith('space-1')
|
|
161
161
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
162
162
|
JSON.stringify({
|
|
163
163
|
id: 'space-1',
|
|
@@ -173,8 +173,7 @@ describe('infoAction', () => {
|
|
|
173
173
|
})
|
|
174
174
|
|
|
175
175
|
test('outputs null for teamId when not present', async () => {
|
|
176
|
-
|
|
177
|
-
clientGetSpaceSpy.mockResolvedValue(spaceWithoutTeam)
|
|
176
|
+
mockGetSpace.mockImplementation(() => Promise.resolve({ ...mockSpace, teamId: undefined }))
|
|
178
177
|
|
|
179
178
|
await infoAction('space-1', {})
|
|
180
179
|
|
|
@@ -206,11 +205,13 @@ describe('infoAction', () => {
|
|
|
206
205
|
})
|
|
207
206
|
|
|
208
207
|
test('not authenticated: outputs error and exits', async () => {
|
|
209
|
-
|
|
208
|
+
mockLogin.mockImplementation(async () => {
|
|
209
|
+
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
210
|
+
})
|
|
210
211
|
|
|
211
|
-
await expect(infoAction('space-1', {})).rejects.toThrow('
|
|
212
|
+
await expect(infoAction('space-1', {})).rejects.toThrow('No Webex credentials found.')
|
|
212
213
|
|
|
213
|
-
expect(
|
|
214
|
-
expect(
|
|
214
|
+
expect(mockGetSpace).not.toHaveBeenCalled()
|
|
215
|
+
expect(mockHandleError).toHaveBeenCalledWith(expect.any(WebexError))
|
|
215
216
|
})
|
|
216
217
|
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as jose from 'node-jose'
|
|
2
|
+
|
|
3
|
+
export class WebexEncryptionService {
|
|
4
|
+
private rawKeys: Map<string, string>
|
|
5
|
+
private keyCache: Map<string, jose.JWK.Key> = new Map()
|
|
6
|
+
|
|
7
|
+
constructor(serializedKeys: Map<string, string>) {
|
|
8
|
+
this.rawKeys = serializedKeys
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async getKey(keyUri: string): Promise<jose.JWK.Key | null> {
|
|
12
|
+
const cached = this.keyCache.get(keyUri)
|
|
13
|
+
if (cached) return cached
|
|
14
|
+
|
|
15
|
+
const raw = this.rawKeys.get(keyUri)
|
|
16
|
+
if (!raw) return null
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(raw) as { jwk: object }
|
|
20
|
+
const joseKey = await jose.JWK.asKey(parsed.jwk)
|
|
21
|
+
this.keyCache.set(keyUri, joseKey)
|
|
22
|
+
return joseKey
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async encryptText(keyUri: string, plaintext: string): Promise<string | null> {
|
|
29
|
+
const key = await this.getKey(keyUri)
|
|
30
|
+
if (!key) return null
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return await jose.JWE.createEncrypt(
|
|
34
|
+
{ format: 'compact', contentAlg: 'A256GCM' },
|
|
35
|
+
{ key, header: { alg: 'dir' }, reference: null },
|
|
36
|
+
).final(plaintext, 'utf8')
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async decryptText(keyUri: string, ciphertext: string): Promise<string | null> {
|
|
43
|
+
const key = await this.getKey(keyUri)
|
|
44
|
+
if (!key) return null
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = await jose.JWE.createDecrypt(key).decrypt(ciphertext)
|
|
48
|
+
return result.plaintext.toString('utf8')
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -31,6 +31,10 @@ export async function ensureWebexAuth(): Promise<void> {
|
|
|
31
31
|
expiresAt: extracted.expiresAt ?? 0,
|
|
32
32
|
tokenType: 'extracted',
|
|
33
33
|
deviceUrl: extracted.deviceUrl,
|
|
34
|
+
userId: extracted.userId,
|
|
35
|
+
encryptionKeys: extracted.encryptionKeys
|
|
36
|
+
? Object.fromEntries(extracted.encryptionKeys)
|
|
37
|
+
: undefined,
|
|
34
38
|
})
|
|
35
39
|
} catch {
|
|
36
40
|
// Intentionally silent — best-effort preflight that should not block commands
|
|
@@ -17,6 +17,8 @@ export interface ExtractedWebexToken {
|
|
|
17
17
|
refreshToken?: string
|
|
18
18
|
expiresAt?: number
|
|
19
19
|
deviceUrl?: string
|
|
20
|
+
userId?: string
|
|
21
|
+
encryptionKeys?: Map<string, string>
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
interface BrowserConfig {
|
|
@@ -73,6 +75,11 @@ const BROWSERS: BrowserConfig[] = [
|
|
|
73
75
|
|
|
74
76
|
const WEBEX_STORAGE_KEY = '_https://web.webex.com\x00\x01webex-web-client-bounded'
|
|
75
77
|
|
|
78
|
+
interface ScanResult {
|
|
79
|
+
token: ExtractedWebexToken | null
|
|
80
|
+
encryptionKeys: Map<string, string>
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
export class WebexTokenExtractor {
|
|
77
84
|
private platform: NodeJS.Platform
|
|
78
85
|
private baseDir: string | null
|
|
@@ -169,11 +176,17 @@ export class WebexTokenExtractor {
|
|
|
169
176
|
for (const leveldbDir of profileDirs) {
|
|
170
177
|
this.debug(`Scanning: ${leveldbDir}`)
|
|
171
178
|
|
|
172
|
-
const
|
|
173
|
-
?? this.
|
|
179
|
+
const result = await this.scanViaClassicLevelCopy(leveldbDir)
|
|
180
|
+
?? this.scanRawFiles(leveldbDir)
|
|
174
181
|
|
|
175
|
-
if (token) {
|
|
182
|
+
if (result?.token) {
|
|
176
183
|
this.debug(`Found token in: ${leveldbDir}`)
|
|
184
|
+
|
|
185
|
+
const token = result.token
|
|
186
|
+
if (result.encryptionKeys.size > 0) {
|
|
187
|
+
token.encryptionKeys = result.encryptionKeys
|
|
188
|
+
}
|
|
189
|
+
|
|
177
190
|
return token
|
|
178
191
|
}
|
|
179
192
|
}
|
|
@@ -182,7 +195,7 @@ export class WebexTokenExtractor {
|
|
|
182
195
|
return null
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
private async
|
|
198
|
+
private async scanViaClassicLevelCopy(dbPath: string): Promise<ScanResult | null> {
|
|
186
199
|
const tempDir = join(tmpdir(), `webex-leveldb-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
187
200
|
|
|
188
201
|
try {
|
|
@@ -199,7 +212,7 @@ export class WebexTokenExtractor {
|
|
|
199
212
|
} catch {}
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
return await this.
|
|
215
|
+
return await this.copyAndScanLevelDB(tempDir)
|
|
203
216
|
} catch {
|
|
204
217
|
return null
|
|
205
218
|
} finally {
|
|
@@ -209,8 +222,11 @@ export class WebexTokenExtractor {
|
|
|
209
222
|
}
|
|
210
223
|
}
|
|
211
224
|
|
|
212
|
-
private async
|
|
225
|
+
private async copyAndScanLevelDB(dbPath: string): Promise<ScanResult | null> {
|
|
213
226
|
let db: ClassicLevel<string, Buffer> | null = null
|
|
227
|
+
let token: ExtractedWebexToken | null = null
|
|
228
|
+
const encryptionKeys = new Map<string, string>()
|
|
229
|
+
|
|
214
230
|
try {
|
|
215
231
|
db = new ClassicLevel(dbPath, { keyEncoding: 'utf8', valueEncoding: 'buffer' })
|
|
216
232
|
|
|
@@ -218,13 +234,21 @@ export class WebexTokenExtractor {
|
|
|
218
234
|
if (!key.includes('web.webex.com')) continue
|
|
219
235
|
|
|
220
236
|
const decoded = this.decodeLevelDBValue(value)
|
|
221
|
-
if (!decoded.includes('"supertoken"') && !decoded.includes('"Credentials"')) continue
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
if (!token && (decoded.includes('"supertoken"') || decoded.includes('"Credentials"'))) {
|
|
239
|
+
token = this.extractTokenFromString(decoded)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (decoded.includes('"Encryption"') && decoded.includes('kms://')) {
|
|
243
|
+
const found = this.extractEncryptionKeysFromString(decoded)
|
|
244
|
+
for (const [uri, keyStr] of found) {
|
|
245
|
+
encryptionKeys.set(uri, keyStr)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
225
248
|
}
|
|
226
249
|
} catch (e) {
|
|
227
250
|
this.debug(`ClassicLevel failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
251
|
+
return null
|
|
228
252
|
} finally {
|
|
229
253
|
if (db) {
|
|
230
254
|
try {
|
|
@@ -232,22 +256,15 @@ export class WebexTokenExtractor {
|
|
|
232
256
|
} catch {}
|
|
233
257
|
}
|
|
234
258
|
}
|
|
235
|
-
return null
|
|
236
|
-
}
|
|
237
259
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Chromium localStorage: 0x00 prefix = UTF-16LE, 0x01 prefix = Latin1/UTF-8
|
|
241
|
-
if (buf[0] === 0x00 && (buf.length - 1) % 2 === 0) {
|
|
242
|
-
return buf.subarray(1).toString('utf16le')
|
|
243
|
-
}
|
|
244
|
-
if (buf[0] === 0x01) {
|
|
245
|
-
return buf.subarray(1).toString('utf8')
|
|
246
|
-
}
|
|
247
|
-
return buf.toString('utf8')
|
|
260
|
+
if (!token) return null
|
|
261
|
+
return { token, encryptionKeys }
|
|
248
262
|
}
|
|
249
263
|
|
|
250
|
-
private
|
|
264
|
+
private scanRawFiles(leveldbDir: string): ScanResult | null {
|
|
265
|
+
let token: ExtractedWebexToken | null = null
|
|
266
|
+
const encryptionKeys = new Map<string, string>()
|
|
267
|
+
|
|
251
268
|
try {
|
|
252
269
|
const files = readdirSync(leveldbDir)
|
|
253
270
|
|
|
@@ -265,32 +282,48 @@ export class WebexTokenExtractor {
|
|
|
265
282
|
})
|
|
266
283
|
|
|
267
284
|
for (const file of sorted) {
|
|
268
|
-
const
|
|
269
|
-
|
|
285
|
+
const filePath = join(leveldbDir, file)
|
|
286
|
+
try {
|
|
287
|
+
const stat = statSync(filePath)
|
|
288
|
+
if (stat.size > 50 * 1024 * 1024) continue
|
|
289
|
+
|
|
290
|
+
const buffer = readFileSync(filePath)
|
|
291
|
+
const candidates = [buffer.toString('utf8'), this.stripNullBytes(buffer)]
|
|
292
|
+
|
|
293
|
+
for (const content of candidates) {
|
|
294
|
+
if (!token && (content.includes('"supertoken"') || content.includes('"Credentials"'))) {
|
|
295
|
+
token = this.extractTokenFromString(content)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (content.includes('"Encryption"') && content.includes('kms://')) {
|
|
299
|
+
const found = this.extractEncryptionKeysFromString(content)
|
|
300
|
+
for (const [uri, keyStr] of found) {
|
|
301
|
+
encryptionKeys.set(uri, keyStr)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
// Skip unreadable files
|
|
307
|
+
}
|
|
270
308
|
}
|
|
271
309
|
} catch {
|
|
272
310
|
this.debug(`Failed to read directory: ${leveldbDir}`)
|
|
273
311
|
}
|
|
274
312
|
|
|
275
|
-
return null
|
|
313
|
+
if (!token) return null
|
|
314
|
+
return { token, encryptionKeys }
|
|
276
315
|
}
|
|
277
316
|
|
|
278
|
-
private
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const buffer = readFileSync(filePath)
|
|
284
|
-
return this.extractTokenFromBuffer(buffer)
|
|
285
|
-
} catch {
|
|
286
|
-
return null
|
|
317
|
+
private decodeLevelDBValue(buf: Buffer): string {
|
|
318
|
+
if (buf.length < 2) return buf.toString('utf8')
|
|
319
|
+
// Chromium localStorage: 0x00 prefix = UTF-16LE, 0x01 prefix = Latin1/UTF-8
|
|
320
|
+
if (buf[0] === 0x00 && (buf.length - 1) % 2 === 0) {
|
|
321
|
+
return buf.subarray(1).toString('utf16le')
|
|
287
322
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return this.extractTokenFromString(buffer.toString('utf8'))
|
|
293
|
-
?? this.extractTokenFromString(this.stripNullBytes(buffer))
|
|
323
|
+
if (buf[0] === 0x01) {
|
|
324
|
+
return buf.subarray(1).toString('utf8')
|
|
325
|
+
}
|
|
326
|
+
return buf.toString('utf8')
|
|
294
327
|
}
|
|
295
328
|
|
|
296
329
|
private stripNullBytes(buffer: Buffer): string {
|
|
@@ -385,9 +418,43 @@ export class WebexTokenExtractor {
|
|
|
385
418
|
result.deviceUrl = deviceUrl
|
|
386
419
|
}
|
|
387
420
|
|
|
421
|
+
const userId = data?.Device?.['@']?.userId ?? null
|
|
422
|
+
if (userId && typeof userId === 'string') {
|
|
423
|
+
result.userId = userId
|
|
424
|
+
}
|
|
425
|
+
|
|
388
426
|
return result
|
|
389
427
|
} catch {
|
|
390
428
|
return null
|
|
391
429
|
}
|
|
392
430
|
}
|
|
431
|
+
|
|
432
|
+
private extractEncryptionKeysFromString(content: string): Map<string, string> {
|
|
433
|
+
const keys = new Map<string, string>()
|
|
434
|
+
|
|
435
|
+
if (!content.includes('"Encryption"') || !content.includes('kms://')) {
|
|
436
|
+
return keys
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Values in the Encryption map are double-encoded: a kms:// URI key maps to a
|
|
440
|
+
// JSON string whose content is the serialized key object {"uri":..., "jwk":{...}}.
|
|
441
|
+
// Pattern: "kms://..." : "{\"uri\":...,\"jwk\":{...}}"
|
|
442
|
+
const kmsPattern = /"(kms:\/\/[^"]+)"\s*:\s*("(?:[^"\\]|\\.)*")/g
|
|
443
|
+
let match: RegExpExecArray | null
|
|
444
|
+
|
|
445
|
+
while ((match = kmsPattern.exec(content)) !== null) {
|
|
446
|
+
const uri = match[1]!
|
|
447
|
+
const rawValue = match[2]!
|
|
448
|
+
try {
|
|
449
|
+
const innerStr = JSON.parse(rawValue) as unknown
|
|
450
|
+
if (typeof innerStr !== 'string') continue
|
|
451
|
+
const keyObj = JSON.parse(innerStr) as Record<string, unknown>
|
|
452
|
+
if (keyObj?.jwk) {
|
|
453
|
+
keys.set(uri, innerStr)
|
|
454
|
+
}
|
|
455
|
+
} catch {}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return keys
|
|
459
|
+
}
|
|
393
460
|
}
|
|
@@ -57,6 +57,8 @@ export interface WebexConfig {
|
|
|
57
57
|
clientSecret?: string
|
|
58
58
|
tokenType?: 'oauth' | 'manual' | 'extracted'
|
|
59
59
|
deviceUrl?: string
|
|
60
|
+
userId?: string
|
|
61
|
+
encryptionKeys?: Record<string, string>
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export class WebexError extends Error {
|
|
@@ -126,4 +128,6 @@ export const WebexConfigSchema = z.object({
|
|
|
126
128
|
clientSecret: z.string().optional(),
|
|
127
129
|
tokenType: z.enum(['oauth', 'manual', 'extracted']).optional(),
|
|
128
130
|
deviceUrl: z.string().optional(),
|
|
131
|
+
userId: z.string().optional(),
|
|
132
|
+
encryptionKeys: z.record(z.string(), z.string()).optional(),
|
|
129
133
|
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {}
|
|
2
|
+
|
|
3
|
+
declare module 'node-jose' {
|
|
4
|
+
namespace JWE {
|
|
5
|
+
interface JWERecipient {
|
|
6
|
+
key: JWK.Key
|
|
7
|
+
header?: Record<string, string | null>
|
|
8
|
+
reference?: string | null | boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createEncrypt(options: EncryptOptions, recipient: JWERecipient): Encryptor
|
|
12
|
+
|
|
13
|
+
interface Encryptor {
|
|
14
|
+
final(data: string | Buffer, encoding?: BufferEncoding): Promise<string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createDecrypt(key: JWK.Key): Decryptor
|
|
18
|
+
|
|
19
|
+
interface Decryptor {
|
|
20
|
+
decrypt(input: string | Buffer): Promise<DecryptResult>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DecryptResult {
|
|
24
|
+
plaintext: Buffer
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
import pkg from '../../../package.json' with { type: 'json' }
|
|
6
|
+
import { authCommand, messageCommand, templateCommand, userCommand } from './commands/index'
|
|
7
|
+
|
|
8
|
+
const program = new Command()
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('agent-wechatbot')
|
|
12
|
+
.description('CLI tool for WeChat Official Account bot integration')
|
|
13
|
+
.version(pkg.version)
|
|
14
|
+
.option('--pretty', 'Pretty-print JSON output')
|
|
15
|
+
.option('--account <id>', 'Account ID to use')
|
|
16
|
+
|
|
17
|
+
program.addCommand(authCommand)
|
|
18
|
+
program.addCommand(messageCommand)
|
|
19
|
+
program.addCommand(templateCommand)
|
|
20
|
+
program.addCommand(userCommand)
|
|
21
|
+
|
|
22
|
+
program.parseAsync(process.argv)
|
|
23
|
+
|
|
24
|
+
export default program
|