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,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authzUrl,
|
|
3
|
+
CONSUMER_TENANT_ID,
|
|
4
|
+
deviceCodeSkypeScope,
|
|
5
|
+
deviceCodeTeamsScope,
|
|
6
|
+
deviceCodeTokenUrl,
|
|
7
|
+
deviceCodeUrl,
|
|
8
|
+
MICROSOFT_SERVICES_TENANT_ID,
|
|
9
|
+
TEAMS_TENANTS_URL,
|
|
10
|
+
WORK_TENANT_ID,
|
|
11
|
+
} from './app-config'
|
|
12
|
+
import type { TeamsAccountType, TeamsRegion } from './types'
|
|
13
|
+
import { TeamsError } from './types'
|
|
14
|
+
|
|
15
|
+
const DEFAULT_SKYPE_TOKEN_TTL_SEC = 21600
|
|
16
|
+
|
|
17
|
+
export interface TeamsTenant {
|
|
18
|
+
tenantId: string
|
|
19
|
+
tenantName?: string
|
|
20
|
+
isInvitationRedeemed?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeviceCodeInfo {
|
|
24
|
+
deviceCode: string
|
|
25
|
+
userCode: string
|
|
26
|
+
verificationUri: string
|
|
27
|
+
verificationUriComplete: string
|
|
28
|
+
expiresIn: number
|
|
29
|
+
interval: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AadToken {
|
|
33
|
+
accessToken: string
|
|
34
|
+
refreshToken?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MintedSkypeToken {
|
|
38
|
+
skypeToken: string
|
|
39
|
+
skypeTokenExpiresAt: string
|
|
40
|
+
region?: TeamsRegion
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DeviceTokenResult =
|
|
44
|
+
| { status: 'success'; token: AadToken }
|
|
45
|
+
| { status: 'pending' }
|
|
46
|
+
| { status: 'slow_down' }
|
|
47
|
+
| { status: 'expired' }
|
|
48
|
+
| { status: 'declined' }
|
|
49
|
+
| { status: 'error'; message: string }
|
|
50
|
+
|
|
51
|
+
async function postForm(
|
|
52
|
+
url: string,
|
|
53
|
+
params: Record<string, string>,
|
|
54
|
+
): Promise<{ status: number; body: Record<string, unknown> }> {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
58
|
+
body: new URLSearchParams(params),
|
|
59
|
+
})
|
|
60
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>
|
|
61
|
+
return { status: res.status, body }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function requestDeviceCode(
|
|
65
|
+
clientId: string,
|
|
66
|
+
accountType: TeamsAccountType = 'personal',
|
|
67
|
+
): Promise<DeviceCodeInfo> {
|
|
68
|
+
const { status, body } = await postForm(deviceCodeUrl(accountType), {
|
|
69
|
+
client_id: clientId,
|
|
70
|
+
scope: deviceCodeTeamsScope(accountType),
|
|
71
|
+
})
|
|
72
|
+
if (status !== 200) {
|
|
73
|
+
throw new TeamsError(`Device code request failed: ${describeAadError(body, status)}`, 'device_code_failed')
|
|
74
|
+
}
|
|
75
|
+
const userCode = String(body.user_code ?? '')
|
|
76
|
+
const verificationUri = String(body.verification_uri ?? 'https://microsoft.com/devicelogin')
|
|
77
|
+
return {
|
|
78
|
+
deviceCode: String(body.device_code ?? ''),
|
|
79
|
+
userCode,
|
|
80
|
+
verificationUri,
|
|
81
|
+
verificationUriComplete: `${verificationUri}?otc=${encodeURIComponent(userCode)}`,
|
|
82
|
+
expiresIn: Number(body.expires_in ?? 900),
|
|
83
|
+
interval: Number(body.interval ?? 5),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function exchangeDeviceCode(
|
|
88
|
+
deviceCode: string,
|
|
89
|
+
clientId: string,
|
|
90
|
+
accountType: TeamsAccountType = 'personal',
|
|
91
|
+
): Promise<DeviceTokenResult> {
|
|
92
|
+
const { status, body } = await postForm(deviceCodeTokenUrl(accountType), {
|
|
93
|
+
client_id: clientId,
|
|
94
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
95
|
+
device_code: deviceCode,
|
|
96
|
+
})
|
|
97
|
+
if (status === 200) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'success',
|
|
100
|
+
token: { accessToken: String(body.access_token), refreshToken: body.refresh_token as string | undefined },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const error = String(body.error ?? '')
|
|
104
|
+
if (error === 'authorization_pending') return { status: 'pending' }
|
|
105
|
+
if (error === 'slow_down') return { status: 'slow_down' }
|
|
106
|
+
if (error === 'expired_token') return { status: 'expired' }
|
|
107
|
+
if (error === 'authorization_declined') return { status: 'declined' }
|
|
108
|
+
return { status: 'error', message: describeAadError(body, status) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function pollDeviceToken(
|
|
112
|
+
deviceCode: string,
|
|
113
|
+
interval: number,
|
|
114
|
+
expiresIn: number,
|
|
115
|
+
clientId: string,
|
|
116
|
+
accountType: TeamsAccountType = 'personal',
|
|
117
|
+
): Promise<AadToken> {
|
|
118
|
+
const deadline = Date.now() + expiresIn * 1000
|
|
119
|
+
let waitMs = Math.max(interval, 1) * 1000
|
|
120
|
+
while (Date.now() < deadline) {
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs))
|
|
122
|
+
const result = await exchangeDeviceCode(deviceCode, clientId, accountType)
|
|
123
|
+
if (result.status === 'success') return result.token
|
|
124
|
+
if (result.status === 'slow_down') {
|
|
125
|
+
waitMs += 5000
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (result.status === 'pending') continue
|
|
129
|
+
if (result.status === 'expired') throw new TeamsError('Device code expired before approval.', 'device_code_expired')
|
|
130
|
+
if (result.status === 'declined') throw new TeamsError('Authorization was declined.', 'device_code_declined')
|
|
131
|
+
throw new TeamsError(`Device authorization failed: ${result.message}`, 'device_code_failed')
|
|
132
|
+
}
|
|
133
|
+
throw new TeamsError('Device code expired before approval.', 'device_code_expired')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function exchangeForSkypeScope(
|
|
137
|
+
refreshToken: string,
|
|
138
|
+
clientId: string,
|
|
139
|
+
accountType: TeamsAccountType = 'personal',
|
|
140
|
+
tenantId?: string,
|
|
141
|
+
): Promise<AadToken> {
|
|
142
|
+
const { status, body } = await postForm(deviceCodeTokenUrl(accountType, tenantId), {
|
|
143
|
+
client_id: clientId,
|
|
144
|
+
grant_type: 'refresh_token',
|
|
145
|
+
refresh_token: refreshToken,
|
|
146
|
+
scope: deviceCodeSkypeScope(accountType),
|
|
147
|
+
})
|
|
148
|
+
if (status !== 200) {
|
|
149
|
+
throw new TeamsError(`Token exchange failed: ${describeAadError(body, status)}`, 'token_exchange_failed')
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
accessToken: String(body.access_token),
|
|
153
|
+
refreshToken: (body.refresh_token as string | undefined) ?? refreshToken,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Work logins go through the `organizations` authority, so the token's `tid` is
|
|
158
|
+
// a placeholder, not a real tenant GUID. The skype authz endpoint then rejects
|
|
159
|
+
// it with `GuestUserNotRedeemed`. Resolving the concrete tenant lets the
|
|
160
|
+
// skype-scope exchange target the tenant-specific authority and succeed.
|
|
161
|
+
export async function resolveWorkTenantId(accessToken: string): Promise<string> {
|
|
162
|
+
const claimTid = decodeJwtTid(accessToken)
|
|
163
|
+
if (claimTid && !isPlaceholderTenant(claimTid)) return claimTid
|
|
164
|
+
|
|
165
|
+
const tenants = await fetchTenants(accessToken)
|
|
166
|
+
const redeemed = tenants.find((tenant) => tenant.isInvitationRedeemed !== false) ?? tenants[0]
|
|
167
|
+
if (!redeemed) {
|
|
168
|
+
throw new TeamsError(
|
|
169
|
+
'No Microsoft Teams tenant is associated with this account. If this is a personal Microsoft account, run `agent-teams auth login --account-type personal`.',
|
|
170
|
+
'teams_tenant_missing',
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
if (redeemed.isInvitationRedeemed === false) {
|
|
174
|
+
throw new TeamsError(
|
|
175
|
+
'This Microsoft Teams guest invitation has not been accepted yet. Accept the invite in Teams, then run `agent-teams auth login` again.',
|
|
176
|
+
'teams_invitation_not_redeemed',
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return redeemed.tenantId
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function fetchTenants(accessToken: string): Promise<TeamsTenant[]> {
|
|
183
|
+
const res = await fetch(TEAMS_TENANTS_URL, { headers: { Authorization: `Bearer ${accessToken}` } })
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
throw new TeamsError(`Tenant discovery failed: HTTP ${res.status}`, 'tenant_discovery_failed')
|
|
186
|
+
}
|
|
187
|
+
const body = (await res.json().catch(() => [])) as unknown
|
|
188
|
+
const rows = Array.isArray(body) ? body : []
|
|
189
|
+
return rows
|
|
190
|
+
.map((row): TeamsTenant | null => {
|
|
191
|
+
if (typeof row !== 'object' || row === null) return null
|
|
192
|
+
const record = row as Record<string, unknown>
|
|
193
|
+
const tenantId = record.tenantId
|
|
194
|
+
if (typeof tenantId !== 'string' || !tenantId) return null
|
|
195
|
+
return {
|
|
196
|
+
tenantId,
|
|
197
|
+
tenantName: typeof record.tenantName === 'string' ? record.tenantName : undefined,
|
|
198
|
+
isInvitationRedeemed:
|
|
199
|
+
typeof record.isInvitationRedeemed === 'boolean' ? record.isInvitationRedeemed : undefined,
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
.filter((tenant): tenant is TeamsTenant => tenant !== null)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function decodeJwtTid(token: string): string | undefined {
|
|
206
|
+
const payload = token.split('.')[1]
|
|
207
|
+
if (!payload) return undefined
|
|
208
|
+
try {
|
|
209
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as { tid?: unknown }
|
|
210
|
+
return typeof claims.tid === 'string' ? claims.tid : undefined
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The consumer tenant unambiguously marks a personal account: it has no org
|
|
217
|
+
// tenant, so a work login carrying it can never resolve a Teams tenant. The
|
|
218
|
+
// Microsoft Services placeholder is intentionally excluded — it also appears on
|
|
219
|
+
// legitimate work tokens, which resolveWorkTenantId() resolves via discovery.
|
|
220
|
+
export function isConsumerTenant(tenantId: string): boolean {
|
|
221
|
+
return tenantId === CONSUMER_TENANT_ID
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isPlaceholderTenant(tenantId: string): boolean {
|
|
225
|
+
return tenantId === WORK_TENANT_ID || tenantId === CONSUMER_TENANT_ID || tenantId === MICROSOFT_SERVICES_TENANT_ID
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function mintSkypeToken(
|
|
229
|
+
spacesAccessToken: string,
|
|
230
|
+
accountType: TeamsAccountType = 'personal',
|
|
231
|
+
): Promise<MintedSkypeToken> {
|
|
232
|
+
const res = await fetch(authzUrl(accountType), {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { Authorization: `Bearer ${spacesAccessToken}`, Accept: 'application/json; ver=1.0' },
|
|
235
|
+
})
|
|
236
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
throw new TeamsError(`Failed to mint skype token: ${describeAuthzError(body, res.status)}`, 'skype_token_failed')
|
|
239
|
+
}
|
|
240
|
+
const nested = body.skypeToken as { skypetoken?: string; skypeTokenExpiresIn?: number } | undefined
|
|
241
|
+
const flat = body.tokens as { skypeToken?: string; expiresIn?: number } | undefined
|
|
242
|
+
const skypeToken = nested?.skypetoken ?? flat?.skypeToken
|
|
243
|
+
if (!skypeToken) {
|
|
244
|
+
throw new TeamsError('Consumer authz response did not contain a skype token.', 'skype_token_missing')
|
|
245
|
+
}
|
|
246
|
+
const ttlSec = nested?.skypeTokenExpiresIn ?? flat?.expiresIn ?? DEFAULT_SKYPE_TOKEN_TTL_SEC
|
|
247
|
+
return {
|
|
248
|
+
skypeToken,
|
|
249
|
+
skypeTokenExpiresAt: new Date(Date.now() + ttlSec * 1000).toISOString(),
|
|
250
|
+
region: parseRegion(body),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function mintConsumerSkypeToken(spacesAccessToken: string): Promise<MintedSkypeToken> {
|
|
255
|
+
return mintSkypeToken(spacesAccessToken, 'personal')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function describeAadError(body: Record<string, unknown>, status: number): string {
|
|
259
|
+
const desc = body.error_description ?? body.error
|
|
260
|
+
return typeof desc === 'string' ? desc.split('\n')[0] : `HTTP ${status}`
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function describeAuthzError(body: Record<string, unknown>, status: number): string {
|
|
264
|
+
const statusObj = body.status as { text?: string } | undefined
|
|
265
|
+
const code = String(body.errorCode ?? body.message ?? statusObj?.text ?? `HTTP ${status}`)
|
|
266
|
+
if (code === 'GuestUserNotRedeemed') {
|
|
267
|
+
return `${code} (the account has no redeemed Teams tenant — if this is a personal account, run \`agent-teams auth login --account-type personal\`; if you are a guest, accept the Teams invite first)`
|
|
268
|
+
}
|
|
269
|
+
return code
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseRegion(body: Record<string, unknown>): TeamsRegion | undefined {
|
|
273
|
+
const regionGtms = body.regionGtms as { region?: string; middleTier?: string; mt?: string } | undefined
|
|
274
|
+
const raw = regionGtms?.region ?? regionGtms?.middleTier ?? regionGtms?.mt
|
|
275
|
+
if (raw === 'amer' || raw === 'emea' || raw === 'apac') return raw
|
|
276
|
+
return undefined
|
|
277
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, it, mock, spyOn } from 'bun:test'
|
|
2
|
+
import { rmSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { TeamsClient } from './client'
|
|
6
|
+
import { TeamsCredentialManager } from './credential-manager'
|
|
7
|
+
import * as deviceCode from './device-code'
|
|
8
|
+
import type { DeviceCodeInfo } from './device-code'
|
|
9
|
+
import { loginWithDeviceCode, refreshDeviceCodeAccount } from './device-login'
|
|
10
|
+
|
|
11
|
+
const CONSUMER_TENANT_ID = '9188040d-6c67-4c5b-b112-36a304b66dad'
|
|
12
|
+
const MICROSOFT_SERVICES_TENANT_ID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'
|
|
13
|
+
const REAL_TENANT_ID = 'c0ffee00-1111-2222-3333-444455556666'
|
|
14
|
+
|
|
15
|
+
function fakeJwt(claims: Record<string, unknown>): string {
|
|
16
|
+
return `header.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.signature`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fakeDeviceCodeInfo(): DeviceCodeInfo {
|
|
20
|
+
return {
|
|
21
|
+
deviceCode: 'device-code',
|
|
22
|
+
userCode: 'USER-CODE',
|
|
23
|
+
verificationUri: 'https://microsoft.com/devicelogin',
|
|
24
|
+
verificationUriComplete: 'https://microsoft.com/devicelogin?otc=USER-CODE',
|
|
25
|
+
expiresIn: 900,
|
|
26
|
+
interval: 1,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const testDirs: string[] = []
|
|
31
|
+
const originalFetch = globalThis.fetch
|
|
32
|
+
|
|
33
|
+
function setup(): TeamsCredentialManager {
|
|
34
|
+
const dir = join(import.meta.dir, `.test-teams-refresh-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
35
|
+
testDirs.push(dir)
|
|
36
|
+
return new TeamsCredentialManager(dir)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mockExchangeAndMint(skypeToken: string, rotatedRefresh: string): { tokenUrls: string[] } {
|
|
40
|
+
const tokenUrls: string[] = []
|
|
41
|
+
globalThis.fetch = mock((input: string | URL | Request) => {
|
|
42
|
+
const url = String(input)
|
|
43
|
+
if (url.includes('/oauth2/v2.0/token')) {
|
|
44
|
+
tokenUrls.push(url)
|
|
45
|
+
return Promise.resolve({
|
|
46
|
+
ok: true,
|
|
47
|
+
status: 200,
|
|
48
|
+
json: () => Promise.resolve({ access_token: 'spaces-at', refresh_token: rotatedRefresh }),
|
|
49
|
+
} as Response)
|
|
50
|
+
}
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
ok: true,
|
|
53
|
+
status: 200,
|
|
54
|
+
json: () => Promise.resolve({ skypeToken: { skypetoken: skypeToken, skypeTokenExpiresIn: 3600 } }),
|
|
55
|
+
} as Response)
|
|
56
|
+
}) as typeof fetch
|
|
57
|
+
return { tokenUrls }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
globalThis.fetch = originalFetch
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
for (const dir of testDirs) rmSync(dir, { recursive: true, force: true })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('refreshDeviceCodeAccount', () => {
|
|
69
|
+
it('re-mints the skype token, rotates the refresh token, and keeps the account current', async () => {
|
|
70
|
+
const manager = setup()
|
|
71
|
+
await manager.setDeviceCodeAccount({
|
|
72
|
+
accountType: 'personal',
|
|
73
|
+
token: 'old-skype',
|
|
74
|
+
tokenExpiresAt: '2000-01-01T00:00:00Z',
|
|
75
|
+
aadRefreshToken: 'old-refresh',
|
|
76
|
+
aadClientId: 'client',
|
|
77
|
+
teams: {},
|
|
78
|
+
currentTeam: null,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
mockExchangeAndMint('new-skype', 'new-refresh')
|
|
82
|
+
const ok = await refreshDeviceCodeAccount('personal', manager)
|
|
83
|
+
|
|
84
|
+
expect(ok).toBe(true)
|
|
85
|
+
const config = await manager.loadConfig()
|
|
86
|
+
expect(config?.accounts.personal?.token).toBe('new-skype')
|
|
87
|
+
expect(config?.accounts.personal?.aad_refresh_token).toBe('new-refresh')
|
|
88
|
+
expect(new Date(config!.accounts.personal!.token_expires_at!).getTime()).toBeGreaterThan(Date.now())
|
|
89
|
+
expect(config?.current_account).toBe('personal')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('refreshes work device-code accounts and persists the rotated refresh token', async () => {
|
|
93
|
+
const manager = setup()
|
|
94
|
+
await manager.setDeviceCodeAccount({
|
|
95
|
+
accountType: 'work',
|
|
96
|
+
token: 'old-skype',
|
|
97
|
+
tokenExpiresAt: '2000-01-01T00:00:00Z',
|
|
98
|
+
aadRefreshToken: 'old-refresh',
|
|
99
|
+
aadClientId: 'client',
|
|
100
|
+
teams: {},
|
|
101
|
+
currentTeam: null,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
mockExchangeAndMint('new-skype', 'new-work-refresh')
|
|
105
|
+
const ok = await refreshDeviceCodeAccount('work', manager)
|
|
106
|
+
|
|
107
|
+
expect(ok).toBe(true)
|
|
108
|
+
const config = await manager.loadConfig()
|
|
109
|
+
expect(config?.accounts.work?.token).toBe('new-skype')
|
|
110
|
+
expect(config?.accounts.work?.aad_refresh_token).toBe('new-work-refresh')
|
|
111
|
+
expect(config?.accounts.work?.auth_method).toBe('device-code')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('refreshes a work account against its stored tenant authority', async () => {
|
|
115
|
+
const manager = setup()
|
|
116
|
+
const tenantId = 'c0ffee00-1111-2222-3333-444455556666'
|
|
117
|
+
await manager.setDeviceCodeAccount({
|
|
118
|
+
accountType: 'work',
|
|
119
|
+
token: 'old-skype',
|
|
120
|
+
tokenExpiresAt: '2000-01-01T00:00:00Z',
|
|
121
|
+
aadRefreshToken: 'old-refresh',
|
|
122
|
+
aadClientId: 'client',
|
|
123
|
+
aadTenantId: tenantId,
|
|
124
|
+
teams: {},
|
|
125
|
+
currentTeam: null,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const { tokenUrls } = mockExchangeAndMint('new-skype', 'new-work-refresh')
|
|
129
|
+
const ok = await refreshDeviceCodeAccount('work', manager)
|
|
130
|
+
|
|
131
|
+
expect(ok).toBe(true)
|
|
132
|
+
expect(tokenUrls.some((url) => url.includes(`/${tenantId}/oauth2/v2.0/token`))).toBe(true)
|
|
133
|
+
const config = await manager.loadConfig()
|
|
134
|
+
expect(config?.accounts.work?.aad_tenant_id).toBe(tenantId)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('does not change current_account when refreshing a non-current account', async () => {
|
|
138
|
+
const manager = setup()
|
|
139
|
+
await manager.setDeviceCodeAccount({
|
|
140
|
+
accountType: 'personal',
|
|
141
|
+
token: 'p',
|
|
142
|
+
tokenExpiresAt: '2000-01-01T00:00:00Z',
|
|
143
|
+
aadRefreshToken: 'r',
|
|
144
|
+
aadClientId: 'c',
|
|
145
|
+
teams: {},
|
|
146
|
+
currentTeam: null,
|
|
147
|
+
})
|
|
148
|
+
await manager.setToken('work-token', 'work', '2100-01-01T00:00:00Z')
|
|
149
|
+
await manager.setCurrentAccount('work')
|
|
150
|
+
|
|
151
|
+
mockExchangeAndMint('new-skype', 'new-refresh')
|
|
152
|
+
await refreshDeviceCodeAccount('personal', manager)
|
|
153
|
+
|
|
154
|
+
const config = await manager.loadConfig()
|
|
155
|
+
expect(config?.current_account).toBe('work')
|
|
156
|
+
expect(config?.accounts.personal?.token).toBe('new-skype')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('returns false for a non-device-code account', async () => {
|
|
160
|
+
const manager = setup()
|
|
161
|
+
await manager.setToken('extracted-token', 'personal', '2100-01-01T00:00:00Z')
|
|
162
|
+
|
|
163
|
+
const ok = await refreshDeviceCodeAccount('personal', manager)
|
|
164
|
+
expect(ok).toBe(false)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('returns false and leaves the token intact when the exchange fails', async () => {
|
|
168
|
+
const manager = setup()
|
|
169
|
+
await manager.setDeviceCodeAccount({
|
|
170
|
+
accountType: 'personal',
|
|
171
|
+
token: 'old-skype',
|
|
172
|
+
tokenExpiresAt: '2000-01-01T00:00:00Z',
|
|
173
|
+
aadRefreshToken: 'old-refresh',
|
|
174
|
+
aadClientId: 'client',
|
|
175
|
+
teams: {},
|
|
176
|
+
currentTeam: null,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
globalThis.fetch = mock(() =>
|
|
180
|
+
Promise.resolve({ ok: false, status: 400, json: () => Promise.resolve({ error: 'invalid_grant' }) } as Response),
|
|
181
|
+
) as typeof fetch
|
|
182
|
+
|
|
183
|
+
const ok = await refreshDeviceCodeAccount('personal', manager)
|
|
184
|
+
expect(ok).toBe(false)
|
|
185
|
+
const config = await manager.loadConfig()
|
|
186
|
+
expect(config?.accounts.personal?.token).toBe('old-skype')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('loginWithDeviceCode personal-account detection', () => {
|
|
191
|
+
const spies: Array<ReturnType<typeof spyOn>> = []
|
|
192
|
+
|
|
193
|
+
function spy<T extends object, K extends keyof T>(obj: T, key: K): ReturnType<typeof spyOn> {
|
|
194
|
+
const s = spyOn(obj, key as never)
|
|
195
|
+
spies.push(s)
|
|
196
|
+
return s
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
for (const s of spies.splice(0)) s.mockRestore()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
function stubFinalizeChain(): void {
|
|
204
|
+
spy(deviceCode, 'exchangeForSkypeScope').mockResolvedValue({ accessToken: 'spaces-at', refreshToken: 'rt' })
|
|
205
|
+
spy(deviceCode, 'mintSkypeToken').mockResolvedValue({
|
|
206
|
+
skypeToken: 'skype',
|
|
207
|
+
skypeTokenExpiresAt: '2100-01-01T00:00:00Z',
|
|
208
|
+
})
|
|
209
|
+
spy(TeamsClient.prototype, 'login').mockResolvedValue(new TeamsClient())
|
|
210
|
+
spy(TeamsClient.prototype, 'testAuth').mockResolvedValue({
|
|
211
|
+
id: 'u',
|
|
212
|
+
displayName: 'Work User',
|
|
213
|
+
email: 'w@example.com',
|
|
214
|
+
})
|
|
215
|
+
spy(TeamsClient.prototype, 'listTeams').mockResolvedValue([])
|
|
216
|
+
spy(TeamsClient.prototype, 'getRegion').mockReturnValue('emea')
|
|
217
|
+
spy(TeamsCredentialManager.prototype, 'setDeviceCodeAccount').mockResolvedValue(undefined)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
it('stops with an actionable hint when a work login resolves to a consumer account', async () => {
|
|
221
|
+
const requestSpy = spy(deviceCode, 'requestDeviceCode').mockResolvedValue(fakeDeviceCodeInfo())
|
|
222
|
+
spy(deviceCode, 'pollDeviceToken').mockResolvedValue({
|
|
223
|
+
accessToken: fakeJwt({ tid: CONSUMER_TENANT_ID }),
|
|
224
|
+
refreshToken: 'work-rt',
|
|
225
|
+
})
|
|
226
|
+
const tenantSpy = spy(deviceCode, 'resolveWorkTenantId')
|
|
227
|
+
const exchangeSpy = spy(deviceCode, 'exchangeForSkypeScope')
|
|
228
|
+
|
|
229
|
+
// given: a work login whose token carries a consumer tenant
|
|
230
|
+
// then: it throws --account-type personal guidance without minting a token
|
|
231
|
+
await expect(loginWithDeviceCode({ accountType: 'work', onCode: () => {} })).rejects.toThrow(
|
|
232
|
+
/--account-type personal/,
|
|
233
|
+
)
|
|
234
|
+
expect(requestSpy).toHaveBeenCalledTimes(1)
|
|
235
|
+
expect(tenantSpy).not.toHaveBeenCalled()
|
|
236
|
+
expect(exchangeSpy).not.toHaveBeenCalled()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('finishes a work login normally when it resolves to a real organizational tenant', async () => {
|
|
240
|
+
const requestSpy = spy(deviceCode, 'requestDeviceCode').mockResolvedValue(fakeDeviceCodeInfo())
|
|
241
|
+
spy(deviceCode, 'pollDeviceToken').mockResolvedValue({
|
|
242
|
+
accessToken: fakeJwt({ tid: REAL_TENANT_ID }),
|
|
243
|
+
refreshToken: 'work-rt',
|
|
244
|
+
})
|
|
245
|
+
const tenantSpy = spy(deviceCode, 'resolveWorkTenantId').mockResolvedValue(REAL_TENANT_ID)
|
|
246
|
+
stubFinalizeChain()
|
|
247
|
+
|
|
248
|
+
const result = await loginWithDeviceCode({ accountType: 'work', onCode: () => {} })
|
|
249
|
+
|
|
250
|
+
expect(requestSpy).toHaveBeenCalledTimes(1)
|
|
251
|
+
expect(tenantSpy).toHaveBeenCalledTimes(1)
|
|
252
|
+
expect(result.accountType).toBe('work')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('runs tenant discovery for a work login carrying the Microsoft Services placeholder', async () => {
|
|
256
|
+
// given: a work token whose tid is the MSA-passthrough placeholder, which
|
|
257
|
+
// legitimate work accounts also carry and which discovery can still resolve
|
|
258
|
+
spy(deviceCode, 'requestDeviceCode').mockResolvedValue(fakeDeviceCodeInfo())
|
|
259
|
+
spy(deviceCode, 'pollDeviceToken').mockResolvedValue({
|
|
260
|
+
accessToken: fakeJwt({ tid: MICROSOFT_SERVICES_TENANT_ID }),
|
|
261
|
+
refreshToken: 'work-rt',
|
|
262
|
+
})
|
|
263
|
+
const tenantSpy = spy(deviceCode, 'resolveWorkTenantId').mockResolvedValue(REAL_TENANT_ID)
|
|
264
|
+
stubFinalizeChain()
|
|
265
|
+
|
|
266
|
+
// then: it is not rejected early — discovery runs and the login completes
|
|
267
|
+
const result = await loginWithDeviceCode({ accountType: 'work', onCode: () => {} })
|
|
268
|
+
|
|
269
|
+
expect(tenantSpy).toHaveBeenCalledTimes(1)
|
|
270
|
+
expect(result.accountType).toBe('work')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('finishes a personal login without tenant discovery', async () => {
|
|
274
|
+
const requestSpy = spy(deviceCode, 'requestDeviceCode').mockResolvedValue(fakeDeviceCodeInfo())
|
|
275
|
+
spy(deviceCode, 'pollDeviceToken').mockResolvedValue({
|
|
276
|
+
accessToken: fakeJwt({ tid: CONSUMER_TENANT_ID }),
|
|
277
|
+
refreshToken: 'personal-rt',
|
|
278
|
+
})
|
|
279
|
+
const tenantSpy = spy(deviceCode, 'resolveWorkTenantId')
|
|
280
|
+
stubFinalizeChain()
|
|
281
|
+
|
|
282
|
+
const result = await loginWithDeviceCode({ accountType: 'personal', onCode: () => {} })
|
|
283
|
+
|
|
284
|
+
expect(requestSpy.mock.calls[0][1]).toBe('personal')
|
|
285
|
+
expect(tenantSpy).not.toHaveBeenCalled()
|
|
286
|
+
expect(result.accountType).toBe('personal')
|
|
287
|
+
})
|
|
288
|
+
})
|