agent-messenger 2.2.0 → 2.3.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.
Files changed (55) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/bun.lock +25 -1
  4. package/dist/package.json +4 -2
  5. package/dist/src/platforms/line/client.d.ts.map +1 -1
  6. package/dist/src/platforms/line/client.js +36 -9
  7. package/dist/src/platforms/line/client.js.map +1 -1
  8. package/dist/src/platforms/webex/client.d.ts +2 -0
  9. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  10. package/dist/src/platforms/webex/client.js +66 -23
  11. package/dist/src/platforms/webex/client.js.map +1 -1
  12. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  13. package/dist/src/platforms/webex/commands/auth.js +4 -0
  14. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  15. package/dist/src/platforms/webex/encryption.d.ts +10 -0
  16. package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
  17. package/dist/src/platforms/webex/encryption.js +49 -0
  18. package/dist/src/platforms/webex/encryption.js.map +1 -0
  19. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  20. package/dist/src/platforms/webex/ensure-auth.js +4 -0
  21. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  22. package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
  23. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  24. package/dist/src/platforms/webex/token-extractor.js +92 -43
  25. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  26. package/dist/src/platforms/webex/types.d.ts +4 -0
  27. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  28. package/dist/src/platforms/webex/types.js +2 -0
  29. package/dist/src/platforms/webex/types.js.map +1 -1
  30. package/docs/content/docs/cli/webex.mdx +4 -2
  31. package/package.json +4 -2
  32. package/skills/agent-channeltalk/SKILL.md +1 -1
  33. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  34. package/skills/agent-discord/SKILL.md +1 -1
  35. package/skills/agent-discordbot/SKILL.md +1 -1
  36. package/skills/agent-instagram/SKILL.md +1 -1
  37. package/skills/agent-kakaotalk/SKILL.md +1 -1
  38. package/skills/agent-line/SKILL.md +1 -1
  39. package/skills/agent-slack/SKILL.md +1 -1
  40. package/skills/agent-slackbot/SKILL.md +1 -1
  41. package/skills/agent-teams/SKILL.md +1 -1
  42. package/skills/agent-telegram/SKILL.md +1 -1
  43. package/skills/agent-webex/SKILL.md +1 -1
  44. package/skills/agent-webex/references/authentication.md +4 -3
  45. package/skills/agent-webex/references/common-patterns.md +1 -1
  46. package/skills/agent-whatsapp/SKILL.md +1 -1
  47. package/skills/agent-whatsappbot/SKILL.md +1 -1
  48. package/src/platforms/line/client.ts +39 -14
  49. package/src/platforms/webex/client.ts +98 -26
  50. package/src/platforms/webex/commands/auth.ts +4 -0
  51. package/src/platforms/webex/encryption.ts +53 -0
  52. package/src/platforms/webex/ensure-auth.ts +4 -0
  53. package/src/platforms/webex/token-extractor.ts +107 -40
  54. package/src/platforms/webex/types.ts +4 -0
  55. package/src/platforms/webex/typings/node-jose.d.ts +27 -0
@@ -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.2.0
4
+ version: 2.3.0
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.2.0
4
+ version: 2.3.0
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.2.0
4
+ version: 2.3.0
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -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.2.0
4
+ version: 2.3.0
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -4,7 +4,7 @@
4
4
 
5
5
  agent-webex supports four authentication methods against the Webex REST API (`https://webexapis.com/v1`):
6
6
 
7
- 1. **Browser Token Extraction**: Extracts your first-party token from a Chromium browser where you're logged into web.webex.com. Currently supports read operations (spaces, members, auth). Zero-config.
7
+ 1. **Browser Token Extraction**: Extracts your first-party token and cached encryption keys from a Chromium browser where you're logged into web.webex.com. Supports all operations including encrypted messaging via the internal API. Zero-config.
8
8
  2. **OAuth Device Grant** (recommended for messaging): Zero-config. Run `auth login`, approve in browser, done. Tokens refresh automatically. Supports all operations including sending messages (shows "via agent-messenger").
9
9
  3. **Bot Token**: Pass via `auth login --token`. Never expires. Best for CI/CD.
10
10
  4. **Personal Access Token (PAT)**: Pass via `auth login --token`. Expires in 12 hours. For quick testing.
