agent-messenger 2.20.2 → 2.20.4
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/bun.lock +41 -0
- package/dist/package.json +2 -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 +29 -5
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/message.js +32 -52
- package/dist/src/platforms/webex/commands/message.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +7 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
- package/dist/src/platforms/webex/encryption.js +12 -1
- package/dist/src/platforms/webex/encryption.js.map +1 -1
- package/dist/src/platforms/webex/kms-key-provider.d.ts +20 -0
- package/dist/src/platforms/webex/kms-key-provider.d.ts.map +1 -0
- package/dist/src/platforms/webex/kms-key-provider.js +78 -0
- package/dist/src/platforms/webex/kms-key-provider.js.map +1 -0
- package/docs/content/docs/cli/webex.mdx +1 -1
- package/package.json +2 -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 +1 -1
- package/skills/agent-webex/references/authentication.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/webex/client.ts +35 -6
- package/src/platforms/webex/commands/message.test.ts +16 -0
- package/src/platforms/webex/commands/message.ts +38 -57
- package/src/platforms/webex/encryption.test.ts +58 -3
- package/src/platforms/webex/encryption.ts +19 -1
- package/src/platforms/webex/kms-key-provider.ts +99 -0
- package/src/platforms/webex/typings/webex-message-handler.d.ts +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kms-key-provider.js","sourceRoot":"","sources":["../../../../src/platforms/webex/kms-key-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -40,7 +40,7 @@ This command:
|
|
|
40
40
|
- Supported browsers: Chrome, Chrome Canary, Edge, Arc, Brave, Vivaldi, Chromium
|
|
41
41
|
- Supports `--browser-profile <path>` for agent-browser profiles, custom Chrome user data dirs, or portable browser profiles; repeatable and comma-separated values are accepted
|
|
42
42
|
- Auto-extraction runs when no valid token is stored, so manual extraction is rarely needed
|
|
43
|
-
- Messages are encrypted client-side (JWE/AES-256-GCM)
|
|
43
|
+
- Messages are encrypted client-side (JWE/AES-256-GCM). Encryption keys are cached at extract time; for end-to-end encrypted conversations whose key is not cached, the key is fetched on demand (Mercury + KMS) and persisted for reuse. Non-encrypted conversations send as plaintext.
|
|
44
44
|
|
|
45
45
|
### OAuth Device Grant (Fallback)
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-messenger",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.4",
|
|
4
4
|
"description": "Multi-platform messaging CLI for AI agents (Slack, Discord, Teams, Webex, Telegram, Telegram Bot, WhatsApp, LINE, Instagram, KakaoTalk, Channel Talk)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
"qrcode": "^1.5.4",
|
|
178
178
|
"thrift": "^0.20.0",
|
|
179
179
|
"tweetnacl": "^1.0.3",
|
|
180
|
+
"webex-message-handler": "^0.6.10",
|
|
180
181
|
"ws": "^8.19.0",
|
|
181
182
|
"zod": "^4.3.6"
|
|
182
183
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-channeltalk
|
|
3
3
|
description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
|
|
4
|
-
version: 2.20.
|
|
4
|
+
version: 2.20.4
|
|
5
5
|
allowed-tools: Bash(agent-channeltalk:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -19,7 +19,7 @@ Extracts your first-party Webex session token from a Chromium-based browser wher
|
|
|
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**:
|
|
22
|
+
- **End-to-end encryption**: Messages are encrypted client-side (JWE with AES-256-GCM) before sending via the internal conversation API, so they appear as encrypted in the Webex client. Keys cached from the browser are used directly; for an end-to-end encrypted conversation whose key was not cached at extract time, the key is fetched on demand over the Mercury websocket + KMS flow and persisted for reuse. Only non-encrypted conversations are sent as plaintext.
|
|
23
23
|
- **Best for**: Interactive use, sending messages as yourself without the "via" label
|
|
24
24
|
|
|
25
25
|
```bash
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { WebexCredentialManager } from './credential-manager'
|
|
2
2
|
import { WebexEncryptionService } from './encryption'
|
|
3
|
+
import { KmsKeyProvider } from './kms-key-provider'
|
|
3
4
|
import { escapeHtml, markdownToHtml, stripMarkdown } from './markdown-to-html'
|
|
4
|
-
import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
|
|
5
|
+
import type { WebexConfig, WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
|
|
5
6
|
import { WebexError } from './types'
|
|
6
7
|
|
|
7
8
|
const BASE_URL = 'https://webexapis.com/v1'
|
|
@@ -44,16 +45,40 @@ export class WebexClient {
|
|
|
44
45
|
this.tokenType = config?.tokenType ?? null
|
|
45
46
|
await this.login({ token })
|
|
46
47
|
|
|
47
|
-
if (this.tokenType === 'extracted'
|
|
48
|
-
const keysMap = new Map(Object.entries(config
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
if (this.tokenType === 'extracted') {
|
|
49
|
+
const keysMap = new Map(Object.entries(config?.encryptionKeys ?? {}))
|
|
50
|
+
this.encryption = new WebexEncryptionService(keysMap)
|
|
51
|
+
const kmsProvider = new KmsKeyProvider({ token })
|
|
52
|
+
this.encryption.setKeyProvider({
|
|
53
|
+
fetchKey: async (keyUri: string) => {
|
|
54
|
+
const serializedKey = await kmsProvider.fetchKey(keyUri)
|
|
55
|
+
if (serializedKey) {
|
|
56
|
+
await this.persistEncryptionKey(credManager, keyUri, serializedKey)
|
|
57
|
+
}
|
|
58
|
+
return serializedKey
|
|
59
|
+
},
|
|
60
|
+
close: () => kmsProvider.close(),
|
|
61
|
+
})
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
return this
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
async dispose(): Promise<void> {
|
|
68
|
+
await this.encryption?.close()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async persistEncryptionKey(
|
|
72
|
+
credManager: WebexCredentialManager,
|
|
73
|
+
keyUri: string,
|
|
74
|
+
serializedKey: string,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const latestConfig = await credManager.loadConfig()
|
|
77
|
+
if (!latestConfig) return
|
|
78
|
+
const encryptionKeys = { ...latestConfig.encryptionKeys, [keyUri]: serializedKey }
|
|
79
|
+
await credManager.saveConfig({ ...latestConfig, encryptionKeys } satisfies WebexConfig)
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
private ensureAuth(): string {
|
|
58
83
|
if (this.token === null) {
|
|
59
84
|
throw new WebexError('Not authenticated. Call .login() first.', 'not_authenticated')
|
|
@@ -287,6 +312,9 @@ export class WebexClient {
|
|
|
287
312
|
if (keyUri) {
|
|
288
313
|
const encryptedDisplayName = await this.encryption.encryptText(keyUri, displayName)
|
|
289
314
|
const encryptedContent = content ? await this.encryption.encryptText(keyUri, content) : undefined
|
|
315
|
+
if (content && !encryptedContent) {
|
|
316
|
+
throw new WebexError('Cannot encrypt message for Webex E2E conversation', 'encryption_failed')
|
|
317
|
+
}
|
|
290
318
|
if (encryptedDisplayName) {
|
|
291
319
|
const object: Record<string, string> = {
|
|
292
320
|
objectType: 'comment',
|
|
@@ -297,6 +325,7 @@ export class WebexClient {
|
|
|
297
325
|
}
|
|
298
326
|
return { object, encryptionKeyUrl: keyUri }
|
|
299
327
|
}
|
|
328
|
+
throw new WebexError('Cannot encrypt message for Webex E2E conversation', 'encryption_failed')
|
|
300
329
|
}
|
|
301
330
|
}
|
|
302
331
|
|
|
@@ -8,6 +8,7 @@ const mockMessage = {
|
|
|
8
8
|
roomId: 'space_456',
|
|
9
9
|
roomType: 'group' as const,
|
|
10
10
|
text: 'Hello world',
|
|
11
|
+
html: '<p>Hello <a href="https://example.com">world</a></p>',
|
|
11
12
|
personId: 'person_789',
|
|
12
13
|
personEmail: 'user@example.com',
|
|
13
14
|
created: '2025-01-29T10:00:00.000Z',
|
|
@@ -18,6 +19,7 @@ const mockMessage2 = {
|
|
|
18
19
|
roomId: 'space_456',
|
|
19
20
|
roomType: 'group' as const,
|
|
20
21
|
text: 'Second message',
|
|
22
|
+
html: '<p>Second message</p>',
|
|
21
23
|
personId: 'person_789',
|
|
22
24
|
personEmail: 'user@example.com',
|
|
23
25
|
created: '2025-01-29T10:01:00.000Z',
|
|
@@ -32,6 +34,7 @@ let mockGetMessage: ReturnType<typeof spyOn>
|
|
|
32
34
|
let mockDeleteMessage: ReturnType<typeof spyOn>
|
|
33
35
|
let mockEditMessage: ReturnType<typeof spyOn>
|
|
34
36
|
let mockLogin: ReturnType<typeof spyOn>
|
|
37
|
+
let mockDispose: ReturnType<typeof spyOn>
|
|
35
38
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
36
39
|
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
37
40
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
@@ -47,6 +50,7 @@ beforeEach(() => {
|
|
|
47
50
|
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
|
|
48
51
|
return this
|
|
49
52
|
})
|
|
53
|
+
mockDispose = protoSpy('dispose').mockResolvedValue(undefined)
|
|
50
54
|
mockSendMessage = protoSpy('sendMessage').mockResolvedValue(mockMessage)
|
|
51
55
|
mockSendDirectMessage = protoSpy('sendDirectMessage').mockResolvedValue(mockMessage)
|
|
52
56
|
mockListMessages = protoSpy('listMessages').mockResolvedValue([mockMessage, mockMessage2])
|
|
@@ -78,6 +82,7 @@ it('calls sendMessage with correct args and outputs result', async () => {
|
|
|
78
82
|
expect(output).toContain('msg_123')
|
|
79
83
|
expect(output).toContain('space_456')
|
|
80
84
|
expect(output).toContain('user@example.com')
|
|
85
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
81
86
|
})
|
|
82
87
|
|
|
83
88
|
it('passes markdown option when --markdown flag is set on send', async () => {
|
|
@@ -86,6 +91,14 @@ it('passes markdown option when --markdown flag is set on send', async () => {
|
|
|
86
91
|
expect(mockSendMessage).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
|
|
87
92
|
})
|
|
88
93
|
|
|
94
|
+
it('disposes the client when send fails', async () => {
|
|
95
|
+
mockSendMessage.mockRejectedValue(new WebexError('Send failed', 'send_failed'))
|
|
96
|
+
|
|
97
|
+
await sendAction('space_456', 'Hello world', { pretty: false })
|
|
98
|
+
|
|
99
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
100
|
+
})
|
|
101
|
+
|
|
89
102
|
it('exits with code 1 when not authenticated on send', async () => {
|
|
90
103
|
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
91
104
|
|
|
@@ -122,6 +135,7 @@ it('calls listMessages with limit and outputs array', async () => {
|
|
|
122
135
|
const output = consoleLogSpy.mock.calls[0][0]
|
|
123
136
|
expect(output).toContain('msg_123')
|
|
124
137
|
expect(output).toContain('msg_124')
|
|
138
|
+
expect(output).toContain('https://example.com')
|
|
125
139
|
})
|
|
126
140
|
|
|
127
141
|
it('calls getMessage with correct id and outputs result', async () => {
|
|
@@ -132,6 +146,7 @@ it('calls getMessage with correct id and outputs result', async () => {
|
|
|
132
146
|
const output = consoleLogSpy.mock.calls[0][0]
|
|
133
147
|
expect(output).toContain('msg_123')
|
|
134
148
|
expect(output).toContain('user@example.com')
|
|
149
|
+
expect(output).toContain('https://example.com')
|
|
135
150
|
})
|
|
136
151
|
|
|
137
152
|
it('calls deleteMessage and outputs deleted id when --force flag is set', async () => {
|
|
@@ -166,6 +181,7 @@ it('calls editMessage with roomId in args and outputs result', async () => {
|
|
|
166
181
|
const output = consoleLogSpy.mock.calls[0][0]
|
|
167
182
|
expect(output).toContain('msg_123')
|
|
168
183
|
expect(output).toContain('Updated message')
|
|
184
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
169
185
|
})
|
|
170
186
|
|
|
171
187
|
it('passes markdown option to editMessage when --markdown flag is set', async () => {
|
|
@@ -6,24 +6,36 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
6
6
|
import { WebexClient } from '../client'
|
|
7
7
|
import type { WebexMessage } from '../types'
|
|
8
8
|
|
|
9
|
+
function formatMessageOutput(message: WebexMessage) {
|
|
10
|
+
return {
|
|
11
|
+
id: message.id,
|
|
12
|
+
roomId: message.roomId,
|
|
13
|
+
text: message.text,
|
|
14
|
+
html: message.html,
|
|
15
|
+
personEmail: message.personEmail,
|
|
16
|
+
created: message.created,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function withWebexClient<T>(run: (client: WebexClient) => Promise<T>): Promise<T> {
|
|
21
|
+
const client = new WebexClient()
|
|
22
|
+
try {
|
|
23
|
+
await client.login()
|
|
24
|
+
return await run(client)
|
|
25
|
+
} finally {
|
|
26
|
+
await client.dispose()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
export async function sendAction(
|
|
10
31
|
spaceId: string,
|
|
11
32
|
text: string,
|
|
12
33
|
options: { markdown?: boolean; pretty?: boolean },
|
|
13
34
|
): Promise<void> {
|
|
14
35
|
try {
|
|
15
|
-
const
|
|
16
|
-
const message = await client.sendMessage(spaceId, text, { markdown: options.markdown })
|
|
17
|
-
|
|
18
|
-
const output = {
|
|
19
|
-
id: message.id,
|
|
20
|
-
roomId: message.roomId,
|
|
21
|
-
text: message.text,
|
|
22
|
-
personEmail: message.personEmail,
|
|
23
|
-
created: message.created,
|
|
24
|
-
}
|
|
36
|
+
const message = await withWebexClient((client) => client.sendMessage(spaceId, text, { markdown: options.markdown }))
|
|
25
37
|
|
|
26
|
-
console.log(formatOutput(
|
|
38
|
+
console.log(formatOutput(formatMessageOutput(message), options.pretty))
|
|
27
39
|
} catch (error) {
|
|
28
40
|
handleError(error as Error)
|
|
29
41
|
}
|
|
@@ -31,17 +43,10 @@ export async function sendAction(
|
|
|
31
43
|
|
|
32
44
|
export async function listAction(spaceId: string, options: { limit?: number; pretty?: boolean }): Promise<void> {
|
|
33
45
|
try {
|
|
34
|
-
const client = await new WebexClient().login()
|
|
35
46
|
const limit = options.limit ?? 50
|
|
36
|
-
const messages = await client.listMessages(spaceId, { max: limit })
|
|
47
|
+
const messages = await withWebexClient((client) => client.listMessages(spaceId, { max: limit }))
|
|
37
48
|
|
|
38
|
-
const output = messages.map(
|
|
39
|
-
id: msg.id,
|
|
40
|
-
roomId: msg.roomId,
|
|
41
|
-
text: msg.text,
|
|
42
|
-
personEmail: msg.personEmail,
|
|
43
|
-
created: msg.created,
|
|
44
|
-
}))
|
|
49
|
+
const output = messages.map(formatMessageOutput)
|
|
45
50
|
|
|
46
51
|
console.log(formatOutput(output, options.pretty))
|
|
47
52
|
} catch (error) {
|
|
@@ -51,18 +56,9 @@ export async function listAction(spaceId: string, options: { limit?: number; pre
|
|
|
51
56
|
|
|
52
57
|
export async function getAction(messageId: string, options: { pretty?: boolean }): Promise<void> {
|
|
53
58
|
try {
|
|
54
|
-
const
|
|
55
|
-
const message = await client.getMessage(messageId)
|
|
56
|
-
|
|
57
|
-
const output = {
|
|
58
|
-
id: message.id,
|
|
59
|
-
roomId: message.roomId,
|
|
60
|
-
text: message.text,
|
|
61
|
-
personEmail: message.personEmail,
|
|
62
|
-
created: message.created,
|
|
63
|
-
}
|
|
59
|
+
const message = await withWebexClient((client) => client.getMessage(messageId))
|
|
64
60
|
|
|
65
|
-
console.log(formatOutput(
|
|
61
|
+
console.log(formatOutput(formatMessageOutput(message), options.pretty))
|
|
66
62
|
} catch (error) {
|
|
67
63
|
handleError(error as Error)
|
|
68
64
|
}
|
|
@@ -75,8 +71,7 @@ export async function deleteAction(messageId: string, options: { force?: boolean
|
|
|
75
71
|
return process.exit(0)
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
await client.deleteMessage(messageId)
|
|
74
|
+
await withWebexClient((client) => client.deleteMessage(messageId))
|
|
80
75
|
|
|
81
76
|
console.log(formatOutput({ deleted: messageId }, options.pretty))
|
|
82
77
|
} catch (error) {
|
|
@@ -91,20 +86,13 @@ export async function editAction(
|
|
|
91
86
|
options: { markdown?: boolean; pretty?: boolean },
|
|
92
87
|
): Promise<void> {
|
|
93
88
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const output = {
|
|
100
|
-
id: message.id,
|
|
101
|
-
roomId: message.roomId,
|
|
102
|
-
text: message.text,
|
|
103
|
-
personEmail: message.personEmail,
|
|
104
|
-
created: message.created,
|
|
105
|
-
}
|
|
89
|
+
const message = await withWebexClient((client) =>
|
|
90
|
+
client.editMessage(messageId, spaceId, text, {
|
|
91
|
+
markdown: options.markdown,
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
106
94
|
|
|
107
|
-
console.log(formatOutput(
|
|
95
|
+
console.log(formatOutput(formatMessageOutput(message), options.pretty))
|
|
108
96
|
} catch (error) {
|
|
109
97
|
handleError(error as Error)
|
|
110
98
|
}
|
|
@@ -116,18 +104,11 @@ export async function dmAction(
|
|
|
116
104
|
options: { markdown?: boolean; pretty?: boolean },
|
|
117
105
|
): Promise<void> {
|
|
118
106
|
try {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const output = {
|
|
123
|
-
id: message.id,
|
|
124
|
-
roomId: message.roomId,
|
|
125
|
-
text: message.text,
|
|
126
|
-
personEmail: message.personEmail,
|
|
127
|
-
created: message.created,
|
|
128
|
-
}
|
|
107
|
+
const message = await withWebexClient((client) =>
|
|
108
|
+
client.sendDirectMessage(email, text, { markdown: options.markdown }),
|
|
109
|
+
)
|
|
129
110
|
|
|
130
|
-
console.log(formatOutput(
|
|
111
|
+
console.log(formatOutput(formatMessageOutput(message), options.pretty))
|
|
131
112
|
} catch (error) {
|
|
132
113
|
handleError(error as Error)
|
|
133
114
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import * as jose from 'node-jose'
|
|
4
4
|
|
|
@@ -11,12 +11,16 @@ const decodeJweHeader = (jwe: string): Record<string, unknown> => {
|
|
|
11
11
|
return JSON.parse(json) as Record<string, unknown>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const createSerializedKey = async (keyUri: string) => {
|
|
15
15
|
const keystore = jose.JWK.createKeyStore()
|
|
16
16
|
const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
|
|
17
17
|
const jwk = key.toJSON(true)
|
|
18
|
+
return JSON.stringify({ uri: keyUri, jwk })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const createKeyring = async (keyUri: string) => {
|
|
18
22
|
const rawKeys = new Map<string, string>()
|
|
19
|
-
rawKeys.set(keyUri,
|
|
23
|
+
rawKeys.set(keyUri, await createSerializedKey(keyUri))
|
|
20
24
|
return new WebexEncryptionService(rawKeys)
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -51,4 +55,55 @@ describe('WebexEncryptionService', () => {
|
|
|
51
55
|
|
|
52
56
|
expect(plaintext).toBe('round trip')
|
|
53
57
|
})
|
|
58
|
+
|
|
59
|
+
it('getKey returns cached key without calling provider when key is present', async () => {
|
|
60
|
+
const service = await createKeyring(keyUri)
|
|
61
|
+
const provider = { fetchKey: mock(async () => null as string | null) }
|
|
62
|
+
service.setKeyProvider(provider)
|
|
63
|
+
|
|
64
|
+
const key = await service.getKey(keyUri)
|
|
65
|
+
|
|
66
|
+
expect(key).not.toBeNull()
|
|
67
|
+
expect(provider.fetchKey).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('getKey calls provider and returns key when key is missing', async () => {
|
|
71
|
+
const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/0d7a0dfb-0464-40ce-8f3d-e65a33b61561'
|
|
72
|
+
const serializedKey = await createSerializedKey(missingKeyUri)
|
|
73
|
+
const service = new WebexEncryptionService(new Map())
|
|
74
|
+
const provider = { fetchKey: mock(async () => serializedKey) }
|
|
75
|
+
service.setKeyProvider(provider)
|
|
76
|
+
|
|
77
|
+
const key = await service.getKey(missingKeyUri)
|
|
78
|
+
|
|
79
|
+
expect(key).not.toBeNull()
|
|
80
|
+
expect(provider.fetchKey).toHaveBeenCalledWith(missingKeyUri)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('getKey returns null when provider returns null', async () => {
|
|
84
|
+
const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/13d6256d-f7f1-4b98-8102-4d3d87b2834a'
|
|
85
|
+
const service = new WebexEncryptionService(new Map())
|
|
86
|
+
const provider = { fetchKey: mock(async () => null as string | null) }
|
|
87
|
+
service.setKeyProvider(provider)
|
|
88
|
+
|
|
89
|
+
const key = await service.getKey(missingKeyUri)
|
|
90
|
+
|
|
91
|
+
expect(key).toBeNull()
|
|
92
|
+
expect(provider.fetchKey).toHaveBeenCalledWith(missingKeyUri)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('getKey reuses provider result from raw keys after first fetch', async () => {
|
|
96
|
+
const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/84afb005-5ba5-49c8-bd46-0c5d7ddf1c30'
|
|
97
|
+
const serializedKey = await createSerializedKey(missingKeyUri)
|
|
98
|
+
const service = new WebexEncryptionService(new Map())
|
|
99
|
+
const provider = { fetchKey: mock(async () => serializedKey) }
|
|
100
|
+
service.setKeyProvider(provider)
|
|
101
|
+
|
|
102
|
+
await service.getKey(missingKeyUri)
|
|
103
|
+
;(service as unknown as { keyCache: Map<string, jose.JWK.Key> }).keyCache.clear()
|
|
104
|
+
const key = await service.getKey(missingKeyUri)
|
|
105
|
+
|
|
106
|
+
expect(key).not.toBeNull()
|
|
107
|
+
expect(provider.fetchKey).toHaveBeenCalledTimes(1)
|
|
108
|
+
})
|
|
54
109
|
})
|
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import * as jose from 'node-jose'
|
|
2
2
|
|
|
3
|
+
export interface WebexKeyProvider {
|
|
4
|
+
fetchKey(keyUri: string): Promise<string | null>
|
|
5
|
+
close?(): Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export class WebexEncryptionService {
|
|
4
9
|
private rawKeys: Map<string, string>
|
|
5
10
|
private keyCache: Map<string, jose.JWK.Key> = new Map()
|
|
11
|
+
private keyProvider: WebexKeyProvider | null = null
|
|
6
12
|
|
|
7
13
|
constructor(serializedKeys: Map<string, string>) {
|
|
8
14
|
this.rawKeys = serializedKeys
|
|
9
15
|
}
|
|
10
16
|
|
|
17
|
+
setKeyProvider(provider: WebexKeyProvider): void {
|
|
18
|
+
this.keyProvider = provider
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async close(): Promise<void> {
|
|
22
|
+
await this.keyProvider?.close?.()
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
async getKey(keyUri: string): Promise<jose.JWK.Key | null> {
|
|
12
26
|
const cached = this.keyCache.get(keyUri)
|
|
13
27
|
if (cached) return cached
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
let raw = this.rawKeys.get(keyUri)
|
|
30
|
+
if (!raw && this.keyProvider) {
|
|
31
|
+
raw = (await this.keyProvider.fetchKey(keyUri)) ?? undefined
|
|
32
|
+
}
|
|
16
33
|
if (!raw) return null
|
|
17
34
|
|
|
18
35
|
try {
|
|
19
36
|
const parsed = JSON.parse(raw) as { jwk: object }
|
|
20
37
|
const joseKey = await jose.JWK.asKey(parsed.jwk)
|
|
38
|
+
this.rawKeys.set(keyUri, raw)
|
|
21
39
|
this.keyCache.set(keyUri, joseKey)
|
|
22
40
|
return joseKey
|
|
23
41
|
} catch {
|