agent-messenger 2.14.1 → 2.15.1

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 (53) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -0
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  5. package/dist/src/platforms/kakaotalk/client.js +15 -0
  6. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  7. package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
  8. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  9. package/dist/src/platforms/kakaotalk/index.js +1 -1
  10. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  11. package/dist/src/platforms/kakaotalk/listener.d.ts +1 -1
  12. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  13. package/dist/src/platforms/kakaotalk/listener.js +58 -6
  14. package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
  15. package/dist/src/platforms/kakaotalk/types.d.ts +46 -1
  16. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  17. package/dist/src/platforms/kakaotalk/types.js +22 -0
  18. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  19. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  20. package/dist/src/platforms/slack/commands/auth.js +6 -2
  21. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  22. package/dist/src/platforms/slack/ensure-auth.d.ts +8 -2
  23. package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -1
  24. package/dist/src/platforms/slack/ensure-auth.js +67 -10
  25. package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
  26. package/docs/content/docs/cli/kakaotalk.mdx +4 -3
  27. package/docs/content/docs/sdk/kakaotalk.mdx +2 -0
  28. package/package.json +1 -1
  29. package/skills/agent-channeltalk/SKILL.md +1 -1
  30. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  31. package/skills/agent-discord/SKILL.md +1 -1
  32. package/skills/agent-discordbot/SKILL.md +1 -1
  33. package/skills/agent-instagram/SKILL.md +1 -1
  34. package/skills/agent-kakaotalk/SKILL.md +5 -4
  35. package/skills/agent-line/SKILL.md +1 -1
  36. package/skills/agent-slack/SKILL.md +1 -1
  37. package/skills/agent-slackbot/SKILL.md +1 -1
  38. package/skills/agent-teams/SKILL.md +1 -1
  39. package/skills/agent-telegram/SKILL.md +1 -1
  40. package/skills/agent-telegrambot/SKILL.md +1 -1
  41. package/skills/agent-webex/SKILL.md +1 -1
  42. package/skills/agent-wechatbot/SKILL.md +1 -1
  43. package/skills/agent-whatsapp/SKILL.md +1 -1
  44. package/skills/agent-whatsappbot/SKILL.md +1 -1
  45. package/src/platforms/kakaotalk/client.test.ts +61 -0
  46. package/src/platforms/kakaotalk/client.ts +13 -0
  47. package/src/platforms/kakaotalk/index.ts +6 -0
  48. package/src/platforms/kakaotalk/listener.test.ts +383 -0
  49. package/src/platforms/kakaotalk/listener.ts +69 -12
  50. package/src/platforms/kakaotalk/types.ts +48 -0
  51. package/src/platforms/slack/commands/auth.ts +6 -4
  52. package/src/platforms/slack/ensure-auth.test.ts +223 -1
  53. package/src/platforms/slack/ensure-auth.ts +80 -11
@@ -10,10 +10,12 @@ let extractSpy: ReturnType<typeof spyOn>
10
10
  let extractCookieSpy: ReturnType<typeof spyOn>
11
11
  let getWorkspaceDomainsSpy: ReturnType<typeof spyOn>
12
12
  let testAuthSpy: ReturnType<typeof spyOn>
13
+ let loginSpy: ReturnType<typeof spyOn>
13
14
  let setWorkspaceSpy: ReturnType<typeof spyOn>
14
15
  let loadSpy: ReturnType<typeof spyOn>
15
16
  let setCurrentWorkspaceSpy: ReturnType<typeof spyOn>
16
17
  let fetchSpy: ReturnType<typeof spyOn>
18
+ let activeToken: string | null = null
17
19
 