@@ -13,12 +13,13 @@ agent-webex supports four authentication methods against the Webex REST API (`ht
13
13
 
14
14
  ### Browser Token Extraction
15
15
 
16
- Extracts your first-party Webex session token from a Chromium-based browser where you're logged into web.webex.com. Currently supports read operations (authentication, listing spaces/members, snapshots). Sending messages via the REST API is not yet supported because the web client's token lacks `spark:messages_write` scope the web client uses internal Cisco APIs for messaging instead.
16
+ Extracts your first-party Webex session token from a Chromium-based browser where you're logged into web.webex.com. Supports full messaging with end-to-end encryption. The extracted token uses Webex's internal conversation API for sending messages. Encryption keys are also extracted from the browser's cached KMS key store, enabling client-side JWE encryption so messages appear as encrypted in the Webex client.
17
17
 
18
- - **How it works**: Run `agent-webex auth extract`. The CLI scans Chromium browser profiles for Webex localStorage data (LevelDB files). It finds the `webex-storage` key containing `Credentials.@.supertoken` and extracts the access token. No browser automation, no password prompts.
18
+ - **How it works**: Run `agent-webex auth extract`. The CLI scans Chromium browser profiles for Webex localStorage data (LevelDB files). It finds the `webex-storage` key containing `Credentials.@.supertoken` and extracts the access token. It also extracts `userId` from the Device namespace and cached KMS encryption keys from the unbounded storage — these keys enable end-to-end encrypted messaging via the internal API. No browser automation, no password prompts.
19
19
  - **Supported browsers**: Chrome, Chrome Canary, Edge, Arc, Brave, Vivaldi, Chromium
20
20
  - **Token lifetime**: Depends on Webex session policy (typically hours to days). Re-extract when expired.
21
21
  - **Auto-extraction**: The CLI attempts browser extraction automatically when no valid token is stored, so you often don't need to run `auth extract` manually.
22
+ - **End-to-end encryption**: When encryption keys are found in the browser's cache, messages are encrypted client-side (JWE with AES-256-GCM) before sending via the internal conversation API. This ensures messages appear as encrypted in the Webex client. If no keys are found (e.g., the conversation hasn't been opened in the browser), messages fall back to plaintext.
22
23
  - **Best for**: Interactive use, sending messages as yourself without the "via" label
23
24
 
24
25
  ```bash
@@ -28,7 +28,7 @@ agent-webex auth login --token "YOUR_BOT_TOKEN_HERE"
28
28
  agent-webex auth login --token "YOUR_PAT_HERE"
29
29
  ```
30
30
 
31
- **When to use**: Before any other command, if not already authenticated. Browser extraction is preferred — it auto-runs when no valid token is stored.
31
+ **When to use**: Before any other command, if not already authenticated. Browser extraction is preferred — it auto-runs when no valid token is stored. It also extracts cached KMS encryption keys from the browser, enabling end-to-end encrypted messaging via the internal API.
32
32
 
33
33
  ### Pattern 2: Check Auth Status
34
34
 
@@ -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.2.0
4
+ version: 2.3.0
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.2.0
4
+ version: 2.3.0
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -2,13 +2,34 @@ import { mkdirSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
 
5
- import {
6
- loginWithQR as linejsLoginWithQR,
7
- loginWithPassword as linejsLoginWithPassword,
8
- loginWithAuthToken as linejsLoginWithAuthToken,
9
- type Client,
10
- } from '@evex/linejs'
11
- import { FileStorage } from '@evex/linejs/storage'
5
+ type LinejsModule = typeof import('@evex/linejs')
6
+ type LinejsStorageModule = typeof import('@evex/linejs/storage')
7
+ type Client = Awaited<ReturnType<LinejsModule['loginWithQR']>>
8
+
9
+ let cachedLinejs: LinejsModule | undefined
10
+ let cachedLinejsStorage: LinejsStorageModule | undefined
11
+
12
+ const INSTALL_HINT = '@evex/linejs is required for LINE support. Install it with: bun add @evex/linejs@npm:@jsr/evex__linejs'
13
+
14
+ async function getLinejs(): Promise<LinejsModule> {
15
+ if (cachedLinejs) return cachedLinejs
16
+ try {
17
+ cachedLinejs = await import('@evex/linejs')
18
+ return cachedLinejs
19
+ } catch {
20
+ throw new Error(INSTALL_HINT)
21
+ }
22
+ }
23
+
24
+ async function getLinejsStorage(): Promise<LinejsStorageModule> {
25
+ if (cachedLinejsStorage) return cachedLinejsStorage
26
+ try {
27
+ cachedLinejsStorage = await import('@evex/linejs/storage')
28
+ return cachedLinejsStorage
29
+ } catch {
30
+ throw new Error(INSTALL_HINT)
31
+ }
32
+ }
12
33
 
13
34
  import { LineCredentialManager } from './credential-manager'
14
35
  import type {
@@ -40,7 +61,8 @@ function getDefaultDevice(): LineDevice {
40
61
  return 'ANDROIDSECONDARY'
41
62
  }
42
63
 
43
- function createStorage(accountId?: string): FileStorage {
64
+ async function createStorage(accountId?: string) {
65
+ const { FileStorage } = await getLinejsStorage()
44
66
  const dir = join(homedir(), '.config', 'agent-messenger', 'line-storage')
45
67
  mkdirSync(dir, { recursive: true })
46
68
  return new FileStorage(join(dir, `${accountId ?? 'default'}.json`))
@@ -60,10 +82,11 @@ export class LineClient {
60
82
  onPincode: (pin: string) => void
61
83
  }): Promise<LineLoginResult> {
62
84
  try {
85
+ const linejs = await getLinejs()
63
86
  const device: LineDevice = options.device ?? getDefaultDevice()
64
- const storage = createStorage()
87
+ const storage = await createStorage()
65
88
 
66
- const client = await linejsLoginWithQR(
89
+ const client = await linejs.loginWithQR(
67
90
  {
68
91
  onReceiveQRUrl: (url) => options.onQRUrl(url),
69
92
  onPincodeRequest: (pin) => options.onPincode(pin),
@@ -103,10 +126,11 @@ export class LineClient {
103
126
  onPincode: (pin: string) => void
104
127
  }): Promise<LineLoginResult> {
105
128
  try {
129
+ const linejs = await getLinejs()
106
130
  const device: LineDevice = options.device ?? getDefaultDevice()
107
- const storage = createStorage()
131
+ const storage = await createStorage()
108
132
 
109
- const client = await linejsLoginWithPassword(
133
+ const client = await linejs.loginWithPassword(
110
134
  {
111
135
  email: options.email,
112
136
  password: options.password,
@@ -154,10 +178,11 @@ export class LineClient {
154
178
  creds = account
155
179
  }
156
180
 
181
+ const linejs = await getLinejs()
157
182
  const device: LineDevice = creds.device ?? getDefaultDevice()
158
- const storage = createStorage()
183
+ const storage = await createStorage()
159
184
 
160
- this.client = await linejsLoginWithAuthToken(creds.auth_token, { device, storage })
185
+ this.client = await linejs.loginWithAuthToken(creds.auth_token, { device, storage })
161
186
  return this
162
187
  } catch (error) {
163
188
  throw wrapError(error, 'login_failed')
@@ -1,6 +1,7 @@
1
1
  import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
2
2
  import { WebexError } from './types'
3
3
  import { WebexCredentialManager } from './credential-manager'
4
+ import { WebexEncryptionService } from './encryption'
4
5
 
5
6
  const BASE_URL = 'https://webexapis.com/v1'
6
7
  const MAX_RETRIES = 3
@@ -17,6 +18,7 @@ export class WebexClient {
17
18
  private tokenType: string | null = null
18
19
  private buckets: Map<string, RateLimitBucket> = new Map()
19
20
  private globalRateLimitUntil: number = 0
21
+ private encryption: WebexEncryptionService | null = null
20
22
 
21
23
  async login(credentials?: { token: string }): Promise<this> {
22
24
  if (credentials) {
@@ -40,7 +42,16 @@ export class WebexClient {
40
42
  }
41
43
  this.deviceUrl = config?.deviceUrl ?? null
42
44
  this.tokenType = config?.tokenType ?? null
43
- return this.login({ token })
45
+ await this.login({ token })
46
+
47
+ if (this.tokenType === 'extracted' && config?.encryptionKeys) {
48
+ const keysMap = new Map(Object.entries(config.encryptionKeys))
49
+ if (keysMap.size > 0) {
50
+ this.encryption = new WebexEncryptionService(keysMap)
51
+ }
52
+ }
53
+
54
+ return this
44
55
  }
45
56
 
46
57
  private ensureAuth(): string {
@@ -210,7 +221,7 @@ export class WebexClient {
210
221
  private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
211
222
  const response = await fetch(`${this.convBaseUrl}${path}`, {
212
223
  ...init,
213
- headers: { ...this.internalHeaders, ...init?.headers as Record<string, string> },
224
+ headers: { ...this.internalHeaders, ...(init?.headers as Record<string, string>) },
214
225
  })
215
226
 
216
227
  if (!response.ok) {
@@ -225,35 +236,78 @@ export class WebexClient {
225
236
  return response.json() as Promise<T>
226
237
  }
227
238
 
228
- private activityToMessage(a: InternalActivity, roomId: string): WebexMessage {
239
+ private async activityToMessage(a: InternalActivity, roomId: string): Promise<WebexMessage> {
240
+ let text = a.object?.content ?? a.object?.displayName
241
+
242
+ if (this.encryption && text?.startsWith('eyJ')) {
243
+ const keyUrl = a.encryptionKeyUrl ?? a.object?.encryptionKeyUrl
244
+ if (keyUrl) {
245
+ const decrypted = await this.encryption.decryptText(keyUrl, text)
246
+ if (decrypted !== null) {
247
+ text = decrypted
248
+ }
249
+ }
250
+ }
251
+
229
252
  return {
230
253
  id: a.id,
231
254
  roomId,
232
255
  roomType: 'group' as const,
233
- text: a.object?.content ?? a.object?.displayName,
256
+ text,
234
257
  personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
235
258
  personEmail: a.actor?.emailAddress ?? '',
236
259
  created: a.published,
237
260
  }
238
261
  }
239
262
 
263
+ private async buildEncryptedObject(
264
+ convUuid: string,
265
+ text: string,
266
+ options?: { markdown?: boolean },
267
+ ): Promise<{ object: Record<string, string>; encryptionKeyUrl?: string }> {
268
+ const buildObject = (content: string): Record<string, string> =>
269
+ options?.markdown
270
+ ? { objectType: 'comment', displayName: content, content, markdown: content }
271
+ : { objectType: 'comment', displayName: content, content }
272
+
273
+ if (this.encryption) {
274
+ const conv = await this.internalRequest<InternalConversation>(
275
+ `/conversations/${convUuid}?activitiesLimit=0&participantsLimit=0`,
276
+ )
277
+ const keyUri = conv.defaultActivityEncryptionKeyUrl
278
+ if (keyUri) {
279
+ const encrypted = await this.encryption.encryptText(keyUri, text)
280
+ if (encrypted) {
281
+ return { object: buildObject(encrypted), encryptionKeyUrl: keyUri }
282
+ }
283
+ }
284
+ }
285
+
286
+ return { object: buildObject(text) }
287
+ }
288
+
240
289
  private async sendMessageInternal(
241
290
  roomId: string,
242
291
  text: string,
243
292
  options?: { markdown?: boolean },
244
293
  ): Promise<WebexMessage> {
245
294
  const convUuid = this.decodeConvUuid(roomId)
246
- const object = options?.markdown
247
- ? { objectType: 'comment', displayName: text, content: text, markdown: text }
248
- : { objectType: 'comment', displayName: text, content: text }
295
+ const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, options)
296
+
297
+ const activity: Record<string, unknown> = {
298
+ verb: 'post',
299
+ object,
300
+ target: { id: convUuid, objectType: 'conversation' },
301
+ clientTempId: `tmp-${Date.now()}`,
302
+ }
303
+
304
+ if (encryptionKeyUrl) {
305
+ activity['encryptionKeyUrl'] = encryptionKeyUrl
306
+ }
307
+
249
308
  const result = await this.internalRequest<InternalActivity>('/activities', {
250
309
  method: 'POST',
251
- body: JSON.stringify({
252
- verb: 'post',
253
- object,
254
- target: { id: convUuid, objectType: 'conversation' },
255
- clientTempId: `tmp-${Date.now()}`,
256
- }),
310
+ body: JSON.stringify(activity),
257
311
  })
258
312
  return this.activityToMessage(result, roomId)
259
313
  }
@@ -297,9 +351,8 @@ export class WebexClient {
297
351
  const conv = await this.internalRequest<InternalConversation>(
298
352
  `/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
299
353
  )
