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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bun.lock +25 -1
- package/dist/package.json +4 -2
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +36 -9
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +2 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +66 -23
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +4 -0
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +10 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
- package/dist/src/platforms/webex/encryption.js +49 -0
- package/dist/src/platforms/webex/encryption.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +4 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +92 -43
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +4 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +2 -0
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/docs/content/docs/cli/webex.mdx +4 -2
- package/package.json +4 -2
- 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-webex/SKILL.md +1 -1
- package/skills/agent-webex/references/authentication.md +4 -3
- package/skills/agent-webex/references/common-patterns.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/line/client.ts +39 -14
- package/src/platforms/webex/client.ts +98 -26
- package/src/platforms/webex/commands/auth.ts +4 -0
- package/src/platforms/webex/encryption.ts +53 -0
- package/src/platforms/webex/ensure-auth.ts +4 -0
- package/src/platforms/webex/token-extractor.ts +107 -40
- package/src/platforms/webex/types.ts +4 -0
- package/src/platforms/webex/typings/node-jose.d.ts +27 -0
|
@@ -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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
|
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?: {
|
|
398
|
-
|
|
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
|