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,42 @@
|
|
|
1
|
+
import { GET_CREDENTIAL_TYPE_URL } from './app-config'
|
|
2
|
+
import type { TeamsAccountType } from './types'
|
|
3
|
+
|
|
4
|
+
// GetCredentialType (the endpoint Microsoft's own login page calls) reports
|
|
5
|
+
// EstsProperties.DomainType, which distinguishes a personal account from a
|
|
6
|
+
// work account before any device code is issued.
|
|
7
|
+
const DOMAIN_TYPE_CONSUMER = 2
|
|
8
|
+
|
|
9
|
+
interface CredentialTypeResponse {
|
|
10
|
+
IfExistsResult?: number
|
|
11
|
+
EstsProperties?: {
|
|
12
|
+
DomainType?: number
|
|
13
|
+
IsConsumerDomain?: boolean
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Returns the account type for an email, or undefined when it can't be
|
|
18
|
+
// determined (network error, throttling, unknown domain) so the caller can
|
|
19
|
+
// fall back to its default flow rather than block sign-in.
|
|
20
|
+
export async function probeAccountType(email: string): Promise<TeamsAccountType | undefined> {
|
|
21
|
+
let body: CredentialTypeResponse
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(GET_CREDENTIAL_TYPE_URL, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify({ Username: email }),
|
|
27
|
+
})
|
|
28
|
+
if (!res.ok) return undefined
|
|
29
|
+
body = (await res.json()) as CredentialTypeResponse
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ests = body.EstsProperties
|
|
35
|
+
if (ests?.DomainType === DOMAIN_TYPE_CONSUMER || ests?.IsConsumerDomain === true) {
|
|
36
|
+
return 'personal'
|
|
37
|
+
}
|
|
38
|
+
if (typeof ests?.DomainType === 'number') {
|
|
39
|
+
return 'work'
|
|
40
|
+
}
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
@@ -49,6 +49,12 @@ const TEAMS_HOST_PATTERNS = [
|
|
|
49
49
|
'.microsoft.com',
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
+
// The trouter real-time WebSocket needs an OAuth Bearer token (a JWE) for its
|
|
53
|
+
// user.authenticate step. Teams stores it in the `authtoken` cookie, prefixed
|
|
54
|
+
// with "Bearer=" and URL-encoded.
|
|
55
|
+
const AUTHTOKEN_COOKIE_NAME = 'authtoken'
|
|
56
|
+
const AUTHTOKEN_HOST_PATTERNS = ['teams.live.com', 'teams.microsoft.com']
|
|
57
|
+
|
|
52
58
|
const TEAMS_KEYCHAIN_VARIANTS: KeychainVariant[] = [
|
|
53
59
|
{ service: 'Microsoft Teams Safe Storage', account: 'Microsoft Teams' },
|
|
54
60
|
{
|
|
@@ -252,6 +258,69 @@ export class TeamsTokenExtractor {
|
|
|
252
258
|
return this.extractFromCookiesDB()
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
async extractIdToken(accountType?: TeamsAccountType): Promise<string | null> {
|
|
262
|
+
await this.decryptor.loadCachedKey()
|
|
263
|
+
|
|
264
|
+
const desktopPaths = this.getDesktopCookiesPaths().filter(
|
|
265
|
+
(p) => accountType === undefined || !p.accountTypeKnown || p.accountType === accountType,
|
|
266
|
+
)
|
|
267
|
+
const candidatePaths = [...desktopPaths, ...this.getBrowserCookiesPaths()]
|
|
268
|
+
|
|
269
|
+
for (const { path: dbPath } of candidatePaths) {
|
|
270
|
+
if (!dbPath || !existsSync(dbPath)) continue
|
|
271
|
+
|
|
272
|
+
const bearer = await this.extractAuthTokenFromSQLite(dbPath)
|
|
273
|
+
if (bearer) return bearer
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async extractAuthTokenFromSQLite(dbPath: string): Promise<string | null> {
|
|
280
|
+
try {
|
|
281
|
+
let localStatePath: string | undefined
|
|
282
|
+
if (this.platform === 'win32') {
|
|
283
|
+
localStatePath = findLocalStatePath(dbPath) ?? undefined
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const hostPattern of AUTHTOKEN_HOST_PATTERNS) {
|
|
287
|
+
const sql = `
|
|
288
|
+
SELECT value, encrypted_value
|
|
289
|
+
FROM cookies
|
|
290
|
+
WHERE name = '${AUTHTOKEN_COOKIE_NAME}'
|
|
291
|
+
AND host_key LIKE '%${hostPattern}%'
|
|
292
|
+
LIMIT 1
|
|
293
|
+
`
|
|
294
|
+
|
|
295
|
+
type CookieRow = { value?: string; encrypted_value?: Uint8Array | Buffer } | null
|
|
296
|
+
const row = await this.cookieReader.queryFirst<CookieRow>(dbPath, sql)
|
|
297
|
+
if (!row) continue
|
|
298
|
+
|
|
299
|
+
let value = row.value ?? ''
|
|
300
|
+
if ((!value || value.length < 20) && row.encrypted_value && row.encrypted_value.length > 0) {
|
|
301
|
+
const decrypted = this.decryptor.decryptCookieRaw(Buffer.from(row.encrypted_value), localStatePath)
|
|
302
|
+
if (!decrypted) continue
|
|
303
|
+
value = ChromiumCookieDecryptor.stripIntegrityHash(decrypted).toString('utf8')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const bearer = this.normalizeAuthToken(value)
|
|
307
|
+
if (bearer) return bearer
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.debug(` authtoken query error: ${(error as Error).message}`)
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private normalizeAuthToken(rawValue: string): string | null {
|
|
318
|
+
if (!rawValue) return null
|
|
319
|
+
const decoded = decodeURIComponent(rawValue)
|
|
320
|
+
const token = decoded.replace(/^Bearer=/i, '').trim()
|
|
321
|
+
return token.length > 20 ? token : null
|
|
322
|
+
}
|
|
323
|
+
|
|
255
324
|
async clearKeyCache(): Promise<void> {
|
|
256
325
|
await this.decryptor.clearKeyCache()
|
|
257
326
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { rmSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { TeamsCredentialManager } from './credential-manager'
|
|
6
|
+
import { TeamsTokenProvider } from './token-provider'
|
|
7
|
+
import { TeamsAuthCapabilityError } from './types'
|
|
8
|
+
|
|
9
|
+
const TENANT_ID = '11111111-1111-1111-1111-111111111111'
|
|
10
|
+
const USER_ID = '22222222-2222-2222-2222-222222222222'
|
|
11
|
+
const CLIENT_ID = '1fec8e78-bce4-4aaf-ab1b-5451cc387264'
|
|
12
|
+
const SUBSTRATE_AUD = 'https://substrate.office.com'
|
|
13
|
+
const GRAPH_AUD = 'https://graph.microsoft.com'
|
|
14
|
+
const originalFetch = globalThis.fetch
|
|
15
|
+
const testDirs: string[] = []
|
|
16
|
+
|
|
17
|
+
interface FetchCall {
|
|
18
|
+
url: string
|
|
19
|
+
init: RequestInit
|
|
20
|
+
body: URLSearchParams
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TokenResponseConfig {
|
|
24
|
+
aud: string
|
|
25
|
+
refreshToken: string
|
|
26
|
+
expiresIn?: number
|
|
27
|
+
marker?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setup(): TeamsCredentialManager {
|
|
31
|
+
const dir = join(import.meta.dir, `.test-teams-provider-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
32
|
+
testDirs.push(dir)
|
|
33
|
+
return new TeamsCredentialManager(dir)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function seedDeviceAccount(manager: TeamsCredentialManager, refreshToken = 'old-refresh'): Promise<void> {
|
|
37
|
+
await manager.setDeviceCodeAccount({
|
|
38
|
+
accountType: 'work',
|
|
39
|
+
token: 'skype-token',
|
|
40
|
+
tokenExpiresAt: '2100-01-01T00:00:00Z',
|
|
41
|
+
aadRefreshToken: refreshToken,
|
|
42
|
+
aadClientId: CLIENT_ID,
|
|
43
|
+
teams: {},
|
|
44
|
+
currentTeam: null,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function seedPersonalDeviceAccount(
|
|
49
|
+
manager: TeamsCredentialManager,
|
|
50
|
+
refreshToken = 'personal-refresh',
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
await manager.setDeviceCodeAccount({
|
|
53
|
+
accountType: 'personal',
|
|
54
|
+
token: 'personal-skype-token',
|
|
55
|
+
tokenExpiresAt: '2100-01-01T00:00:00Z',
|
|
56
|
+
aadRefreshToken: refreshToken,
|
|
57
|
+
aadClientId: CLIENT_ID,
|
|
58
|
+
teams: {},
|
|
59
|
+
currentTeam: null,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createJwt(aud: string, marker = 'token'): string {
|
|
64
|
+
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
|
|
65
|
+
const payload = Buffer.from(
|
|
66
|
+
JSON.stringify({ aud, tid: TENANT_ID, oid: USER_ID, exp: Math.floor(Date.now() / 1000) + 3600, jti: marker }),
|
|
67
|
+
).toString('base64url')
|
|
68
|
+
return `${header}.${payload}.signature`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function bodyParams(body: BodyInit | null | undefined): URLSearchParams {
|
|
72
|
+
if (body instanceof URLSearchParams) return body
|
|
73
|
+
if (typeof body === 'string') return new URLSearchParams(body)
|
|
74
|
+
return new URLSearchParams()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function headerValue(init: RequestInit, name: string): string | undefined {
|
|
78
|
+
const headers = init.headers
|
|
79
|
+
if (headers instanceof Headers) return headers.get(name) ?? undefined
|
|
80
|
+
if (Array.isArray(headers)) {
|
|
81
|
+
const pair = headers.find(([key]) => key.toLowerCase() === name.toLowerCase())
|
|
82
|
+
return pair?.[1]
|
|
83
|
+
}
|
|
84
|
+
return headers?.[name]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mockTokenResponses(responses: TokenResponseConfig[]): { calls: FetchCall[] } {
|
|
88
|
+
const calls: FetchCall[] = []
|
|
89
|
+
const queue = [...responses]
|
|
90
|
+
globalThis.fetch = mock((input: string | URL | Request, init?: RequestInit) => {
|
|
91
|
+
const requestInit = init ?? {}
|
|
92
|
+
calls.push({ url: String(input), init: requestInit, body: bodyParams(requestInit.body) })
|
|
93
|
+
const next = queue.shift()
|
|
94
|
+
if (!next) {
|
|
95
|
+
return Promise.resolve(new Response(JSON.stringify({ error: 'no_mock_response' }), { status: 500 }))
|
|
96
|
+
}
|
|
97
|
+
return Promise.resolve(
|
|
98
|
+
new Response(
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
access_token: createJwt(next.aud, next.marker),
|
|
101
|
+
refresh_token: next.refreshToken,
|
|
102
|
+
expires_in: next.expiresIn ?? 3600,
|
|
103
|
+
}),
|
|
104
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
}) as typeof fetch
|
|
108
|
+
return { calls }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
globalThis.fetch = originalFetch
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
afterAll(() => {
|
|
116
|
+
for (const dir of testDirs) rmSync(dir, { recursive: true, force: true })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('TeamsTokenProvider', () => {
|
|
120
|
+
it('refresh-exchanges substrate tokens with Origin and persists rotated refresh tokens', async () => {
|
|
121
|
+
const manager = setup()
|
|
122
|
+
await seedDeviceAccount(manager)
|
|
123
|
+
const { calls } = mockTokenResponses([{ aud: SUBSTRATE_AUD, refreshToken: 'rotated-refresh' }])
|
|
124
|
+
|
|
125
|
+
const provider = new TeamsTokenProvider(manager)
|
|
126
|
+
const token = await provider.getSubstrateToken()
|
|
127
|
+
|
|
128
|
+
expect(token).toContain('.signature')
|
|
129
|
+
expect(calls[0].url).toContain('/organizations/oauth2/v2.0/token')
|
|
130
|
+
expect(calls[0].body.get('grant_type')).toBe('refresh_token')
|
|
131
|
+
expect(calls[0].body.get('client_id')).toBe(CLIENT_ID)
|
|
132
|
+
expect(calls[0].body.get('refresh_token')).toBe('old-refresh')
|
|
133
|
+
expect(calls[0].body.get('scope')).toBe('https://substrate.office.com/.default offline_access')
|
|
134
|
+
expect(headerValue(calls[0].init, 'Origin')).toBe('https://teams.microsoft.com')
|
|
135
|
+
|
|
136
|
+
const config = await manager.loadConfig()
|
|
137
|
+
expect(config?.accounts.work?.aad_refresh_token).toBe('rotated-refresh')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('throws when the acquired token audience does not match the requested audience', async () => {
|
|
141
|
+
const manager = setup()
|
|
142
|
+
await seedDeviceAccount(manager)
|
|
143
|
+
mockTokenResponses([{ aud: GRAPH_AUD, refreshToken: 'rotated-refresh' }])
|
|
144
|
+
|
|
145
|
+
const provider = new TeamsTokenProvider(manager)
|
|
146
|
+
await expect(provider.getSubstrateToken()).rejects.toThrow(/audience/i)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('throws TeamsAuthCapabilityError for cookie-only credentials', async () => {
|
|
150
|
+
const manager = setup()
|
|
151
|
+
await manager.setToken('skype-token', 'work', '2100-01-01T00:00:00Z')
|
|
152
|
+
|
|
153
|
+
const provider = new TeamsTokenProvider(manager)
|
|
154
|
+
await expect(provider.getSubstrateToken()).rejects.toThrow(TeamsAuthCapabilityError)
|
|
155
|
+
await expect(provider.getSubstrateToken()).rejects.toThrow(/auth login/)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('uses the in-memory cache until the five-minute refresh buffer', async () => {
|
|
159
|
+
const manager = setup()
|
|
160
|
+
await seedDeviceAccount(manager)
|
|
161
|
+
const { calls } = mockTokenResponses([{ aud: SUBSTRATE_AUD, refreshToken: 'rotated-refresh', marker: 'cached' }])
|
|
162
|
+
|
|
163
|
+
const provider = new TeamsTokenProvider(manager)
|
|
164
|
+
const first = await provider.getSubstrateToken()
|
|
165
|
+
const second = await provider.getSubstrateToken()
|
|
166
|
+
|
|
167
|
+
expect(second).toBe(first)
|
|
168
|
+
expect(calls).toHaveLength(1)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('refreshes cached tokens inside the five-minute buffer', async () => {
|
|
172
|
+
const manager = setup()
|
|
173
|
+
await seedDeviceAccount(manager)
|
|
174
|
+
const { calls } = mockTokenResponses([
|
|
175
|
+
{ aud: SUBSTRATE_AUD, refreshToken: 'near-expiry-refresh', expiresIn: 299, marker: 'near-expiry' },
|
|
176
|
+
{ aud: SUBSTRATE_AUD, refreshToken: 'fresh-refresh', expiresIn: 3600, marker: 'fresh' },
|
|
177
|
+
])
|
|
178
|
+
|
|
179
|
+
const provider = new TeamsTokenProvider(manager)
|
|
180
|
+
const first = await provider.getSubstrateToken()
|
|
181
|
+
const second = await provider.getSubstrateToken()
|
|
182
|
+
|
|
183
|
+
expect(second).not.toBe(first)
|
|
184
|
+
expect(calls).toHaveLength(2)
|
|
185
|
+
const config = await manager.loadConfig()
|
|
186
|
+
expect(config?.accounts.work?.aad_refresh_token).toBe('fresh-refresh')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('does not reuse bearer tokens across bound accounts', async () => {
|
|
190
|
+
const manager = setup()
|
|
191
|
+
await seedDeviceAccount(manager, 'work-refresh')
|
|
192
|
+
await seedPersonalDeviceAccount(manager, 'personal-refresh')
|
|
193
|
+
const { calls } = mockTokenResponses([
|
|
194
|
+
{ aud: SUBSTRATE_AUD, refreshToken: 'work-rotated-refresh', marker: 'work-token' },
|
|
195
|
+
{ aud: SUBSTRATE_AUD, refreshToken: 'personal-rotated-refresh', marker: 'personal-token' },
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
const provider = new TeamsTokenProvider(manager)
|
|
199
|
+
const workToken = await provider.bindAccount('work').getSubstrateToken()
|
|
200
|
+
const personalToken = await provider.bindAccount('personal').getSubstrateToken()
|
|
201
|
+
|
|
202
|
+
expect(personalToken).not.toBe(workToken)
|
|
203
|
+
expect(calls).toHaveLength(2)
|
|
204
|
+
expect(calls[0].body.get('refresh_token')).toBe('work-refresh')
|
|
205
|
+
expect(calls[1].body.get('refresh_token')).toBe('personal-refresh')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('restores tenant/user identity on a cached bearer hit after switching accounts back', async () => {
|
|
209
|
+
const manager = setup()
|
|
210
|
+
await seedDeviceAccount(manager, 'work-refresh')
|
|
211
|
+
await seedPersonalDeviceAccount(manager, 'personal-refresh')
|
|
212
|
+
mockTokenResponses([
|
|
213
|
+
{ aud: SUBSTRATE_AUD, refreshToken: 'work-rotated', marker: 'work-token' },
|
|
214
|
+
{ aud: SUBSTRATE_AUD, refreshToken: 'personal-rotated', marker: 'personal-token' },
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
const provider = new TeamsTokenProvider(manager)
|
|
218
|
+
// work mints + caches (identity set), personal switch resets identity,
|
|
219
|
+
// then work again is served from cache and must re-restore identity
|
|
220
|
+
await provider.bindAccount('work').getSubstrateToken()
|
|
221
|
+
await provider.bindAccount('personal').getSubstrateToken()
|
|
222
|
+
await provider.bindAccount('work').getSubstrateToken()
|
|
223
|
+
|
|
224
|
+
expect(await provider.getTenantId()).toBe(TENANT_ID)
|
|
225
|
+
expect(await provider.getUserId()).toBe(USER_ID)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AAD_AUDIENCE_GRAPH,
|
|
3
|
+
AAD_AUDIENCE_SUBSTRATE,
|
|
4
|
+
AAD_SCOPE_GRAPH,
|
|
5
|
+
AAD_SCOPE_SUBSTRATE,
|
|
6
|
+
consumerTokenUrl,
|
|
7
|
+
getTeamsAppClientId,
|
|
8
|
+
organizationsTokenUrl,
|
|
9
|
+
TEAMS_WEB_ORIGIN,
|
|
10
|
+
} from './app-config'
|
|
11
|
+
import { TeamsCredentialManager } from './credential-manager'
|
|
12
|
+
import type { TeamsAccount, TeamsAccountType } from './types'
|
|
13
|
+
import { TeamsAuthCapabilityError, TeamsError } from './types'
|
|
14
|
+
|
|
15
|
+
export type TokenAudience = 'skype' | 'substrate' | 'graph'
|
|
16
|
+
|
|
17
|
+
type BearerAudience = Exclude<TokenAudience, 'skype'>
|
|
18
|
+
|
|
19
|
+
interface CachedToken {
|
|
20
|
+
accessToken: string
|
|
21
|
+
expiresAt: number
|
|
22
|
+
aud: string
|
|
23
|
+
tenantId?: string
|
|
24
|
+
userId?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type CacheKey = TokenAudience | `${TeamsAccountType}:${BearerAudience}`
|
|
28
|
+
|
|
29
|
+
interface CurrentAccountContext {
|
|
30
|
+
accountType: TeamsAccountType
|
|
31
|
+
account: TeamsAccount
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface BearerAudienceConfig {
|
|
35
|
+
scope: string
|
|
36
|
+
aud: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface JwtClaims {
|
|
40
|
+
aud?: string
|
|
41
|
+
tid?: string
|
|
42
|
+
oid?: string
|
|
43
|
+
exp?: number
|
|
44
|
+
[key: string]: unknown
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1000
|
|
48
|
+
const DEFAULT_ACCESS_TOKEN_TTL_SEC = 3600
|
|
49
|
+
|
|
50
|
+
const BEARER_AUDIENCES: Record<BearerAudience, BearerAudienceConfig> = {
|
|
51
|
+
substrate: { scope: AAD_SCOPE_SUBSTRATE, aud: AAD_AUDIENCE_SUBSTRATE },
|
|
52
|
+
graph: { scope: AAD_SCOPE_GRAPH, aud: AAD_AUDIENCE_GRAPH },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class TeamsTokenProvider {
|
|
56
|
+
private cache: Map<CacheKey, CachedToken> = new Map()
|
|
57
|
+
private tenantId?: string
|
|
58
|
+
private userId?: string
|
|
59
|
+
private boundAccountType?: TeamsAccountType
|
|
60
|
+
private lastAccountType?: TeamsAccountType
|
|
61
|
+
|
|
62
|
+
constructor(private credManager: TeamsCredentialManager = new TeamsCredentialManager()) {}
|
|
63
|
+
|
|
64
|
+
bindAccount(accountType: TeamsAccountType): this {
|
|
65
|
+
this.boundAccountType = accountType
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getSkypeToken(): Promise<string> {
|
|
70
|
+
const cached = this.getCached('skype')
|
|
71
|
+
if (cached) return cached.accessToken
|
|
72
|
+
|
|
73
|
+
const cred = await this.credManager.getTokenWithExpiry()
|
|
74
|
+
if (!cred?.token) {
|
|
75
|
+
throw new TeamsError(
|
|
76
|
+
'No Teams credentials found. Run `agent-teams auth login` or `agent-teams auth extract`.',
|
|
77
|
+
'no_credentials',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const expiresAt = cred.tokenExpiresAt ? new Date(cred.tokenExpiresAt).getTime() : Number.POSITIVE_INFINITY
|
|
82
|
+
if (expiresAt <= Date.now()) {
|
|
83
|
+
throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.cache.set('skype', { accessToken: cred.token, expiresAt, aud: 'skype' })
|
|
87
|
+
return cred.token
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getSubstrateToken(): Promise<string> {
|
|
91
|
+
return this.getBearerToken('substrate')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getGraphToken(): Promise<string> {
|
|
95
|
+
return this.getBearerToken('graph')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getTenantId(): Promise<string> {
|
|
99
|
+
await this.ensureIdentity()
|
|
100
|
+
if (!this.tenantId) {
|
|
101
|
+
throw new TeamsError(
|
|
102
|
+
'Microsoft token did not include a tenant id needed for Teams search.',
|
|
103
|
+
'aad_identity_missing',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return this.tenantId
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getUserId(): Promise<string> {
|
|
110
|
+
await this.ensureIdentity()
|
|
111
|
+
if (!this.userId) {
|
|
112
|
+
throw new TeamsError('Microsoft token did not include a user id needed for Teams search.', 'aad_identity_missing')
|
|
113
|
+
}
|
|
114
|
+
return this.userId
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private getCached(key: CacheKey): CachedToken | undefined {
|
|
118
|
+
const cached = this.cache.get(key)
|
|
119
|
+
if (!cached) return undefined
|
|
120
|
+
return cached.expiresAt - Date.now() > REFRESH_BUFFER_MS ? cached : undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async getBearerToken(audience: BearerAudience): Promise<string> {
|
|
124
|
+
const current = await this.getCurrentAccount()
|
|
125
|
+
this.resetIdentityIfAccountChanged(current.accountType)
|
|
126
|
+
const cacheKey: CacheKey = `${current.accountType}:${audience}`
|
|
127
|
+
const cached = this.getCached(cacheKey)
|
|
128
|
+
if (cached) {
|
|
129
|
+
// a cache hit after an account switch reset our identity, so restore the
|
|
130
|
+
// tenant/user claims captured when this token was minted
|
|
131
|
+
this.tenantId = cached.tenantId ?? this.tenantId
|
|
132
|
+
this.userId = cached.userId ?? this.userId
|
|
133
|
+
return cached.accessToken
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!current.account.aad_refresh_token) {
|
|
137
|
+
throw new TeamsAuthCapabilityError()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const clientId = current.account.aad_client_id ?? getTeamsAppClientId(current.accountType).clientId
|
|
141
|
+
const config = BEARER_AUDIENCES[audience]
|
|
142
|
+
const token = await this.refreshBearerToken({
|
|
143
|
+
accountType: current.accountType,
|
|
144
|
+
clientId,
|
|
145
|
+
refreshToken: current.account.aad_refresh_token,
|
|
146
|
+
audience,
|
|
147
|
+
config,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
this.cache.set(cacheKey, token)
|
|
151
|
+
return token.accessToken
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async refreshBearerToken(params: {
|
|
155
|
+
accountType: TeamsAccountType
|
|
156
|
+
clientId: string
|
|
157
|
+
refreshToken: string
|
|
158
|
+
audience: BearerAudience
|
|
159
|
+
config: BearerAudienceConfig
|
|
160
|
+
}): Promise<CachedToken> {
|
|
161
|
+
const response = await fetch(tokenUrlForAccount(params.accountType), {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
165
|
+
Origin: TEAMS_WEB_ORIGIN,
|
|
166
|
+
},
|
|
167
|
+
body: new URLSearchParams({
|
|
168
|
+
grant_type: 'refresh_token',
|
|
169
|
+
client_id: params.clientId,
|
|
170
|
+
refresh_token: params.refreshToken,
|
|
171
|
+
scope: params.config.scope,
|
|
172
|
+
}),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const body = (await response.json().catch(() => ({}))) as Record<string, unknown>
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
throw new TeamsError(
|
|
178
|
+
`Microsoft token refresh failed: ${describeAadError(body, response.status)}`,
|
|
179
|
+
'aad_refresh_failed',
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const accessToken = stringField(body, 'access_token')
|
|
184
|
+
if (!accessToken) {
|
|
185
|
+
throw new TeamsError('Microsoft token refresh response did not include an access token.', 'aad_access_missing')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const claims = decodeJwt(accessToken)
|
|
189
|
+
if (claims.aud !== params.config.aud) {
|
|
190
|
+
throw new TeamsError(
|
|
191
|
+
`Microsoft token audience mismatch: expected ${params.config.aud}, got ${claims.aud ?? 'missing'}.`,
|
|
192
|
+
'aad_audience_mismatch',
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.tenantId = claims.tid ?? this.tenantId
|
|
197
|
+
this.userId = claims.oid ?? this.userId
|
|
198
|
+
|
|
199
|
+
const rotatedRefreshToken = stringField(body, 'refresh_token')
|
|
200
|
+
if (rotatedRefreshToken) {
|
|
201
|
+
await this.credManager.updateAadRefreshToken(params.accountType, rotatedRefreshToken, params.clientId)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const expiresIn = numberField(body, 'expires_in') ?? DEFAULT_ACCESS_TOKEN_TTL_SEC
|
|
205
|
+
return {
|
|
206
|
+
accessToken,
|
|
207
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
208
|
+
aud: params.config.aud,
|
|
209
|
+
tenantId: claims.tid,
|
|
210
|
+
userId: claims.oid,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async getCurrentAccount(): Promise<CurrentAccountContext> {
|
|
215
|
+
const config = await this.credManager.loadConfig()
|
|
216
|
+
if (!config) {
|
|
217
|
+
throw new TeamsError(
|
|
218
|
+
'No Teams credentials found. Run `agent-teams auth login` or `agent-teams auth extract`.',
|
|
219
|
+
'no_credentials',
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
const accountKey = this.boundAccountType ?? TeamsCredentialManager.accountOverride ?? config.current_account
|
|
223
|
+
if (accountKey !== 'work' && accountKey !== 'personal') {
|
|
224
|
+
throw new TeamsError(
|
|
225
|
+
'No active Teams account selected. Run `agent-teams auth switch-account`.',
|
|
226
|
+
'no_current_account',
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
const account = config.accounts[accountKey]
|
|
230
|
+
if (!account) {
|
|
231
|
+
throw new TeamsError(
|
|
232
|
+
'No active Teams account selected. Run `agent-teams auth switch-account`.',
|
|
233
|
+
'no_current_account',
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
return { accountType: accountKey, account }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private resetIdentityIfAccountChanged(accountType: TeamsAccountType): void {
|
|
240
|
+
if (this.lastAccountType === accountType) return
|
|
241
|
+
this.tenantId = undefined
|
|
242
|
+
this.userId = undefined
|
|
243
|
+
this.lastAccountType = accountType
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async ensureIdentity(): Promise<void> {
|
|
247
|
+
if (this.tenantId && this.userId) return
|
|
248
|
+
await this.getSubstrateToken()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function tokenUrlForAccount(accountType: TeamsAccountType): string {
|
|
253
|
+
return accountType === 'personal' ? consumerTokenUrl() : organizationsTokenUrl()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function decodeJwt(token: string): JwtClaims {
|
|
257
|
+
const payload = token.split('.')[1]
|
|
258
|
+
if (!payload) {
|
|
259
|
+
throw new TeamsError('Microsoft token response was not a JWT.', 'aad_token_invalid')
|
|
260
|
+
}
|
|
261
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>
|
|
262
|
+
return {
|
|
263
|
+
...decoded,
|
|
264
|
+
aud: typeof decoded.aud === 'string' ? decoded.aud : undefined,
|
|
265
|
+
tid: typeof decoded.tid === 'string' ? decoded.tid : undefined,
|
|
266
|
+
oid: typeof decoded.oid === 'string' ? decoded.oid : undefined,
|
|
267
|
+
exp: typeof decoded.exp === 'number' ? decoded.exp : undefined,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function stringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
272
|
+
const value = record[key]
|
|
273
|
+
return typeof value === 'string' ? value : undefined
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function numberField(record: Record<string, unknown>, key: string): number | undefined {
|
|
277
|
+
const value = record[key]
|
|
278
|
+
return typeof value === 'number' ? value : undefined
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function describeAadError(body: Record<string, unknown>, status: number): string {
|
|
282
|
+
const desc = body.error_description ?? body.error
|
|
283
|
+
return typeof desc === 'string' ? desc.split('\n')[0] : `HTTP ${status}`
|
|
284
|
+
}
|