300
- return (conv.activities?.items ?? [])
301
- .filter((a) => a.verb === 'post')
302
- .map((a) => this.activityToMessage(a, roomId))
354
+ const activities = (conv.activities?.items ?? []).filter((a) => a.verb === 'post')
355
+ return Promise.all(activities.map((a) => this.activityToMessage(a, roomId)))
303
356
  }
304
357
  const params = new URLSearchParams()
305
358
  params.set('roomId', roomId)
@@ -312,7 +365,9 @@ export class WebexClient {
312
365
  if (this.useInternalAPI) {
313
366
  const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
314
367
  const convId = activity.target?.id ?? ''
315
- const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
368
+ const roomId = convId
369
+ ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64')
370
+ : ''
316
371
  return this.activityToMessage(activity, roomId)
317
372
  }
318
373
  return this.request<WebexMessage>('GET', `/messages/${messageId}`)
@@ -344,15 +399,23 @@ export class WebexClient {
344
399
  ): Promise<WebexMessage> {
345
400
  if (this.useInternalAPI) {
346
401
  const convUuid = this.decodeConvUuid(roomId)
402
+ const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, options)
403
+
404
+ const activity: Record<string, unknown> = {
405
+ verb: 'post',
406
+ object,
407
+ target: { id: convUuid, objectType: 'conversation' },
408
+ parent: { id: messageId, type: 'edit' },
409
+ clientTempId: `tmp-${Date.now()}`,
410
+ }
411
+
412
+ if (encryptionKeyUrl) {
413
+ activity['encryptionKeyUrl'] = encryptionKeyUrl
414
+ }
415
+
347
416
  const result = await this.internalRequest<InternalActivity>('/activities', {
348
417
  method: 'POST',
349
- body: JSON.stringify({
350
- verb: 'post',
351
- object: { objectType: 'comment', displayName: text, content: text },
352
- target: { id: convUuid, objectType: 'conversation' },
353
- parent: { id: messageId, type: 'edit' },
354
- clientTempId: `tmp-${Date.now()}`,
355
- }),
418
+ body: JSON.stringify(activity),
356
419
  })
357
420
  return this.activityToMessage(result, roomId)
358
421
  }
@@ -394,12 +457,21 @@ interface InternalActivity {
394
457
  id: string
395
458
  verb: string
396
459
  actor?: { displayName?: string; emailAddress?: string; entryUUID?: string; id?: string }
397
- object?: { content?: string; displayName?: string; objectType?: string }
398
- target?: { id: string }
460
+ object?: {
461
+ content?: string
462
+ displayName?: string
463
+ objectType?: string
464
+ encryptionKeyUrl?: string
465
+ }
466
+ target?: { id: string; encryptionKeyUrl?: string }
399
467
  published: string
468
+ encryptionKeyUrl?: string
400
469
  }
401
470
 
402
471
  interface InternalConversation {
403
472
  id: string
404
473
  activities?: { items: InternalActivity[] }
474
+ defaultActivityEncryptionKeyUrl?: string
475
+ kmsResourceObjectUrl?: string
476
+ encryptionKeyUrl?: string
405
477
  }
@@ -174,6 +174,10 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
174
174
  expiresAt: extracted.expiresAt ?? 0,
175
175
  tokenType: 'extracted',
176
176
  deviceUrl: extracted.deviceUrl,
177
+ userId: extracted.userId,
178
+ encryptionKeys: extracted.encryptionKeys
179
+ ? Object.fromEntries(extracted.encryptionKeys)
180
+ : undefined,
177
181
  })
