agent-messenger 2.23.1 → 2.23.3

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 (130) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/webex/client.d.ts +18 -0
  4. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  5. package/dist/src/platforms/webex/client.js +202 -49
  6. package/dist/src/platforms/webex/client.js.map +1 -1
  7. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  8. package/dist/src/platforms/webex/commands/auth.js +9 -6
  9. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/webex/commands/member.d.ts.map +1 -1
  11. package/dist/src/platforms/webex/commands/member.js +2 -0
  12. package/dist/src/platforms/webex/commands/member.js.map +1 -1
  13. package/dist/src/platforms/webex/commands/message.d.ts.map +1 -1
  14. package/dist/src/platforms/webex/commands/message.js +2 -0
  15. package/dist/src/platforms/webex/commands/message.js.map +1 -1
  16. package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -1
  17. package/dist/src/platforms/webex/commands/snapshot.js +3 -1
  18. package/dist/src/platforms/webex/commands/snapshot.js.map +1 -1
  19. package/dist/src/platforms/webex/commands/space.d.ts.map +1 -1
  20. package/dist/src/platforms/webex/commands/space.js +5 -0
  21. package/dist/src/platforms/webex/commands/space.js.map +1 -1
  22. package/dist/src/platforms/webex/commands/whoami.d.ts.map +1 -1
  23. package/dist/src/platforms/webex/commands/whoami.js +2 -0
  24. package/dist/src/platforms/webex/commands/whoami.js.map +1 -1
  25. package/dist/src/platforms/webex/id-normalizer.d.ts +11 -0
  26. package/dist/src/platforms/webex/id-normalizer.d.ts.map +1 -1
  27. package/dist/src/platforms/webex/id-normalizer.js +102 -20
  28. package/dist/src/platforms/webex/id-normalizer.js.map +1 -1
  29. package/dist/src/platforms/webex/index.d.ts +2 -2
  30. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  31. package/dist/src/platforms/webex/index.js +1 -1
  32. package/dist/src/platforms/webex/index.js.map +1 -1
  33. package/dist/src/platforms/webex/types.d.ts +20 -0
  34. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  35. package/dist/src/platforms/webex/types.js +10 -0
  36. package/dist/src/platforms/webex/types.js.map +1 -1
  37. package/dist/src/platforms/webexbot/client.d.ts +0 -4
  38. package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
  39. package/dist/src/platforms/webexbot/client.js +8 -65
  40. package/dist/src/platforms/webexbot/client.js.map +1 -1
  41. package/dist/src/platforms/webexbot/commands/file.d.ts +2 -0
  42. package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -1
  43. package/dist/src/platforms/webexbot/commands/file.js +2 -0
  44. package/dist/src/platforms/webexbot/commands/file.js.map +1 -1
  45. package/dist/src/platforms/webexbot/commands/member.d.ts +2 -0
  46. package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -1
  47. package/dist/src/platforms/webexbot/commands/member.js +2 -0
  48. package/dist/src/platforms/webexbot/commands/member.js.map +1 -1
  49. package/dist/src/platforms/webexbot/commands/message.d.ts +4 -0
  50. package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -1
  51. package/dist/src/platforms/webexbot/commands/message.js +6 -0
  52. package/dist/src/platforms/webexbot/commands/message.js.map +1 -1
  53. package/dist/src/platforms/webexbot/commands/snapshot.d.ts +2 -0
  54. package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -1
  55. package/dist/src/platforms/webexbot/commands/snapshot.js +10 -2
  56. package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -1
  57. package/dist/src/platforms/webexbot/commands/space.d.ts +4 -0
  58. package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -1
  59. package/dist/src/platforms/webexbot/commands/space.js +5 -0
  60. package/dist/src/platforms/webexbot/commands/space.js.map +1 -1
  61. package/dist/src/platforms/webexbot/commands/user.d.ts +3 -0
  62. package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -1
  63. package/dist/src/platforms/webexbot/commands/user.js +3 -0
  64. package/dist/src/platforms/webexbot/commands/user.js.map +1 -1
  65. package/dist/src/platforms/webexbot/commands/whoami.d.ts +2 -0
  66. package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -1
  67. package/dist/src/platforms/webexbot/commands/whoami.js +2 -0
  68. package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -1
  69. package/dist/src/platforms/webexbot/index.d.ts +2 -2
  70. package/dist/src/platforms/webexbot/index.d.ts.map +1 -1
  71. package/dist/src/platforms/webexbot/index.js +1 -1
  72. package/dist/src/platforms/webexbot/index.js.map +1 -1
  73. package/dist/src/tui/adapters/types.d.ts +3 -0
  74. package/dist/src/tui/adapters/types.d.ts.map +1 -1
  75. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -1
  76. package/dist/src/tui/adapters/webex-adapter.js +4 -0
  77. package/dist/src/tui/adapters/webex-adapter.js.map +1 -1
  78. package/docs/content/docs/cli/webex.mdx +2 -2
  79. package/package.json +1 -1
  80. package/skills/agent-channeltalk/SKILL.md +1 -1
  81. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  82. package/skills/agent-discord/SKILL.md +1 -1
  83. package/skills/agent-discordbot/SKILL.md +1 -1
  84. package/skills/agent-instagram/SKILL.md +1 -1
  85. package/skills/agent-kakaotalk/SKILL.md +1 -1
  86. package/skills/agent-line/SKILL.md +1 -1
  87. package/skills/agent-slack/SKILL.md +1 -1
  88. package/skills/agent-slackbot/SKILL.md +1 -1
  89. package/skills/agent-teams/SKILL.md +1 -1
  90. package/skills/agent-telegram/SKILL.md +1 -1
  91. package/skills/agent-telegrambot/SKILL.md +1 -1
  92. package/skills/agent-webex/SKILL.md +3 -3
  93. package/skills/agent-webexbot/SKILL.md +2 -2
  94. package/skills/agent-webexbot/references/common-patterns.md +1 -1
  95. package/skills/agent-wechatbot/SKILL.md +1 -1
  96. package/skills/agent-whatsapp/SKILL.md +1 -1
  97. package/skills/agent-whatsappbot/SKILL.md +1 -1
  98. package/src/platforms/webex/client.test.ts +94 -6
  99. package/src/platforms/webex/client.ts +226 -44
  100. package/src/platforms/webex/commands/auth.test.ts +3 -1
  101. package/src/platforms/webex/commands/auth.ts +12 -7
  102. package/src/platforms/webex/commands/member.test.ts +24 -8
  103. package/src/platforms/webex/commands/member.ts +2 -0
  104. package/src/platforms/webex/commands/message.test.ts +37 -23
  105. package/src/platforms/webex/commands/message.ts +2 -0
  106. package/src/platforms/webex/commands/snapshot.test.ts +18 -10
  107. package/src/platforms/webex/commands/snapshot.ts +3 -1
  108. package/src/platforms/webex/commands/space.test.ts +36 -17
  109. package/src/platforms/webex/commands/space.ts +5 -0
  110. package/src/platforms/webex/commands/whoami.test.ts +16 -6
  111. package/src/platforms/webex/commands/whoami.ts +2 -0
  112. package/src/platforms/webex/id-normalizer.test.ts +282 -2
  113. package/src/platforms/webex/id-normalizer.ts +112 -20
  114. package/src/platforms/webex/index.ts +2 -2
  115. package/src/platforms/webex/listener.test.ts +3 -0
  116. package/src/platforms/webex/types.test.ts +20 -0
  117. package/src/platforms/webex/types.ts +20 -0
  118. package/src/platforms/webex/typings/webex-message-handler.d.ts +40 -2
  119. package/src/platforms/webexbot/client.ts +8 -74
  120. package/src/platforms/webexbot/commands/file.ts +4 -0
  121. package/src/platforms/webexbot/commands/member.ts +4 -0
  122. package/src/platforms/webexbot/commands/message.ts +10 -0
  123. package/src/platforms/webexbot/commands/snapshot.ts +12 -2
  124. package/src/platforms/webexbot/commands/space.ts +9 -0
  125. package/src/platforms/webexbot/commands/user.test.ts +15 -5
  126. package/src/platforms/webexbot/commands/user.ts +6 -0
  127. package/src/platforms/webexbot/commands/whoami.ts +4 -0
  128. package/src/platforms/webexbot/index.ts +2 -2
  129. package/src/tui/adapters/types.ts +3 -0
  130. package/src/tui/adapters/webex-adapter.ts +4 -0
