agent-messenger 2.23.4 → 2.23.5

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 (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +2 -2
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/webex/cli.d.ts.map +1 -1
  5. package/dist/src/platforms/webex/cli.js +2 -1
  6. package/dist/src/platforms/webex/cli.js.map +1 -1
  7. package/dist/src/platforms/webex/client.d.ts +4 -0
  8. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  9. package/dist/src/platforms/webex/client.js +161 -0
  10. package/dist/src/platforms/webex/client.js.map +1 -1
  11. package/dist/src/platforms/webex/commands/file.d.ts +12 -0
  12. package/dist/src/platforms/webex/commands/file.d.ts.map +1 -0
  13. package/dist/src/platforms/webex/commands/file.js +64 -0
  14. package/dist/src/platforms/webex/commands/file.js.map +1 -0
  15. package/dist/src/platforms/webex/commands/index.d.ts +1 -0
  16. package/dist/src/platforms/webex/commands/index.d.ts.map +1 -1
  17. package/dist/src/platforms/webex/commands/index.js +1 -0
  18. package/dist/src/platforms/webex/commands/index.js.map +1 -1
  19. package/dist/src/platforms/webex/encryption.d.ts +14 -0
  20. package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
  21. package/dist/src/platforms/webex/encryption.js +36 -0
  22. package/dist/src/platforms/webex/encryption.js.map +1 -1
  23. package/docs/content/docs/cli/webex.mdx +13 -0
  24. package/docs/content/docs/sdk/webex.mdx +12 -0
  25. package/package.json +1 -1
  26. package/skills/agent-channeltalk/SKILL.md +1 -1
  27. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  28. package/skills/agent-discord/SKILL.md +1 -1
  29. package/skills/agent-discordbot/SKILL.md +1 -1
  30. package/skills/agent-instagram/SKILL.md +1 -1
  31. package/skills/agent-kakaotalk/SKILL.md +1 -1
  32. package/skills/agent-line/SKILL.md +1 -1
  33. package/skills/agent-slack/SKILL.md +1 -1
  34. package/skills/agent-slackbot/SKILL.md +1 -1
  35. package/skills/agent-teams/SKILL.md +1 -1
  36. package/skills/agent-telegram/SKILL.md +1 -1
  37. package/skills/agent-telegrambot/SKILL.md +1 -1
  38. package/skills/agent-webex/SKILL.md +14 -2
  39. package/skills/agent-webexbot/SKILL.md +1 -1
  40. package/skills/agent-wechatbot/SKILL.md +1 -1
  41. package/skills/agent-whatsapp/SKILL.md +1 -1
  42. package/skills/agent-whatsappbot/SKILL.md +1 -1
  43. package/src/platforms/webex/cli.ts +10 -1
  44. package/src/platforms/webex/client.test.ts +131 -0
  45. package/src/platforms/webex/client.ts +195 -0
  46. package/src/platforms/webex/commands/file.test.ts +96 -0
  47. package/src/platforms/webex/commands/file.ts +87 -0
  48. package/src/platforms/webex/commands/index.ts +1 -0
  49. package/src/platforms/webex/encryption.test.ts +38 -0
  50. package/src/platforms/webex/encryption.ts +59 -0
@@ -1 +1 @@
1
- {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../../../../src/platforms/webex/encryption.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAOjC,MAAM,OAAO,sBAAsB;IACzB,OAAO,CAAqB;IAC5B,QAAQ,GAA8B,IAAI,GAAG,EAAE,CAAA;IAC/C,WAAW,GAA4B,IAAI,CAAA;IAEnD,YAAY,cAAmC;QAC7C,IAAI,CAAC,OAAO,GAAG,cAAc,CAAA;IAC/B,CAAC;IAED,cAAc,CAAC,QAA0B;QACvC,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAA;IAC7B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,CAAA;IACnC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAc;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACxC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAA;QAEzB,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC7B,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,SAAS,CAAA;QAC9D,CAAC;QACD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QAErB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAA;YACjD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAClC,OAAO,OAAO,CAAA;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAc,EAAE,SAAiB;QACjD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrC,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QAErB,IAAI,CAAC;YACH,gFAAgF;YAChF,8EAA8E;YAC9E,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,aAAa,CACjC,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,EAC5C,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAC9D,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAc,EAAE,UAAkB;QAClD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrC,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QAErB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;YACpE,OAAO,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../../../../src/platforms/webex/encryption.ts"],"names":[],"mappings":"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"}
@@ -203,6 +203,19 @@ agent-webex member list <space-id>
203
203
  agent-webex member list abc123 --limit 100
204
204
  ```
205
205
 
206
+ ### File Commands
207
+
208
+ ```bash
209
+ # Upload a local file to a space
210
+ agent-webex file upload <space-id> <path>
211
+ agent-webex file upload abc123 ./report.pdf --text "Latest report"
212
+ agent-webex file upload abc123 ./image.png --text "**Done**" --markdown
213
+
214
+ # Download a file attachment by content URL or ID
215
+ agent-webex file download <content-url-or-id>
216
+ agent-webex file download <content-url-or-id> ./out.pdf
217
+ ```
218
+
206
219
  ### Snapshot Command
207
220
 
208
221
  Get workspace overview for AI agents (brief by default):
@@ -104,6 +104,18 @@ const limited = await client.listMemberships(roomId, { max: 100 })
104
104
  // → WebexMembership[]
105
105
  ```
106
106
 
107
+ ### Files
108
+
109
+ ```typescript
110
+ // Upload a file to a space (optionally with a text comment)
111
+ const msg = await client.uploadFile(roomId, { content: blob, filename: 'report.pdf' }, { text: 'Latest report' })
112
+ // → WebexMessage { id, roomId, files, ... }
113
+
114
+ // Download a file attachment by content URL or ID
115
+ const { data, filename, contentType } = await client.downloadContent(contentUrlOrId)
116
+ // → { data: ArrayBuffer, filename: string, contentType: string }
117
+ ```
118
+
107
119
  ## WebexCredentialManager
108
120
 
109
121
  Manages Webex credentials stored at `~/.config/agent-messenger/webex-credentials.json`. Files are written with `0o600` permissions. Supports OAuth Device Grant flow, bot tokens, and PATs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-messenger",
3
- "version": "2.23.4",
3
+ "version": "2.23.5",
4
4
  "description": "Multi-platform messaging CLI for AI agents (Slack, Discord, Teams, Webex, Telegram, Telegram Bot, WhatsApp, LINE, Instagram, KakaoTalk, Channel Talk)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-channeltalk
3
3
  description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-channeltalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-channeltalkbot
3
3
  description: Interact with Channel Talk workspaces using API credentials - send messages, read chats, manage groups and bots
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-channeltalkbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-discord
3
3
  description: Interact with Discord servers - send messages, read channels, manage reactions
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-discord:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-discordbot
3
3
  description: Interact with Discord servers using bot tokens - send messages, read channels, manage reactions
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-discordbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-instagram
3
3
  description: Interact with Instagram DMs - send messages, read conversations, manage accounts
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-instagram:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-kakaotalk
3
3
  description: Interact with KakaoTalk - send messages, read chats, manage conversations
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-kakaotalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-line
3
3
  description: Interact with LINE - send messages, read chats, manage conversations
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-line:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -314,6 +314,19 @@ agent-webex member list <space-id>
314
314
  agent-webex member list <space-id> --limit 100
315
315
  ```
316
316
 
317
+ ### File Commands
318
+
319
+ ```bash
320
+ # Upload a local file to a space
321
+ agent-webex file upload <space-id> <path>
322
+ agent-webex file upload <space-id> ./report.pdf --text "Latest report"
323
+ agent-webex file upload <space-id> ./image.png --text "**Done**" --markdown
324
+
325
+ # Download a file attachment by content URL or ID
326
+ agent-webex file download <content-url-or-id>
327
+ agent-webex file download <content-url-or-id> ./out.pdf
328
+ ```
329
+
317
330
  ### Snapshot Command
318
331
 
319
332
  Get workspace overview for AI agents (brief by default):
@@ -451,7 +464,6 @@ See the [Webex SDK documentation](https://agent-messenger.dev/docs/sdk/webex) fo
451
464
 
452
465
  ## Limitations
453
466
 
454
- - No file upload or download
455
467
  - No reactions / emoji support
456
468
  - No thread support
457
469
  - No message search
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webexbot
3
3
  description: Interact with Cisco Webex using bot tokens - send messages, reply in threads, upload and download files, look up people, read spaces, manage memberships, stream real-time events
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-webexbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.23.4
4
+ version: 2.23.5
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -4,7 +4,15 @@ import type { Command as CommandType } from 'commander'
4
4
  import { Command } from 'commander'
5
5
 
6
6
  import pkg from '../../../package.json' with { type: 'json' }
7
- import { authCommand, memberCommand, messageCommand, snapshotCommand, spaceCommand, whoamiCommand } from './commands'
7
+ import {
8
+ authCommand,
9
+ fileCommand,
10
+ memberCommand,
11
+ messageCommand,
12
+ snapshotCommand,
13
+ spaceCommand,
14
+ whoamiCommand,
15
+ } from './commands'
8
16
  import { ensureWebexAuth } from './ensure-auth'
9
17
 
10
18
  function isAuthCommand(command: CommandType): boolean {
@@ -26,6 +34,7 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
26
34
  })
27
35
 
28
36
  program.addCommand(authCommand)
37
+ program.addCommand(fileCommand)
29
38
  program.addCommand(memberCommand)
30
39
  program.addCommand(messageCommand)
31
40
  program.addCommand(snapshotCommand)
@@ -1018,6 +1018,137 @@ describe('WebexClient', () => {
1018
1018
  })
1019
1019
  })
1020
1020
 
1021
+ describe('uploadFile', () => {
1022
+ const mockUploadFlow = () => {
1023
+ // given: the full internal share flow — conv lookup, space, session, PUT, finish, content
1024
+ mockResponse({ id: TEST_CONV_UUID })
1025
+ mockResponse({ spaceUrl: 'https://files.wbx2.com/spaces/sp1' })
1026
+ mockResponse({
1027
+ uploadUrl: 'https://up.wbx2.com/upload/sess1',
1028
+ finishUploadUrl: 'https://up.wbx2.com/upload/sess1/finish',
1029
+ })
1030
+ mockResponse({}, 200)
1031
+ mockResponse({ downloadUrl: 'https://files.wbx2.com/files/f1' })
1032
+ mockResponse({ ...mockActivity(''), verb: 'share' })
1033
+ }
1034
+
1035
+ const file = () => ({ content: new Blob(['hello world']), filename: 'note.txt' })
1036
+
1037
+ it('routes to the internal conversation API instead of the public messages endpoint', async () => {
1038
+ mockUploadFlow()
1039
+
1040
+ const client = await createExtractedClient()
1041
+ await client.uploadFile(TEST_ROOM_ID, file())
1042
+
1043
+ expect(fetchCalls.every((c) => !c.url.includes('webexapis.com/v1/messages'))).toBe(true)
1044
+ expect(fetchCalls.at(-1)?.url).toBe(`${CONV_BASE}/conversations/${TEST_CONV_UUID}/content`)
1045
+ expect(fetchCalls.at(-1)?.options?.method).toBe('POST')
1046
+ })
1047
+
1048
+ it('requests a space, opens an upload session, PUTs the bytes, then finalizes', async () => {
1049
+ mockUploadFlow()
1050
+
1051
+ const client = await createExtractedClient()
1052
+ await client.uploadFile(TEST_ROOM_ID, file())
1053
+
1054
+ expect(fetchCalls[1].url).toBe(`${CONV_BASE}/conversations/${TEST_CONV_UUID}/space`)
1055
+ expect(fetchCalls[1].options?.method).toBe('PUT')
1056
+ expect(fetchCalls[2].url).toBe('https://files.wbx2.com/spaces/sp1/upload_sessions')
1057
+ expect(fetchCalls[3].url).toBe('https://up.wbx2.com/upload/sess1')
1058
+ expect(fetchCalls[3].options?.method).toBe('PUT')
1059
+ expect(fetchCalls[4].url).toBe('https://up.wbx2.com/upload/sess1/finish')
1060
+ })
1061
+
1062
+ it('finalize body carries fileSize and a sha256 fileHash of the uploaded bytes', async () => {
1063
+ mockUploadFlow()
1064
+
1065
+ const client = await createExtractedClient()
1066
+ await client.uploadFile(TEST_ROOM_ID, file())
1067
+
1068
+ const body = JSON.parse(fetchCalls[4].options?.body as string)
1069
+ expect(body.fileSize).toBe(11)
1070
+ expect(body.fileHash).toMatch(/^[0-9a-f]{64}$/)
1071
+ })
1072
+
1073
+ it('share activity references the uploaded file with download url and metadata', async () => {
1074
+ mockUploadFlow()
1075
+
1076
+ const client = await createExtractedClient()
1077
+ await client.uploadFile(TEST_ROOM_ID, file())
1078
+
1079
+ const body = JSON.parse(fetchCalls.at(-1)?.options?.body as string)
1080
+ expect(body.verb).toBe('share')
1081
+ expect(body.object.objectType).toBe('content')
1082
+ expect(body.object.contentCategory).toBe('documents')
1083
+ const item = body.object.files.items[0]
1084
+ expect(item.objectType).toBe('file')
1085
+ expect(item.url).toBe('https://files.wbx2.com/files/f1')
1086
+ expect(item.fileSize).toBe(11)
1087
+ expect(item.mimeType).toBe('text/plain')
1088
+ expect(item.displayName).toBe('note.txt')
1089
+ })
1090
+
1091
+ it('attaches an optional text comment to the share activity', async () => {
1092
+ mockUploadFlow()
1093
+
1094
+ const client = await createExtractedClient()
1095
+ await client.uploadFile(TEST_ROOM_ID, file(), { text: 'see attached' })
1096
+
1097
+ const body = JSON.parse(fetchCalls.at(-1)?.options?.body as string)
1098
+ expect(body.object.displayName).toBe('see attached')
1099
+ })
1100
+
1101
+ it('categorizes images by mime type', async () => {
1102
+ mockUploadFlow()
1103
+
1104
+ const client = await createExtractedClient()
1105
+ await client.uploadFile(TEST_ROOM_ID, { content: new Blob(['x']), filename: 'photo.png' })
1106
+
1107
+ const body = JSON.parse(fetchCalls.at(-1)?.options?.body as string)
1108
+ expect(body.object.contentCategory).toBe('images')
1109
+ expect(body.object.files.items[0].mimeType).toBe('image/png')
1110
+ })
1111
+
1112
+ it('refuses to upload when the server returns an untrusted space url', async () => {
1113
+ mockResponse({ id: TEST_CONV_UUID })
1114
+ mockResponse({ spaceUrl: 'https://evil.example.com/spaces/sp1' })
1115
+
1116
+ const client = await createExtractedClient()
1117
+
1118
+ await expect(client.uploadFile(TEST_ROOM_ID, file())).rejects.toThrow('untrusted host')
1119
+ expect(fetchCalls.every((c) => !c.url.includes('evil.example.com'))).toBe(true)
1120
+ })
1121
+
1122
+ it('refuses to upload when the server returns a non-https upload url', async () => {
1123
+ mockResponse({ id: TEST_CONV_UUID })
1124
+ mockResponse({ spaceUrl: 'https://files.wbx2.com/spaces/sp1' })
1125
+ mockResponse({
1126
+ uploadUrl: 'http://up.wbx2.com/upload/sess1',
1127
+ finishUploadUrl: 'https://up.wbx2.com/upload/sess1/finish',
1128
+ })
1129
+
1130
+ const client = await createExtractedClient()
1131
+
1132
+ await expect(client.uploadFile(TEST_ROOM_ID, file())).rejects.toThrow('untrusted host')
1133
+ })
1134
+
1135
+ it('accepts trusted Webex urls that carry an explicit port', async () => {
1136
+ mockResponse({ id: TEST_CONV_UUID })
1137
+ mockResponse({ spaceUrl: 'https://files.wbx2.com:443/spaces/sp1' })
1138
+ mockResponse({
1139
+ uploadUrl: 'https://up.wbx2.com:443/upload/sess1',
1140
+ finishUploadUrl: 'https://up.wbx2.com:443/upload/sess1/finish',
1141
+ })
1142
+ mockResponse({}, 200)
1143
+ mockResponse({ downloadUrl: 'https://files.wbx2.com/files/f1' })
1144
+ mockResponse({ ...mockActivity(''), verb: 'share' })
1145
+
1146
+ const client = await createExtractedClient()
1147
+
1148
+ await expect(client.uploadFile(TEST_ROOM_ID, file())).resolves.toBeDefined()
1149
+ })
1150
+ })
1151
+
1021
1152
  describe('error handling', () => {
1022
1153
  it('throws WebexError when internal API returns non-OK response', async () => {
1023
1154
  fetchResponses.push(
@@ -1,5 +1,8 @@
1
+ import { createHash } from 'node:crypto'
2
+
1
3
  import { WebexCredentialManager } from './credential-manager'
2
4
  import { WebexEncryptionService } from './encryption'
5
+ import type { WebexScr } from './encryption'
3
6
  import {
4
7
  decodeWebexId,
5
8
  normalizeSdkMembership,
@@ -655,6 +658,11 @@ export class WebexClient {
655
658
  options?: { text?: string; markdown?: boolean; parentId?: string },
656
659
  ): Promise<WebexMessage> {
657
660
  const resolvedRoomId = await this.resolveRoomId(roomId)
661
+
662
+ if (this.useInternalAPI) {
663
+ return this.uploadFileInternal(resolvedRoomId, file, options)
664
+ }
665
+
658
666
  const resolvedParentId = options?.parentId ? this.resolveMessageId(options.parentId) : undefined
659
667
  const form = new FormData()
660
668
  form.set('roomId', resolvedRoomId)
@@ -677,6 +685,137 @@ export class WebexClient {
677
685
  return normalizeSdkMessage((await response.json()) as WebexMessage)
678
686
  }
679
687
 
688
+ private async uploadFileInternal(
689
+ roomId: string,
690
+ file: { content: Blob; filename: string },
691
+ options?: { text?: string; markdown?: boolean; parentId?: string },
692
+ ): Promise<WebexMessage> {
693
+ const convUuid = this.decodeConvUuid(roomId)
694
+ const conversationUrl = `${this.convBaseUrl}/conversations/${convUuid}`
695
+ const conv = await this.internalRequest<InternalConversation>(
696
+ `/conversations/${convUuid}?activitiesLimit=0&participantsLimit=0`,
697
+ )
698
+ const keyUri = conv.defaultActivityEncryptionKeyUrl
699
+
700
+ const bytes = new Uint8Array(await file.content.arrayBuffer())
701
+ const fileItem = await this.uploadFileContent(conversationUrl, file.filename, bytes, keyUri)
702
+
703
+ const object: Record<string, unknown> = {
704
+ objectType: 'content',
705
+ contentCategory: contentCategoryFor(fileItem.mimeType),
706
+ files: { items: [fileItem.item] },
707
+ }
708
+ let encryptionKeyUrl: string | undefined
709
+ if (options?.text) {
710
+ const built = await this.buildEncryptedObject(convUuid, options.text, { markdown: options.markdown })
711
+ object.displayName = built.object.displayName
712
+ if (built.object.content) object.content = built.object.content
713
+ encryptionKeyUrl = built.encryptionKeyUrl
714
+ }
715
+
716
+ const activity: Record<string, unknown> = {
717
+ verb: 'share',
718
+ object,
719
+ target: { id: convUuid, objectType: 'conversation' },
720
+ clientTempId: `tmp-${Date.now()}-share`,
721
+ }
722
+ if (options?.parentId) {
723
+ activity.parent = { id: this.toMessageRef(options.parentId), type: 'reply' }
724
+ }
725
+ if (encryptionKeyUrl ?? keyUri) {
726
+ activity.encryptionKeyUrl = encryptionKeyUrl ?? keyUri
727
+ }
728
+
729
+ const result = await this.internalActivityRequest<InternalActivity>(`${conversationUrl}/content`, {
730
+ method: 'POST',
731
+ body: JSON.stringify(activity),
732
+ })
733
+ return this.activityToMessage(result, roomId)
734
+ }
735
+
736
+ private async uploadFileContent(
737
+ conversationUrl: string,
738
+ filename: string,
739
+ bytes: Uint8Array,
740
+ keyUri: string | undefined,
741
+ ): Promise<{ item: Record<string, unknown>; mimeType: string }> {
742
+ const space = await this.internalActivityRequest<{ spaceUrl: string }>(`${conversationUrl}/space`, {
743
+ method: 'PUT',
744
+ })
745
+
746
+ let body: Uint8Array
747
+ let scr: WebexScr | undefined
748
+ if (this.encryption && keyUri) {
749
+ const encrypted = this.encryption.encryptBinary(bytes)
750
+ body = encrypted.ciphertext
751
+ scr = encrypted.scr
752
+ } else {
753
+ body = bytes
754
+ }
755
+
756
+ const downloadUrl = await this.uploadToSpace(space.spaceUrl, body)
757
+
758
+ const mimeType = guessMimeType(filename)
759
+ const item: Record<string, unknown> = {
760
+ objectType: 'file',
761
+ displayName: filename,
762
+ fileSize: bytes.byteLength,
763
+ mimeType,
764
+ url: downloadUrl,
765
+ }
766
+
767
+ if (scr && keyUri && this.encryption) {
768
+ scr.loc = downloadUrl
769
+ const encryptedScr = await this.encryption.encryptScr(keyUri, scr)
770
+ if (!encryptedScr) {
771
+ throw new WebexError('Cannot encrypt file for Webex E2E conversation', 'encryption_failed')
772
+ }
773
+ item.scr = encryptedScr
774
+ item.displayName = (await this.encryption.encryptText(keyUri, filename)) ?? filename
775
+ }
776
+
777
+ return { item, mimeType }
778
+ }
779
+
780
+ private async uploadToSpace(spaceUrl: string, body: Uint8Array): Promise<string> {
781
+ const session = await this.internalActivityRequest<{ uploadUrl: string; finishUploadUrl: string }>(
782
+ `${spaceUrl}/upload_sessions`,
783
+ {
784
+ method: 'POST',
785
+ body: JSON.stringify({ uploadProtocol: 'content-length', fileSize: body.byteLength }),
786
+ },
787
+ )
788
+
789
+ const putResponse = await fetch(assertTrustedWebexUrl(session.uploadUrl), {
790
+ method: 'PUT',
791
+ headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': String(body.byteLength) },
792
+ body,
793
+ })
794
+ if (!putResponse.ok) {
795
+ throw new WebexError(`File upload failed: HTTP ${putResponse.status}`, `http_${putResponse.status}`)
796
+ }
797
+
798
+ const fileHash = createHash('sha256').update(body).digest('hex')
799
+ const finished = await this.internalActivityRequest<{ downloadUrl: string }>(session.finishUploadUrl, {
800
+ method: 'POST',
801
+ body: JSON.stringify({ fileSize: body.byteLength, fileHash }),
802
+ })
803
+ return finished.downloadUrl
804
+ }
805
+
806
+ private async internalActivityRequest<T>(url: string, init: RequestInit): Promise<T> {
807
+ const response = await fetch(assertTrustedWebexUrl(url), {
808
+ ...init,
809
+ headers: { ...this.internalHeaders, ...(init.headers as Record<string, string>) },
810
+ })
811
+ if (!response.ok) {
812
+ const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
813
+ throw new WebexError(errorBody?.message ?? `HTTP ${response.status}`, `http_${response.status}`)
814
+ }
815
+ if (response.status === 204) return undefined as T
816
+ return response.json() as Promise<T>
817
+ }
818
+
680
819
  private async lookupRoomId(uuid: string, fallback: string): Promise<string> {
681
820
  try {
682
821
  // Page through every room the account belongs to, stopping as soon as the
@@ -816,6 +955,62 @@ function looksLikeUuid(value: string): boolean {
816
955
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
817
956
  }
818
957
 
958
+ function isTrustedWebexHost(host: string): boolean {
959
+ return (
960
+ host === 'webex.com' ||
961
+ host.endsWith('.webex.com') ||
962
+ host === 'wbx2.com' ||
963
+ host.endsWith('.wbx2.com') ||
964
+ host === 'ciscospark.com' ||
965
+ host.endsWith('.ciscospark.com')
966
+ )
967
+ }
968
+
969
+ // Pin server-returned upload URLs to HTTPS Webex hosts: they receive the bearer
970
+ // token (activity calls) and file bytes, so a compromised response must not be
971
+ // able to exfiltrate them to an attacker-controlled host (SSRF/token leak).
972
+ function assertTrustedWebexUrl(url: string): string {
973
+ let parsed: URL
974
+ try {
975
+ parsed = new URL(url)
976
+ } catch {
977
+ throw new WebexError(`Invalid Webex URL: ${url}`, 'invalid_url')
978
+ }
979
+ if (parsed.protocol !== 'https:' || !isTrustedWebexHost(parsed.hostname)) {
980
+ throw new WebexError(`Refusing to send request to untrusted host: ${parsed.origin}`, 'untrusted_url')
981
+ }
982
+ return parsed.toString()
983
+ }
984
+
985
+ const MIME_TYPES: Record<string, string> = {
986
+ png: 'image/png',
987
+ jpg: 'image/jpeg',
988
+ jpeg: 'image/jpeg',
989
+ gif: 'image/gif',
990
+ webp: 'image/webp',
991
+ svg: 'image/svg+xml',
992
+ mp4: 'video/mp4',
993
+ mov: 'video/quicktime',
994
+ webm: 'video/webm',
995
+ pdf: 'application/pdf',
996
+ txt: 'text/plain',
997
+ md: 'text/markdown',
998
+ json: 'application/json',
999
+ csv: 'text/csv',
1000
+ zip: 'application/zip',
1001
+ }
1002
+
1003
+ function guessMimeType(filename: string): string {
1004
+ const ext = filename.split('.').pop()?.toLowerCase() ?? ''
1005
+ return MIME_TYPES[ext] ?? 'application/octet-stream'
1006
+ }
1007
+
1008
+ function contentCategoryFor(mimeType: string): string {
1009
+ if (mimeType.startsWith('image/')) return 'images'
1010
+ if (mimeType.startsWith('video/')) return 'videos'
1011
+ return 'documents'
1012
+ }
1013
+
819
1014
  function looksLikeEmail(value: string): boolean {
820
1015
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
821
1016
  }