18
20
  beforeEach(() => {
19
21
  getWorkspaceSpy = spyOn(CredentialManager.prototype, 'getWorkspace').mockResolvedValue(null)
@@ -31,6 +33,15 @@ beforeEach(() => {
31
33
 
32
34
  getWorkspaceDomainsSpy = spyOn(TokenExtractor.prototype, 'getWorkspaceDomains').mockReturnValue({})
33
35
 
36
+ activeToken = null
37
+ loginSpy = spyOn(SlackClient.prototype, 'login').mockImplementation(function (
38
+ this: SlackClient,
39
+ credentials?: { token: string; cookie: string },
40
+ ) {
41
+ activeToken = credentials?.token ?? null
42
+ return Promise.resolve(this)
43
+ })
44
+
34
45
  testAuthSpy = spyOn(SlackClient.prototype, 'testAuth').mockResolvedValue({
35
46
  user_id: 'U123',
36
47
  team_id: 'T123',
@@ -55,6 +66,7 @@ afterEach(() => {
55
66
  extractSpy?.mockRestore()
56
67
  extractCookieSpy?.mockRestore()
57
68
  getWorkspaceDomainsSpy?.mockRestore()
69
+ loginSpy?.mockRestore()
58
70
  testAuthSpy?.mockRestore()
59
71
  setWorkspaceSpy?.mockRestore()
60
72
  loadSpy?.mockRestore()
@@ -62,6 +74,16 @@ afterEach(() => {
62
74
  fetchSpy?.mockRestore()
63
75
  })
64
76
 
77
+ function authResponseByToken(map: Record<string, { team_id: string; team?: string } | Error>) {
78
+ testAuthSpy.mockImplementation(() => {
79
+ const token = activeToken ?? '<no-token>'
80
+ const result = map[token]
81
+ if (!result) throw new Error('invalid_auth')
82
+ if (result instanceof Error) throw result
83
+ return Promise.resolve({ user_id: 'U1', user: 'user', team: result.team, team_id: result.team_id })
84
+ })
85
+ }
86
+
65
87
  describe('ensureSlackAuth', () => {
66
88
  it('skips extraction when stored credentials are valid', async () => {
67
89
  // given
@@ -278,7 +300,7 @@ describe('ensureSlackAuth', () => {
278
300
  )
279
301
  })
280
302
 
281
- it('skips web refresh when no domain is known for workspace', async () => {
303
+ it('skips web refresh when domain map is empty', async () => {
282
304
  // given — domain mapping is empty
283
305
  extractSpy.mockResolvedValue([
284
306
  { workspace_id: 'T-stale', workspace_name: 'stale-ws', token: 'xoxc-stale', cookie: 'xoxd-valid' },
@@ -294,6 +316,206 @@ describe('ensureSlackAuth', () => {
294
316
  expect(setWorkspaceSpy).not.toHaveBeenCalled()
295
317
  })
296
318
 
319
+ it('falls back to other known domains when workspace_id has no domain mapping', async () => {
320
+ // given — workspace_id 'unknown' (extractor couldn't resolve team id); cookie is valid
321
+ // for the second candidate domain in root-state.json
322
+ extractSpy.mockResolvedValue([
323
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale', cookie: 'xoxd-valid' },
324
+ ])
325
+ getWorkspaceDomainsSpy.mockReturnValue({
326
+ T_OTHER_A: 'other-a',
327
+ T_OTHER_B: 'other-b',
328
+ })
329
+
330
+ fetchSpy.mockImplementation((url: string) => {
331
+ if (url.startsWith('https://other-a.slack.com/')) {
332
+ return Promise.resolve(new Response('<html>"api_token":"xoxc-fresh-a"</html>', { status: 200 }))
333
+ }
334
+ if (url.startsWith('https://other-b.slack.com/')) {
335
+ return Promise.resolve(new Response('<html>"api_token":"xoxc-fresh-b"</html>', { status: 200 }))
336
+ }
337
+ return Promise.resolve(new Response('', { status: 500 }))
338
+ })
339
+
340
+ authResponseByToken({
341
+ 'xoxc-fresh-b': { team_id: 'T_OTHER_B', team: 'Other B' },
342
+ })
343
+
344
+ // when
345
+ await ensureSlackAuth()
346
+
347
+ // then — both candidate domains were tried; the workspace saves with the resolved team_id
348
+ expect(fetchSpy).toHaveBeenCalledWith(
349
+ 'https://other-a.slack.com/ssb/redirect',
350
+ expect.objectContaining({ headers: { Cookie: 'd=xoxd-valid' } }),
351
+ )
352
+ expect(fetchSpy).toHaveBeenCalledWith(
353
+ 'https://other-b.slack.com/ssb/redirect',
354
+ expect.objectContaining({ headers: { Cookie: 'd=xoxd-valid' } }),
355
+ )
356
+ expect(setWorkspaceSpy).toHaveBeenCalledWith(
357
+ expect.objectContaining({ workspace_id: 'T_OTHER_B', token: 'xoxc-fresh-b', workspace_name: 'Other B' }),
358
+ )
359
+ })
360
+
361
+ it('stops trying domains once a refresh+verify succeeds', async () => {
362
+ // given — exact-match domain succeeds on first attempt; the fallback domain must not be fetched
363
+ extractSpy.mockResolvedValue([
364
+ { workspace_id: 'T_TARGET', workspace_name: 'target', token: 'xoxc-stale', cookie: 'xoxd-valid' },
365
+ ])
366
+ getWorkspaceDomainsSpy.mockReturnValue({
367
+ T_TARGET: 'target-domain',
368
+ T_OTHER: 'other-domain',
369
+ })
370
+
371
+ fetchSpy.mockResolvedValue(new Response('<html>"api_token":"xoxc-fresh"</html>', { status: 200 }))
372
+
373
+ authResponseByToken({
374
+ 'xoxc-fresh': { team_id: 'T_TARGET', team: 'Target' },
375
+ })
376
+
377
+ // when
378
+ await ensureSlackAuth()
379
+
380
+ // then — the exact-match domain is tried, fallback domain is not
381
+ expect(fetchSpy).toHaveBeenCalledWith(
382
+ 'https://target-domain.slack.com/ssb/redirect',
383
+ expect.objectContaining({ headers: { Cookie: 'd=xoxd-valid' } }),
384
+ )
385
+ expect(fetchSpy).not.toHaveBeenCalledWith('https://other-domain.slack.com/ssb/redirect', expect.anything())
386
+ })
387
+
388
+ it('returns failure when no known domain validates the cookie', async () => {
389
+ // given — none of the domains in the map produce a valid token+cookie pair
390
+ extractSpy.mockResolvedValue([
391
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale', cookie: 'xoxd-valid' },
392
+ ])
393
+ getWorkspaceDomainsSpy.mockReturnValue({
394
+ T_A: 'a',
395
+ T_B: 'b',
396
+ })
397
+
398
+ fetchSpy.mockResolvedValue(new Response('<html>"api_token":"xoxc-fresh"</html>', { status: 200 }))
399
+ testAuthSpy.mockRejectedValue(new Error('invalid_auth'))
400
+
401
+ // when
402
+ await ensureSlackAuth()
403
+
404
+ // then — every candidate is tried, none save
405
+ expect(fetchSpy).toHaveBeenCalledWith('https://a.slack.com/ssb/redirect', expect.anything())
406
+ expect(fetchSpy).toHaveBeenCalledWith('https://b.slack.com/ssb/redirect', expect.anything())
407
+ expect(setWorkspaceSpy).not.toHaveBeenCalled()
408
+ })
409
+
410
+ it('skips domains with non-subdomain characters', async () => {
411
+ // given — a tampered root-state.json with a domain that contains a dot/slash/colon
412
+ extractSpy.mockResolvedValue([
413
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale', cookie: 'xoxd-valid' },
414
+ ])
415
+ getWorkspaceDomainsSpy.mockReturnValue({
416
+ T_EVIL: 'attacker.com#',
417
+ T_GOOD: 'good',
418
+ })
419
+
420
+ fetchSpy.mockResolvedValue(new Response('<html>"api_token":"xoxc-fresh"</html>', { status: 200 }))
421
+ authResponseByToken({ 'xoxc-fresh': { team_id: 'T_GOOD', team: 'Good' } })
422
+
423
+ // when
424
+ await ensureSlackAuth()
425
+
426
+ // then — the bad domain is never fetched; the good domain succeeds
427
+ expect(fetchSpy).not.toHaveBeenCalledWith(expect.stringContaining('attacker.com'), expect.anything())
428
+ expect(fetchSpy).toHaveBeenCalledWith('https://good.slack.com/ssb/redirect', expect.anything())
429
+ expect(setWorkspaceSpy).toHaveBeenCalledWith(expect.objectContaining({ workspace_id: 'T_GOOD' }))
430
+ })
431
+
432
+ it('rejects refresh result when testAuth returns empty team_id', async () => {
433
+ // given — the fresh token verifies but Slack returns no team_id
434
+ extractSpy.mockResolvedValue([
435
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale', cookie: 'xoxd-valid' },
436
+ ])
437
+ getWorkspaceDomainsSpy.mockReturnValue({ T_A: 'a' })
438
+ fetchSpy.mockResolvedValue(new Response('<html>"api_token":"xoxc-fresh"</html>', { status: 200 }))
439
+ testAuthSpy.mockResolvedValue({ user_id: 'U1', team_id: '', user: 'user', team: undefined })
440
+
441
+ // when
442
+ await ensureSlackAuth()
443
+
444
+ // then — nothing is saved despite successful refresh+login
445
+ expect(setWorkspaceSpy).not.toHaveBeenCalled()
446
+ })
447
+
448
+ it('does not resolve multiple unknown workspaces to the same team', async () => {
449
+ // given — two unknown tokens share a cookie that validates against the first candidate domain.
450
+ // Naive iteration would resolve both to T_A; the resolved-team-id tracker must prevent the
451
+ // second unknown from claiming an already-saved team.
452
+ extractSpy.mockResolvedValue([
453
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale-1', cookie: 'xoxd-valid' },
454
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale-2', cookie: 'xoxd-valid' },
455
+ ])
456
+ getWorkspaceDomainsSpy.mockReturnValue({ T_A: 'a', T_B: 'b' })
457
+
458
+ fetchSpy.mockImplementation((url: string) => {
459
+ if (url.startsWith('https://a.slack.com/')) {
460
+ return Promise.resolve(new Response('<html>"api_token":"xoxc-fresh-a"</html>', { status: 200 }))
461
+ }
462
+ if (url.startsWith('https://b.slack.com/')) {
463
+ return Promise.resolve(new Response('<html>"api_token":"xoxc-fresh-b"</html>', { status: 200 }))
464
+ }
465
+ return Promise.resolve(new Response('', { status: 500 }))
466
+ })
467
+ authResponseByToken({
468
+ 'xoxc-fresh-a': { team_id: 'T_A', team: 'A' },
469
+ 'xoxc-fresh-b': { team_id: 'T_B', team: 'B' },
470
+ })
471
+
472
+ // when
473
+ await ensureSlackAuth()
474
+
475
+ // then — T_A and T_B each saved exactly once
476
+ const savedIds = setWorkspaceSpy.mock.calls.map((c) => (c[0] as { workspace_id: string }).workspace_id)
477
+ expect(savedIds.sort()).toEqual(['T_A', 'T_B'])
478
+ })
479
+
480
+ it('caches refresh attempts per (cookie, domain) within one extraction', async () => {
481
+ // given — two unknown tokens with the same cookie; both will iterate the same domains
482
+ extractSpy.mockResolvedValue([
483
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale-1', cookie: 'xoxd-valid' },
484
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale-2', cookie: 'xoxd-valid' },
485
+ ])
486
+ getWorkspaceDomainsSpy.mockReturnValue({ T_A: 'a', T_B: 'b' })
487
+
488
+ fetchSpy.mockResolvedValue(new Response('', { status: 500 }))
489
+ testAuthSpy.mockRejectedValue(new Error('invalid_auth'))
490
+
491
+ // when
492
+ await ensureSlackAuth()
493
+
494
+ // then — each domain fetched at most once across both workspaces (2 total, not 4)
495
+ const refreshCalls = fetchSpy.mock.calls.filter((c) => String(c[0]).includes('/ssb/redirect'))
496
+ expect(refreshCalls.length).toBe(2)
497
+ })
498
+
499
+ it('caps domain attempts at MAX_DOMAIN_ATTEMPTS', async () => {
500
+ // given — 20 candidate domains; only the first 16 should be attempted
501
+ extractSpy.mockResolvedValue([
502
+ { workspace_id: 'unknown', workspace_name: 'unknown', token: 'xoxc-stale', cookie: 'xoxd-valid' },
503
+ ])
504
+ const manyDomains: Record<string, string> = {}
505
+ for (let i = 0; i < 20; i++) manyDomains[`T_${i}`] = `dom${i}`
506
+ getWorkspaceDomainsSpy.mockReturnValue(manyDomains)
507
+
508
+ fetchSpy.mockResolvedValue(new Response('', { status: 500 }))
509
+ testAuthSpy.mockRejectedValue(new Error('invalid_auth'))
510
+
511
+ // when
512
+ await ensureSlackAuth()
513
+
514
+ // then — exactly 16 refresh attempts
515
+ const refreshCalls = fetchSpy.mock.calls.filter((c) => String(c[0]).includes('/ssb/redirect'))
516
+ expect(refreshCalls.length).toBe(16)
517
+ })
518
+
297
519
  it('skips web refresh when workspace has no cookie', async () => {
298
520
  // given — cookie is empty
299
521
  extractSpy.mockResolvedValue([
@@ -21,20 +21,29 @@ export async function ensureSlackAuth(): Promise<void> {
21
21
  const workspaces = await extractor.extract()
22
22
  const workspaceDomains = extractor.getWorkspaceDomains()
23
23
 
24
- const validWorkspaces = []
24
+ const validWorkspaces: ExtractedWorkspace[] = []
25
+ const resolvedTeamIds = new Set<string>()
26
+ const refreshCache = new Map<string, RefreshResult | null>()
27
+
25
28
  for (const ws of workspaces) {
26
29
  try {
27
30
  const client = await new SlackClient().login({ token: ws.token, cookie: ws.cookie })
28
31
  const authInfo = await client.testAuth()
32
+ if (!authInfo.team_id) throw new Error('testAuth returned empty team_id')
29
33
  ws.workspace_id = authInfo.team_id
30
34
  ws.workspace_name = authInfo.team || ws.workspace_name
31
- await credManager.setWorkspace(ws)
32
- validWorkspaces.push(ws)
35
+ if (!resolvedTeamIds.has(ws.workspace_id)) {
36
+ resolvedTeamIds.add(ws.workspace_id)
37
+ await credManager.setWorkspace(ws)
38
+ validWorkspaces.push(ws)
39
+ }
33
40
  } catch {
34
- const refreshed = await tryWebTokenRefresh(ws, workspaceDomains)
35
- if (refreshed) {
41
+ const refreshed = await tryWebTokenRefresh(ws, workspaceDomains, { resolvedTeamIds, refreshCache })
42
+ if (refreshed && !resolvedTeamIds.has(refreshed.workspace_id)) {
36
43
  ws.token = refreshed.token
44
+ ws.workspace_id = refreshed.workspace_id
37
45
  ws.workspace_name = refreshed.workspace_name
46
+ resolvedTeamIds.add(ws.workspace_id)
38
47
  await credManager.setWorkspace(ws)
39
48
  validWorkspaces.push(ws)
40
49
  }
@@ -54,21 +63,81 @@ export async function ensureSlackAuth(): Promise<void> {
54
63
  }
55
64
  }
56
65
 
66
+ // Bound worst-case HTTP traffic for users with very large root-state.json workspace lists.
67
+ const MAX_DOMAIN_ATTEMPTS = 16
68
+
69
+ // Slack workspace subdomains match `^[a-z][a-z0-9-]*$` per signup rules; validating here
70
+ // prevents a tampered root-state.json from steering the cookie-bearing fetch to a non-Slack
71
+ // host (e.g. a domain value containing `.`, `/`, `:`, `@`, `#`, or `?`).
72
+ const SLACK_DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/
73
+
74
+ export type RefreshResult = { token: string; workspace_id: string; workspace_name: string }
75
+
76
+ export type RefreshContext = {
77
+ resolvedTeamIds?: Set<string>
78
+ refreshCache?: Map<string, RefreshResult | null>
79
+ }
80
+
57
81
  export async function tryWebTokenRefresh(
58
82
  ws: ExtractedWorkspace,
59
83
  workspaceDomains: Record<string, string>,
60
- ): Promise<{ token: string; workspace_name: string } | null> {
84
+ context: RefreshContext = {},
85
+ ): Promise<RefreshResult | null> {
61
86
  if (!ws.cookie) return null
62
- const domain = workspaceDomains[ws.workspace_id]
63
- if (!domain) return null
64
87
 
88
+ // Try the domain that maps to this workspace_id first (when known), then fall back to
89
+ // every other known domain. Slack tokens extracted from LevelDB blobs sometimes carry
90
+ // workspace_id="unknown" because the regex window around the xoxc- bytes does not
91
+ // contain the T... team id; in that case the cookie may still be valid for one of the
92
+ // other workspaces the user is signed into.
93
+ const candidates = orderCandidateDomains(ws.workspace_id, workspaceDomains)
94
+ if (candidates.length === 0) return null
95
+
96
+ for (const { domain } of candidates) {
97
+ if (!SLACK_DOMAIN_REGEX.test(domain)) continue
98
+
99
+ const cacheKey = `${ws.cookie}\u0000${domain}`
100
+ const cached = context.refreshCache?.get(cacheKey)
101
+ let result: RefreshResult | null
102
+ if (cached !== undefined) {
103
+ result = cached
104
+ } else {
105
+ result = await refreshAndVerify(ws.cookie, domain, ws.workspace_name)
106
+ context.refreshCache?.set(cacheKey, result)
107
+ }
108
+ if (!result) continue
109
+ if (context.resolvedTeamIds?.has(result.workspace_id)) continue
110
+ return result
111
+ }
112
+ return null
113
+ }
114
+
115
+ function orderCandidateDomains(
116
+ workspaceId: string,
117
+ workspaceDomains: Record<string, string>,
118
+ ): Array<{ workspace_id: string; domain: string }> {
119
+ const entries = Object.entries(workspaceDomains).map(([workspace_id, domain]) => ({ workspace_id, domain }))
120
+ const exact = entries.findIndex((e) => e.workspace_id === workspaceId)
121
+ if (exact > 0) {
122
+ const [match] = entries.splice(exact, 1)
123
+ entries.unshift(match)
124
+ }
125
+ return entries.slice(0, MAX_DOMAIN_ATTEMPTS)
126
+ }
127
+
128
+ async function refreshAndVerify(cookie: string, domain: string, fallbackName: string): Promise<RefreshResult | null> {
65
129
  try {
66
- const freshToken = await refreshTokenFromWeb(domain, ws.cookie)
130
+ const freshToken = await refreshTokenFromWeb(domain, cookie)
67
131
  if (!freshToken) return null
68
132
 
69
- const client = await new SlackClient().login({ token: freshToken, cookie: ws.cookie })
133
+ const client = await new SlackClient().login({ token: freshToken, cookie })
70
134
  const authInfo = await client.testAuth()
71
- return { token: freshToken, workspace_name: authInfo.team || ws.workspace_name }
135
+ if (!authInfo.team_id) return null
136
+ return {
137
+ token: freshToken,
138
+ workspace_id: authInfo.team_id,
139
+ workspace_name: authInfo.team || fallbackName,
140
+ }
72
141
  } catch {
73
142
  return null
74
143
  }