agent-messenger 2.19.4 → 2.19.5
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 +5 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/line/client.d.ts +6 -1
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +65 -12
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/index.d.ts +1 -1
- package/dist/src/platforms/line/index.d.ts.map +1 -1
- package/dist/src/platforms/line/index.js.map +1 -1
- package/dist/src/platforms/line/listener.d.ts.map +1 -1
- package/dist/src/platforms/line/listener.js +3 -2
- package/dist/src/platforms/line/listener.js.map +1 -1
- package/dist/src/platforms/line/types.d.ts +13 -0
- package/dist/src/platforms/line/types.d.ts.map +1 -1
- package/dist/src/platforms/line/types.js +6 -0
- package/dist/src/platforms/line/types.js.map +1 -1
- package/dist/src/vendor/linejs/_dist/client/login.d.ts +2 -1
- package/dist/src/vendor/linejs/client/login.js +3 -2
- package/dist/src/vendor/linejs/client/login.test.ts +11 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-line/references/common-patterns.md +9 -3
- 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-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/line/client.test.ts +36 -0
- package/src/platforms/line/client.ts +79 -13
- package/src/platforms/line/index.test.ts +10 -0
- package/src/platforms/line/index.ts +1 -0
- package/src/platforms/line/listener.test.ts +32 -0
- package/src/platforms/line/listener.ts +5 -4
- package/src/platforms/line/types.test.ts +17 -0
- package/src/platforms/line/types.ts +13 -0
- package/src/platforms/slack/commands/auth.test.ts +16 -6
- package/src/platforms/slack/token-extractor.test.ts +34 -7
- package/src/vendor/linejs/_dist/client/login.d.ts +2 -1
- package/src/vendor/linejs/client/login.js +3 -2
- package/src/vendor/linejs/client/login.test.ts +11 -0
|
@@ -105,6 +105,30 @@ describe('LineClient', () => {
|
|
|
105
105
|
expect(result.map((m) => m.message_id)).toEqual(['30', '20'])
|
|
106
106
|
expect(result.map((m) => m.text)).toEqual(['c', 'b'])
|
|
107
107
|
})
|
|
108
|
+
|
|
109
|
+
it('marks encrypted chunk messages without text as missing E2EE keys', async () => {
|
|
110
|
+
const client = clientWithTalk({
|
|
111
|
+
getServerTime: async () => 1700000000000,
|
|
112
|
+
getPreviousMessagesV2WithRequest: async () => [
|
|
113
|
+
{
|
|
114
|
+
id: '40',
|
|
115
|
+
from: 'u1',
|
|
116
|
+
text: null,
|
|
117
|
+
contentType: 'NONE',
|
|
118
|
+
createdTime: 1700000004000,
|
|
119
|
+
chunks: ['a', 'b'],
|
|
120
|
+
metadata: { e2eeMark: '2', e2eeVersion: '2' },
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const result = await client.getMessages('chat1', { count: 1 })
|
|
126
|
+
expect(result[0].text).toBeNull()
|
|
127
|
+
expect(result[0].decryption_error).toEqual({
|
|
128
|
+
code: 'missing_e2ee_key',
|
|
129
|
+
message: 'LINE message is encrypted with Letter Sealing, but this session has no saved E2EE key material.',
|
|
130
|
+
})
|
|
131
|
+
})
|
|
108
132
|
})
|
|
109
133
|
|
|
110
134
|
describe('sendMessage()', () => {
|
|
@@ -206,6 +230,18 @@ describe('LineClient', () => {
|
|
|
206
230
|
const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
|
|
207
231
|
expect(msg.message.from.id).toBe('u1')
|
|
208
232
|
expect(msg.message.text).toBeNull()
|
|
233
|
+
expect(msg.message.decryption_error).toEqual({ code: 'decrypt_failed', message: 'E2EE decrypt failed' })
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('marks missing E2EE key failures explicitly', async () => {
|
|
237
|
+
const op = { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me' } }
|
|
238
|
+
const client = clientWithStream([op], async () => {
|
|
239
|
+
throw new Error('NoE2EEKey: E2EE Key has not been saved')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const events = await collect(client)
|
|
243
|
+
const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
|
|
244
|
+
expect(msg.message.decryption_error?.code).toBe('missing_e2ee_key')
|
|
209
245
|
})
|
|
210
246
|
|
|
211
247
|
it('propagates polling errors so the listener can reconnect', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs'
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import type { Operation as LineOperation } from '@jsr/evex__linejs-types'
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
LineAccountCredentials,
|
|
18
18
|
LineChat,
|
|
19
19
|
LineDevice,
|
|
20
|
+
LineDecryptionError,
|
|
20
21
|
LineFriend,
|
|
21
22
|
LineLoginResult,
|
|
22
23
|
LineMessage,
|
|
@@ -26,11 +27,23 @@ import type {
|
|
|
26
27
|
import { LineError } from './types'
|
|
27
28
|
|
|
28
29
|
export interface LineRawMessage {
|
|
29
|
-
raw: {
|
|
30
|
+
raw: {
|
|
31
|
+
id: unknown
|
|
32
|
+
contentType?: unknown
|
|
33
|
+
contentMetadata?: unknown
|
|
34
|
+
createdTime?: unknown
|
|
35
|
+
toType?: unknown
|
|
36
|
+
to?: unknown
|
|
37
|
+
from?: unknown
|
|
38
|
+
text?: unknown
|
|
39
|
+
chunks?: unknown
|
|
40
|
+
metadata?: unknown
|
|
41
|
+
}
|
|
30
42
|
to: { id: unknown }
|
|
31
43
|
from: { id: unknown }
|
|
32
44
|
isMyMessage: boolean
|
|
33
45
|
text: string | null
|
|
46
|
+
decryption_error?: LineDecryptionError
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export type LineRawEvent = { kind: 'message'; message: LineRawMessage } | { kind: 'event'; op: LineOperation }
|
|
@@ -79,7 +92,14 @@ function getDefaultDevice(): LineDevice {
|
|
|
79
92
|
function createStorage(accountId?: string): FileStorage {
|
|
80
93
|
const dir = join(getConfigDir(), 'line-storage')
|
|
81
94
|
mkdirSync(dir, { recursive: true })
|
|
82
|
-
|
|
95
|
+
const defaultPath = join(dir, 'default.json')
|
|
96
|
+
if (!accountId) return new FileStorage(defaultPath)
|
|
97
|
+
|
|
98
|
+
const accountPath = join(dir, `${accountId}.json`)
|
|
99
|
+
if (!existsSync(accountPath) && existsSync(defaultPath)) {
|
|
100
|
+
copyFileSync(defaultPath, accountPath)
|
|
101
|
+
}
|
|
102
|
+
return new FileStorage(accountPath)
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
export class LineClient {
|
|
@@ -110,6 +130,7 @@ export class LineClient {
|
|
|
110
130
|
this.client = client
|
|
111
131
|
|
|
112
132
|
const profile = await client.base.talk.getProfile()
|
|
133
|
+
createStorage(profile.mid)
|
|
113
134
|
const now = new Date().toISOString()
|
|
114
135
|
|
|
115
136
|
await this.credManager.setAccount({
|
|
@@ -146,6 +167,7 @@ export class LineClient {
|
|
|
146
167
|
{
|
|
147
168
|
email: options.email,
|
|
148
169
|
password: options.password,
|
|
170
|
+
e2ee: true,
|
|
149
171
|
onPincodeRequest: (pin) => options.onPincode(pin),
|
|
150
172
|
},
|
|
151
173
|
{ device, storage },
|
|
@@ -154,6 +176,7 @@ export class LineClient {
|
|
|
154
176
|
this.client = client
|
|
155
177
|
|
|
156
178
|
const profile = await client.base.talk.getProfile()
|
|
179
|
+
createStorage(profile.mid)
|
|
157
180
|
const now = new Date().toISOString()
|
|
158
181
|
|
|
159
182
|
await this.credManager.setAccount({
|
|
@@ -188,7 +211,7 @@ export class LineClient {
|
|
|
188
211
|
}
|
|
189
212
|
|
|
190
213
|
const device: LineDevice = creds.device ?? getDefaultDevice()
|
|
191
|
-
const storage = createStorage()
|
|
214
|
+
const storage = createStorage(creds.account_id)
|
|
192
215
|
|
|
193
216
|
this.client = await linejsLoginWithAuthToken(creds.auth_token, { device, storage })
|
|
194
217
|
return this
|
|
@@ -337,14 +360,18 @@ export class LineClient {
|
|
|
337
360
|
},
|
|
338
361
|
})
|
|
339
362
|
|
|
340
|
-
return (rawMessages ?? []).map((msg) =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
363
|
+
return (rawMessages ?? []).map((msg) => {
|
|
364
|
+
const decryptionError = getUndecryptableMessageError(msg)
|
|
365
|
+
return {
|
|
366
|
+
message_id: String(msg.id),
|
|
367
|
+
chat_id: chatId,
|
|
368
|
+
author_id: String(msg.from ?? ''),
|
|
369
|
+
text: msg.text || null,
|
|
370
|
+
...(decryptionError && { decryption_error: decryptionError }),
|
|
371
|
+
content_type: String(msg.contentType ?? 'NONE'),
|
|
372
|
+
sent_at: new Date(Number(msg.createdTime)).toISOString(),
|
|
373
|
+
}
|
|
374
|
+
})
|
|
348
375
|
} catch (error) {
|
|
349
376
|
throw wrapError(error, 'get_messages_failed')
|
|
350
377
|
}
|
|
@@ -418,12 +445,15 @@ export class LineClient {
|
|
|
418
445
|
// surface the message with null text, since its text is unreadable.
|
|
419
446
|
let raw = op.message
|
|
420
447
|
let decrypted = true
|
|
448
|
+
let decryptionError: LineDecryptionError | undefined
|
|
421
449
|
try {
|
|
422
450
|
raw = await client.base.e2ee.decryptE2EEMessage(op.message)
|
|
423
|
-
} catch {
|
|
451
|
+
} catch (error) {
|
|
424
452
|
raw = op.message
|
|
425
453
|
decrypted = false
|
|
454
|
+
decryptionError = getDecryptionError(error)
|
|
426
455
|
}
|
|
456
|
+
decryptionError ??= getUndecryptableMessageError(raw)
|
|
427
457
|
yield {
|
|
428
458
|
kind: 'message',
|
|
429
459
|
message: {
|
|
@@ -432,6 +462,7 @@ export class LineClient {
|
|
|
432
462
|
from: { id: raw.from },
|
|
433
463
|
isMyMessage: selfMid === raw.from,
|
|
434
464
|
text: decrypted ? (raw.text ?? null) : null,
|
|
465
|
+
...(decryptionError && { decryption_error: decryptionError }),
|
|
435
466
|
},
|
|
436
467
|
}
|
|
437
468
|
}
|
|
@@ -449,3 +480,38 @@ export class LineClient {
|
|
|
449
480
|
return this.client
|
|
450
481
|
}
|
|
451
482
|
}
|
|
483
|
+
|
|
484
|
+
function getUndecryptableMessageError(raw: unknown): LineDecryptionError | undefined {
|
|
485
|
+
if (!isEncryptedChunkMessage(raw) || hasPlainText(raw)) return undefined
|
|
486
|
+
return {
|
|
487
|
+
code: 'missing_e2ee_key',
|
|
488
|
+
message: 'LINE message is encrypted with Letter Sealing, but this session has no saved E2EE key material.',
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getDecryptionError(error: unknown): LineDecryptionError {
|
|
493
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
494
|
+
return {
|
|
495
|
+
code: /NoE2EEKey|E2EE Key has not been saved|saveE2EE/i.test(message) ? 'missing_e2ee_key' : 'decrypt_failed',
|
|
496
|
+
message,
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function hasPlainText(raw: unknown): boolean {
|
|
501
|
+
if (!raw || typeof raw !== 'object') return false
|
|
502
|
+
const text = (raw as { text?: unknown }).text
|
|
503
|
+
return typeof text === 'string' && text.length > 0
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isEncryptedChunkMessage(raw: unknown): boolean {
|
|
507
|
+
if (!raw || typeof raw !== 'object') return false
|
|
508
|
+
const message = raw as { chunks?: unknown; contentMetadata?: unknown; metadata?: unknown }
|
|
509
|
+
if (!Array.isArray(message.chunks) || message.chunks.length === 0) return false
|
|
510
|
+
return hasE2EEMetadata(message.contentMetadata) || hasE2EEMetadata(message.metadata)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function hasE2EEMetadata(raw: unknown): boolean {
|
|
514
|
+
if (!raw || typeof raw !== 'object') return false
|
|
515
|
+
const metadata = raw as { e2eeMark?: unknown; e2eeVersion?: unknown }
|
|
516
|
+
return metadata.e2eeMark !== undefined || metadata.e2eeVersion !== undefined
|
|
517
|
+
}
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
LineMessageSchema,
|
|
12
12
|
LineSendResultSchema,
|
|
13
13
|
} from '@/platforms/line/index'
|
|
14
|
+
import type { LineDecryptionError } from '@/platforms/line/index'
|
|
15
|
+
|
|
16
|
+
const lineDecryptionError: LineDecryptionError = {
|
|
17
|
+
code: 'missing_e2ee_key',
|
|
18
|
+
message: 'E2EE key material is missing',
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
it('LineClient is exported from barrel', () => {
|
|
16
22
|
expect(typeof LineClient).toBe('function')
|
|
@@ -47,3 +53,7 @@ it('LineAccountCredentialsSchema is exported from barrel', () => {
|
|
|
47
53
|
it('LineConfigSchema is exported from barrel', () => {
|
|
48
54
|
expect(typeof LineConfigSchema.parse).toBe('function')
|
|
49
55
|
})
|
|
56
|
+
|
|
57
|
+
it('LineDecryptionError type is exported from barrel', () => {
|
|
58
|
+
expect(lineDecryptionError.code).toBe('missing_e2ee_key')
|
|
59
|
+
})
|
|
@@ -230,6 +230,38 @@ describe('LineListener', () => {
|
|
|
230
230
|
expect(messages[0].content_type).toBe('NONE')
|
|
231
231
|
})
|
|
232
232
|
|
|
233
|
+
it('forwards LINE decryption errors on message events', async () => {
|
|
234
|
+
const client = createMockLineClient()
|
|
235
|
+
listener = new LineListener(client)
|
|
236
|
+
|
|
237
|
+
const messages: LinePushMessageEvent[] = []
|
|
238
|
+
listener.on('message', (event) => messages.push(event))
|
|
239
|
+
|
|
240
|
+
await listener.start()
|
|
241
|
+
mockInternalClientInstance.simulateMessage({
|
|
242
|
+
isMyMessage: false,
|
|
243
|
+
from: { type: 'USER', id: 'u456' },
|
|
244
|
+
to: { type: 'USER', id: 'u123' },
|
|
245
|
+
text: null,
|
|
246
|
+
decryption_error: {
|
|
247
|
+
code: 'missing_e2ee_key',
|
|
248
|
+
message: 'LINE message is encrypted with Letter Sealing, but this session has no saved E2EE key material.',
|
|
249
|
+
},
|
|
250
|
+
raw: {
|
|
251
|
+
id: 'msg013',
|
|
252
|
+
contentType: 'NONE',
|
|
253
|
+
createdTime: 1700000010000,
|
|
254
|
+
chunks: ['a', 'b'],
|
|
255
|
+
metadata: { e2eeMark: '2', e2eeVersion: '2' },
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
await flush()
|
|
259
|
+
|
|
260
|
+
expect(messages.length).toBe(1)
|
|
261
|
+
expect(messages[0].text).toBeNull()
|
|
262
|
+
expect(messages[0].decryption_error?.code).toBe('missing_e2ee_key')
|
|
263
|
+
})
|
|
264
|
+
|
|
233
265
|
it('coerces non-string contentMetadata values to strings', async () => {
|
|
234
266
|
const client = createMockLineClient()
|
|
235
267
|
listener = new LineListener(client)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events'
|
|
2
2
|
|
|
3
|
-
import type { LineClient } from './client'
|
|
3
|
+
import type { LineClient, LineRawMessage } from './client'
|
|
4
4
|
import type { LineListenerEventMap, LinePushGenericEvent, LinePushMessageEvent } from './types'
|
|
5
5
|
|
|
6
6
|
const RECONNECT_BASE_DELAY = 1_000
|
|
@@ -94,19 +94,20 @@ export class LineListener {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
private emitMessage(msg:
|
|
97
|
+
private emitMessage(msg: LineRawMessage): void {
|
|
98
98
|
try {
|
|
99
99
|
const toType = msg.raw.toType
|
|
100
100
|
const isGroupOrRoom = toType === 'GROUP' || toType === 'ROOM' || toType === 0 || toType === 1
|
|
101
|
-
const chatId = isGroupOrRoom ? msg.to.id : msg.isMyMessage ? msg.to.id : msg.from.id
|
|
101
|
+
const chatId = String(isGroupOrRoom ? msg.to.id : msg.isMyMessage ? msg.to.id : msg.from.id)
|
|
102
102
|
|
|
103
103
|
const contentType = String(msg.raw.contentType ?? 'NONE')
|
|
104
104
|
const event: LinePushMessageEvent = {
|
|
105
105
|
type: 'message',
|
|
106
106
|
chat_id: chatId,
|
|
107
107
|
message_id: String(msg.raw.id),
|
|
108
|
-
author_id: msg.from.id,
|
|
108
|
+
author_id: String(msg.from.id),
|
|
109
109
|
text: getMessageText(msg.text, msg.raw, contentType),
|
|
110
|
+
...(msg.decryption_error && { decryption_error: msg.decryption_error }),
|
|
110
111
|
content_type: contentType,
|
|
111
112
|
content_metadata: normalizeContentMetadata(msg.raw.contentMetadata),
|
|
112
113
|
sent_at: new Date(Number(msg.raw.createdTime)).toISOString(),
|
|
@@ -87,6 +87,23 @@ describe('LineMessageSchema', () => {
|
|
|
87
87
|
expect(result.text).toBeNull()
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
+
it('parses message with decryption error', () => {
|
|
91
|
+
const data = {
|
|
92
|
+
message_id: 'msg789',
|
|
93
|
+
chat_id: 'u1234567890abcdef',
|
|
94
|
+
author_id: 'u9876543210fedcba',
|
|
95
|
+
text: null,
|
|
96
|
+
decryption_error: {
|
|
97
|
+
code: 'missing_e2ee_key',
|
|
98
|
+
message: 'LINE message is encrypted with Letter Sealing, but this session has no saved E2EE key material.',
|
|
99
|
+
},
|
|
100
|
+
content_type: 'NONE',
|
|
101
|
+
sent_at: '2026-03-29T00:00:00.000Z',
|
|
102
|
+
}
|
|
103
|
+
const result = LineMessageSchema.parse(data)
|
|
104
|
+
expect(result.decryption_error?.code).toBe('missing_e2ee_key')
|
|
105
|
+
})
|
|
106
|
+
|
|
90
107
|
it('rejects missing required fields', () => {
|
|
91
108
|
expect(() => LineMessageSchema.parse({ message_id: 'msg123' })).toThrow()
|
|
92
109
|
})
|
|
@@ -43,10 +43,16 @@ export interface LineMessage {
|
|
|
43
43
|
author_id: string
|
|
44
44
|
author_name?: string
|
|
45
45
|
text: string | null
|
|
46
|
+
decryption_error?: LineDecryptionError
|
|
46
47
|
content_type: string
|
|
47
48
|
sent_at: string
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export interface LineDecryptionError {
|
|
52
|
+
code: 'missing_e2ee_key' | 'decrypt_failed'
|
|
53
|
+
message: string
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
export interface LineSendResult {
|
|
51
57
|
success: boolean
|
|
52
58
|
chat_id: string
|
|
@@ -106,6 +112,12 @@ export const LineMessageSchema = z.object({
|
|
|
106
112
|
author_id: z.string(),
|
|
107
113
|
author_name: z.string().optional(),
|
|
108
114
|
text: z.string().nullable(),
|
|
115
|
+
decryption_error: z
|
|
116
|
+
.object({
|
|
117
|
+
code: z.enum(['missing_e2ee_key', 'decrypt_failed']),
|
|
118
|
+
message: z.string(),
|
|
119
|
+
})
|
|
120
|
+
.optional(),
|
|
109
121
|
content_type: z.string(),
|
|
110
122
|
sent_at: z.string(),
|
|
111
123
|
})
|
|
@@ -137,6 +149,7 @@ export interface LinePushMessageEvent {
|
|
|
137
149
|
message_id: string
|
|
138
150
|
author_id: string
|
|
139
151
|
text: string | null
|
|
152
|
+
decryption_error?: LineDecryptionError
|
|
140
153
|
content_type: string
|
|
141
154
|
// Raw LINE contentMetadata (sticker IDs, file name/size, media URLs); empty for plain text.
|
|
142
155
|
content_metadata: Record<string, string>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterAll, beforeEach, describe, expect, mock, it } from 'bun:test'
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, mock, spyOn, it } from 'bun:test'
|
|
2
2
|
import { mkdirSync, rmSync } from 'node:fs'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
@@ -14,6 +14,16 @@ import { type ExtractedWorkspace, TokenExtractor } from '@/platforms/slack/token
|
|
|
14
14
|
const testConfigDir = join(import.meta.dir, '.test-auth-config')
|
|
15
15
|
const testSlackDir = join(import.meta.dir, '.test-slack-data')
|
|
16
16
|
|
|
17
|
+
async function extractWithoutBrowserFallback(extractor: TokenExtractor): Promise<ExtractedWorkspace[]> {
|
|
18
|
+
const extractFromBrowsersSpy = spyOn(TokenExtractor.prototype, 'extractFromBrowsers').mockResolvedValue([])
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
return await extractor.extract()
|
|
22
|
+
} finally {
|
|
23
|
+
extractFromBrowsersSpy.mockRestore()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
describe('TokenExtractor', () => {
|
|
18
28
|
let extractor: TokenExtractor
|
|
19
29
|
|
|
@@ -85,7 +95,7 @@ describe('TokenExtractor', () => {
|
|
|
85
95
|
extractor = new TokenExtractor('darwin', nonExistentPath)
|
|
86
96
|
|
|
87
97
|
// when
|
|
88
|
-
const result = await extractor
|
|
98
|
+
const result = await extractWithoutBrowserFallback(extractor)
|
|
89
99
|
|
|
90
100
|
// then
|
|
91
101
|
expect(result).toEqual([])
|
|
@@ -97,7 +107,7 @@ describe('TokenExtractor', () => {
|
|
|
97
107
|
extractor = new TokenExtractor('darwin', testSlackDir)
|
|
98
108
|
|
|
99
109
|
// When: extract is called
|
|
100
|
-
const result = await extractor
|
|
110
|
+
const result = await extractWithoutBrowserFallback(extractor)
|
|
101
111
|
|
|
102
112
|
// Then: Should return empty array
|
|
103
113
|
expect(result).toEqual([])
|
|
@@ -463,7 +473,7 @@ describe('Error Handling', () => {
|
|
|
463
473
|
const extractor = new TokenExtractor('darwin', nonExistentPath)
|
|
464
474
|
|
|
465
475
|
// when/then — falls back to browser profiles, returns empty array
|
|
466
|
-
const result = await extractor
|
|
476
|
+
const result = await extractWithoutBrowserFallback(extractor)
|
|
467
477
|
expect(result).toEqual([])
|
|
468
478
|
})
|
|
469
479
|
|
|
@@ -472,7 +482,7 @@ describe('Error Handling', () => {
|
|
|
472
482
|
const extractor = new TokenExtractor('darwin', testSlackDir)
|
|
473
483
|
|
|
474
484
|
// When: Trying to extract from empty directory
|
|
475
|
-
const result = await extractor
|
|
485
|
+
const result = await extractWithoutBrowserFallback(extractor)
|
|
476
486
|
|
|
477
487
|
// Then: Should return empty array
|
|
478
488
|
expect(result).toEqual([])
|
|
@@ -484,7 +494,7 @@ describe('Error Handling', () => {
|
|
|
484
494
|
const extractor = new TokenExtractor('darwin', testSlackDir)
|
|
485
495
|
|
|
486
496
|
// When: Trying to extract
|
|
487
|
-
const result = await extractor
|
|
497
|
+
const result = await extractWithoutBrowserFallback(extractor)
|
|
488
498
|
|
|
489
499
|
// Then: Should return empty array (no tokens found)
|
|
490
500
|
expect(result).toEqual([])
|
|
@@ -12,6 +12,7 @@ import { ExtractedWorkspace, TokenExtractor } from './token-extractor'
|
|
|
12
12
|
|
|
13
13
|
const tempDirs: string[] = []
|
|
14
14
|
const originalAgentBrowserProfile = process.env.AGENT_BROWSER_PROFILE
|
|
15
|
+
const originalLocalAppData = process.env.LOCALAPPDATA
|
|
15
16
|
|
|
16
17
|
afterEach(() => {
|
|
17
18
|
if (originalAgentBrowserProfile) {
|
|
@@ -20,12 +21,34 @@ afterEach(() => {
|
|
|
20
21
|
delete process.env.AGENT_BROWSER_PROFILE
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
if (originalLocalAppData) {
|
|
25
|
+
process.env.LOCALAPPDATA = originalLocalAppData
|
|
26
|
+
} else {
|
|
27
|
+
delete process.env.LOCALAPPDATA
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
for (const dir of tempDirs) {
|
|
24
31
|
rmSync(dir, { recursive: true, force: true })
|
|
25
32
|
}
|
|
26
33
|
tempDirs.length = 0
|
|
27
34
|
})
|
|
28
35
|
|
|
36
|
+
async function extractDesktopOnly(extractor: TokenExtractor): Promise<ExtractedWorkspace[]> {
|
|
37
|
+
const extractFromBrowsersSpy = spyOn(TokenExtractor.prototype, 'extractFromBrowsers').mockResolvedValue([])
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return await extractor.extract()
|
|
41
|
+
} finally {
|
|
42
|
+
extractFromBrowsersSpy.mockRestore()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function useEmptyWindowsBrowserRoot(): void {
|
|
47
|
+
const browserRoot = mkdtempSync(join(tmpdir(), 'empty-browser-root-'))
|
|
48
|
+
tempDirs.push(browserRoot)
|
|
49
|
+
process.env.LOCALAPPDATA = browserRoot
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
function createCookiesDb(
|
|
30
53
|
dbPath: string,
|
|
31
54
|
cookies: { name: string; value: string; encrypted_value: Uint8Array; host_key: string; last_access_utc: number }[],
|
|
@@ -60,7 +83,7 @@ describe('TokenExtractor token deduplication', () => {
|
|
|
60
83
|
|
|
61
84
|
// when
|
|
62
85
|
const extractor = new TokenExtractor('darwin', slackDir)
|
|
63
|
-
const result = await extractor
|
|
86
|
+
const result = await extractDesktopOnly(extractor)
|
|
64
87
|
|
|
65
88
|
// then — first token wins, but team name is upgraded
|
|
66
89
|
expect(result.length).toBe(1)
|
|
@@ -383,7 +406,7 @@ describe('TokenExtractor debug logging', () => {
|
|
|
383
406
|
|
|
384
407
|
// when
|
|
385
408
|
const extractor = new TokenExtractor('darwin', slackDir, undefined, debugLog)
|
|
386
|
-
await extractor
|
|
409
|
+
await extractDesktopOnly(extractor)
|
|
387
410
|
|
|
388
411
|
// then — should have emitted debug messages
|
|
389
412
|
expect(messages.length).toBeGreaterThan(0)
|
|
@@ -397,7 +420,7 @@ describe('TokenExtractor debug logging', () => {
|
|
|
397
420
|
|
|
398
421
|
// when — then — should not throw
|
|
399
422
|
const extractor = new TokenExtractor('darwin', slackDir)
|
|
400
|
-
const result = await extractor
|
|
423
|
+
const result = await extractDesktopOnly(extractor)
|
|
401
424
|
expect(result).toEqual([])
|
|
402
425
|
})
|
|
403
426
|
})
|
|
@@ -576,7 +599,7 @@ describe('TokenExtractor Windows DPAPI', () => {
|
|
|
576
599
|
|
|
577
600
|
// when
|
|
578
601
|
const extractor = new TestTokenExtractor('win32', slackDir)
|
|
579
|
-
const result = await extractor
|
|
602
|
+
const result = await extractDesktopOnly(extractor)
|
|
580
603
|
|
|
581
604
|
// then
|
|
582
605
|
expect(result).toEqual([
|
|
@@ -711,7 +734,7 @@ describe('TokenExtractor IndexedDB blob files', () => {
|
|
|
711
734
|
|
|
712
735
|
// when
|
|
713
736
|
const extractor = new TokenExtractor('darwin', slackDir)
|
|
714
|
-
const result = await extractor
|
|
737
|
+
const result = await extractDesktopOnly(extractor)
|
|
715
738
|
|
|
716
739
|
// then
|
|
717
740
|
expect(result.length).toBe(0)
|
|
@@ -802,17 +825,21 @@ describe('TokenExtractor getWorkspaceDomains', () => {
|
|
|
802
825
|
|
|
803
826
|
describe('TokenExtractor browser fallback', () => {
|
|
804
827
|
it('extractFromBrowsers returns empty array when no browser profiles have tokens', async () => {
|
|
828
|
+
useEmptyWindowsBrowserRoot()
|
|
829
|
+
|
|
805
830
|
const slackDir = mkdtempSync(join(tmpdir(), 'slack-nonexistent-'))
|
|
806
831
|
tempDirs.push(slackDir)
|
|
807
832
|
rmSync(slackDir, { recursive: true, force: true })
|
|
808
833
|
|
|
809
|
-
const extractor = new TokenExtractor('
|
|
834
|
+
const extractor = new TokenExtractor('win32', slackDir)
|
|
810
835
|
const result = await extractor.extractFromBrowsers()
|
|
811
836
|
expect(result).toEqual([])
|
|
812
837
|
})
|
|
813
838
|
|
|
814
839
|
it('resolves Local State from agent-browser profile root for encrypted cookies', async () => {
|
|
815
840
|
// given
|
|
841
|
+
useEmptyWindowsBrowserRoot()
|
|
842
|
+
|
|
816
843
|
const agentBrowserProfile = mkdtempSync(join(tmpdir(), 'agent-browser-slack-profile-'))
|
|
817
844
|
tempDirs.push(agentBrowserProfile)
|
|
818
845
|
process.env.AGENT_BROWSER_PROFILE = agentBrowserProfile
|
|
@@ -839,7 +866,7 @@ describe('TokenExtractor browser fallback', () => {
|
|
|
839
866
|
|
|
840
867
|
try {
|
|
841
868
|
// when
|
|
842
|
-
const extractor = new TokenExtractor('
|
|
869
|
+
const extractor = new TokenExtractor('win32', join(agentBrowserProfile, 'missing-desktop'))
|
|
843
870
|
const result = await extractor.extractFromBrowsers()
|
|
844
871
|
|
|
845
872
|
// then
|
|
@@ -34,8 +34,9 @@ export interface WithPasswordOptions {
|
|
|
34
34
|
email: string;
|
|
35
35
|
password: string;
|
|
36
36
|
/** @default 114514 */ pincode?: string;
|
|
37
|
+
/** @default true */ e2ee?: boolean;
|
|
37
38
|
onPincodeRequest(pin: string): void | Promise<void>;
|
|
38
39
|
}
|
|
39
40
|
export declare const loginWithPassword: (opts: WithPasswordOptions, init: InitOptions) => Promise<Client>;
|
|
40
41
|
export declare const loginWithAuthToken: (authToken: string, init: InitOptions) => Promise<Client>;
|
|
41
|
-
//# sourceMappingURL=login.d.ts.map
|
|
42
|
+
//# sourceMappingURL=login.d.ts.map
|