agent-messenger 2.27.2 → 2.29.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/README.md +11 -1
- package/.claude-plugin/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/AGENTS.md +4 -0
- package/README.md +58 -10
- package/dist/package.json +10 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/discord/client.d.ts +6 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +10 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discordbot/cli.js +1 -1
- package/dist/src/platforms/imessage/cli.d.ts +5 -0
- package/dist/src/platforms/imessage/cli.d.ts.map +1 -0
- package/dist/src/platforms/imessage/cli.js +31 -0
- package/dist/src/platforms/imessage/cli.js.map +1 -0
- package/dist/src/platforms/imessage/client.d.ts +43 -0
- package/dist/src/platforms/imessage/client.d.ts.map +1 -0
- package/dist/src/platforms/imessage/client.js +190 -0
- package/dist/src/platforms/imessage/client.js.map +1 -0
- package/dist/src/platforms/imessage/commands/auth.d.ts +31 -0
- package/dist/src/platforms/imessage/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/auth.js +100 -0
- package/dist/src/platforms/imessage/commands/auth.js.map +1 -0
- package/dist/src/platforms/imessage/commands/chat.d.ts +3 -0
- package/dist/src/platforms/imessage/commands/chat.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/chat.js +42 -0
- package/dist/src/platforms/imessage/commands/chat.js.map +1 -0
- package/dist/src/platforms/imessage/commands/doctor.d.ts +25 -0
- package/dist/src/platforms/imessage/commands/doctor.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/doctor.js +101 -0
- package/dist/src/platforms/imessage/commands/doctor.js.map +1 -0
- package/dist/src/platforms/imessage/commands/index.d.ts +7 -0
- package/dist/src/platforms/imessage/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/index.js +7 -0
- package/dist/src/platforms/imessage/commands/index.js.map +1 -0
- package/dist/src/platforms/imessage/commands/message.d.ts +5 -0
- package/dist/src/platforms/imessage/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/message.js +127 -0
- package/dist/src/platforms/imessage/commands/message.js.map +1 -0
- package/dist/src/platforms/imessage/commands/setup.d.ts +20 -0
- package/dist/src/platforms/imessage/commands/setup.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/setup.js +57 -0
- package/dist/src/platforms/imessage/commands/setup.js.map +1 -0
- package/dist/src/platforms/imessage/commands/shared.d.ts +10 -0
- package/dist/src/platforms/imessage/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/shared.js +53 -0
- package/dist/src/platforms/imessage/commands/shared.js.map +1 -0
- package/dist/src/platforms/imessage/commands/whoami.d.ts +3 -0
- package/dist/src/platforms/imessage/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/whoami.js +20 -0
- package/dist/src/platforms/imessage/commands/whoami.js.map +1 -0
- package/dist/src/platforms/imessage/credential-manager.d.ts +22 -0
- package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/imessage/credential-manager.js +111 -0
- package/dist/src/platforms/imessage/credential-manager.js.map +1 -0
- package/dist/src/platforms/imessage/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/imessage/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/imessage/ensure-auth.js +13 -0
- package/dist/src/platforms/imessage/ensure-auth.js.map +1 -0
- package/dist/src/platforms/imessage/errors.d.ts +3 -0
- package/dist/src/platforms/imessage/errors.d.ts.map +1 -0
- package/dist/src/platforms/imessage/errors.js +25 -0
- package/dist/src/platforms/imessage/errors.js.map +1 -0
- package/dist/src/platforms/imessage/index.d.ts +7 -0
- package/dist/src/platforms/imessage/index.d.ts.map +1 -0
- package/dist/src/platforms/imessage/index.js +5 -0
- package/dist/src/platforms/imessage/index.js.map +1 -0
- package/dist/src/platforms/imessage/rpc.d.ts +20 -0
- package/dist/src/platforms/imessage/rpc.d.ts.map +1 -0
- package/dist/src/platforms/imessage/rpc.js +133 -0
- package/dist/src/platforms/imessage/rpc.js.map +1 -0
- package/dist/src/platforms/imessage/types.d.ts +59 -0
- package/dist/src/platforms/imessage/types.d.ts.map +1 -0
- package/dist/src/platforms/imessage/types.js +29 -0
- package/dist/src/platforms/imessage/types.js.map +1 -0
- package/dist/src/platforms/instagram/cli.js +1 -1
- package/dist/src/platforms/instagram/client.d.ts +29 -2
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +360 -53
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +123 -11
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.js +17 -1
- package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
- package/dist/src/platforms/instagram/hybrid-listener.d.ts +48 -0
- package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -0
- package/dist/src/platforms/instagram/hybrid-listener.js +154 -0
- package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -0
- package/dist/src/platforms/instagram/index.d.ts +2 -0
- package/dist/src/platforms/instagram/index.d.ts.map +1 -1
- package/dist/src/platforms/instagram/index.js +2 -0
- package/dist/src/platforms/instagram/index.js.map +1 -1
- package/dist/src/platforms/instagram/mqtt/connection.d.ts +4 -0
- package/dist/src/platforms/instagram/mqtt/connection.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/connection.js +66 -0
- package/dist/src/platforms/instagram/mqtt/connection.js.map +1 -0
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts +22 -0
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/thrift.js +143 -0
- package/dist/src/platforms/instagram/mqtt/thrift.js.map +1 -0
- package/dist/src/platforms/instagram/mqtt/transport.d.ts +42 -0
- package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/transport.js +246 -0
- package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -0
- package/dist/src/platforms/instagram/realtime-listener.d.ts +33 -0
- package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -0
- package/dist/src/platforms/instagram/realtime-listener.js +151 -0
- package/dist/src/platforms/instagram/realtime-listener.js.map +1 -0
- package/dist/src/platforms/instagram/types.d.ts +10 -8
- package/dist/src/platforms/instagram/types.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.js.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +1 -1
- package/dist/src/platforms/line/cli.js +1 -1
- package/dist/src/platforms/teams/app-config.d.ts +36 -0
- package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
- package/dist/src/platforms/teams/app-config.js +69 -0
- package/dist/src/platforms/teams/app-config.js.map +1 -0
- package/dist/src/platforms/teams/client.d.ts +19 -2
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +295 -7
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +137 -3
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts +3 -0
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +59 -2
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts +11 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +121 -2
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +46 -2
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/device-code.d.ts +48 -0
- package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-code.js +199 -0
- package/dist/src/platforms/teams/device-code.js.map +1 -0
- package/dist/src/platforms/teams/device-login.d.ts +37 -0
- package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-login.js +129 -0
- package/dist/src/platforms/teams/device-login.js.map +1 -0
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +12 -0
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/index.d.ts +7 -3
- package/dist/src/platforms/teams/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/index.js +5 -2
- package/dist/src/platforms/teams/index.js.map +1 -1
- package/dist/src/platforms/teams/listener.d.ts +35 -0
- package/dist/src/platforms/teams/listener.d.ts.map +1 -0
- package/dist/src/platforms/teams/listener.js +236 -0
- package/dist/src/platforms/teams/listener.js.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.js +33 -0
- package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +60 -0
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/token-provider.d.ts +25 -0
- package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
- package/dist/src/platforms/teams/token-provider.js +190 -0
- package/dist/src/platforms/teams/token-provider.js.map +1 -0
- package/dist/src/platforms/teams/trouter.d.ts +31 -0
- package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
- package/dist/src/platforms/teams/trouter.js +181 -0
- package/dist/src/platforms/teams/trouter.js.map +1 -0
- package/dist/src/platforms/teams/types.d.ts +94 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +30 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/cli.js +1 -1
- package/dist/src/platforms/webex/types.d.ts +1 -1
- package/dist/src/platforms/webexbot/cli.js +1 -1
- package/dist/src/platforms/wechatbot/cli.js +1 -1
- package/dist/src/platforms/whatsapp/cli.js +1 -1
- package/dist/src/platforms/whatsappbot/cli.js +1 -1
- package/dist/src/tui/adapters/imessage-adapter.d.ts +21 -0
- package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -0
- package/dist/src/tui/adapters/imessage-adapter.js +110 -0
- package/dist/src/tui/adapters/imessage-adapter.js.map +1 -0
- package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/instagram-adapter.js +16 -0
- package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
- package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.js +21 -0
- package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +12 -3
- package/dist/src/tui/app.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +1 -1
- package/docs/content/docs/cli/imessage.mdx +99 -0
- package/docs/content/docs/cli/instagram.mdx +2 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/teams.mdx +22 -0
- package/docs/content/docs/index.mdx +9 -6
- package/docs/content/docs/quick-start.mdx +2 -0
- package/docs/content/docs/sdk/instagram.mdx +90 -5
- package/docs/content/docs/tui.mdx +4 -3
- package/package.json +10 -2
- package/scripts/postbuild.ts +12 -15
- 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-imessage/SKILL.md +133 -0
- package/skills/agent-imessage/references/authentication.md +66 -0
- package/skills/agent-imessage/references/permissions.md +31 -0
- package/skills/agent-imessage/references/setup.md +49 -0
- package/skills/agent-instagram/SKILL.md +72 -2
- package/skills/agent-instagram/references/authentication.md +11 -0
- package/skills/agent-instagram/references/common-patterns.md +36 -0
- package/skills/agent-instagram/templates/monitor-chat.sh +5 -0
- 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 +59 -6
- package/skills/agent-teams/references/authentication.md +31 -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-webexbot/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/cli.ts +4 -0
- package/src/platforms/discord/client.test.ts +86 -0
- package/src/platforms/discord/client.ts +10 -3
- package/src/platforms/imessage/cli.ts +39 -0
- package/src/platforms/imessage/client.test.ts +137 -0
- package/src/platforms/imessage/client.ts +253 -0
- package/src/platforms/imessage/commands/auth.ts +142 -0
- package/src/platforms/imessage/commands/chat.ts +51 -0
- package/src/platforms/imessage/commands/doctor.test.ts +80 -0
- package/src/platforms/imessage/commands/doctor.ts +139 -0
- package/src/platforms/imessage/commands/index.ts +6 -0
- package/src/platforms/imessage/commands/message.test.ts +20 -0
- package/src/platforms/imessage/commands/message.ts +151 -0
- package/src/platforms/imessage/commands/setup.test.ts +52 -0
- package/src/platforms/imessage/commands/setup.ts +76 -0
- package/src/platforms/imessage/commands/shared.test.ts +19 -0
- package/src/platforms/imessage/commands/shared.ts +69 -0
- package/src/platforms/imessage/commands/whoami.ts +22 -0
- package/src/platforms/imessage/credential-manager.test.ts +122 -0
- package/src/platforms/imessage/credential-manager.ts +123 -0
- package/src/platforms/imessage/ensure-auth.ts +18 -0
- package/src/platforms/imessage/errors.ts +27 -0
- package/src/platforms/imessage/index.test.ts +13 -0
- package/src/platforms/imessage/index.ts +16 -0
- package/src/platforms/imessage/rpc.ts +157 -0
- package/src/platforms/imessage/test-stub-imsg.mjs +147 -0
- package/src/platforms/imessage/types.test.ts +47 -0
- package/src/platforms/imessage/types.ts +95 -0
- package/src/platforms/instagram/client.test.ts +478 -33
- package/src/platforms/instagram/client.ts +472 -68
- package/src/platforms/instagram/commands/auth.test.ts +177 -1
- package/src/platforms/instagram/commands/auth.ts +182 -11
- package/src/platforms/instagram/credential-manager.test.ts +30 -0
- package/src/platforms/instagram/credential-manager.ts +23 -1
- package/src/platforms/instagram/hybrid-listener.test.ts +135 -0
- package/src/platforms/instagram/hybrid-listener.ts +196 -0
- package/src/platforms/instagram/index.ts +10 -0
- package/src/platforms/instagram/mqtt/connection.ts +72 -0
- package/src/platforms/instagram/mqtt/thrift.test.ts +91 -0
- package/src/platforms/instagram/mqtt/thrift.ts +159 -0
- package/src/platforms/instagram/mqtt/transport.ts +285 -0
- package/src/platforms/instagram/realtime-listener.test.ts +34 -0
- package/src/platforms/instagram/realtime-listener.ts +201 -0
- package/src/platforms/instagram/types.ts +11 -8
- package/src/platforms/teams/app-config.ts +92 -0
- package/src/platforms/teams/client.test.ts +335 -1
- package/src/platforms/teams/client.ts +342 -7
- package/src/platforms/teams/commands/auth.test.ts +93 -1
- package/src/platforms/teams/commands/auth.ts +182 -3
- package/src/platforms/teams/commands/file.test.ts +92 -1
- package/src/platforms/teams/commands/file.ts +76 -1
- package/src/platforms/teams/commands/message.test.ts +95 -1
- package/src/platforms/teams/commands/message.ts +145 -3
- package/src/platforms/teams/credential-manager.test.ts +20 -0
- package/src/platforms/teams/credential-manager.ts +68 -3
- package/src/platforms/teams/device-code.test.ts +311 -0
- package/src/platforms/teams/device-code.ts +277 -0
- package/src/platforms/teams/device-login.test.ts +288 -0
- package/src/platforms/teams/device-login.ts +206 -0
- package/src/platforms/teams/ensure-auth.ts +11 -0
- package/src/platforms/teams/index.test.ts +10 -0
- package/src/platforms/teams/index.ts +16 -1
- package/src/platforms/teams/listener.ts +295 -0
- package/src/platforms/teams/realm-discovery.test.ts +73 -0
- package/src/platforms/teams/realm-discovery.ts +42 -0
- package/src/platforms/teams/token-extractor.ts +69 -0
- package/src/platforms/teams/token-provider.test.ts +227 -0
- package/src/platforms/teams/token-provider.ts +284 -0
- package/src/platforms/teams/trouter.test.ts +114 -0
- package/src/platforms/teams/trouter.ts +228 -0
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +86 -0
- package/src/platforms/webexbot/cli.ts +0 -0
- package/src/tui/adapters/imessage-adapter.ts +131 -0
- package/src/tui/adapters/instagram-adapter.ts +13 -0
- package/src/tui/adapters/teams-adapter.ts +23 -0
- package/src/tui/app.ts +16 -3
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import { readFile } from 'node:fs/promises'
|
|
2
3
|
import { basename } from 'node:path'
|
|
3
4
|
|
|
5
|
+
import { SUBSTRATE_SEARCH_URL } from './app-config'
|
|
4
6
|
import { TeamsCredentialManager } from './credential-manager'
|
|
7
|
+
import { TeamsTokenProvider } from './token-provider'
|
|
5
8
|
import type {
|
|
6
9
|
TeamsAccountType,
|
|
7
10
|
TeamsChannel,
|
|
@@ -10,6 +13,7 @@ import type {
|
|
|
10
13
|
TeamsFile,
|
|
11
14
|
TeamsMessage,
|
|
12
15
|
TeamsRegion,
|
|
16
|
+
TeamsSearchResult,
|
|
13
17
|
TeamsTeam,
|
|
14
18
|
TeamsUser,
|
|
15
19
|
} from './types'
|
|
@@ -20,12 +24,35 @@ interface RateLimitBucket {
|
|
|
20
24
|
resetAt: number
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
interface RawTeamsMessage extends TeamsMessage {
|
|
28
|
+
rootMessageId?: string
|
|
29
|
+
parentMessageId?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type JsonRecord = Record<string, unknown>
|
|
33
|
+
|
|
23
34
|
const PERSONAL_MSG_API_BASE = 'https://msgapi.teams.live.com/v1'
|
|
24
35
|
const CSA_API_BASE = 'https://teams.microsoft.com/api'
|
|
25
36
|
const MAX_RETRIES = 3
|
|
26
37
|
const BASE_BACKOFF_MS = 100
|
|
27
38
|
const DEFAULT_REGION: TeamsRegion = 'amer'
|
|
28
39
|
const REGIONS: TeamsRegion[] = ['amer', 'emea', 'apac']
|
|
40
|
+
const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0'
|
|
41
|
+
|
|
42
|
+
// Personal (Teams for Life) skypetokens carry a consumer `skypeid` (e.g.
|
|
43
|
+
// "live:..." or "8:live:..."); work/school tokens carry an org identity. Used
|
|
44
|
+
// only to guess the account type when a caller logs in with a bare token.
|
|
45
|
+
function isPersonalToken(token: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {
|
|
48
|
+
skypeid?: string
|
|
49
|
+
}
|
|
50
|
+
const skypeId = payload.skypeid ?? ''
|
|
51
|
+
return skypeId.includes('live:') || skypeId.startsWith('8:live:')
|
|
52
|
+
} catch {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
29
56
|
|
|
30
57
|
function stripHtml(content: string | undefined): string | undefined {
|
|
31
58
|
if (content === undefined) return undefined
|
|
@@ -40,6 +67,169 @@ function stripHtml(content: string | undefined): string | undefined {
|
|
|
40
67
|
.trim()
|
|
41
68
|
}
|
|
42
69
|
|
|
70
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
71
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stringFrom(record: JsonRecord, keys: string[]): string | undefined {
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const value = record[key]
|
|
77
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
78
|
+
}
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function recordFrom(record: JsonRecord, keys: string[]): JsonRecord | undefined {
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
const value = record[key]
|
|
85
|
+
if (isRecord(value)) return value
|
|
86
|
+
}
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function arrayFrom(record: JsonRecord, keys: string[]): unknown[] {
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
const value = record[key]
|
|
93
|
+
if (Array.isArray(value)) return value
|
|
94
|
+
}
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function propertyValue(record: JsonRecord, names: string[]): string | undefined {
|
|
99
|
+
const properties = arrayFrom(record, ['Properties', 'properties'])
|
|
100
|
+
const normalized = names.map((name) => name.toLowerCase())
|
|
101
|
+
for (const property of properties) {
|
|
102
|
+
if (!isRecord(property)) continue
|
|
103
|
+
const name = stringFrom(property, ['Name', 'name', 'Key', 'key'])
|
|
104
|
+
if (!name || !normalized.includes(name.toLowerCase())) continue
|
|
105
|
+
const value = property.Value ?? property.value
|
|
106
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
107
|
+
}
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resultString(record: JsonRecord, keys: string[]): string | undefined {
|
|
112
|
+
return stringFrom(record, keys) ?? propertyValue(record, keys)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseSubstrateResult(value: unknown): TeamsSearchResult | null {
|
|
116
|
+
if (!isRecord(value)) return null
|
|
117
|
+
const author = recordFrom(value, ['Author', 'author', 'From', 'from'])
|
|
118
|
+
const id = resultString(value, ['id', 'Id', 'ReferenceId', 'MessageId'])
|
|
119
|
+
const channelId = resultString(value, ['channel_id', 'ChannelId', 'ConversationId', 'ThreadId'])
|
|
120
|
+
if (!id || !channelId) return null
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
content:
|
|
125
|
+
stripHtml(resultString(value, ['content', 'Content', 'HitHighlightedSummary', 'Summary', 'Preview'])) ?? '',
|
|
126
|
+
author: {
|
|
127
|
+
id: author ? (stringFrom(author, ['id', 'Id', 'ObjectId']) ?? '') : (propertyValue(value, ['AuthorId']) ?? ''),
|
|
128
|
+
displayName: author
|
|
129
|
+
? (stringFrom(author, ['displayName', 'DisplayName', 'Name']) ?? 'Unknown')
|
|
130
|
+
: (propertyValue(value, ['AuthorDisplayName', 'Author']) ?? 'Unknown'),
|
|
131
|
+
},
|
|
132
|
+
channel_id: channelId,
|
|
133
|
+
thread_id: resultString(value, ['thread_id', 'ThreadId']),
|
|
134
|
+
team_name: resultString(value, ['team_name', 'TeamName']),
|
|
135
|
+
channel_name: resultString(value, ['channel_name', 'ChannelName']),
|
|
136
|
+
timestamp: resultString(value, ['timestamp', 'Timestamp', 'DateTimeSent', 'LastModifiedTime']) ?? '',
|
|
137
|
+
permalink: resultString(value, ['permalink', 'Permalink', 'WebUrl', 'Url']),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseSubstrateResults(data: unknown): TeamsSearchResult[] {
|
|
142
|
+
if (!isRecord(data)) return []
|
|
143
|
+
const results: TeamsSearchResult[] = []
|
|
144
|
+
for (const entitySet of arrayFrom(data, ['EntitySets', 'entitySets'])) {
|
|
145
|
+
if (!isRecord(entitySet)) continue
|
|
146
|
+
for (const resultSet of arrayFrom(entitySet, ['ResultSets', 'resultSets'])) {
|
|
147
|
+
if (!isRecord(resultSet)) continue
|
|
148
|
+
for (const rawResult of arrayFrom(resultSet, ['Results', 'results'])) {
|
|
149
|
+
const result = parseSubstrateResult(rawResult)
|
|
150
|
+
if (result) results.push(result)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateSearchLimit(value: number | undefined): number {
|
|
158
|
+
if (value === undefined) return 20
|
|
159
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
160
|
+
throw new TeamsError('Search limit must be a positive integer.', 'invalid_pagination')
|
|
161
|
+
}
|
|
162
|
+
return value
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateSearchFrom(value: number | undefined): number {
|
|
166
|
+
if (value === undefined) return 0
|
|
167
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
168
|
+
throw new TeamsError('Search from offset must be a non-negative integer.', 'invalid_pagination')
|
|
169
|
+
}
|
|
170
|
+
return value
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isSharePointOrOneDriveUrl(url: string): boolean {
|
|
174
|
+
try {
|
|
175
|
+
const { hostname } = new URL(url)
|
|
176
|
+
const normalizedHost = hostname.toLowerCase()
|
|
177
|
+
return (
|
|
178
|
+
normalizedHost.includes('sharepoint.com') ||
|
|
179
|
+
normalizedHost.includes('-my.sharepoint') ||
|
|
180
|
+
normalizedHost === '1drv.ms' ||
|
|
181
|
+
normalizedHost.endsWith('.1drv.ms') ||
|
|
182
|
+
normalizedHost === 'onedrive.live.com' ||
|
|
183
|
+
normalizedHost.endsWith('.onedrive.live.com')
|
|
184
|
+
)
|
|
185
|
+
} catch {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Only these hosts receive the Skype token on a raw download fetch. Teams file
|
|
191
|
+
// metadata can carry arbitrary URLs, so we never attach credentials to a host
|
|
192
|
+
// outside this allowlist — that would leak the token to a third party.
|
|
193
|
+
function isTrustedSkypeDownloadHost(url: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
const host = new URL(url).hostname.toLowerCase()
|
|
196
|
+
return (
|
|
197
|
+
host === 'teams.microsoft.com' ||
|
|
198
|
+
host.endsWith('.teams.microsoft.com') ||
|
|
199
|
+
host.endsWith('.asm.skype.com') ||
|
|
200
|
+
host.endsWith('.asyncgw.teams.microsoft.com') ||
|
|
201
|
+
host === 'substrate.office.com' ||
|
|
202
|
+
host.endsWith('.substrate.office.com')
|
|
203
|
+
)
|
|
204
|
+
} catch {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getFileDownloadSource(file: TeamsFile): { route: 'graph' | 'skype'; url: string } {
|
|
210
|
+
const shareUrl = [file.sharepoint_url, file.url, file.object_url].find((candidate): candidate is string =>
|
|
211
|
+
Boolean(candidate && isSharePointOrOneDriveUrl(candidate)),
|
|
212
|
+
)
|
|
213
|
+
if (shareUrl) return { route: 'graph', url: shareUrl }
|
|
214
|
+
|
|
215
|
+
const directUrl = file.object_url ?? file.url
|
|
216
|
+
if (!directUrl) {
|
|
217
|
+
throw new TeamsError(`File has no downloadable URL: ${file.id}`, 'file_url_missing')
|
|
218
|
+
}
|
|
219
|
+
if (!isTrustedSkypeDownloadHost(directUrl)) {
|
|
220
|
+
throw new TeamsError(`Refusing to download ${file.id} from an untrusted host: ${directUrl}`, 'file_url_untrusted')
|
|
221
|
+
}
|
|
222
|
+
return { route: 'skype', url: directUrl }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function readDownloadResponse(response: Response, codePrefix: string): Promise<Buffer> {
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new TeamsError(`File download failed with HTTP ${response.status}`, `${codePrefix}_${response.status}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Buffer.from(await response.arrayBuffer())
|
|
231
|
+
}
|
|
232
|
+
|
|
43
233
|
function escapeHtml(value: string): string {
|
|
44
234
|
return value
|
|
45
235
|
.replaceAll('&', '&')
|
|
@@ -49,6 +239,23 @@ function escapeHtml(value: string): string {
|
|
|
49
239
|
.replaceAll("'", ''')
|
|
50
240
|
}
|
|
51
241
|
|
|
242
|
+
function withThreadMetadata(message: RawTeamsMessage, rootMessageId?: string): TeamsMessage {
|
|
243
|
+
const { rootMessageId: messageRootMessageId, parentMessageId, ...teamsMessage } = message
|
|
244
|
+
const rawRootMessageId = rootMessageId ?? messageRootMessageId
|
|
245
|
+
const isThreadReply = Boolean(
|
|
246
|
+
rootMessageId ||
|
|
247
|
+
(messageRootMessageId !== undefined && messageRootMessageId !== message.id) ||
|
|
248
|
+
(parentMessageId !== undefined && parentMessageId !== message.id),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...teamsMessage,
|
|
253
|
+
root_message_id: isThreadReply ? rawRootMessageId : undefined,
|
|
254
|
+
parent_message_id: isThreadReply ? (parentMessageId ?? rawRootMessageId) : undefined,
|
|
255
|
+
is_thread_reply: isThreadReply ? true : undefined,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
52
259
|
// groupId => Teams/channel thread (handled by listTeams). "48:notes"/
|
|
53
260
|
// streamofnotes => the user's self ("to me") chat. Anything else without a
|
|
54
261
|
// non-chat threadType is a normal 1:1 (no topic) or group (has topic) chat.
|
|
@@ -68,9 +275,12 @@ export class TeamsClient {
|
|
|
68
275
|
private isPersonalAccount: boolean = false
|
|
69
276
|
private region: TeamsRegion = DEFAULT_REGION
|
|
70
277
|
private regionDiscovered: boolean = false
|
|
278
|
+
private tokenProvider?: TeamsTokenProvider
|
|
71
279
|
private buckets: Map<string, RateLimitBucket> = new Map()
|
|
72
280
|
private globalRateLimitUntil: number = 0
|
|
73
281
|
|
|
282
|
+
constructor(private credManager: TeamsCredentialManager = new TeamsCredentialManager()) {}
|
|
283
|
+
|
|
74
284
|
async login(credentials?: {
|
|
75
285
|
token: string
|
|
76
286
|
tokenExpiresAt?: string
|
|
@@ -85,18 +295,22 @@ export class TeamsClient {
|
|
|
85
295
|
if (credentials.tokenExpiresAt) {
|
|
86
296
|
this.tokenExpiresAt = new Date(credentials.tokenExpiresAt)
|
|
87
297
|
}
|
|
88
|
-
this.isPersonalAccount = credentials.accountType
|
|
298
|
+
this.isPersonalAccount = credentials.accountType
|
|
299
|
+
? credentials.accountType === 'personal'
|
|
300
|
+
: isPersonalToken(credentials.token)
|
|
89
301
|
if (credentials.region) {
|
|
90
302
|
this.region = credentials.region
|
|
91
303
|
this.regionDiscovered = true
|
|
92
304
|
}
|
|
305
|
+
if (credentials.accountType) {
|
|
306
|
+
this.getTokenProvider().bindAccount(credentials.accountType)
|
|
307
|
+
}
|
|
93
308
|
return this
|
|
94
309
|
}
|
|
95
310
|
|
|
96
311
|
const { ensureTeamsAuth } = await import('./ensure-auth')
|
|
97
312
|
await ensureTeamsAuth()
|
|
98
|
-
const
|
|
99
|
-
const creds = await credManager.getTokenWithExpiry()
|
|
313
|
+
const creds = await this.credManager.getTokenWithExpiry()
|
|
100
314
|
if (!creds) {
|
|
101
315
|
throw new TeamsError(
|
|
102
316
|
'No Teams credentials found. Make sure Microsoft Teams is logged in via the desktop app or a supported Chromium browser.',
|
|
@@ -115,6 +329,20 @@ export class TeamsClient {
|
|
|
115
329
|
return this.region
|
|
116
330
|
}
|
|
117
331
|
|
|
332
|
+
getToken(): string {
|
|
333
|
+
return this.ensureAuth()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
getAccountType(): TeamsAccountType {
|
|
337
|
+
return this.isPersonalAccount ? 'personal' : 'work'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getIdToken(): Promise<string | null> {
|
|
341
|
+
const { TeamsTokenExtractor } = await import('./token-extractor')
|
|
342
|
+
const extractor = new TeamsTokenExtractor()
|
|
343
|
+
return extractor.extractIdToken(this.getAccountType())
|
|
344
|
+
}
|
|
345
|
+
|
|
118
346
|
private ensureAuth(): string {
|
|
119
347
|
if (this.token === null) {
|
|
120
348
|
throw new TeamsError('Not authenticated. Call .login() first.', 'not_authenticated')
|
|
@@ -210,7 +438,7 @@ export class TeamsClient {
|
|
|
210
438
|
|
|
211
439
|
private async request<T>(method: string, path: string, body?: unknown, baseUrl?: string): Promise<T> {
|
|
212
440
|
if (this.isTokenExpired()) {
|
|
213
|
-
throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
|
|
441
|
+
throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
|
|
214
442
|
}
|
|
215
443
|
|
|
216
444
|
if (baseUrl === undefined && !this.regionDiscovered) {
|
|
@@ -279,7 +507,7 @@ export class TeamsClient {
|
|
|
279
507
|
|
|
280
508
|
private async requestFormData<T>(path: string, formData: FormData, baseUrl?: string): Promise<T> {
|
|
281
509
|
if (this.isTokenExpired()) {
|
|
282
|
-
throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
|
|
510
|
+
throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
|
|
283
511
|
}
|
|
284
512
|
|
|
285
513
|
if (baseUrl === undefined && !this.regionDiscovered) {
|
|
@@ -474,7 +702,17 @@ export class TeamsClient {
|
|
|
474
702
|
)
|
|
475
703
|
}
|
|
476
704
|
|
|
477
|
-
async sendMessage(teamId: string, channelId: string, content: string): Promise<TeamsMessage> {
|
|
705
|
+
async sendMessage(teamId: string, channelId: string, content: string, rootMessageId?: string): Promise<TeamsMessage> {
|
|
706
|
+
if (rootMessageId) {
|
|
707
|
+
const response = await this.request<RawTeamsMessage>(
|
|
708
|
+
'POST',
|
|
709
|
+
`/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies`,
|
|
710
|
+
{ content, parentMessageId: rootMessageId },
|
|
711
|
+
CSA_API_BASE,
|
|
712
|
+
)
|
|
713
|
+
return withThreadMetadata(response, rootMessageId)
|
|
714
|
+
}
|
|
715
|
+
|
|
478
716
|
return this.request<TeamsMessage>(
|
|
479
717
|
'POST',
|
|
480
718
|
`/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages`,
|
|
@@ -484,12 +722,75 @@ export class TeamsClient {
|
|
|
484
722
|
}
|
|
485
723
|
|
|
486
724
|
async getMessages(teamId: string, channelId: string, limit: number = 50): Promise<TeamsMessage[]> {
|
|
487
|
-
|
|
725
|
+
const messages = await this.request<RawTeamsMessage[]>(
|
|
488
726
|
'GET',
|
|
489
727
|
`/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`,
|
|
490
728
|
undefined,
|
|
491
729
|
CSA_API_BASE,
|
|
492
730
|
)
|
|
731
|
+
return messages.map((message) => withThreadMetadata(message))
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async getThreadReplies(
|
|
735
|
+
teamId: string,
|
|
736
|
+
channelId: string,
|
|
737
|
+
rootMessageId: string,
|
|
738
|
+
limit: number = 50,
|
|
739
|
+
): Promise<TeamsMessage[]> {
|
|
740
|
+
const replies = await this.request<RawTeamsMessage[]>(
|
|
741
|
+
'GET',
|
|
742
|
+
`/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies?limit=${limit}`,
|
|
743
|
+
undefined,
|
|
744
|
+
CSA_API_BASE,
|
|
745
|
+
)
|
|
746
|
+
return replies.map((reply) => withThreadMetadata(reply, rootMessageId))
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async searchMessages(query: string, opts: { limit?: number; from?: number } = {}): Promise<TeamsSearchResult[]> {
|
|
750
|
+
const size = validateSearchLimit(opts.limit)
|
|
751
|
+
const from = validateSearchFrom(opts.from)
|
|
752
|
+
const tokenProvider = this.getTokenProvider()
|
|
753
|
+
const substrateToken = await tokenProvider.getSubstrateToken()
|
|
754
|
+
const tenantId = await tokenProvider.getTenantId()
|
|
755
|
+
const userId = await tokenProvider.getUserId()
|
|
756
|
+
|
|
757
|
+
const response = await fetch(SUBSTRATE_SEARCH_URL, {
|
|
758
|
+
method: 'POST',
|
|
759
|
+
headers: {
|
|
760
|
+
Authorization: `Bearer ${substrateToken}`,
|
|
761
|
+
'Content-Type': 'application/json',
|
|
762
|
+
'x-anchormailbox': `Oid:${userId}@${tenantId}`,
|
|
763
|
+
},
|
|
764
|
+
body: JSON.stringify({
|
|
765
|
+
cvid: randomUUID(),
|
|
766
|
+
logicalId: randomUUID(),
|
|
767
|
+
query: { queryString: query },
|
|
768
|
+
entityRequests: [
|
|
769
|
+
{
|
|
770
|
+
entityType: 'Message',
|
|
771
|
+
contentSources: ['Teams'],
|
|
772
|
+
from,
|
|
773
|
+
size,
|
|
774
|
+
query: { queryString: query },
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
}),
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
const data = (await response.json().catch(() => ({}))) as unknown
|
|
781
|
+
if (!response.ok) {
|
|
782
|
+
const message = isRecord(data)
|
|
783
|
+
? stringFrom(data, ['message', 'Message', 'error_description', 'error'])
|
|
784
|
+
: undefined
|
|
785
|
+
throw new TeamsError(message ?? `HTTP ${response.status}`, `substrate_${response.status}`)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return parseSubstrateResults(data)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private getTokenProvider(): TeamsTokenProvider {
|
|
792
|
+
this.tokenProvider ??= new TeamsTokenProvider(this.credManager)
|
|
793
|
+
return this.tokenProvider
|
|
493
794
|
}
|
|
494
795
|
|
|
495
796
|
async getMessage(teamId: string, channelId: string, messageId: string): Promise<TeamsMessage> {
|
|
@@ -558,4 +859,38 @@ export class TeamsClient {
|
|
|
558
859
|
CSA_API_BASE,
|
|
559
860
|
)
|
|
560
861
|
}
|
|
862
|
+
|
|
863
|
+
async downloadFile(teamId: string, channelId: string, fileId: string): Promise<{ buffer: Buffer; file: TeamsFile }> {
|
|
864
|
+
const files = await this.listFiles(teamId, channelId)
|
|
865
|
+
const file = files.find((candidate) => candidate.id === fileId)
|
|
866
|
+
if (!file) {
|
|
867
|
+
throw new TeamsError(`File not found: ${fileId}`, 'file_not_found')
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const source = getFileDownloadSource(file)
|
|
871
|
+
if (source.route === 'graph') {
|
|
872
|
+
const graphToken = await new TeamsTokenProvider(this.credManager).getGraphToken()
|
|
873
|
+
const shareId = `u!${Buffer.from(source.url).toString('base64url').replace(/=+$/, '')}`
|
|
874
|
+
const response = await fetch(`${GRAPH_API_BASE}/shares/${shareId}/driveItem/content`, {
|
|
875
|
+
headers: {
|
|
876
|
+
Authorization: `Bearer ${graphToken}`,
|
|
877
|
+
},
|
|
878
|
+
redirect: 'follow',
|
|
879
|
+
})
|
|
880
|
+
return { buffer: await readDownloadResponse(response, 'graph_download'), file }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (this.isTokenExpired()) {
|
|
884
|
+
throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
|
|
885
|
+
}
|
|
886
|
+
const skypeToken = this.getToken()
|
|
887
|
+
const response = await fetch(source.url, {
|
|
888
|
+
headers: {
|
|
889
|
+
Authorization: `Bearer ${skypeToken}`,
|
|
890
|
+
'X-Skypetoken': skypeToken,
|
|
891
|
+
},
|
|
892
|
+
redirect: 'follow',
|
|
893
|
+
})
|
|
894
|
+
return { buffer: await readDownloadResponse(response, 'skype_download'), file }
|
|
895
|
+
}
|
|
561
896
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import * as interactive from '@/shared/utils/interactive'
|
|
4
|
+
|
|
3
5
|
import { TeamsClient } from '../client'
|
|
4
6
|
import { TeamsCredentialManager } from '../credential-manager'
|
|
7
|
+
import * as deviceLogin from '../device-login'
|
|
8
|
+
import * as realmDiscovery from '../realm-discovery'
|
|
5
9
|
import { TeamsTokenExtractor } from '../token-extractor'
|
|
6
|
-
import { getNoTeamsTokenFoundMessage } from './auth'
|
|
10
|
+
import { getNoTeamsTokenFoundMessage, loginAction } from './auth'
|
|
7
11
|
|
|
8
12
|
let extractorExtractSpy: ReturnType<typeof spyOn>
|
|
9
13
|
let clientTestAuthSpy: ReturnType<typeof spyOn>
|
|
@@ -13,6 +17,7 @@ let credManagerSaveConfigSpy: ReturnType<typeof spyOn>
|
|
|
13
17
|
let credManagerClearCredentialsSpy: ReturnType<typeof spyOn>
|
|
14
18
|
let credManagerIsTokenExpiredSpy: ReturnType<typeof spyOn>
|
|
15
19
|
let clientGetRegionSpy: ReturnType<typeof spyOn>
|
|
20
|
+
const originalConsoleLog = console.log
|
|
16
21
|
|
|
17
22
|
beforeEach(() => {
|
|
18
23
|
extractorExtractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue([
|
|
@@ -52,6 +57,7 @@ afterEach(() => {
|
|
|
52
57
|
credManagerClearCredentialsSpy?.mockRestore()
|
|
53
58
|
credManagerIsTokenExpiredSpy?.mockRestore()
|
|
54
59
|
clientGetRegionSpy?.mockRestore()
|
|
60
|
+
console.log = originalConsoleLog
|
|
55
61
|
})
|
|
56
62
|
|
|
57
63
|
it('extract: calls TeamsTokenExtractor', async () => {
|
|
@@ -98,3 +104,89 @@ it('status: checks token expiry', async () => {
|
|
|
98
104
|
it('no-token message mentions desktop app and browser fallback', () => {
|
|
99
105
|
expect(getNoTeamsTokenFoundMessage()).toContain('desktop app or a supported Chromium browser')
|
|
100
106
|
})
|
|
107
|
+
|
|
108
|
+
it('login pending hint preserves explicit account type', async () => {
|
|
109
|
+
const completeSpy = spyOn(deviceLogin, 'completeDeviceCode').mockRejectedValue(new deviceLogin.PendingApprovalError())
|
|
110
|
+
const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
111
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
112
|
+
throw new Error(`exit:${code}`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await expect(loginAction({ deviceCode: 'device-code', accountType: 'personal', pretty: false })).rejects.toThrow(
|
|
116
|
+
'exit:1',
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
expect(completeSpy).toHaveBeenCalled()
|
|
120
|
+
expect(exitSpy.mock.calls[0][0]).toBe(0)
|
|
121
|
+
const output = consoleSpy.mock.calls[0][0]
|
|
122
|
+
expect(output).toContain('agent-teams auth login --device-code <device_code> --account-type personal')
|
|
123
|
+
completeSpy.mockRestore()
|
|
124
|
+
consoleSpy.mockRestore()
|
|
125
|
+
exitSpy.mockRestore()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('routes to the personal flow when the email probe reports a personal account', async () => {
|
|
129
|
+
const interactiveSpy = spyOn(interactive, 'isInteractive').mockReturnValue(true)
|
|
130
|
+
const probeSpy = spyOn(realmDiscovery, 'probeAccountType').mockResolvedValue('personal')
|
|
131
|
+
const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
132
|
+
const loginSpy = spyOn(deviceLogin, 'loginWithDeviceCode').mockResolvedValue({
|
|
133
|
+
accountType: 'personal',
|
|
134
|
+
userName: 'Personal User',
|
|
135
|
+
teams: [],
|
|
136
|
+
current: null,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await loginAction({ email: 'user@outlook.com', pretty: false })
|
|
140
|
+
|
|
141
|
+
expect(probeSpy).toHaveBeenCalledWith('user@outlook.com')
|
|
142
|
+
expect(loginSpy.mock.calls[0][0].accountType).toBe('personal')
|
|
143
|
+
|
|
144
|
+
interactiveSpy.mockRestore()
|
|
145
|
+
probeSpy.mockRestore()
|
|
146
|
+
consoleSpy.mockRestore()
|
|
147
|
+
loginSpy.mockRestore()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('skips the email probe when an account type is forced explicitly', async () => {
|
|
151
|
+
const interactiveSpy = spyOn(interactive, 'isInteractive').mockReturnValue(true)
|
|
152
|
+
const probeSpy = spyOn(realmDiscovery, 'probeAccountType').mockResolvedValue('personal')
|
|
153
|
+
const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
154
|
+
const loginSpy = spyOn(deviceLogin, 'loginWithDeviceCode').mockResolvedValue({
|
|
155
|
+
accountType: 'work',
|
|
156
|
+
userName: 'Work User',
|
|
157
|
+
teams: [],
|
|
158
|
+
current: null,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await loginAction({ email: 'user@outlook.com', accountType: 'work', pretty: false })
|
|
162
|
+
|
|
163
|
+
expect(probeSpy).not.toHaveBeenCalled()
|
|
164
|
+
expect(loginSpy.mock.calls[0][0].accountType).toBe('work')
|
|
165
|
+
|
|
166
|
+
interactiveSpy.mockRestore()
|
|
167
|
+
probeSpy.mockRestore()
|
|
168
|
+
consoleSpy.mockRestore()
|
|
169
|
+
loginSpy.mockRestore()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('falls back to the default flow when the email probe is inconclusive', async () => {
|
|
173
|
+
const interactiveSpy = spyOn(interactive, 'isInteractive').mockReturnValue(true)
|
|
174
|
+
const probeSpy = spyOn(realmDiscovery, 'probeAccountType').mockResolvedValue(undefined)
|
|
175
|
+
const consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
176
|
+
const loginSpy = spyOn(deviceLogin, 'loginWithDeviceCode').mockResolvedValue({
|
|
177
|
+
accountType: 'work',
|
|
178
|
+
userName: 'Work User',
|
|
179
|
+
teams: [],
|
|
180
|
+
current: null,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
await loginAction({ email: 'user@unknown.test', pretty: false })
|
|
184
|
+
|
|
185
|
+
expect(probeSpy).toHaveBeenCalledWith('user@unknown.test')
|
|
186
|
+
expect(loginSpy.mock.calls[0][0].accountType).toBe('work')
|
|
187
|
+
|
|
188
|
+
interactiveSpy.mockRestore()
|
|
189
|
+
probeSpy.mockRestore()
|
|
190
|
+
consoleSpy.mockRestore()
|
|
191
|
+
loginSpy.mockRestore()
|
|
192
|
+
})
|