agent-messenger 2.21.0 → 2.23.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 +21 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/webex/client.d.ts +25 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +115 -5
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts +9 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +141 -25
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/webex/credential-manager.js +8 -4
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/webex/id-normalizer.d.ts +19 -0
- package/dist/src/platforms/webex/id-normalizer.d.ts.map +1 -0
- package/dist/src/platforms/webex/id-normalizer.js +60 -0
- package/dist/src/platforms/webex/id-normalizer.js.map +1 -0
- package/dist/src/platforms/webex/index.d.ts +4 -0
- package/dist/src/platforms/webex/index.d.ts.map +1 -1
- package/dist/src/platforms/webex/index.js +2 -0
- package/dist/src/platforms/webex/index.js.map +1 -1
- package/dist/src/platforms/webex/listener.d.ts +61 -0
- package/dist/src/platforms/webex/listener.d.ts.map +1 -0
- package/dist/src/platforms/webex/listener.js +222 -0
- package/dist/src/platforms/webex/listener.js.map +1 -0
- package/dist/src/platforms/webex/password-login.d.ts +18 -0
- package/dist/src/platforms/webex/password-login.d.ts.map +1 -0
- package/dist/src/platforms/webex/password-login.js +259 -0
- package/dist/src/platforms/webex/password-login.js.map +1 -0
- package/dist/src/platforms/webex/types.d.ts +2 -1
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +1 -1
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/dist/src/platforms/webex/wdm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/{webexbot → webex}/wdm-discovery.js +3 -3
- package/dist/src/platforms/webex/wdm-discovery.js.map +1 -0
- package/dist/src/platforms/webexbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/cli.js +4 -1
- package/dist/src/platforms/webexbot/cli.js.map +1 -1
- package/dist/src/platforms/webexbot/client.d.ts +24 -0
- package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/client.js +81 -5
- package/dist/src/platforms/webexbot/client.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/file.d.ts +22 -0
- package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/file.js +64 -0
- package/dist/src/platforms/webexbot/commands/file.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/index.d.ts +3 -0
- package/dist/src/platforms/webexbot/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/index.js +3 -0
- package/dist/src/platforms/webexbot/commands/index.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/message.d.ts +7 -0
- package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/commands/message.js +52 -1
- package/dist/src/platforms/webexbot/commands/message.js.map +1 -1
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts +24 -0
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/snapshot.js +37 -0
- package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts +30 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/user.js +66 -0
- package/dist/src/platforms/webexbot/commands/user.js.map +1 -0
- package/dist/src/platforms/webexbot/index.d.ts +2 -0
- package/dist/src/platforms/webexbot/index.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/index.js +1 -0
- package/dist/src/platforms/webexbot/index.js.map +1 -1
- package/dist/src/platforms/webexbot/listener.d.ts +3 -41
- package/dist/src/platforms/webexbot/listener.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/listener.js +13 -208
- package/dist/src/platforms/webexbot/listener.js.map +1 -1
- package/dist/src/platforms/webexbot/types.d.ts +1 -18
- package/dist/src/platforms/webexbot/types.d.ts.map +1 -1
- package/dist/src/platforms/webexbot/types.js.map +1 -1
- package/docs/content/docs/cli/webex.mdx +38 -12
- package/docs/content/docs/cli/webexbot.mdx +2 -0
- package/docs/content/docs/sdk/webexbot.mdx +18 -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 +76 -22
- package/skills/agent-webex/references/authentication.md +55 -14
- package/skills/agent-webex/references/common-patterns.md +5 -2
- package/skills/agent-webexbot/SKILL.md +60 -5
- package/skills/agent-webexbot/references/common-patterns.md +118 -0
- 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.test.ts +31 -1
- package/src/platforms/webex/client.test.ts +67 -0
- package/src/platforms/webex/client.ts +136 -7
- package/src/platforms/webex/commands/auth.test.ts +189 -28
- package/src/platforms/webex/commands/auth.ts +194 -35
- package/src/platforms/webex/credential-manager.test.ts +40 -0
- package/src/platforms/webex/credential-manager.ts +7 -4
- package/src/platforms/webex/id-normalizer.test.ts +207 -0
- package/src/platforms/webex/id-normalizer.ts +76 -0
- package/src/platforms/webex/index.test.ts +6 -0
- package/src/platforms/webex/index.ts +4 -0
- package/src/platforms/webex/listener.test.ts +243 -0
- package/src/platforms/webex/listener.ts +285 -0
- package/src/platforms/webex/password-login.test.ts +193 -0
- package/src/platforms/webex/password-login.ts +332 -0
- package/src/platforms/webex/types.test.ts +16 -0
- package/src/platforms/webex/types.ts +2 -2
- package/src/platforms/{webexbot → webex}/wdm-discovery.ts +3 -3
- package/src/platforms/webexbot/cli.ts +6 -0
- package/src/platforms/webexbot/client.test.ts +322 -0
- package/src/platforms/webexbot/client.ts +104 -7
- package/src/platforms/webexbot/commands/file.ts +104 -0
- package/src/platforms/webexbot/commands/index.ts +3 -0
- package/src/platforms/webexbot/commands/message.ts +68 -2
- package/src/platforms/webexbot/commands/snapshot.ts +60 -0
- package/src/platforms/webexbot/commands/user.test.ts +77 -0
- package/src/platforms/webexbot/commands/user.ts +98 -0
- package/src/platforms/webexbot/index.ts +2 -0
- package/src/platforms/webexbot/listener.test.ts +37 -224
- package/src/platforms/webexbot/listener.ts +18 -250
- package/src/platforms/webexbot/types.ts +2 -23
- package/dist/src/platforms/webexbot/wdm-discovery.d.ts.map +0 -1
- package/dist/src/platforms/webexbot/wdm-discovery.js.map +0 -1
- /package/dist/src/platforms/{webexbot → webex}/wdm-discovery.d.ts +0 -0
- /package/src/platforms/{webexbot → webex}/wdm-discovery.test.ts +0 -0
|
@@ -220,6 +220,124 @@ MESSAGE_ID="Y2lzY29zcGFyazovL..."
|
|
|
220
220
|
agent-webexbot message delete "$MESSAGE_ID"
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
+
## Thread Patterns
|
|
224
|
+
|
|
225
|
+
### Pattern 12a: Reply in a Thread
|
|
226
|
+
|
|
227
|
+
**Use case**: Keep a conversation organized under a parent message
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
#!/bin/bash
|
|
231
|
+
|
|
232
|
+
SPACE_ID="Y2lzY29zcGFyazovL..."
|
|
233
|
+
|
|
234
|
+
# Send a parent message and capture its ID
|
|
235
|
+
PARENT=$(agent-webexbot message send "$SPACE_ID" "Deploy started" | jq -r '.id')
|
|
236
|
+
|
|
237
|
+
# Reply within the thread
|
|
238
|
+
agent-webexbot message reply "$SPACE_ID" "$PARENT" "Step 1 complete"
|
|
239
|
+
|
|
240
|
+
# Equivalent using send --parent
|
|
241
|
+
agent-webexbot message send "$SPACE_ID" "Step 2 complete" --parent "$PARENT"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Pattern 12b: Read Thread Replies
|
|
245
|
+
|
|
246
|
+
**Use case**: Fetch all replies under a parent message
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
#!/bin/bash
|
|
250
|
+
|
|
251
|
+
SPACE_ID="Y2lzY29zcGFyazovL..."
|
|
252
|
+
PARENT_ID="Y2lzY29zcGFyazovL..."
|
|
253
|
+
|
|
254
|
+
agent-webexbot message replies "$SPACE_ID" "$PARENT_ID" --max 20 \
|
|
255
|
+
| jq -r '.messages[] | "[\(.created)] \(.personEmail): \(.text)"'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## File Patterns
|
|
259
|
+
|
|
260
|
+
### Pattern 12c: Upload a File
|
|
261
|
+
|
|
262
|
+
**Use case**: Attach a local file (report, log, image) to a space
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
#!/bin/bash
|
|
266
|
+
|
|
267
|
+
SPACE_ID="Y2lzY29zcGFyazovL..."
|
|
268
|
+
|
|
269
|
+
# Upload with an accompanying message
|
|
270
|
+
agent-webexbot file upload "$SPACE_ID" ./coverage.html --text "Latest coverage report"
|
|
271
|
+
|
|
272
|
+
# Upload as a threaded reply
|
|
273
|
+
agent-webexbot file upload "$SPACE_ID" ./build.log --parent "$PARENT_ID"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Note**: Max file size is 100 MB, one file per message.
|
|
277
|
+
|
|
278
|
+
### Pattern 12d: Download an Attachment
|
|
279
|
+
|
|
280
|
+
**Use case**: Save a file someone shared in a space
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
#!/bin/bash
|
|
284
|
+
|
|
285
|
+
SPACE_ID="Y2lzY29zcGFyazovL..."
|
|
286
|
+
|
|
287
|
+
# Get the content URL from a message's "files" array
|
|
288
|
+
CONTENT_URL=$(agent-webexbot message list "$SPACE_ID" --max 20 \
|
|
289
|
+
| jq -r 'first(.messages[].files[]? // empty)')
|
|
290
|
+
|
|
291
|
+
# Download (defaults to the original filename in the current directory)
|
|
292
|
+
agent-webexbot file download "$CONTENT_URL"
|
|
293
|
+
|
|
294
|
+
# Or choose an output path explicitly
|
|
295
|
+
agent-webexbot file download "$CONTENT_URL" ./downloaded-report.html
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Security note**: Downloads are restricted to `https://webexapis.com/v1/contents/*` URLs — the bot token is never sent to other hosts. Server-provided filenames are reduced to their base name, so the default output always stays in the current directory.
|
|
299
|
+
|
|
300
|
+
## People Patterns
|
|
301
|
+
|
|
302
|
+
### Pattern 12e: Look Up a Person by Email
|
|
303
|
+
|
|
304
|
+
**Use case**: Resolve a person ID or display name from an email address
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
#!/bin/bash
|
|
308
|
+
|
|
309
|
+
agent-webexbot user list --email alice@example.com \
|
|
310
|
+
| jq -r '.users[] | "\(.displayName) — \(.id)"'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Pattern 12f: Get Person Details
|
|
314
|
+
|
|
315
|
+
**Use case**: Fetch full profile details for a known person ID
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
#!/bin/bash
|
|
319
|
+
|
|
320
|
+
PERSON_ID="Y2lzY29zcGFyazovL..."
|
|
321
|
+
|
|
322
|
+
agent-webexbot user info "$PERSON_ID" | jq '{displayName, emails, type}'
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Snapshot Patterns
|
|
326
|
+
|
|
327
|
+
### Pattern 12g: Workspace Overview
|
|
328
|
+
|
|
329
|
+
**Use case**: Give an AI agent a quick picture of the bot's workspace
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
#!/bin/bash
|
|
333
|
+
|
|
334
|
+
# Brief: bot identity + space IDs/titles
|
|
335
|
+
agent-webexbot snapshot
|
|
336
|
+
|
|
337
|
+
# Full: includes space type and last activity
|
|
338
|
+
agent-webexbot snapshot --full --max 50
|
|
339
|
+
```
|
|
340
|
+
|
|
223
341
|
## Member Patterns
|
|
224
342
|
|
|
225
343
|
### Pattern 13: List Space Members
|
|
@@ -20,6 +20,21 @@ describe('Webex CLI program structure', () => {
|
|
|
20
20
|
expect(output).toContain('space')
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
+
it('auth --help lists login and oauth subcommands', async () => {
|
|
24
|
+
const proc = spawn(['bun', 'run', './src/platforms/webex/cli.ts', 'auth', '--help'], {
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const output = await new Response(proc.stdout).text()
|
|
30
|
+
|
|
31
|
+
expect(output).toContain('login')
|
|
32
|
+
expect(output).toContain('oauth')
|
|
33
|
+
expect(output).toContain('extract')
|
|
34
|
+
expect(output).toContain('status')
|
|
35
|
+
expect(output).toContain('logout')
|
|
36
|
+
})
|
|
37
|
+
|
|
23
38
|
it('--version shows package version', async () => {
|
|
24
39
|
const proc = spawn(['bun', 'run', './src/platforms/webex/cli.ts', '--version'], {
|
|
25
40
|
cwd: process.cwd(),
|
|
@@ -30,7 +45,7 @@ describe('Webex CLI program structure', () => {
|
|
|
30
45
|
expect(output.trim()).toBe(pkg.version)
|
|
31
46
|
})
|
|
32
47
|
|
|
33
|
-
it('auth login --help shows
|
|
48
|
+
it('auth login --help shows email/password options', async () => {
|
|
34
49
|
const proc = spawn(['bun', 'run', './src/platforms/webex/cli.ts', 'auth', 'login', '--help'], {
|
|
35
50
|
cwd: process.cwd(),
|
|
36
51
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -38,7 +53,22 @@ describe('Webex CLI program structure', () => {
|
|
|
38
53
|
|
|
39
54
|
const output = await new Response(proc.stdout).text()
|
|
40
55
|
|
|
56
|
+
expect(output).toContain('--email')
|
|
57
|
+
expect(output).toContain('--password')
|
|
58
|
+
expect(output).toContain('--password-stdin')
|
|
41
59
|
expect(output).toContain('--token')
|
|
60
|
+
expect(output).toContain('--pretty')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('auth oauth --help shows Device Grant options', async () => {
|
|
64
|
+
const proc = spawn(['bun', 'run', './src/platforms/webex/cli.ts', 'auth', 'oauth', '--help'], {
|
|
65
|
+
cwd: process.cwd(),
|
|
66
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const output = await new Response(proc.stdout).text()
|
|
70
|
+
|
|
71
|
+
expect(output).toContain('--device-code')
|
|
42
72
|
expect(output).toContain('--client-id')
|
|
43
73
|
expect(output).toContain('--client-secret')
|
|
44
74
|
expect(output).toContain('--pretty')
|
|
@@ -60,6 +60,16 @@ describe('WebexClient', () => {
|
|
|
60
60
|
await expect(new WebexClient().login({ token: '' })).rejects.toThrow('Token is required')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
it('returns the authenticated token', async () => {
|
|
64
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
65
|
+
|
|
66
|
+
expect(client.getToken()).toBe('test-token')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('throws when reading token before login', () => {
|
|
70
|
+
expect(() => new WebexClient().getToken()).toThrow('Not authenticated. Call .login() first.')
|
|
71
|
+
})
|
|
72
|
+
|
|
63
73
|
it('accepts deviceUrl and tokenType', async () => {
|
|
64
74
|
const client = await new WebexClient().login({
|
|
65
75
|
token: 'test-token',
|
|
@@ -191,6 +201,53 @@ describe('WebexClient', () => {
|
|
|
191
201
|
})
|
|
192
202
|
})
|
|
193
203
|
|
|
204
|
+
describe('iterateSpaces', () => {
|
|
205
|
+
it('follows the Link header across pages and yields every room', async () => {
|
|
206
|
+
// given two pages chained by a rel="next" Link header
|
|
207
|
+
mockResponse({ items: [{ id: 'room1', title: 'One', type: 'group' }] }, 200, {
|
|
208
|
+
Link: '<https://webexapis.com/v1/rooms?max=1000&before=cursor1>; rel="next"',
|
|
209
|
+
})
|
|
210
|
+
mockResponse({ items: [{ id: 'room2', title: 'Two', type: 'group' }] })
|
|
211
|
+
|
|
212
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
213
|
+
const ids: string[] = []
|
|
214
|
+
for await (const room of client.iterateSpaces({ max: 1000 })) {
|
|
215
|
+
ids.push(room.id)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expect(ids).toEqual(['room1', 'room2'])
|
|
219
|
+
expect(fetchCalls[0].url).toContain('/rooms?max=1000')
|
|
220
|
+
expect(fetchCalls[1].url).toContain('before=cursor1')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('stops after a single page when no next Link is present', async () => {
|
|
224
|
+
mockResponse({ items: [{ id: 'room1', title: 'One', type: 'group' }] })
|
|
225
|
+
|
|
226
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
227
|
+
const ids: string[] = []
|
|
228
|
+
for await (const room of client.iterateSpaces()) {
|
|
229
|
+
ids.push(room.id)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(ids).toEqual(['room1'])
|
|
233
|
+
expect(fetchCalls).toHaveLength(1)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('stops consuming the generator early without fetching the next page', async () => {
|
|
237
|
+
mockResponse({ items: [{ id: 'room1', title: 'One', type: 'group' }] }, 200, {
|
|
238
|
+
Link: '<https://webexapis.com/v1/rooms?max=1000&before=cursor1>; rel="next"',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
242
|
+
for await (const room of client.iterateSpaces({ max: 1000 })) {
|
|
243
|
+
expect(room.id).toBe('room1')
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
expect(fetchCalls).toHaveLength(1)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
194
251
|
describe('getSpace', () => {
|
|
195
252
|
it('calls GET /rooms/{spaceId}', async () => {
|
|
196
253
|
mockResponse({ id: 'room1', title: 'Test Room', type: 'group' })
|
|
@@ -274,6 +331,16 @@ describe('WebexClient', () => {
|
|
|
274
331
|
|
|
275
332
|
expect(fetchCalls[0].url).toContain('max=10')
|
|
276
333
|
})
|
|
334
|
+
|
|
335
|
+
it('passes mentionedPeople when requested', async () => {
|
|
336
|
+
mockResponse({ items: [] })
|
|
337
|
+
|
|
338
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
339
|
+
await client.listMessages('room1', { max: 10, mentionedPeople: 'me' })
|
|
340
|
+
|
|
341
|
+
const url = new URL(fetchCalls[0].url)
|
|
342
|
+
expect(url.searchParams.get('mentionedPeople')).toBe('me')
|
|
343
|
+
})
|
|
277
344
|
})
|
|
278
345
|
|
|
279
346
|
describe('getMessage', () => {
|
|
@@ -6,6 +6,7 @@ import type { WebexConfig, WebexMembership, WebexMessage, WebexPerson, WebexSpac
|
|
|
6
6
|
import { WebexError } from './types'
|
|
7
7
|
|
|
8
8
|
const BASE_URL = 'https://webexapis.com/v1'
|
|
9
|
+
const CONTENT_HOST = 'webexapis.com'
|
|
9
10
|
const MAX_RETRIES = 3
|
|
10
11
|
const BASE_BACKOFF_MS = 100
|
|
11
12
|
|
|
@@ -45,7 +46,7 @@ export class WebexClient {
|
|
|
45
46
|
this.tokenType = config?.tokenType ?? null
|
|
46
47
|
await this.login({ token })
|
|
47
48
|
|
|
48
|
-
if (this.tokenType === 'extracted') {
|
|
49
|
+
if (this.tokenType === 'extracted' || this.tokenType === 'password') {
|
|
49
50
|
const keysMap = new Map(Object.entries(config?.encryptionKeys ?? {}))
|
|
50
51
|
this.encryption = new WebexEncryptionService(keysMap)
|
|
51
52
|
const kmsProvider = new KmsKeyProvider({ token })
|
|
@@ -68,6 +69,10 @@ export class WebexClient {
|
|
|
68
69
|
await this.encryption?.close()
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
getToken(): string {
|
|
73
|
+
return this.ensureAuth()
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
private async persistEncryptionKey(
|
|
72
77
|
credManager: WebexCredentialManager,
|
|
73
78
|
keyUri: string,
|
|
@@ -130,6 +135,14 @@ export class WebexClient {
|
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
138
|
+
return (await this.requestWithLink<T>(method, path, body)).data
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async requestWithLink<T>(
|
|
142
|
+
method: string,
|
|
143
|
+
path: string,
|
|
144
|
+
body?: unknown,
|
|
145
|
+
): Promise<{ data: T; nextPath: string | null }> {
|
|
133
146
|
const url = `${BASE_URL}${path}`
|
|
134
147
|
const bucketKey = this.getBucketKey(method, path)
|
|
135
148
|
|
|
@@ -178,10 +191,11 @@ export class WebexClient {
|
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
if (response.status === 204) {
|
|
181
|
-
return undefined as T
|
|
194
|
+
return { data: undefined as T, nextPath: null }
|
|
182
195
|
}
|
|
183
196
|
|
|
184
|
-
|
|
197
|
+
const data = (await response.json()) as T
|
|
198
|
+
return { data, nextPath: parseNextPath(response.headers.get('Link')) }
|
|
185
199
|
}
|
|
186
200
|
|
|
187
201
|
throw new WebexError('Request failed after retries', 'max_retries')
|
|
@@ -219,20 +233,40 @@ export class WebexClient {
|
|
|
219
233
|
return data.items
|
|
220
234
|
}
|
|
221
235
|
|
|
236
|
+
async *iterateSpaces(options?: { type?: string; max?: number }): AsyncGenerator<WebexSpace> {
|
|
237
|
+
const params = new URLSearchParams()
|
|
238
|
+
if (options?.type) params.set('type', options.type)
|
|
239
|
+
params.set('max', String(options?.max ?? 100))
|
|
240
|
+
let path: string | null = `/rooms?${params.toString()}`
|
|
241
|
+
while (path) {
|
|
242
|
+
const page: { data: { items: WebexSpace[] }; nextPath: string | null } = await this.requestWithLink('GET', path)
|
|
243
|
+
yield* page.data.items
|
|
244
|
+
path = page.nextPath
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
222
248
|
async getSpace(spaceId: string): Promise<WebexSpace> {
|
|
223
249
|
return this.request<WebexSpace>('GET', `/rooms/${spaceId}`)
|
|
224
250
|
}
|
|
225
251
|
|
|
226
|
-
async sendMessage(
|
|
252
|
+
async sendMessage(
|
|
253
|
+
roomId: string,
|
|
254
|
+
text: string,
|
|
255
|
+
options?: { markdown?: boolean; parentId?: string; files?: string[] },
|
|
256
|
+
): Promise<WebexMessage> {
|
|
227
257
|
if (this.useInternalAPI) {
|
|
228
258
|
return this.sendMessageInternal(roomId, text, options)
|
|
229
259
|
}
|
|
230
|
-
const body
|
|
260
|
+
const body: Record<string, unknown> = { roomId }
|
|
261
|
+
if (options?.markdown) body.markdown = text
|
|
262
|
+
else body.text = text
|
|
263
|
+
if (options?.parentId) body.parentId = options.parentId
|
|
264
|
+
if (options?.files?.length) body.files = options.files
|
|
231
265
|
return this.request<WebexMessage>('POST', '/messages', body)
|
|
232
266
|
}
|
|
233
267
|
|
|
234
268
|
private get useInternalAPI(): boolean {
|
|
235
|
-
return this.tokenType === 'extracted' && this.deviceUrl !== null
|
|
269
|
+
return (this.tokenType === 'extracted' || this.tokenType === 'password') && this.deviceUrl !== null
|
|
236
270
|
}
|
|
237
271
|
|
|
238
272
|
private get convBaseUrl(): string {
|
|
@@ -387,7 +421,10 @@ export class WebexClient {
|
|
|
387
421
|
return null
|
|
388
422
|
}
|
|
389
423
|
|
|
390
|
-
async listMessages(
|
|
424
|
+
async listMessages(
|
|
425
|
+
roomId: string,
|
|
426
|
+
options?: { max?: number; mentionedPeople?: string; parentId?: string },
|
|
427
|
+
): Promise<WebexMessage[]> {
|
|
391
428
|
if (this.useInternalAPI) {
|
|
392
429
|
const convUuid = this.decodeConvUuid(roomId)
|
|
393
430
|
const max = options?.max ?? 50
|
|
@@ -400,6 +437,8 @@ export class WebexClient {
|
|
|
400
437
|
const params = new URLSearchParams()
|
|
401
438
|
params.set('roomId', roomId)
|
|
402
439
|
params.set('max', String(options?.max ?? 50))
|
|
440
|
+
if (options?.mentionedPeople) params.set('mentionedPeople', options.mentionedPeople)
|
|
441
|
+
if (options?.parentId) params.set('parentId', options.parentId)
|
|
403
442
|
const data = await this.request<{ items: WebexMessage[] }>('GET', `/messages?${params}`)
|
|
404
443
|
return data.items
|
|
405
444
|
}
|
|
@@ -493,6 +532,10 @@ export class WebexClient {
|
|
|
493
532
|
return data.items
|
|
494
533
|
}
|
|
495
534
|
|
|
535
|
+
async getPerson(personId: string): Promise<WebexPerson> {
|
|
536
|
+
return this.request<WebexPerson>('GET', `/people/${personId}`)
|
|
537
|
+
}
|
|
538
|
+
|
|
496
539
|
async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
|
|
497
540
|
const params = new URLSearchParams()
|
|
498
541
|
params.set('max', String(options?.max ?? 100))
|
|
@@ -507,6 +550,92 @@ export class WebexClient {
|
|
|
507
550
|
const data = await this.request<{ items: WebexMembership[] }>('GET', `/memberships?${params}`)
|
|
508
551
|
return data.items
|
|
509
552
|
}
|
|
553
|
+
|
|
554
|
+
async uploadFile(
|
|
555
|
+
roomId: string,
|
|
556
|
+
file: { content: Blob; filename: string },
|
|
557
|
+
options?: { text?: string; markdown?: boolean; parentId?: string },
|
|
558
|
+
): Promise<WebexMessage> {
|
|
559
|
+
const form = new FormData()
|
|
560
|
+
form.set('roomId', roomId)
|
|
561
|
+
if (options?.text) {
|
|
562
|
+
form.set(options.markdown ? 'markdown' : 'text', options.text)
|
|
563
|
+
}
|
|
564
|
+
if (options?.parentId) form.set('parentId', options.parentId)
|
|
565
|
+
form.set('files', file.content, file.filename)
|
|
566
|
+
|
|
567
|
+
const response = await fetch(`${BASE_URL}/messages`, {
|
|
568
|
+
method: 'POST',
|
|
569
|
+
headers: { Authorization: `Bearer ${this.ensureAuth()}` },
|
|
570
|
+
body: form,
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
if (!response.ok) {
|
|
574
|
+
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
575
|
+
throw new WebexError(errorBody?.message ?? `HTTP ${response.status}`, `http_${response.status}`)
|
|
576
|
+
}
|
|
577
|
+
return response.json() as Promise<WebexMessage>
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
|
|
581
|
+
const url = this.resolveContentUrl(contentRef)
|
|
582
|
+
const response = await fetch(url, {
|
|
583
|
+
headers: { Authorization: `Bearer ${this.ensureAuth()}` },
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
if (!response.ok) {
|
|
587
|
+
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
588
|
+
throw new WebexError(errorBody?.message ?? `HTTP ${response.status}`, `http_${response.status}`)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const disposition = response.headers.get('Content-Disposition') ?? ''
|
|
592
|
+
const match = disposition.match(/filename="?([^"]+)"?/)
|
|
593
|
+
const filename = sanitizeFilename(match?.[1]) ?? sanitizeFilename(contentRef.split('/').pop()) ?? 'download'
|
|
594
|
+
const contentType = response.headers.get('Content-Type') ?? 'application/octet-stream'
|
|
595
|
+
const data = await response.arrayBuffer()
|
|
596
|
+
return { data, filename, contentType }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private resolveContentUrl(contentRef: string): string {
|
|
600
|
+
// A bare content id never contains a scheme or path separators.
|
|
601
|
+
if (!contentRef.includes('://') && !contentRef.includes('/')) {
|
|
602
|
+
return `${BASE_URL}/contents/${encodeURIComponent(contentRef)}`
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Only attach the bearer token to HTTPS Webex content URLs to avoid
|
|
606
|
+
// leaking credentials to attacker-controlled hosts (SSRF/token exfiltration).
|
|
607
|
+
let parsed: URL
|
|
608
|
+
try {
|
|
609
|
+
parsed = new URL(contentRef)
|
|
610
|
+
} catch {
|
|
611
|
+
throw new WebexError(`Invalid content reference: ${contentRef}`, 'invalid_content_ref')
|
|
612
|
+
}
|
|
613
|
+
if (parsed.protocol !== 'https:' || parsed.host !== CONTENT_HOST || !parsed.pathname.startsWith('/v1/contents/')) {
|
|
614
|
+
throw new WebexError(
|
|
615
|
+
`Refusing to download from untrusted location: ${parsed.origin}${parsed.pathname}`,
|
|
616
|
+
'untrusted_content_url',
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
return parsed.toString()
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Webex paginates List endpoints via an RFC 5988 `Link` header
|
|
624
|
+
// (`<https://webexapis.com/v1/rooms?...&before=cursor>; rel="next"`). Return the
|
|
625
|
+
// next page as a BASE_URL-relative path, or null when there is no next page.
|
|
626
|
+
function parseNextPath(linkHeader: string | null): string | null {
|
|
627
|
+
if (!linkHeader) return null
|
|
628
|
+
const next = linkHeader.match(/<([^>]+)>\s*;\s*rel="next"/i)
|
|
629
|
+
if (!next) return null
|
|
630
|
+
return next[1].startsWith(BASE_URL) ? next[1].slice(BASE_URL.length) : null
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function sanitizeFilename(name: string | undefined): string | undefined {
|
|
634
|
+
if (!name) return undefined
|
|
635
|
+
// Strip any path components so a server-supplied name cannot escape the target directory.
|
|
636
|
+
const base = name.replace(/\\/g, '/').split('/').pop()
|
|
637
|
+
if (!base || base === '.' || base === '..') return undefined
|
|
638
|
+
return base
|
|
510
639
|
}
|
|
511
640
|
|
|
512
641
|
interface InternalActivity {
|