@@ -16,8 +16,13 @@ import {
16
16
  normalizeMembership,
17
17
  normalizeMessage,
18
18
  normalizeRoomActivity,
19
+ normalizeSdkMembership,
20
+ normalizeSdkMessage,
21
+ normalizeSdkPerson,
19
22
  toRestId,
23
+ toRef,
20
24
  } from './id-normalizer'
25
+ import type { WebexMembership, WebexMessage, WebexPerson } from './types'
21
26
 
22
27
  const RAW: MercuryActivity = {
23
28
  id: 'activity-uuid',
@@ -28,6 +33,8 @@ const RAW: MercuryActivity = {
28
33
  published: '2024-01-01T00:00:00Z',
29
34
  }
30
35
 
36
+ const restId = (type: string, ref: string) => Buffer.from(`ciscospark://us/${type}/${ref}`).toString('base64url')
37
+
31
38
  describe('toRestId / fromRestId', () => {
32
39
  it('encodes a uuid into a ciscospark REST id round-trippable to the uuid', () => {
33
40
  const restId = toRestId('abc-123', 'PEOPLE')
@@ -58,6 +65,40 @@ describe('toRestId / fromRestId', () => {
58
65
  })
59
66
  })
60
67
 
68
+ describe('toRef', () => {
69
+ it('returns a room uuid ref', () => {
70
+ const uuid = '12345678-1234-1234-1234-1234567890ab'
71
+
72
+ expect(toRef(toRestId(uuid, 'ROOM'))).toBe(uuid)
73
+ })
74
+
75
+ it('returns a person uuid ref', () => {
76
+ const uuid = '22222222-2222-2222-2222-222222222222'
77
+
78
+ expect(toRef(toRestId(uuid, 'PEOPLE'))).toBe(uuid)
79
+ })
80
+
81
+ it('returns a legacy person email ref', () => {
82
+ expect(toRef(toRestId('legacy@example.com', 'PEOPLE'))).toBe('legacy@example.com')
83
+ })
84
+
85
+ it('returns an organization uuid ref', () => {
86
+ const uuid = '33333333-3333-3333-3333-333333333333'
87
+
88
+ expect(toRef(restId('ORGANIZATION', uuid))).toBe(uuid)
89
+ })
90
+
91
+ it('returns a membership person-room pair ref', () => {
92
+ const membershipRef = '44444444-4444-4444-4444-444444444444:55555555-5555-5555-5555-555555555555'
93
+
94
+ expect(toRef(restId('MEMBERSHIP', membershipRef))).toBe(membershipRef)
95
+ })
96
+
97
+ it('returns empty input unchanged', () => {
98
+ expect(toRef('')).toBe('')
99
+ })
100
+ })
101
+
61
102
  describe('normalizeMessage', () => {
62
103
  const message: DecryptedMessage = {
63
104
  id: 'msg-uuid',
@@ -85,6 +126,23 @@ describe('normalizeMessage', () => {
85
126
  expect(result.mentionedPeople).toEqual([toRestId('mention-uuid-1', 'PEOPLE'), toRestId('mention-uuid-2', 'PEOPLE')])
86
127
  })
87
128
 
129
+ it('adds a raw uuid ref alongside every id', () => {
130
+ const result = normalizeMessage(message)
131
+
132
+ expect(result.ref).toBe('msg-uuid')
133
+ expect(result.parentRef).toBe('parent-uuid')
134
+ expect(result.roomRef).toBe('room-uuid')
135
+ expect(result.personRef).toBe('person-uuid')
136
+ expect(result.mentionedPeopleRefs).toEqual(['mention-uuid-1', 'mention-uuid-2'])
137
+ })
138
+
139
+ it('omits parentRef when parentId is absent', () => {
140
+ const { parentId: _omit, ...withoutParent } = message
141
+ const result = normalizeMessage(withoutParent)
142
+
143
+ expect(result.parentRef).toBeUndefined()
144
+ })
145
+
88
146
  it('leaves non-id fields and raw untouched', () => {
89
147
  const result = normalizeMessage(message)
90
148
 
@@ -120,8 +178,11 @@ describe('normalizeDeletedMessage', () => {
120
178
 
121
179
  expect(normalizeDeletedMessage(deleted)).toEqual({
122
180
  messageId: toRestId('msg-uuid', 'MESSAGE'),
181
+ messageRef: 'msg-uuid',
123
182
  roomId: toRestId('room-uuid', 'ROOM'),
183
+ roomRef: 'room-uuid',
124
184
  personId: toRestId('person-uuid', 'PEOPLE'),
185
+ personRef: 'person-uuid',
125
186
  })
126
187
  })
127
188
  })
@@ -146,6 +207,25 @@ describe('normalizeMembership', () => {
146
207
  expect(result.roomId).toBe(toRestId('room-uuid', 'ROOM'))
147
208
  expect(result.raw).toBe(RAW)
148
209
  })
210
+
211
+ it('sets ref to the raw activity id and adds refs for the rest', () => {
212
+ const membership: MembershipActivity = {
213
+ id: 'activity-uuid',
214
+ actorId: 'actor-uuid',
215
+ personId: 'member-uuid',
216
+ roomId: 'room-uuid',
217
+ action: 'add',
218
+ created: '2024-01-01T00:00:00Z',
219
+ raw: RAW,
220
+ }
221
+
222
+ const result = normalizeMembership(membership)
223
+
224
+ expect(result.ref).toBe('activity-uuid')
225
+ expect(result.actorRef).toBe('actor-uuid')
226
+ expect(result.personRef).toBe('member-uuid')
227
+ expect(result.roomRef).toBe('room-uuid')
228
+ })
149
229
  })
150
230
 
151
231
  describe('normalizeAttachmentAction', () => {
@@ -170,7 +250,27 @@ describe('normalizeAttachmentAction', () => {
170
250
  expect(result.inputs).toEqual({ choice: 'yes' })
171
251
  })
172
252
 
173
- it('preserves an empty messageId', () => {
253
+ it('adds a raw uuid ref alongside every id', () => {
254
+ const action: AttachmentAction = {
255
+ id: 'action-uuid',
256
+ messageId: 'msg-uuid',
257
+ personId: 'person-uuid',
258
+ personEmail: 'user@example.com',
259
+ roomId: 'room-uuid',
260
+ inputs: { choice: 'yes' },
261
+ created: '2024-01-01T00:00:00Z',
262
+ raw: RAW,
263
+ }
264
+
265
+ const result = normalizeAttachmentAction(action)
266
+
267
+ expect(result.ref).toBe('action-uuid')
268
+ expect(result.messageRef).toBe('msg-uuid')
269
+ expect(result.personRef).toBe('person-uuid')
270
+ expect(result.roomRef).toBe('room-uuid')
271
+ })
272
+
273
+ it('preserves an empty messageId and its ref', () => {
174
274
  const action: AttachmentAction = {
175
275
  id: 'action-uuid',
176
276
  messageId: '',
@@ -182,7 +282,9 @@ describe('normalizeAttachmentAction', () => {
182
282
  raw: RAW,
183
283
  }
184
284
 
185
- expect(normalizeAttachmentAction(action).messageId).toBe('')
285
+ const result = normalizeAttachmentAction(action)
286
+ expect(result.messageId).toBe('')
287
+ expect(result.messageRef).toBe('')
186
288
  })
187
289
  })
188
290
 
@@ -204,4 +306,182 @@ describe('normalizeRoomActivity', () => {
204
306
  expect(result.actorId).toBe(toRestId('actor-uuid', 'PEOPLE'))
205
307
  expect(result.raw).toBe(RAW)
206
308
  })
309
+
310
+ it('sets ref to the raw activity id and adds refs for the rest', () => {
311
+ const room: RoomActivity = {
312
+ id: 'activity-uuid',
313
+ roomId: 'room-uuid',
314
+ actorId: 'actor-uuid',
315
+ action: 'created',
316
+ created: '2024-01-01T00:00:00Z',
317
+ raw: RAW,
318
+ }
319
+
320
+ const result = normalizeRoomActivity(room)
321
+
322
+ expect(result.ref).toBe('activity-uuid')
323
+ expect(result.roomRef).toBe('room-uuid')
324
+ expect(result.actorRef).toBe('actor-uuid')
325
+ })
326
+ })
327
+
328
+ describe('normalizeSdkPerson', () => {
329
+ const person: WebexPerson = {
330
+ id: restId('PEOPLE', 'person-uuid'),
331
+ ref: '',
332
+ emails: ['user@example.com'],
333
+ displayName: 'User',
334
+ orgId: restId('ORGANIZATION', 'org-uuid'),
335
+ orgRef: '',
336
+ type: 'person',
337
+ created: '2024-01-01T00:00:00Z',
338
+ }
339
+
340
+ it('decodes the bot identity id and orgId into raw uuid refs', () => {
341
+ const result = normalizeSdkPerson(person)
342
+
343
+ expect(result.ref).toBe('person-uuid')
344
+ expect(result.orgRef).toBe('org-uuid')
345
+ })
346
+
347
+ it('does not re-encode the already-REST ids', () => {
348
+ const result = normalizeSdkPerson(person)
349
+
350
+ expect(result.id).toBe(restId('PEOPLE', 'person-uuid'))
351
+ expect(result.orgId).toBe(restId('ORGANIZATION', 'org-uuid'))
352
+ })
353
+
354
+ it('leaves empty ids and refs empty', () => {
355
+ const result = normalizeSdkPerson({ ...person, id: '', orgId: '' })
356
+
357
+ expect(result.ref).toBe('')
358
+ expect(result.orgRef).toBe('')
359
+ })
360
+
361
+ it('returns a new object without mutating the input', () => {
362
+ const result = normalizeSdkPerson(person)
363
+
364
+ expect(result).not.toBe(person)
365
+ expect(person.ref).toBe('')
366
+ })
367
+ })
368
+
369
+ describe('normalizeSdkMessage', () => {
370
+ const message: WebexMessage = {
371
+ id: restId('MESSAGE', 'msg-uuid'),
372
+ ref: '',
373
+ roomId: restId('ROOM', 'room-uuid'),
374
+ roomRef: '',
375
+ roomType: 'group',
376
+ text: 'hello',
377
+ personId: restId('PEOPLE', 'person-uuid'),
378
+ personRef: '',
379
+ personEmail: 'user@example.com',
380
+ created: '2024-01-01T00:00:00Z',
381
+ parentId: restId('MESSAGE', 'parent-uuid'),
382
+ mentionedPeople: [restId('PEOPLE', 'mention-1'), restId('PEOPLE', 'mention-2')],
383
+ }
384
+
385
+ it('decodes id, roomId, personId and parentId into raw uuid refs', () => {
386
+ const result = normalizeSdkMessage(message)
387
+
388
+ expect(result.ref).toBe('msg-uuid')
389
+ expect(result.roomRef).toBe('room-uuid')
390
+ expect(result.personRef).toBe('person-uuid')
391
+ expect(result.parentRef).toBe('parent-uuid')
392
+ })
393
+
394
+ it('adds a mentionedPeopleRefs array of raw uuids', () => {
395
+ const result = normalizeSdkMessage(message)
396
+
397
+ expect(result.mentionedPeopleRefs).toEqual(['mention-1', 'mention-2'])
398
+ })
399
+
400
+ it('omits parentRef when parentId is absent', () => {
401
+ const { parentId: _omit, ...withoutParent } = message
402
+ const result = normalizeSdkMessage(withoutParent)
403
+
404
+ expect(result.parentRef).toBeUndefined()
405
+ })
406
+
407
+ it('omits mentionedPeopleRefs when mentionedPeople is absent', () => {
408
+ const { mentionedPeople: _omit, ...withoutMentions } = message
409
+ const result = normalizeSdkMessage(withoutMentions)
410
+
411
+ expect(result.mentionedPeopleRefs).toBeUndefined()
412
+ })
413
+
414
+ it('does not re-encode the already-REST ids', () => {
415
+ const result = normalizeSdkMessage(message)
416
+
417
+ expect(result.id).toBe(restId('MESSAGE', 'msg-uuid'))
418
+ expect(result.roomId).toBe(restId('ROOM', 'room-uuid'))
419
+ })
420
+ })
421
+
422
+ describe('normalizeSdkMembership', () => {
423
+ const membership: WebexMembership = {
424
+ id: restId('MEMBERSHIP', 'membership-uuid'),
425
+ ref: '',
426
+ roomId: restId('ROOM', 'room-uuid'),
427
+ roomRef: '',
428
+ personId: restId('PEOPLE', 'person-uuid'),
429
+ personRef: '',
430
+ personEmail: 'user@example.com',
431
+ personDisplayName: 'User',
432
+ isModerator: false,
433
+ created: '2024-01-01T00:00:00Z',
434
+ }
435
+
436
+ it('decodes id, roomId and personId into raw uuid refs', () => {
437
+ const result = normalizeSdkMembership(membership)
438
+
439
+ expect(result.ref).toBe('membership-uuid')
440
+ expect(result.roomRef).toBe('room-uuid')
441
+ expect(result.personRef).toBe('person-uuid')
442
+ })
443
+
444
+ it('does not re-encode the already-REST ids', () => {
445
+ const result = normalizeSdkMembership(membership)
446
+
447
+ expect(result.id).toBe(restId('MEMBERSHIP', 'membership-uuid'))
448
+ expect(result.personId).toBe(restId('PEOPLE', 'person-uuid'))
449
+ })
450
+ })
451
+
452
+ describe('SDK and event refs agree for the same person', () => {
453
+ it('matches the bot identity ref against an event mention ref so self/mention detection holds', () => {
454
+ const personUuid = 'bot-person-uuid'
455
+
456
+ // given: the bot identity from a REST response (testAuth path)
457
+ const bot = normalizeSdkPerson({
458
+ id: restId('PEOPLE', personUuid),
459
+ ref: '',
460
+ emails: ['bot@webex.bot'],
461
+ displayName: 'Bot',
462
+ orgId: restId('ORGANIZATION', 'org-uuid'),
463
+ orgRef: '',
464
+ type: 'bot',
465
+ created: '2024-01-01T00:00:00Z',
466
+ })
467
+
468
+ // when: an event carries the same person as a raw Mercury uuid
469
+ const event = normalizeMessage({
470
+ id: 'msg-uuid',
471
+ roomId: 'room-uuid',
472
+ personId: personUuid,
473
+ personEmail: 'bot@webex.bot',
474
+ text: 'hi',
475
+ created: '2024-01-01T00:00:00Z',
476
+ mentionedPeople: [personUuid],
477
+ mentionedGroups: [],
478
+ files: [],
479
+ raw: RAW,
480
+ })
481
+
482
+ // then: both paths decode to the same raw uuid ref
483
+ expect(bot.ref).toBe(personUuid)
484
+ expect(event.personRef).toBe(personUuid)
485
+ expect(event.mentionedPeopleRefs.includes(bot.ref)).toBe(true)
486
+ })
207
487
  })
@@ -7,12 +7,37 @@ import type {
7
7
  RoomActivity,
8
8
  } from 'webex-message-handler'
9
9
 
10
+ import type { WebexMembership, WebexMessage, WebexPerson } from './types'
11
+
10
12
  export { fromRestId }
11
13
 
12
14
  // Superset of webex-message-handler's toRestId union, which omits ATTACHMENT_ACTION
13
15
  // (the resource type behind GET /v1/attachment/actions).
14
16
  export type WebexRestIdType = 'MESSAGE' | 'PEOPLE' | 'ROOM' | 'ATTACHMENT_ACTION'
15
17
 
18
+ export interface DecodedWebexId {
19
+ cluster: string
20
+ type: string
21
+ uuid: string
22
+ }
23
+
24
+ export function toRef(id: string): string {
25
+ // fromRestId throws on values that are not base64-encoded ciscospark ids; fail
26
+ // open so a non-REST id (legacy/sentinel) yields the id itself instead of crashing.
27
+ if (!id || !decodeWebexId(id)) return id
28
+ return fromRestId(id)
29
+ }
30
+
31
+ // Webex REST ids are base64(url) of `ciscospark://<cluster>/<TYPE>/<uuid>`; room
32
+ // cluster correction needs all three parts, not just the trailing ref value.
33
+ export function decodeWebexId(restId: string): DecodedWebexId | null {
34
+ if (!restId) return null
35
+ const decoded = Buffer.from(restId, 'base64').toString('utf-8')
36
+ const match = decoded.match(/^ciscospark:\/\/([^/]+)\/([^/]+)\/(.+)$/)
37
+ if (!match) return null
38
+ return { cluster: match[1], type: match[2], uuid: match[3] }
39
+ }
40
+
16
41
  /**
17
42
  * Encode a raw Mercury UUID as a Webex REST ID. Empty input is returned unchanged
18
43
  * so an absent ID never becomes a bogus `ciscospark://us/{TYPE}/` value.
@@ -27,50 +52,117 @@ export function toRestId(uuid: string, type: WebexRestIdType): string {
27
52
  }
28
53
 
29
54
  export function normalizeMessage(message: DecryptedMessage): DecryptedMessage {
55
+ const id = toRestId(message.id, 'MESSAGE')
56
+ const parentId = message.parentId ? toRestId(message.parentId, 'MESSAGE') : message.parentId
57
+ const roomId = toRestId(message.roomId, 'ROOM')
58
+ const personId = toRestId(message.personId, 'PEOPLE')
59
+ const mentionedPeople = message.mentionedPeople.map((person) => toRestId(person, 'PEOPLE'))
30
60
  return {
31
61
  ...message,
32
- id: toRestId(message.id, 'MESSAGE'),
33
- parentId: message.parentId ? toRestId(message.parentId, 'MESSAGE') : message.parentId,
34
- roomId: toRestId(message.roomId, 'ROOM'),
35
- personId: toRestId(message.personId, 'PEOPLE'),
36
- mentionedPeople: message.mentionedPeople.map((id) => toRestId(id, 'PEOPLE')),
62
+ id,
63
+ ref: toRef(id),
64
+ parentId,
65
+ parentRef: parentId ? toRef(parentId) : parentId,
66
+ roomId,
67
+ roomRef: toRef(roomId),
68
+ personId,
69
+ personRef: toRef(personId),
70
+ mentionedPeople,
71
+ mentionedPeopleRefs: mentionedPeople.map(toRef),
37
72
  }
38
73
  }
39
74
 
40
75
  export function normalizeDeletedMessage(message: DeletedMessage): DeletedMessage {
76
+ const messageId = toRestId(message.messageId, 'MESSAGE')
77
+ const roomId = toRestId(message.roomId, 'ROOM')
78
+ const personId = toRestId(message.personId, 'PEOPLE')
41
79
  return {
42
- messageId: toRestId(message.messageId, 'MESSAGE'),
43
- roomId: toRestId(message.roomId, 'ROOM'),
44
- personId: toRestId(message.personId, 'PEOPLE'),
80
+ messageId,
81
+ messageRef: toRef(messageId),
82
+ roomId,
83
+ roomRef: toRef(roomId),
84
+ personId,
85
+ personRef: toRef(personId),
45
86
  }
46
87
  }
47
88
 
48
89
  export function normalizeMembership(activity: MembershipActivity): MembershipActivity {
49
- // `id` stays raw: it is a Mercury activity UUID, not a REST membership ID.
90
+ // `id` stays raw: it is a Mercury activity UUID, not a REST membership ID, so
91
+ // its ref is the id itself rather than a decoded REST id.
92
+ const actorId = toRestId(activity.actorId, 'PEOPLE')
93
+ const personId = toRestId(activity.personId, 'PEOPLE')
94
+ const roomId = toRestId(activity.roomId, 'ROOM')
50
95
  return {
51
96
  ...activity,
52
- actorId: toRestId(activity.actorId, 'PEOPLE'),
53
- personId: toRestId(activity.personId, 'PEOPLE'),
54
- roomId: toRestId(activity.roomId, 'ROOM'),
97
+ ref: activity.id,
98
+ actorId,
99
+ actorRef: toRef(actorId),
100
+ personId,
101
+ personRef: toRef(personId),
102
+ roomId,
103
+ roomRef: toRef(roomId),
55
104
  }
56
105
  }
57
106
 
58
107
  export function normalizeAttachmentAction(action: AttachmentAction): AttachmentAction {
108
+ const id = toRestId(action.id, 'ATTACHMENT_ACTION')
109
+ const messageId = action.messageId ? toRestId(action.messageId, 'MESSAGE') : action.messageId
110
+ const personId = toRestId(action.personId, 'PEOPLE')
111
+ const roomId = toRestId(action.roomId, 'ROOM')
59
112
  return {
60
113
  ...action,
61
- id: toRestId(action.id, 'ATTACHMENT_ACTION'),
62
- messageId: action.messageId ? toRestId(action.messageId, 'MESSAGE') : action.messageId,
63
- personId: toRestId(action.personId, 'PEOPLE'),
64
- roomId: toRestId(action.roomId, 'ROOM'),
114
+ id,
115
+ ref: toRef(id),
116
+ messageId,
117
+ messageRef: toRef(messageId),
118
+ personId,
119
+ personRef: toRef(personId),
120
+ roomId,
121
+ roomRef: toRef(roomId),
65
122
  }
66
123
  }
67
124
 
68
125
  export function normalizeRoomActivity(activity: RoomActivity): RoomActivity {
69
- // `id` stays raw: the Mercury conversation activity UUID has no
70
- // consumer-facing REST resource (the comparable REST ID is `roomId`).
126
+ // `id` stays raw: the Mercury conversation activity UUID has no consumer-facing
127
+ // REST resource, so its ref is the id itself (the comparable REST id is `roomId`).
128
+ const roomId = toRestId(activity.roomId, 'ROOM')
129
+ const actorId = toRestId(activity.actorId, 'PEOPLE')
71
130
  return {
72
131
  ...activity,
73
- roomId: toRestId(activity.roomId, 'ROOM'),
74
- actorId: toRestId(activity.actorId, 'PEOPLE'),
132
+ ref: activity.id,
133
+ roomId,
134
+ roomRef: toRef(roomId),
135
+ actorId,
136
+ actorRef: toRef(actorId),
137
+ }
138
+ }
139
+
140
+ // SDK REST responses (people/messages/memberships) already carry REST-encoded ids,
141
+ // so unlike the event normalizers above we only attach raw uuid refs — no re-encoding.
142
+ export function normalizeSdkPerson(person: WebexPerson): WebexPerson {
143
+ return {
144
+ ...person,
145
+ ref: toRef(person.id),
146
+ orgRef: toRef(person.orgId),
147
+ }
148
+ }
149
+
150
+ export function normalizeSdkMessage(message: WebexMessage): WebexMessage {
151
+ return {
152
+ ...message,
153
+ ref: toRef(message.id),
154
+ roomRef: toRef(message.roomId),
155
+ personRef: toRef(message.personId),
156
+ parentRef: message.parentId ? toRef(message.parentId) : message.parentId,
157
+ mentionedPeopleRefs: message.mentionedPeople?.map(toRef),
158
+ }
159
+ }
160
+
161
+ export function normalizeSdkMembership(membership: WebexMembership): WebexMembership {
162
+ return {
163
+ ...membership,
164
+ ref: toRef(membership.id),
165
+ roomRef: toRef(membership.roomId),
166
+ personRef: toRef(membership.personId),
75
167
  }
76
168
  }
@@ -1,7 +1,7 @@
1
1
  export { WebexClient } from './client'
2
2
  export { WebexCredentialManager } from './credential-manager'
3
- export { fromRestId, toRestId } from './id-normalizer'
4
- export type { WebexRestIdType } from './id-normalizer'
3
+ export { decodeWebexId, fromRestId, toRef, toRestId } from './id-normalizer'
4
+ export type { DecodedWebexId, WebexRestIdType } from './id-normalizer'
5
5
  export { WebexListener } from './listener'
6
6
  export type { WebexListenerClient, WebexListenerEventMap, WebexListenerOptions } from './listener'
7
7
  export { loginWithPassword } from './password-login'
@@ -81,8 +81,11 @@ describe('WebexListener', () => {
81
81
 
82
82
  const expected = expect.objectContaining({
83
83
  id: toRestId('message-123', 'MESSAGE'),
84
+ ref: 'message-123',
84
85
  roomId: toRestId('room-123', 'ROOM'),
86
+ roomRef: 'room-123',
85
87
  personId: toRestId('person-123', 'PEOPLE'),
88
+ personRef: 'person-123',
86
89
  personEmail: 'user@example.com',
87
90
  text: 'hello',
88
91
  raw: RAW_ACTIVITY,
@@ -73,10 +73,13 @@ it('WebexSpaceSchema rejects invalid type', () => {
73
73
  it('WebexMessageSchema validates valid message', () => {
74
74
  const result = WebexMessageSchema.safeParse({
75
75
  id: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvbXNn',
76
+ ref: 'msg',
76
77
  roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
78
+ roomRef: 'abc',
77
79
  roomType: 'group',
78
80
  text: 'Hello world',
79
81
  personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
82
+ personRef: 'abc',
80
83
  personEmail: 'user@example.com',
81
84
  created: '2024-01-15T10:30:00.000Z',
82
85
  })
@@ -86,17 +89,22 @@ it('WebexMessageSchema validates valid message', () => {
86
89
  it('WebexMessageSchema validates message with optional fields', () => {
87
90
  const result = WebexMessageSchema.safeParse({
88
91
  id: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvbXNn',
92
+ ref: 'msg',
89
93
  roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
94
+ roomRef: 'abc',
90
95
  roomType: 'group',
91
96
  text: 'Hello world',
92
97
  markdown: '**Hello world**',
93
98
  html: '<strong>Hello world</strong>',
94
99
  files: ['https://webexapis.com/v1/contents/file1'],
95
100
  personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
101
+ personRef: 'abc',
96
102
  personEmail: 'user@example.com',
97
103
  created: '2024-01-15T10:30:00.000Z',
98
104
  parentId: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvcGFyZW50',
105
+ parentRef: 'parent',
99
106
  mentionedPeople: ['Y2lzY29zcGFyazovL3VzL1BFT1BMRS9tZW50aW9u'],
107
+ mentionedPeopleRefs: ['mention'],
100
108
  })
101
109
  expect(result.success).toBe(true)
102
110
  })
@@ -125,9 +133,11 @@ it('WebexMessageSchema rejects invalid roomType', () => {
125
133
  it('WebexPersonSchema validates valid person', () => {
126
134
  const result = WebexPersonSchema.safeParse({
127
135
  id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
136
+ ref: 'abc',
128
137
  emails: ['user@example.com'],
129
138
  displayName: 'Test User',
130
139
  orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
140
+ orgRef: 'org',
131
141
  type: 'person',
132
142
  created: '2024-01-01T00:00:00.000Z',
133
143
  })
@@ -137,6 +147,7 @@ it('WebexPersonSchema validates valid person', () => {
137
147
  it('WebexPersonSchema validates person with optional fields', () => {
138
148
  const result = WebexPersonSchema.safeParse({
139
149
  id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
150
+ ref: 'abc',
140
151
  emails: ['user@example.com', 'user@work.com'],
141
152
  displayName: 'Test User',
142
153
  nickName: 'Tester',
@@ -144,6 +155,7 @@ it('WebexPersonSchema validates person with optional fields', () => {
144
155
  lastName: 'User',
145
156
  avatar: 'https://example.com/avatar.jpg',
146
157
  orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
158
+ orgRef: 'org',
147
159
  type: 'person',
148
160
  created: '2024-01-01T00:00:00.000Z',
149
161
  })
@@ -153,9 +165,11 @@ it('WebexPersonSchema validates person with optional fields', () => {
153
165
  it('WebexPersonSchema validates bot type', () => {
154
166
  const result = WebexPersonSchema.safeParse({
155
167
  id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9ib3Q',
168
+ ref: 'bot',
156
169
  emails: ['bot@webex.bot'],
157
170
  displayName: 'My Bot',
158
171
  orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
172
+ orgRef: 'org',
159
173
  type: 'bot',
160
174
  created: '2024-01-01T00:00:00.000Z',
161
175
  })
@@ -185,8 +199,11 @@ it('WebexPersonSchema rejects invalid type', () => {
185
199
  it('WebexMembershipSchema validates valid membership', () => {
186
200
  const result = WebexMembershipSchema.safeParse({
187
201
  id: 'Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvbWVt',
202
+ ref: 'mem',
188
203
  roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
204
+ roomRef: 'abc',
189
205
  personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
206
+ personRef: 'abc',
190
207
  personEmail: 'user@example.com',
191
208
  personDisplayName: 'Test User',
192
209
  isModerator: false,
@@ -198,8 +215,11 @@ it('WebexMembershipSchema validates valid membership', () => {
198
215
  it('WebexMembershipSchema validates moderator membership', () => {
199
216
  const result = WebexMembershipSchema.safeParse({
200
217
  id: 'Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvbWVt',
218
+ ref: 'mem',
201
219
  roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
220
+ roomRef: 'abc',
202
221
  personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
222
+ personRef: 'abc',
203
223
  personEmail: 'moderator@example.com',
204
224
  personDisplayName: 'Moderator User',
205
225
  isModerator: true,