178
182
 
179
183
  console.log(
@@ -0,0 +1,53 @@
1
+ import * as jose from 'node-jose'
2
+
3
+ export class WebexEncryptionService {
4
+ private rawKeys: Map<string, string>
5
+ private keyCache: Map<string, jose.JWK.Key> = new Map()
6
+
7
+ constructor(serializedKeys: Map<string, string>) {
8
+ this.rawKeys = serializedKeys
9
+ }
10
+
11
+ async getKey(keyUri: string): Promise<jose.JWK.Key | null> {
12
+ const cached = this.keyCache.get(keyUri)
13
+ if (cached) return cached
14
+
15
+ const raw = this.rawKeys.get(keyUri)
16
+ if (!raw) return null
17
+
18
+ try {
19
+ const parsed = JSON.parse(raw) as { jwk: object }
20
+ const joseKey = await jose.JWK.asKey(parsed.jwk)
21
+ this.keyCache.set(keyUri, joseKey)
22
+ return joseKey
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ async encryptText(keyUri: string, plaintext: string): Promise<string | null> {
29
+ const key = await this.getKey(keyUri)
30
+ if (!key) return null
31
+
32
+ try {
33
+ return await jose.JWE.createEncrypt(
34
+ { format: 'compact', contentAlg: 'A256GCM' },
35
+ { key, header: { alg: 'dir' }, reference: null },
36
+ ).final(plaintext, 'utf8')
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ async decryptText(keyUri: string, ciphertext: string): Promise<string | null> {
43
+ const key = await this.getKey(keyUri)
44
+ if (!key) return null
45
+
46
+ try {
47
+ const result = await jose.JWE.createDecrypt(key).decrypt(ciphertext)
48
+ return result.plaintext.toString('utf8')
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+ }
@@ -31,6 +31,10 @@ export async function ensureWebexAuth(): Promise<void> {
31
31
  expiresAt: extracted.expiresAt ?? 0,
32
32
  tokenType: 'extracted',
33
33
  deviceUrl: extracted.deviceUrl,
34
+ userId: extracted.userId,
35
+ encryptionKeys: extracted.encryptionKeys
36
+ ? Object.fromEntries(extracted.encryptionKeys)
37
+ : undefined,
34
38
  })
35
39
  } catch {
36
40
  // Intentionally silent — best-effort preflight that should not block commands