agent-messenger 2.23.4 → 2.23.6
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 +2 -2
- package/dist/package.json +1 -1
- package/dist/src/platforms/webex/cli.d.ts.map +1 -1
- package/dist/src/platforms/webex/cli.js +2 -1
- package/dist/src/platforms/webex/cli.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +5 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +190 -16
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/file.d.ts +12 -0
- package/dist/src/platforms/webex/commands/file.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/file.js +64 -0
- package/dist/src/platforms/webex/commands/file.js.map +1 -0
- package/dist/src/platforms/webex/commands/index.d.ts +1 -0
- package/dist/src/platforms/webex/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/index.js +1 -0
- package/dist/src/platforms/webex/commands/index.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +14 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
- package/dist/src/platforms/webex/encryption.js +36 -0
- package/dist/src/platforms/webex/encryption.js.map +1 -1
- package/docs/content/docs/cli/webex.mdx +13 -0
- package/docs/content/docs/sdk/webex.mdx +12 -0
- package/package.json +1 -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 +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-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +14 -2
- 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/webex/cli.ts +10 -1
- package/src/platforms/webex/client.test.ts +194 -1
- package/src/platforms/webex/client.ts +230 -17
- package/src/platforms/webex/commands/file.test.ts +96 -0
- package/src/platforms/webex/commands/file.ts +87 -0
- package/src/platforms/webex/commands/index.ts +1 -0
- package/src/platforms/webex/encryption.test.ts +38 -0
- package/src/platforms/webex/encryption.ts +59 -0
|
@@ -3,6 +3,18 @@ export interface WebexKeyProvider {
|
|
|
3
3
|
fetchKey(keyUri: string): Promise<string | null>;
|
|
4
4
|
close?(): Promise<void>;
|
|
5
5
|
}
|
|
6
|
+
export interface WebexScr {
|
|
7
|
+
enc: 'A256GCM';
|
|
8
|
+
key: string;
|
|
9
|
+
iv: string;
|
|
10
|
+
aad: string;
|
|
11
|
+
loc?: string;
|
|
12
|
+
tag: string;
|
|
13
|
+
}
|
|
14
|
+
export interface WebexEncryptedBinary {
|
|
15
|
+
scr: WebexScr;
|
|
16
|
+
ciphertext: Uint8Array;
|
|
17
|
+
}
|
|
6
18
|
export declare class WebexEncryptionService {
|
|
7
19
|
private rawKeys;
|
|
8
20
|
private keyCache;
|
|
@@ -13,5 +25,7 @@ export declare class WebexEncryptionService {
|
|
|
13
25
|
getKey(keyUri: string): Promise<jose.JWK.Key | null>;
|
|
14
26
|
encryptText(keyUri: string, plaintext: string): Promise<string | null>;
|
|
15
27
|
decryptText(keyUri: string, ciphertext: string): Promise<string | null>;
|
|
28
|
+
encryptBinary(plaintext: Uint8Array): WebexEncryptedBinary;
|
|
29
|
+
encryptScr(keyUri: string, scr: WebexScr): Promise<string | null>;
|
|
16
30
|
}
|
|
17
31
|
//# sourceMappingURL=encryption.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../../../src/platforms/webex/encryption.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../../../src/platforms/webex/encryption.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAChD,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB;AAID,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,QAAQ,CAAA;IACb,UAAU,EAAE,UAAU,CAAA;CACvB;AAMD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,WAAW,CAAgC;gBAEvC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAI/C,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAI1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC;IAqBpD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAgBtE,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAY7E,aAAa,CAAC,SAAS,EAAE,UAAU,GAAG,oBAAoB;IAsBpD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAcxE"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { createCipheriv, randomBytes } from 'node:crypto';
|
|
1
2
|
import * as jose from 'node-jose';
|
|
3
|
+
function toBase64Url(buffer) {
|
|
4
|
+
return buffer.toString('base64url');
|
|
5
|
+
}
|
|
2
6
|
export class WebexEncryptionService {
|
|
3
7
|
rawKeys;
|
|
4
8
|
keyCache = new Map();
|
|
@@ -58,5 +62,37 @@ export class WebexEncryptionService {
|
|
|
58
62
|
return null;
|
|
59
63
|
}
|
|
60
64
|
}
|
|
65
|
+
encryptBinary(plaintext) {
|
|
66
|
+
const key = randomBytes(32);
|
|
67
|
+
const iv = randomBytes(12);
|
|
68
|
+
const aad = new Date().toISOString();
|
|
69
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
70
|
+
cipher.setAAD(Buffer.from(aad, 'utf8'));
|
|
71
|
+
const ciphertext = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
|
|
72
|
+
const tag = cipher.getAuthTag();
|
|
73
|
+
return {
|
|
74
|
+
scr: {
|
|
75
|
+
enc: 'A256GCM',
|
|
76
|
+
key: toBase64Url(key),
|
|
77
|
+
iv: toBase64Url(iv),
|
|
78
|
+
aad,
|
|
79
|
+
tag: toBase64Url(tag),
|
|
80
|
+
},
|
|
81
|
+
ciphertext,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async encryptScr(keyUri, scr) {
|
|
85
|
+
if (!scr.loc)
|
|
86
|
+
return null;
|
|
87
|
+
const key = await this.getKey(keyUri);
|
|
88
|
+
if (!key)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
return await jose.JWE.createEncrypt({ format: 'compact', contentAlg: 'A256GCM' }, { key, header: { alg: 'dir', kid: keyUri }, reference: null }).final(JSON.stringify(scr), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
61
97
|
}
|
|
62
98
|
//# sourceMappingURL=encryption.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encryption.js","sourceRoot":"","sources":["../../../../src/platforms/webex/encryption.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;
|
|
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.
|
|
3
|
+
"version": "2.23.6",
|
|
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
|
+
version: 2.23.6
|
|
5
5
|
allowed-tools: Bash(agent-channeltalk:*)
|
|
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
|
+
version: 2.23.6
|
|
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
|
+
version: 2.23.6
|
|
5
5
|
allowed-tools: Bash(agent-webexbot:*)
|
|
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 {
|
|
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)
|
|
@@ -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', () => {
|
|
@@ -1018,6 +1080,137 @@ describe('WebexClient', () => {
|
|
|
1018
1080
|
})
|
|
1019
1081
|
})
|
|
1020
1082
|
|
|
1083
|
+
describe('uploadFile', () => {
|
|
1084
|
+
const mockUploadFlow = () => {
|
|
1085
|
+
// given: the full internal share flow — conv lookup, space, session, PUT, finish, content
|
|
1086
|
+
mockResponse({ id: TEST_CONV_UUID })
|
|
1087
|
+
mockResponse({ spaceUrl: 'https://files.wbx2.com/spaces/sp1' })
|
|
1088
|
+
mockResponse({
|
|
1089
|
+
uploadUrl: 'https://up.wbx2.com/upload/sess1',
|
|
1090
|
+
finishUploadUrl: 'https://up.wbx2.com/upload/sess1/finish',
|
|
1091
|
+
})
|
|
1092
|
+
mockResponse({}, 200)
|
|
1093
|
+
mockResponse({ downloadUrl: 'https://files.wbx2.com/files/f1' })
|
|
1094
|
+
mockResponse({ ...mockActivity(''), verb: 'share' })
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const file = () => ({ content: new Blob(['hello world']), filename: 'note.txt' })
|
|
1098
|
+
|
|
1099
|
+
it('routes to the internal conversation API instead of the public messages endpoint', async () => {
|
|
1100
|
+
mockUploadFlow()
|
|
1101
|
+
|
|
1102
|
+
const client = await createExtractedClient()
|
|
1103
|
+
await client.uploadFile(TEST_ROOM_ID, file())
|
|
1104
|
+
|
|
1105
|
+
expect(fetchCalls.every((c) => !c.url.includes('webexapis.com/v1/messages'))).toBe(true)
|
|
1106
|
+
expect(fetchCalls.at(-1)?.url).toBe(`${CONV_BASE}/conversations/${TEST_CONV_UUID}/content`)
|
|
1107
|
+
expect(fetchCalls.at(-1)?.options?.method).toBe('POST')
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
it('requests a space, opens an upload session, PUTs the bytes, then finalizes', async () => {
|
|
1111
|
+
mockUploadFlow()
|
|
1112
|
+
|
|
1113
|
+
const client = await createExtractedClient()
|
|
1114
|
+
await client.uploadFile(TEST_ROOM_ID, file())
|
|
1115
|
+
|
|
1116
|
+
expect(fetchCalls[1].url).toBe(`${CONV_BASE}/conversations/${TEST_CONV_UUID}/space`)
|
|
1117
|
+
expect(fetchCalls[1].options?.method).toBe('PUT')
|
|
1118
|
+
expect(fetchCalls[2].url).toBe('https://files.wbx2.com/spaces/sp1/upload_sessions')
|
|
1119
|
+
expect(fetchCalls[3].url).toBe('https://up.wbx2.com/upload/sess1')
|
|
1120
|
+
expect(fetchCalls[3].options?.method).toBe('PUT')
|
|
1121
|
+
expect(fetchCalls[4].url).toBe('https://up.wbx2.com/upload/sess1/finish')
|
|
1122
|
+
})
|
|
1123
|
+
|
|
1124
|
+
it('finalize body carries fileSize and a sha256 fileHash of the uploaded bytes', async () => {
|
|
1125
|
+
mockUploadFlow()
|
|
1126
|
+
|
|
1127
|
+
const client = await createExtractedClient()
|
|
1128
|
+
await client.uploadFile(TEST_ROOM_ID, file())
|
|
1129
|
+
|
|
1130
|
+
const body = JSON.parse(fetchCalls[4].options?.body as string)
|
|
1131
|
+
expect(body.fileSize).toBe(11)
|
|
1132
|
+
expect(body.fileHash).toMatch(/^[0-9a-f]{64}$/)
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
it('share activity references the uploaded file with download url and metadata', async () => {
|
|
1136
|
+
mockUploadFlow()
|
|
1137
|
+
|
|
1138
|
+
const client = await createExtractedClient()
|
|
1139
|
+
await client.uploadFile(TEST_ROOM_ID, file())
|
|
1140
|
+
|
|
1141
|
+
const body = JSON.parse(fetchCalls.at(-1)?.options?.body as string)
|
|
1142
|
+
expect(body.verb).toBe('share')
|
|
1143
|
+
expect(body.object.objectType).toBe('content')
|
|
1144
|
+
expect(body.object.contentCategory).toBe('documents')
|
|
1145
|
+
const item = body.object.files.items[0]
|
|
1146
|
+
expect(item.objectType).toBe('file')
|
|
1147
|
+
expect(item.url).toBe('https://files.wbx2.com/files/f1')
|
|
1148
|
+
expect(item.fileSize).toBe(11)
|
|
1149
|
+
expect(item.mimeType).toBe('text/plain')
|
|
1150
|
+
expect(item.displayName).toBe('note.txt')
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
it('attaches an optional text comment to the share activity', async () => {
|
|
1154
|
+
mockUploadFlow()
|
|
1155
|
+
|
|
1156
|
+
const client = await createExtractedClient()
|
|
1157
|
+
await client.uploadFile(TEST_ROOM_ID, file(), { text: 'see attached' })
|
|
1158
|
+
|
|
1159
|
+
const body = JSON.parse(fetchCalls.at(-1)?.options?.body as string)
|
|
1160
|
+
expect(body.object.displayName).toBe('see attached')
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
it('categorizes images by mime type', async () => {
|
|
1164
|
+
mockUploadFlow()
|
|
1165
|
+
|
|
1166
|
+
const client = await createExtractedClient()
|
|
1167
|
+
await client.uploadFile(TEST_ROOM_ID, { content: new Blob(['x']), filename: 'photo.png' })
|
|
1168
|
+
|
|
1169
|
+
const body = JSON.parse(fetchCalls.at(-1)?.options?.body as string)
|
|
1170
|
+
expect(body.object.contentCategory).toBe('images')
|
|
1171
|
+
expect(body.object.files.items[0].mimeType).toBe('image/png')
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
it('refuses to upload when the server returns an untrusted space url', async () => {
|
|
1175
|
+
mockResponse({ id: TEST_CONV_UUID })
|
|
1176
|
+
mockResponse({ spaceUrl: 'https://evil.example.com/spaces/sp1' })
|
|
1177
|
+
|
|
1178
|
+
const client = await createExtractedClient()
|
|
1179
|
+
|
|
1180
|
+
await expect(client.uploadFile(TEST_ROOM_ID, file())).rejects.toThrow('untrusted host')
|
|
1181
|
+
expect(fetchCalls.every((c) => !c.url.includes('evil.example.com'))).toBe(true)
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
it('refuses to upload when the server returns a non-https upload url', async () => {
|
|
1185
|
+
mockResponse({ id: TEST_CONV_UUID })
|
|
1186
|
+
mockResponse({ spaceUrl: 'https://files.wbx2.com/spaces/sp1' })
|
|
1187
|
+
mockResponse({
|
|
1188
|
+
uploadUrl: 'http://up.wbx2.com/upload/sess1',
|
|
1189
|
+
finishUploadUrl: 'https://up.wbx2.com/upload/sess1/finish',
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
const client = await createExtractedClient()
|
|
1193
|
+
|
|
1194
|
+
await expect(client.uploadFile(TEST_ROOM_ID, file())).rejects.toThrow('untrusted host')
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1197
|
+
it('accepts trusted Webex urls that carry an explicit port', async () => {
|
|
1198
|
+
mockResponse({ id: TEST_CONV_UUID })
|
|
1199
|
+
mockResponse({ spaceUrl: 'https://files.wbx2.com:443/spaces/sp1' })
|
|
1200
|
+
mockResponse({
|
|
1201
|
+
uploadUrl: 'https://up.wbx2.com:443/upload/sess1',
|
|
1202
|
+
finishUploadUrl: 'https://up.wbx2.com:443/upload/sess1/finish',
|
|
1203
|
+
})
|
|
1204
|
+
mockResponse({}, 200)
|
|
1205
|
+
mockResponse({ downloadUrl: 'https://files.wbx2.com/files/f1' })
|
|
1206
|
+
mockResponse({ ...mockActivity(''), verb: 'share' })
|
|
1207
|
+
|
|
1208
|
+
const client = await createExtractedClient()
|
|
1209
|
+
|
|
1210
|
+
await expect(client.uploadFile(TEST_ROOM_ID, file())).resolves.toBeDefined()
|
|
1211
|
+
})
|
|
1212
|
+
})
|
|
1213
|
+
|
|
1021
1214
|
describe('error handling', () => {
|
|
1022
1215
|
it('throws WebexError when internal API returns non-OK response', async () => {
|
|
1023
1216
|
fetchResponses.push(
|