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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { getTeamsAppClientId } from './app-config'
|
|
2
|
+
import { TeamsClient } from './client'
|
|
3
|
+
import { TeamsCredentialManager } from './credential-manager'
|
|
4
|
+
import {
|
|
5
|
+
type AadToken,
|
|
6
|
+
type DeviceCodeInfo,
|
|
7
|
+
decodeJwtTid,
|
|
8
|
+
exchangeDeviceCode,
|
|
9
|
+
exchangeForSkypeScope,
|
|
10
|
+
isConsumerTenant,
|
|
11
|
+
mintSkypeToken,
|
|
12
|
+
pollDeviceToken,
|
|
13
|
+
requestDeviceCode,
|
|
14
|
+
resolveWorkTenantId,
|
|
15
|
+
} from './device-code'
|
|
16
|
+
import { type TeamsAccountType, TeamsError } from './types'
|
|
17
|
+
|
|
18
|
+
const PERSONAL_ACCOUNT_HINT =
|
|
19
|
+
'This looks like a personal Microsoft account. Run `agent-teams auth login --account-type personal` to sign in.'
|
|
20
|
+
|
|
21
|
+
export interface DeviceCodePrompt {
|
|
22
|
+
verificationUri: string
|
|
23
|
+
verificationUriComplete: string
|
|
24
|
+
userCode: string
|
|
25
|
+
expiresAt: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeviceLoginResult {
|
|
29
|
+
accountType: TeamsAccountType
|
|
30
|
+
userName: string
|
|
31
|
+
teams: Array<{ id: string; name: string }>
|
|
32
|
+
current: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface LoginCallbacks {
|
|
36
|
+
onCode: (prompt: DeviceCodePrompt) => void | Promise<void>
|
|
37
|
+
onPending?: () => void
|
|
38
|
+
debug?: (message: string) => void
|
|
39
|
+
clientId?: string
|
|
40
|
+
accountType?: TeamsAccountType
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function startDeviceCode(
|
|
44
|
+
clientIdOverride?: string,
|
|
45
|
+
accountType: TeamsAccountType = 'personal',
|
|
46
|
+
): Promise<{ info: DeviceCodeInfo; clientId: string }> {
|
|
47
|
+
const clientId = clientIdOverride ?? getTeamsAppClientId(accountType).clientId
|
|
48
|
+
const info = await requestDeviceCode(clientId, accountType)
|
|
49
|
+
return { info, clientId }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function completeDeviceCode(
|
|
53
|
+
deviceCode: string,
|
|
54
|
+
clientId: string,
|
|
55
|
+
credManager: TeamsCredentialManager = new TeamsCredentialManager(),
|
|
56
|
+
accountType: TeamsAccountType = 'personal',
|
|
57
|
+
): Promise<DeviceLoginResult> {
|
|
58
|
+
const first = await exchangeDeviceCode(deviceCode, clientId, accountType)
|
|
59
|
+
if (first.status === 'pending' || first.status === 'slow_down') {
|
|
60
|
+
throw new PendingApprovalError()
|
|
61
|
+
}
|
|
62
|
+
if (first.status !== 'success') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
first.status === 'expired'
|
|
65
|
+
? 'Device code expired.'
|
|
66
|
+
: first.status === 'declined'
|
|
67
|
+
? 'Authorization declined.'
|
|
68
|
+
: `Login failed: ${first.message}`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return finalize(first.token, clientId, accountType, credManager)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function loginWithDeviceCode(
|
|
75
|
+
callbacks: LoginCallbacks,
|
|
76
|
+
credManager: TeamsCredentialManager = new TeamsCredentialManager(),
|
|
77
|
+
): Promise<DeviceLoginResult> {
|
|
78
|
+
const accountType = callbacks.accountType ?? 'personal'
|
|
79
|
+
const { info, clientId } = await startDeviceCode(callbacks.clientId, accountType)
|
|
80
|
+
await callbacks.onCode({
|
|
81
|
+
verificationUri: info.verificationUri,
|
|
82
|
+
verificationUriComplete: info.verificationUriComplete,
|
|
83
|
+
userCode: info.userCode,
|
|
84
|
+
expiresAt: Date.now() + info.expiresIn * 1000,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const aad = await pollDeviceToken(info.deviceCode, info.interval, info.expiresIn, clientId, accountType)
|
|
88
|
+
|
|
89
|
+
// A work login can resolve to a personal (MSA) account, whose token carries a
|
|
90
|
+
// consumer tenant. Such accounts have no org tenant and can't be finished on
|
|
91
|
+
// the work flow, so stop with an actionable hint rather than fail obscurely.
|
|
92
|
+
if (accountType === 'work' && isPersonalAccount(aad)) {
|
|
93
|
+
throw new TeamsError(PERSONAL_ACCOUNT_HINT, 'teams_personal_account_on_work_login')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
callbacks.onPending?.()
|
|
97
|
+
return finalize(aad, clientId, accountType, credManager, callbacks.debug)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPersonalAccount(aad: AadToken): boolean {
|
|
101
|
+
const tid = decodeJwtTid(aad.accessToken)
|
|
102
|
+
return tid !== undefined && isConsumerTenant(tid)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function finalize(
|
|
106
|
+
initialAad: AadToken,
|
|
107
|
+
clientId: string,
|
|
108
|
+
accountType: TeamsAccountType,
|
|
109
|
+
credManager: TeamsCredentialManager,
|
|
110
|
+
debug?: (message: string) => void,
|
|
111
|
+
): Promise<DeviceLoginResult> {
|
|
112
|
+
if (!initialAad.refreshToken) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Sign-in did not return a refresh token (needed to mint the Teams token). Retry `auth login`, or use `auth extract` if this persists.',
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let tenantId: string | undefined
|
|
119
|
+
if (accountType === 'work') {
|
|
120
|
+
debug?.('Resolving Teams tenant...')
|
|
121
|
+
tenantId = await resolveWorkTenantId(initialAad.accessToken)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
debug?.('Exchanging token for skype audience...')
|
|
125
|
+
const skypeScoped = await exchangeForSkypeScope(initialAad.refreshToken, clientId, accountType, tenantId)
|
|
126
|
+
|
|
127
|
+
debug?.('Minting skype token...')
|
|
128
|
+
const minted = await mintSkypeToken(skypeScoped.accessToken, accountType)
|
|
129
|
+
|
|
130
|
+
const client = await new TeamsClient().login({ token: minted.skypeToken, accountType })
|
|
131
|
+
const authInfo = await client.testAuth()
|
|
132
|
+
const teams = await client.listTeams()
|
|
133
|
+
|
|
134
|
+
const teamMap: Record<string, { team_id: string; team_name: string }> = {}
|
|
135
|
+
for (const team of teams) {
|
|
136
|
+
teamMap[team.id] = { team_id: team.id, team_name: team.name }
|
|
137
|
+
}
|
|
138
|
+
const currentTeam = teams[0]?.id ?? null
|
|
139
|
+
|
|
140
|
+
await credManager.setDeviceCodeAccount({
|
|
141
|
+
accountType,
|
|
142
|
+
token: minted.skypeToken,
|
|
143
|
+
tokenExpiresAt: minted.skypeTokenExpiresAt,
|
|
144
|
+
aadRefreshToken: skypeScoped.refreshToken,
|
|
145
|
+
aadClientId: clientId,
|
|
146
|
+
aadTenantId: tenantId,
|
|
147
|
+
region: minted.region ?? (accountType === 'work' ? client.getRegion() : undefined),
|
|
148
|
+
userName: authInfo.displayName,
|
|
149
|
+
teams: teamMap,
|
|
150
|
+
currentTeam,
|
|
151
|
+
authMethod: 'device-code',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
accountType,
|
|
156
|
+
userName: authInfo.displayName,
|
|
157
|
+
teams: teams.map((t) => ({ id: t.id, name: t.name })),
|
|
158
|
+
current: currentTeam,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function refreshDeviceCodeAccount(
|
|
163
|
+
accountType: TeamsAccountType,
|
|
164
|
+
credManager: TeamsCredentialManager = new TeamsCredentialManager(),
|
|
165
|
+
debug?: (message: string) => void,
|
|
166
|
+
): Promise<boolean> {
|
|
167
|
+
const config = await credManager.loadConfig()
|
|
168
|
+
const account = config?.accounts[accountType]
|
|
169
|
+
if (!account || account.auth_method !== 'device-code' || !account.aad_refresh_token) {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
const clientId = account.aad_client_id ?? getTeamsAppClientId(accountType).clientId
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
debug?.('Silently refreshing skype token...')
|
|
176
|
+
const tenantId = accountType === 'work' ? account.aad_tenant_id : undefined
|
|
177
|
+
const skypeScoped = await exchangeForSkypeScope(account.aad_refresh_token, clientId, accountType, tenantId)
|
|
178
|
+
const minted = await mintSkypeToken(skypeScoped.accessToken, accountType)
|
|
179
|
+
|
|
180
|
+
await credManager.setDeviceCodeAccount({
|
|
181
|
+
accountType,
|
|
182
|
+
token: minted.skypeToken,
|
|
183
|
+
tokenExpiresAt: minted.skypeTokenExpiresAt,
|
|
184
|
+
aadRefreshToken: skypeScoped.refreshToken,
|
|
185
|
+
aadClientId: clientId,
|
|
186
|
+
aadTenantId: tenantId,
|
|
187
|
+
region: minted.region ?? account.region,
|
|
188
|
+
userName: account.user_name,
|
|
189
|
+
teams: account.teams,
|
|
190
|
+
currentTeam: account.current_team,
|
|
191
|
+
authMethod: 'device-code',
|
|
192
|
+
makeCurrent: false,
|
|
193
|
+
})
|
|
194
|
+
return true
|
|
195
|
+
} catch (error) {
|
|
196
|
+
debug?.(`Silent refresh failed: ${(error as Error).message}`)
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class PendingApprovalError extends Error {
|
|
202
|
+
constructor() {
|
|
203
|
+
super('Authorization pending. Approve in the browser, then retry.')
|
|
204
|
+
this.name = 'PendingApprovalError'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -2,6 +2,7 @@ import { warn } from '@/shared/utils/stderr'
|
|
|
2
2
|
|
|
3
3
|
import { TeamsClient } from './client'
|
|
4
4
|
import { TeamsCredentialManager } from './credential-manager'
|
|
5
|
+
import { refreshDeviceCodeAccount } from './device-login'
|
|
5
6
|
import { TeamsTokenExtractor } from './token-extractor'
|
|
6
7
|
import type { TeamsAccount, TeamsAccountType, TeamsConfig } from './types'
|
|
7
8
|
|
|
@@ -12,6 +13,8 @@ export async function ensureTeamsAuth(): Promise<void> {
|
|
|
12
13
|
|
|
13
14
|
if (config && hasValidToken(config)) return
|
|
14
15
|
|
|
16
|
+
if (config && (await trySilentRefresh(config, credManager))) return
|
|
17
|
+
|
|
15
18
|
const extractor = new TeamsTokenExtractor()
|
|
16
19
|
const extracted = await extractor.extract()
|
|
17
20
|
if (extracted.length === 0) return
|
|
@@ -92,6 +95,14 @@ async function resolveAccountType(
|
|
|
92
95
|
throw lastError ?? new Error('Token validation failed')
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
async function trySilentRefresh(config: TeamsConfig, credManager: TeamsCredentialManager): Promise<boolean> {
|
|
99
|
+
const key = TeamsCredentialManager.accountOverride ?? config.current_account
|
|
100
|
+
if (!key) return false
|
|
101
|
+
const account = config.accounts[key]
|
|
102
|
+
if (account?.auth_method !== 'device-code' || !account.aad_refresh_token) return false
|
|
103
|
+
return refreshDeviceCodeAccount(key as TeamsAccountType, credManager)
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
function hasValidToken(config: TeamsConfig): boolean {
|
|
96
107
|
const key = TeamsCredentialManager.accountOverride ?? config.current_account
|
|
97
108
|
if (!key) return false
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
TeamsFileSchema,
|
|
14
14
|
TeamsMessageSchema,
|
|
15
15
|
TeamsReactionSchema,
|
|
16
|
+
TeamsSearchResultSchema,
|
|
16
17
|
TeamsTeamSchema,
|
|
18
|
+
TeamsTokenProvider,
|
|
17
19
|
TeamsUserSchema,
|
|
18
20
|
} from '@/platforms/teams/index'
|
|
19
21
|
|
|
@@ -29,6 +31,10 @@ it('TeamsCredentialManager is exported from barrel', () => {
|
|
|
29
31
|
expect(typeof TeamsCredentialManager).toBe('function')
|
|
30
32
|
})
|
|
31
33
|
|
|
34
|
+
it('TeamsTokenProvider is exported from barrel', () => {
|
|
35
|
+
expect(typeof TeamsTokenProvider).toBe('function')
|
|
36
|
+
})
|
|
37
|
+
|
|
32
38
|
it('TeamsTeamSchema is exported from barrel', () => {
|
|
33
39
|
expect(typeof TeamsTeamSchema.parse).toBe('function')
|
|
34
40
|
})
|
|
@@ -41,6 +47,10 @@ it('TeamsMessageSchema is exported from barrel', () => {
|
|
|
41
47
|
expect(typeof TeamsMessageSchema.parse).toBe('function')
|
|
42
48
|
})
|
|
43
49
|
|
|
50
|
+
it('TeamsSearchResultSchema is exported from barrel', () => {
|
|
51
|
+
expect(typeof TeamsSearchResultSchema.parse).toBe('function')
|
|
52
|
+
})
|
|
53
|
+
|
|
44
54
|
it('TeamsUserSchema is exported from barrel', () => {
|
|
45
55
|
expect(typeof TeamsUserSchema.parse).toBe('function')
|
|
46
56
|
})
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export { TeamsClient } from './client'
|
|
2
2
|
export { TeamsCredentialManager } from './credential-manager'
|
|
3
|
-
export {
|
|
3
|
+
export { TeamsTokenProvider } from './token-provider'
|
|
4
|
+
export {
|
|
5
|
+
completeDeviceCode,
|
|
6
|
+
loginWithDeviceCode,
|
|
7
|
+
PendingApprovalError,
|
|
8
|
+
refreshDeviceCodeAccount,
|
|
9
|
+
startDeviceCode,
|
|
10
|
+
} from './device-login'
|
|
11
|
+
export type { DeviceCodePrompt, DeviceLoginResult } from './device-login'
|
|
12
|
+
export { TeamsListener } from './listener'
|
|
13
|
+
export { TeamsAuthCapabilityError, TeamsError } from './types'
|
|
4
14
|
export type {
|
|
5
15
|
TeamsAccount,
|
|
6
16
|
TeamsAccountType,
|
|
@@ -9,9 +19,13 @@ export type {
|
|
|
9
19
|
TeamsConfigLegacy,
|
|
10
20
|
TeamsCredentials,
|
|
11
21
|
TeamsFile,
|
|
22
|
+
TeamsListenerEventMap,
|
|
12
23
|
TeamsMessage,
|
|
24
|
+
TeamsRealtimeMessage,
|
|
13
25
|
TeamsReaction,
|
|
26
|
+
TeamsSearchResult,
|
|
14
27
|
TeamsTeam,
|
|
28
|
+
TeamsTrouterGenericEvent,
|
|
15
29
|
TeamsUser,
|
|
16
30
|
} from './types'
|
|
17
31
|
export {
|
|
@@ -24,6 +38,7 @@ export {
|
|
|
24
38
|
TeamsFileSchema,
|
|
25
39
|
TeamsMessageSchema,
|
|
26
40
|
TeamsReactionSchema,
|
|
41
|
+
TeamsSearchResultSchema,
|
|
27
42
|
TeamsTeamSchema,
|
|
28
43
|
TeamsUserSchema,
|
|
29
44
|
} from './types'
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
import WebSocket from 'ws'
|
|
5
|
+
|
|
6
|
+
import type { TeamsClient } from './client'
|
|
7
|
+
import {
|
|
8
|
+
buildActivityFrame,
|
|
9
|
+
buildAuthenticateFrame,
|
|
10
|
+
buildEventAck,
|
|
11
|
+
buildPingFrame,
|
|
12
|
+
buildRequestAck,
|
|
13
|
+
buildWebSocketUrl,
|
|
14
|
+
decodeMessageBody,
|
|
15
|
+
extractChatId,
|
|
16
|
+
fetchTrouterInfo,
|
|
17
|
+
fetchTrouterSessionId,
|
|
18
|
+
isMessageLossFrame,
|
|
19
|
+
parseRequestFrame,
|
|
20
|
+
registerEndpoint,
|
|
21
|
+
type TrouterInfo,
|
|
22
|
+
} from './trouter'
|
|
23
|
+
import { TeamsError } from './types'
|
|
24
|
+
import type { TeamsListenerEventMap, TeamsRealtimeMessage } from './types'
|
|
25
|
+
|
|
26
|
+
const PING_INTERVAL = 30_000
|
|
27
|
+
const RECONNECT_BASE_DELAY = 1_000
|
|
28
|
+
const RECONNECT_MAX_DELAY = 30_000
|
|
29
|
+
const REGISTRATION_REFRESH_INTERVAL = (86400 - 60) * 1000
|
|
30
|
+
const MESSAGE_CACHE_LIMIT = 500
|
|
31
|
+
const TEXT_MESSAGE_TYPES = new Set(['Text', 'RichText/Html'])
|
|
32
|
+
|
|
33
|
+
type EventKey = keyof TeamsListenerEventMap
|
|
34
|
+
|
|
35
|
+
interface IncomingResource {
|
|
36
|
+
id?: string
|
|
37
|
+
messagetype?: string
|
|
38
|
+
content?: string
|
|
39
|
+
from?: string
|
|
40
|
+
imdisplayname?: string
|
|
41
|
+
conversationLink?: string
|
|
42
|
+
resourceLink?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class TeamsListener {
|
|
46
|
+
private client: TeamsClient
|
|
47
|
+
private running = false
|
|
48
|
+
private ws: WebSocket | null = null
|
|
49
|
+
private emitter = new EventEmitter()
|
|
50
|
+
private endpointId = randomUUID()
|
|
51
|
+
private idToken: string | null = null
|
|
52
|
+
private info: TrouterInfo | null = null
|
|
53
|
+
private sequence = 0
|
|
54
|
+
private reconnectAttempts = 0
|
|
55
|
+
private pingTimer: ReturnType<typeof setInterval> | null = null
|
|
56
|
+
private reregisterTimer: ReturnType<typeof setInterval> | null = null
|
|
57
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
58
|
+
private seenMessageIds = new Set<string>()
|
|
59
|
+
|
|
60
|
+
constructor(client: TeamsClient) {
|
|
61
|
+
this.client = client
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async start(): Promise<void> {
|
|
65
|
+
if (this.running) return
|
|
66
|
+
this.running = true
|
|
67
|
+
this.reconnectAttempts = 0
|
|
68
|
+
await this.connect()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
stop(): void {
|
|
72
|
+
this.running = false
|
|
73
|
+
this.clearTimers()
|
|
74
|
+
if (this.ws) {
|
|
75
|
+
this.ws.close()
|
|
76
|
+
this.ws = null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
on<K extends EventKey>(event: K, listener: (...args: TeamsListenerEventMap[K]) => void): this {
|
|
81
|
+
this.emitter.on(event, listener as (...args: any[]) => void)
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
off<K extends EventKey>(event: K, listener: (...args: TeamsListenerEventMap[K]) => void): this {
|
|
86
|
+
this.emitter.off(event, listener as (...args: any[]) => void)
|
|
87
|
+
return this
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
once<K extends EventKey>(event: K, listener: (...args: TeamsListenerEventMap[K]) => void): this {
|
|
91
|
+
this.emitter.once(event, listener as (...args: any[]) => void)
|
|
92
|
+
return this
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async connect(): Promise<void> {
|
|
96
|
+
if (!this.running) return
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const skypeToken = this.client.getToken()
|
|
100
|
+
|
|
101
|
+
// Re-extract on every attempt so a reconnect after token expiry picks up
|
|
102
|
+
// a fresh id_token rather than reusing the stale cached one.
|
|
103
|
+
this.idToken = await this.client.getIdToken()
|
|
104
|
+
if (!this.idToken) {
|
|
105
|
+
throw new TeamsError(
|
|
106
|
+
'Could not obtain Teams id_token for real-time auth. Ensure the Teams desktop app is logged in, then re-run "auth extract".',
|
|
107
|
+
'no_id_token',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const info = await fetchTrouterInfo(skypeToken, this.endpointId)
|
|
112
|
+
if (!this.running) return
|
|
113
|
+
const sessionId = await fetchTrouterSessionId(info, skypeToken, this.endpointId)
|
|
114
|
+
if (!this.running) return
|
|
115
|
+
|
|
116
|
+
this.info = info
|
|
117
|
+
const ws = new WebSocket(buildWebSocketUrl(info, sessionId, this.endpointId), {
|
|
118
|
+
headers: { 'X-Skypetoken': skypeToken, 'User-Agent': 'AgentMessenger' },
|
|
119
|
+
})
|
|
120
|
+
this.ws = ws
|
|
121
|
+
|
|
122
|
+
ws.on('message', (raw) => this.handleFrame(raw.toString()))
|
|
123
|
+
|
|
124
|
+
ws.on('close', () => {
|
|
125
|
+
this.clearTimers()
|
|
126
|
+
if (this.ws === ws) this.ws = null
|
|
127
|
+
if (this.running) {
|
|
128
|
+
this.emitter.emit('disconnected')
|
|
129
|
+
this.scheduleReconnect()
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
ws.on('error', (err) => {
|
|
134
|
+
this.emitter.emit('error', err instanceof Error ? err : new Error(String(err)))
|
|
135
|
+
})
|
|
136
|
+
} catch (error) {
|
|
137
|
+
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
138
|
+
if (this.running) this.scheduleReconnect()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private handleFrame(frame: string): void {
|
|
143
|
+
if (frame.startsWith('1::')) {
|
|
144
|
+
void this.onConnected()
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (frame.startsWith('3:::')) {
|
|
149
|
+
this.handleRequestFrame(frame)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (frame.startsWith('5:')) {
|
|
154
|
+
const ack = buildEventAck(frame)
|
|
155
|
+
if (ack) this.ws?.send(ack)
|
|
156
|
+
if (isMessageLossFrame(frame)) void this.register()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async onConnected(): Promise<void> {
|
|
161
|
+
if (!this.ws || !this.info || !this.idToken) return
|
|
162
|
+
|
|
163
|
+
this.ws.send(buildAuthenticateFrame(this.info, this.idToken))
|
|
164
|
+
this.ws.send(buildActivityFrame(++this.sequence))
|
|
165
|
+
|
|
166
|
+
// Only report the endpoint as connected once it has actually registered;
|
|
167
|
+
// an unregistered endpoint receives no messages. On failure, drop the
|
|
168
|
+
// possibly-stale id_token and close so the reconnect path re-authenticates.
|
|
169
|
+
const registered = await this.register()
|
|
170
|
+
if (!registered) {
|
|
171
|
+
this.idToken = null
|
|
172
|
+
this.ws.close()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.reconnectAttempts = 0
|
|
177
|
+
this.pingTimer = setInterval(() => {
|
|
178
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(buildPingFrame(++this.sequence))
|
|
179
|
+
}, PING_INTERVAL)
|
|
180
|
+
this.reregisterTimer = setInterval(() => void this.register(), REGISTRATION_REFRESH_INTERVAL)
|
|
181
|
+
|
|
182
|
+
this.emitter.emit('connected', { endpointId: this.endpointId })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async register(): Promise<boolean> {
|
|
186
|
+
if (!this.info || !this.idToken) return false
|
|
187
|
+
try {
|
|
188
|
+
await registerEndpoint(
|
|
189
|
+
this.info,
|
|
190
|
+
this.client.getToken(),
|
|
191
|
+
this.idToken,
|
|
192
|
+
this.endpointId,
|
|
193
|
+
this.client.getAccountType(),
|
|
194
|
+
() => randomUUID(),
|
|
195
|
+
)
|
|
196
|
+
return true
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private handleRequestFrame(frame: string): void {
|
|
204
|
+
const request = parseRequestFrame(frame)
|
|
205
|
+
if (!request) return
|
|
206
|
+
|
|
207
|
+
this.ws?.send(buildRequestAck(request.id))
|
|
208
|
+
|
|
209
|
+
if (!request.url?.endsWith('/messaging') || !request.body) return
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const decoded = decodeMessageBody(request.headers ?? {}, request.body) as {
|
|
213
|
+
resourceType?: string
|
|
214
|
+
resource?: IncomingResource
|
|
215
|
+
}
|
|
216
|
+
if (decoded.resourceType === 'NewMessage' && decoded.resource) {
|
|
217
|
+
this.emitMessage(decoded.resource)
|
|
218
|
+
}
|
|
219
|
+
this.emitter.emit('teams_event', { resourceType: decoded.resourceType ?? 'unknown', ...decoded })
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private emitMessage(resource: IncomingResource): void {
|
|
226
|
+
if (!resource.messagetype || !TEXT_MESSAGE_TYPES.has(resource.messagetype)) return
|
|
227
|
+
|
|
228
|
+
const chatId = extractChatId(resource.conversationLink ?? resource.resourceLink)
|
|
229
|
+
if (!chatId || !resource.id) return
|
|
230
|
+
if (this.seenMessageIds.has(resource.id)) return
|
|
231
|
+
this.rememberMessageId(resource.id)
|
|
232
|
+
|
|
233
|
+
const message: TeamsRealtimeMessage = {
|
|
234
|
+
id: resource.id,
|
|
235
|
+
chatId,
|
|
236
|
+
content: stripHtml(resource.content ?? ''),
|
|
237
|
+
author: {
|
|
238
|
+
id: extractUserId(resource.from) ?? 'unknown',
|
|
239
|
+
displayName: resource.imdisplayname || extractUserId(resource.from) || 'unknown',
|
|
240
|
+
},
|
|
241
|
+
messageType: resource.messagetype,
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
}
|
|
244
|
+
this.emitter.emit('message', message)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private rememberMessageId(id: string): void {
|
|
248
|
+
this.seenMessageIds.add(id)
|
|
249
|
+
if (this.seenMessageIds.size > MESSAGE_CACHE_LIMIT) {
|
|
250
|
+
const oldest = this.seenMessageIds.values().next().value
|
|
251
|
+
if (oldest !== undefined) this.seenMessageIds.delete(oldest)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private scheduleReconnect(): void {
|
|
256
|
+
const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
|
|
257
|
+
this.reconnectAttempts++
|
|
258
|
+
this.endpointId = randomUUID()
|
|
259
|
+
this.sequence = 0
|
|
260
|
+
this.reconnectTimer = setTimeout(() => void this.connect(), delay)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private clearTimers(): void {
|
|
264
|
+
if (this.pingTimer) {
|
|
265
|
+
clearInterval(this.pingTimer)
|
|
266
|
+
this.pingTimer = null
|
|
267
|
+
}
|
|
268
|
+
if (this.reregisterTimer) {
|
|
269
|
+
clearInterval(this.reregisterTimer)
|
|
270
|
+
this.reregisterTimer = null
|
|
271
|
+
}
|
|
272
|
+
if (this.reconnectTimer) {
|
|
273
|
+
clearTimeout(this.reconnectTimer)
|
|
274
|
+
this.reconnectTimer = null
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function stripHtml(html: string): string {
|
|
280
|
+
return html
|
|
281
|
+
.replace(/<[^>]*>/g, '')
|
|
282
|
+
.replace(/&/g, '&')
|
|
283
|
+
.replace(/</g, '<')
|
|
284
|
+
.replace(/>/g, '>')
|
|
285
|
+
.replace(/"/g, '"')
|
|
286
|
+
.replace(/'/g, "'")
|
|
287
|
+
.replace(/\s+/g, ' ')
|
|
288
|
+
.trim()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function extractUserId(from: string | undefined): string | null {
|
|
292
|
+
if (!from) return null
|
|
293
|
+
const match = from.match(/contacts\/(.+)$/)
|
|
294
|
+
return match ? match[1] : from
|
|
295
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { probeAccountType } from './realm-discovery'
|
|
4
|
+
|
|
5
|
+
const originalFetch = globalThis.fetch
|
|
6
|
+
|
|
7
|
+
function mockFetch(response: { status?: number; json?: unknown; reject?: boolean }): {
|
|
8
|
+
calls: Array<{ url: string; init: RequestInit }>
|
|
9
|
+
} {
|
|
10
|
+
const calls: Array<{ url: string; init: RequestInit }> = []
|
|
11
|
+
globalThis.fetch = mock((input: string | URL | Request, init?: RequestInit) => {
|
|
12
|
+
calls.push({ url: String(input), init: init ?? {} })
|
|
13
|
+
if (response.reject) return Promise.reject(new Error('network down'))
|
|
14
|
+
const status = response.status ?? 200
|
|
15
|
+
return Promise.resolve({
|
|
16
|
+
ok: status >= 200 && status < 300,
|
|
17
|
+
status,
|
|
18
|
+
json: () => Promise.resolve(response.json),
|
|
19
|
+
} as Response)
|
|
20
|
+
}) as typeof fetch
|
|
21
|
+
return { calls }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
globalThis.fetch = originalFetch
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('probeAccountType', () => {
|
|
29
|
+
it('posts the email to GetCredentialType as JSON', async () => {
|
|
30
|
+
const { calls } = mockFetch({ json: { EstsProperties: { DomainType: 3 } } })
|
|
31
|
+
|
|
32
|
+
await probeAccountType('user@contoso.com')
|
|
33
|
+
|
|
34
|
+
expect(calls[0].url).toBe('https://login.microsoftonline.com/common/GetCredentialType')
|
|
35
|
+
expect(calls[0].init.method).toBe('POST')
|
|
36
|
+
expect(JSON.parse(calls[0].init.body as string)).toEqual({ Username: 'user@contoso.com' })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('maps a consumer DomainType to personal', async () => {
|
|
40
|
+
mockFetch({ json: { EstsProperties: { DomainType: 2 } } })
|
|
41
|
+
expect(await probeAccountType('user@outlook.com')).toBe('personal')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('treats a known consumer domain as personal even without DomainType', async () => {
|
|
45
|
+
mockFetch({ json: { EstsProperties: { IsConsumerDomain: true } } })
|
|
46
|
+
expect(await probeAccountType('user@hotmail.com')).toBe('personal')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('maps a managed DomainType to work', async () => {
|
|
50
|
+
mockFetch({ json: { EstsProperties: { DomainType: 3 } } })
|
|
51
|
+
expect(await probeAccountType('user@contoso.com')).toBe('work')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('maps a federated DomainType to work', async () => {
|
|
55
|
+
mockFetch({ json: { EstsProperties: { DomainType: 4 } } })
|
|
56
|
+
expect(await probeAccountType('user@fabrikam.com')).toBe('work')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns undefined when the domain type is missing', async () => {
|
|
60
|
+
mockFetch({ json: { EstsProperties: {} } })
|
|
61
|
+
expect(await probeAccountType('user@unknown.test')).toBeUndefined()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns undefined on a non-ok response', async () => {
|
|
65
|
+
mockFetch({ status: 429, json: {} })
|
|
66
|
+
expect(await probeAccountType('user@contoso.com')).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns undefined when the request throws', async () => {
|
|
70
|
+
mockFetch({ reject: true })
|
|
71
|
+
expect(await probeAccountType('user@contoso.com')).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
})
|