@patimweb/pi-teams 1.0.0 → 1.1.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/README.md +112 -28
- package/package.json +3 -2
- package/prompts/teams-doctor.md +9 -2
- package/skills/teams-collaboration/SKILL.md +4 -2
- package/src/auth/cache-plugin.ts +215 -0
- package/src/auth/index.ts +301 -54
- package/src/auth/msal.ts +168 -0
- package/src/config/index.ts +52 -18
- package/src/safety/index.ts +3 -3
- package/src/status.ts +21 -7
- package/src/tools/teams-accounts.ts +22 -17
- package/src/tools/teams-doctor.ts +36 -24
- package/src/tools/teams-login.ts +111 -60
- package/src/tools/teams-setup.ts +12 -2
- package/src/tools/teams-status.ts +8 -8
- package/src/utils/environment.ts +41 -0
- package/src/auth/client-credentials.ts +0 -72
- package/src/auth/device-code.ts +0 -276
- package/src/auth/token-store.ts +0 -156
package/src/config/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type PermissionBlock,
|
|
38
38
|
type ResolvedPermissions,
|
|
39
39
|
} from "./scope.ts";
|
|
40
|
+
import { canOpenBrowser } from "../utils/environment.ts";
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Types
|
|
@@ -45,15 +46,23 @@ import {
|
|
|
45
46
|
/**
|
|
46
47
|
* How pi obtains a token.
|
|
47
48
|
*
|
|
48
|
-
* - `
|
|
49
|
-
*
|
|
49
|
+
* - `interactive`: delegated. The browser opens at the Microsoft sign-in page
|
|
50
|
+
* and MSAL catches the redirect on a loopback port. pi then acts **as the
|
|
51
|
+
* signed-in user** — messages come from their account, and everything they
|
|
52
|
+
* can see, pi can see.
|
|
53
|
+
* - `device-code`: delegated as well, but the user types a short code on
|
|
54
|
+
* another device. For SSH sessions, containers, and anything headless.
|
|
50
55
|
* - `client-credentials`: app-only. No user identity; Microsoft Graph does not
|
|
51
56
|
* allow posting chat or channel messages this way (outside of migration
|
|
52
57
|
* scenarios), so writes are refused with an explanation.
|
|
53
|
-
* - `auto`:
|
|
54
|
-
* `device-code
|
|
58
|
+
* - `auto`: `client-credentials` when a secret is configured, otherwise
|
|
59
|
+
* `interactive` where a browser is reachable and `device-code` where it is
|
|
60
|
+
* not.
|
|
55
61
|
*/
|
|
56
|
-
export type AuthMode = "device-code" | "client-credentials" | "auto";
|
|
62
|
+
export type AuthMode = "interactive" | "device-code" | "client-credentials" | "auto";
|
|
63
|
+
|
|
64
|
+
/** An auth mode after "auto" has been resolved away. */
|
|
65
|
+
export type ResolvedAuthMode = "interactive" | "device-code" | "client-credentials";
|
|
57
66
|
|
|
58
67
|
/** Safety level for mutation operations. */
|
|
59
68
|
export type SafetyLevel = "open" | "confirm" | "readonly";
|
|
@@ -72,6 +81,8 @@ export interface TenantConfig {
|
|
|
72
81
|
authMode?: AuthMode;
|
|
73
82
|
/** Requested Graph scopes override */
|
|
74
83
|
scopes?: string[];
|
|
84
|
+
/** Fixed loopback port for the browser sign-in redirect */
|
|
85
|
+
loopbackPort?: number;
|
|
75
86
|
/** Safety level override — most specific wins */
|
|
76
87
|
safetyLevel?: SafetyLevel;
|
|
77
88
|
/** Scope rules layered on top of the account's */
|
|
@@ -94,6 +105,14 @@ export interface AccountConfig {
|
|
|
94
105
|
authMode?: AuthMode;
|
|
95
106
|
/** Requested Graph scopes (default: DEFAULT_SCOPES) */
|
|
96
107
|
scopes?: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Fixed loopback port for the browser sign-in redirect.
|
|
110
|
+
*
|
|
111
|
+
* Only needed where the app registration lists an exact redirect URI such
|
|
112
|
+
* as `http://localhost:3000`; by default MSAL picks a free port and Entra
|
|
113
|
+
* accepts any port on localhost for a public client.
|
|
114
|
+
*/
|
|
115
|
+
loopbackPort?: number;
|
|
97
116
|
/** Safety level override */
|
|
98
117
|
safetyLevel?: SafetyLevel;
|
|
99
118
|
/** Scope rules layered on top of the global ones */
|
|
@@ -136,8 +155,10 @@ export interface TeamsConnection {
|
|
|
136
155
|
clientId: string;
|
|
137
156
|
clientSecret?: string;
|
|
138
157
|
/** Effective auth mode — never "auto" after resolution */
|
|
139
|
-
authMode:
|
|
158
|
+
authMode: ResolvedAuthMode;
|
|
140
159
|
scopes: string[];
|
|
160
|
+
/** Fixed loopback port for the browser sign-in, when one is configured */
|
|
161
|
+
loopbackPort?: number;
|
|
141
162
|
safetyLevel: SafetyLevel;
|
|
142
163
|
permissions: ResolvedPermissions;
|
|
143
164
|
maxMessages: number;
|
|
@@ -165,13 +186,11 @@ export class ConfigError extends Error {
|
|
|
165
186
|
/**
|
|
166
187
|
* Delegated scopes requested by default.
|
|
167
188
|
*
|
|
168
|
-
* `
|
|
169
|
-
*
|
|
189
|
+
* `openid`, `profile` and `offline_access` are absent on purpose: MSAL always
|
|
190
|
+
* requests them and rejects them in an explicit scope list. The lasting
|
|
191
|
+
* sign-in they buy is therefore automatic.
|
|
170
192
|
*/
|
|
171
193
|
export const DEFAULT_SCOPES: string[] = [
|
|
172
|
-
"offline_access",
|
|
173
|
-
"openid",
|
|
174
|
-
"profile",
|
|
175
194
|
"User.Read",
|
|
176
195
|
"User.ReadBasic.All",
|
|
177
196
|
"Team.ReadBasic.All",
|
|
@@ -201,7 +220,12 @@ const DEFAULTS = {
|
|
|
201
220
|
authorityHost: "https://login.microsoftonline.com",
|
|
202
221
|
};
|
|
203
222
|
|
|
204
|
-
const VALID_AUTH_MODES = new Set<string>([
|
|
223
|
+
const VALID_AUTH_MODES = new Set<string>([
|
|
224
|
+
"interactive",
|
|
225
|
+
"device-code",
|
|
226
|
+
"client-credentials",
|
|
227
|
+
"auto",
|
|
228
|
+
]);
|
|
205
229
|
const VALID_SAFETY_LEVELS = new Set<string>(["open", "confirm", "readonly"]);
|
|
206
230
|
|
|
207
231
|
// ---------------------------------------------------------------------------
|
|
@@ -299,18 +323,27 @@ export function resolveEffectiveSafetyLevel(
|
|
|
299
323
|
);
|
|
300
324
|
}
|
|
301
325
|
|
|
302
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* Effective auth mode, resolving "auto" against the available credentials and
|
|
328
|
+
* the environment.
|
|
329
|
+
*
|
|
330
|
+
* `browserAvailable` is injected rather than detected here so the cascade stays
|
|
331
|
+
* a pure function — the detection itself lives in ../auth/msal.ts.
|
|
332
|
+
*/
|
|
303
333
|
export function resolveAuthMode(
|
|
304
334
|
account: AccountConfig | undefined,
|
|
305
335
|
tenant: TenantConfig | undefined,
|
|
306
336
|
hasSecret: boolean,
|
|
307
|
-
|
|
337
|
+
browserAvailable = true,
|
|
338
|
+
): ResolvedAuthMode {
|
|
308
339
|
const configured =
|
|
309
340
|
validateAuthMode(tenant?.authMode) ??
|
|
310
341
|
validateAuthMode(account?.authMode) ??
|
|
311
342
|
DEFAULTS.authMode;
|
|
312
|
-
|
|
313
|
-
return configured;
|
|
343
|
+
|
|
344
|
+
if (configured !== "auto") return configured;
|
|
345
|
+
if (hasSecret) return "client-credentials";
|
|
346
|
+
return browserAvailable ? "interactive" : "device-code";
|
|
314
347
|
}
|
|
315
348
|
|
|
316
349
|
// ---------------------------------------------------------------------------
|
|
@@ -395,7 +428,7 @@ export function resolveConnection(
|
|
|
395
428
|
if (missing.length > 0) throw new ConfigError(missing);
|
|
396
429
|
|
|
397
430
|
const clientSecret = tenant?.clientSecret ?? account.clientSecret ?? undefined;
|
|
398
|
-
const authMode = resolveAuthMode(account, tenant, !!clientSecret);
|
|
431
|
+
const authMode = resolveAuthMode(account, tenant, !!clientSecret, canOpenBrowser());
|
|
399
432
|
|
|
400
433
|
if (authMode === "client-credentials" && !clientSecret) {
|
|
401
434
|
throw new ConfigError(
|
|
@@ -417,6 +450,7 @@ export function resolveConnection(
|
|
|
417
450
|
clientId,
|
|
418
451
|
clientSecret,
|
|
419
452
|
authMode,
|
|
453
|
+
loopbackPort: tenant?.loopbackPort ?? account.loopbackPort,
|
|
420
454
|
scopes,
|
|
421
455
|
safetyLevel: resolveEffectiveSafetyLevel(config.safetyLevel, account, tenant),
|
|
422
456
|
permissions: resolvePermissions([config.permissions, account.permissions, tenant?.permissions]),
|
|
@@ -644,7 +678,7 @@ const TEMPLATE_JSON = `{
|
|
|
644
678
|
"displayName": "Work account",
|
|
645
679
|
"tenantId": "contoso.onmicrosoft.com",
|
|
646
680
|
"clientId": "00000000-0000-0000-0000-000000000000",
|
|
647
|
-
"authMode": "
|
|
681
|
+
"authMode": "interactive",
|
|
648
682
|
"safetyLevel": "confirm",
|
|
649
683
|
"permissions": {
|
|
650
684
|
"read": {
|
package/src/safety/index.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* which channel a message is actually headed for.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { SafetyLevel, TeamsConnection } from "../config/index.ts";
|
|
21
|
+
import type { ResolvedAuthMode, SafetyLevel, TeamsConnection } from "../config/index.ts";
|
|
22
22
|
import { checkScope, type ScopeCategory, type ScopeMode } from "../config/scope.ts";
|
|
23
23
|
import { ScopeDeniedError } from "../utils/errors.ts";
|
|
24
24
|
import { MUTATION_TOOLS } from "../tools/tool-names.ts";
|
|
@@ -47,7 +47,7 @@ export const LOCAL_CONFIG_TOOLS = new Set<string>(["teams_setup", "teams_logout"
|
|
|
47
47
|
*/
|
|
48
48
|
export function blockReason(
|
|
49
49
|
safetyLevel: SafetyLevel,
|
|
50
|
-
authMode:
|
|
50
|
+
authMode: ResolvedAuthMode,
|
|
51
51
|
toolName: string,
|
|
52
52
|
): string | undefined {
|
|
53
53
|
if (!isMutationTool(toolName)) return undefined;
|
|
@@ -64,7 +64,7 @@ export function blockReason(
|
|
|
64
64
|
return (
|
|
65
65
|
`"${toolName}" cannot run on an app-only token. Microsoft Graph only permits app-only writes to ` +
|
|
66
66
|
`chats and channels for migration scenarios, so pi cannot post as a person this way. ` +
|
|
67
|
-
`Set authMode to "
|
|
67
|
+
`Set authMode to "interactive" for this account and run teams_login.`
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
package/src/status.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TeamsConnection } from "./config/index.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { readCacheSummary } from "./auth/cache-plugin.ts";
|
|
10
10
|
import { formatPermissions } from "./config/scope.ts";
|
|
11
11
|
|
|
12
12
|
/** Immutable snapshot of the active Teams connection. */
|
|
@@ -34,7 +34,7 @@ export function buildConnectionCard(
|
|
|
34
34
|
): ConnectionCard | undefined {
|
|
35
35
|
if (!conn) return undefined;
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const cache = readCacheSummary(conn.account, conn.tenantId);
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
40
|
account: conn.account,
|
|
@@ -43,9 +43,11 @@ export function buildConnectionCard(
|
|
|
43
43
|
tenantId: conn.tenantId,
|
|
44
44
|
authMode: conn.authMode,
|
|
45
45
|
safetyLevel: conn.safetyLevel,
|
|
46
|
-
user:
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
user: cache.username ?? cache.name,
|
|
47
|
+
// A cached session counts as signed in even when the access token has
|
|
48
|
+
// expired: MSAL renews it silently on the next call.
|
|
49
|
+
signedIn: cache.present,
|
|
50
|
+
expiresAt: cache.expiresAt ? new Date(cache.expiresAt).toISOString() : undefined,
|
|
49
51
|
otherAccounts: conn.allAccounts.map((a) => a.name).filter((name) => name !== conn.account),
|
|
50
52
|
permissionSummary: formatPermissions(conn.permissions),
|
|
51
53
|
};
|
|
@@ -78,9 +80,21 @@ export function formatStatusText(card: ConnectionCard | undefined): string {
|
|
|
78
80
|
if (card.tenant !== card.account) lines.push(`- **Tenant:** ${card.tenant}`);
|
|
79
81
|
lines.push(`- **Tenant ID:** ${card.tenantId}`);
|
|
80
82
|
lines.push(`- **Acting as:** ${card.signedIn ? (card.user ?? "(signed in)") : "not signed in — run teams_login"}`);
|
|
81
|
-
lines.push(
|
|
83
|
+
lines.push(
|
|
84
|
+
`- **Auth mode:** ${card.authMode}${
|
|
85
|
+
card.authMode === "client-credentials"
|
|
86
|
+
? " (app-only — cannot post as a person)"
|
|
87
|
+
: card.authMode === "interactive"
|
|
88
|
+
? " (browser sign-in)"
|
|
89
|
+
: " (device code)"
|
|
90
|
+
}`,
|
|
91
|
+
);
|
|
82
92
|
lines.push(`- **Safety level:** ${card.safetyLevel}`);
|
|
83
|
-
if (card.expiresAt)
|
|
93
|
+
if (card.expiresAt) {
|
|
94
|
+
lines.push(
|
|
95
|
+
`- **Access token expires:** ${new Date(card.expiresAt).toLocaleString()} (renewed silently)`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
84
98
|
if (card.otherAccounts.length > 0) {
|
|
85
99
|
lines.push(`- **Other accounts:** ${card.otherAccounts.join(", ")}`);
|
|
86
100
|
}
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
setSafetyLevel,
|
|
13
13
|
validateSafetyLevel,
|
|
14
14
|
} from "../config/index.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { readCacheSummary, removeCache } from "../auth/cache-plugin.ts";
|
|
16
|
+
import { resetApps } from "../auth/index.ts";
|
|
16
17
|
import { clearUserCache, errorResult, run, textResult, type ToolResult } from "./shared.ts";
|
|
17
18
|
|
|
18
19
|
interface AccountsParams {
|
|
@@ -69,12 +70,12 @@ export const teamsAccountsTool = {
|
|
|
69
70
|
const lines = ["## Configured Teams accounts", ""];
|
|
70
71
|
for (const account of config.accounts) {
|
|
71
72
|
const isDefault = config.defaultAccount === account.name;
|
|
72
|
-
const
|
|
73
|
-
const state =
|
|
74
|
-
? "signed in"
|
|
75
|
-
:
|
|
76
|
-
? "
|
|
77
|
-
: "
|
|
73
|
+
const cache = readCacheSummary(account.name, account.tenantId);
|
|
74
|
+
const state = !cache.present
|
|
75
|
+
? "not signed in"
|
|
76
|
+
: cache.fresh
|
|
77
|
+
? "signed in"
|
|
78
|
+
: "session saved";
|
|
78
79
|
|
|
79
80
|
lines.push(
|
|
80
81
|
`### ${account.displayName ?? account.name}${isDefault ? " (default)" : ""}`,
|
|
@@ -83,16 +84,16 @@ export const teamsAccountsTool = {
|
|
|
83
84
|
`- tenant: ${account.tenantId}`,
|
|
84
85
|
`- auth mode: ${account.authMode ?? "device-code"}`,
|
|
85
86
|
`- safety: ${account.safetyLevel ?? config.safetyLevel ?? "confirm"}`,
|
|
86
|
-
`- status: ${state}${
|
|
87
|
+
`- status: ${state}${cache.username ? ` as ${cache.username}` : ""}`,
|
|
87
88
|
);
|
|
88
89
|
|
|
89
90
|
for (const tenant of account.tenants ?? []) {
|
|
90
|
-
const
|
|
91
|
-
const tenantState =
|
|
92
|
-
? "signed in"
|
|
93
|
-
:
|
|
94
|
-
? "
|
|
95
|
-
: "
|
|
91
|
+
const tenantCache = readCacheSummary(account.name, tenant.tenantId);
|
|
92
|
+
const tenantState = !tenantCache.present
|
|
93
|
+
? "not signed in"
|
|
94
|
+
: tenantCache.fresh
|
|
95
|
+
? "signed in"
|
|
96
|
+
: "session saved";
|
|
96
97
|
lines.push(
|
|
97
98
|
` - tenant \`${tenant.name}\` (${tenant.tenantId}) — ${tenantState}` +
|
|
98
99
|
`${tenant.safetyLevel ? `, safety: ${tenant.safetyLevel}` : ""}`,
|
|
@@ -125,7 +126,10 @@ export const teamsAccountsTool = {
|
|
|
125
126
|
.find((a) => a.name === params.account)
|
|
126
127
|
?.tenants?.find((t) => t.name === params.tenant);
|
|
127
128
|
const removed = removeTenant(params.account, params.tenant);
|
|
128
|
-
if (removed && tenantConfig)
|
|
129
|
+
if (removed && tenantConfig) {
|
|
130
|
+
removeCache(params.account, tenantConfig.tenantId);
|
|
131
|
+
resetApps();
|
|
132
|
+
}
|
|
129
133
|
return removed
|
|
130
134
|
? textResult(`✅ Removed tenant "${params.tenant}" from account "${params.account}".`, {})
|
|
131
135
|
: errorResult(`Tenant "${params.tenant}" not found in account "${params.account}".`);
|
|
@@ -134,10 +138,11 @@ export const teamsAccountsTool = {
|
|
|
134
138
|
const accountConfig = config.accounts.find((a) => a.name === params.account);
|
|
135
139
|
const removed = removeAccount(params.account);
|
|
136
140
|
if (removed && accountConfig) {
|
|
137
|
-
|
|
141
|
+
removeCache(params.account, accountConfig.tenantId);
|
|
138
142
|
for (const tenant of accountConfig.tenants ?? []) {
|
|
139
|
-
|
|
143
|
+
removeCache(params.account, tenant.tenantId);
|
|
140
144
|
}
|
|
145
|
+
resetApps();
|
|
141
146
|
}
|
|
142
147
|
clearUserCache();
|
|
143
148
|
return removed
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
DEFAULT_SCOPES,
|
|
14
14
|
type TeamsConnection,
|
|
15
15
|
} from "../config/index.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { getCacheDir, readCacheSummary } from "../auth/cache-plugin.ts";
|
|
17
17
|
import { tokenScopes } from "../auth/jwt.ts";
|
|
18
|
-
import { getAccessToken } from "../auth/index.ts";
|
|
18
|
+
import { browserUnavailableReason, canOpenBrowser, getAccessToken } from "../auth/index.ts";
|
|
19
19
|
import { getMe } from "../graph/me.ts";
|
|
20
20
|
import { listJoinedTeams } from "../graph/teams.ts";
|
|
21
21
|
import { formatGraphError, describeScope } from "../utils/errors.ts";
|
|
@@ -47,28 +47,46 @@ async function checkConnection(
|
|
|
47
47
|
"- ⚠️ app-only: pi cannot post messages as a person with this mode " +
|
|
48
48
|
"(Microsoft Graph restricts app-only message writes to migration scenarios)",
|
|
49
49
|
);
|
|
50
|
+
} else if (conn.authMode === "interactive" && !canOpenBrowser()) {
|
|
51
|
+
lines.push(
|
|
52
|
+
`- ⚠️ browser sign-in is configured but unavailable here (${browserUnavailableReason()}); ` +
|
|
53
|
+
"`teams_login` will use the device code flow",
|
|
54
|
+
);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
57
|
+
const cache = readCacheSummary(conn.account, conn.tenantId);
|
|
58
|
+
if (!cache.present) {
|
|
54
59
|
lines.push("- ❌ not signed in — run `teams_login`");
|
|
55
60
|
return lines;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
lines.push(
|
|
59
|
-
|
|
60
|
-
? `- ✅ token valid until ${new Date(
|
|
61
|
-
: token
|
|
62
|
-
? "- ⚠️ access token expired, refresh token present (will renew on the next call)"
|
|
63
|
-
: "- ❌ token expired and no refresh token — run `teams_login`",
|
|
64
|
+
cache.fresh
|
|
65
|
+
? `- ✅ access token valid until ${new Date(cache.expiresAt!).toLocaleString()}`
|
|
66
|
+
: "- ⚠️ access token expired; MSAL renews it silently on the next call",
|
|
64
67
|
);
|
|
68
|
+
if (cache.username) lines.push(`- cached identity: ${cache.username}`);
|
|
69
|
+
|
|
70
|
+
if (offline) {
|
|
71
|
+
lines.push("- _offline check: scopes and connectivity not verified_");
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Everything below needs a live token. Acquiring it also proves that silent
|
|
76
|
+
// renewal works, which is the failure users actually hit.
|
|
77
|
+
let accessToken: string;
|
|
78
|
+
try {
|
|
79
|
+
const token = await getAccessToken(conn, signal);
|
|
80
|
+
accessToken = token.accessToken;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
lines.push(`- ❌ could not obtain a token: ${formatGraphError(err)}`);
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
65
85
|
|
|
66
|
-
//
|
|
67
|
-
const granted = tokenScopes(
|
|
86
|
+
// The granted scopes are inside the token, so this needs no extra call.
|
|
87
|
+
const granted = tokenScopes(accessToken);
|
|
68
88
|
if (granted.length > 0) {
|
|
69
|
-
const wanted =
|
|
70
|
-
(scope) => !["openid", "profile", "offline_access"].includes(scope),
|
|
71
|
-
);
|
|
89
|
+
const wanted = conn.scopes.length > 0 ? conn.scopes : DEFAULT_SCOPES;
|
|
72
90
|
const missing = wanted.filter(
|
|
73
91
|
(scope) => !granted.some((g) => g.toLowerCase() === scope.toLowerCase()),
|
|
74
92
|
);
|
|
@@ -86,15 +104,6 @@ async function checkConnection(
|
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
if (offline) return lines;
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
await getAccessToken(conn, signal);
|
|
93
|
-
} catch (err) {
|
|
94
|
-
lines.push(`- ❌ could not obtain a token: ${formatGraphError(err)}`);
|
|
95
|
-
return lines;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
107
|
try {
|
|
99
108
|
const me = await getMe(conn, signal);
|
|
100
109
|
lines.push(`- ✅ acting as **${me.displayName}** <${me.upn}>`);
|
|
@@ -146,8 +155,11 @@ export const teamsDoctorTool = {
|
|
|
146
155
|
|
|
147
156
|
const lines = ["## Teams doctor", ""];
|
|
148
157
|
lines.push(`- config: \`${getConfigPath()}\``);
|
|
149
|
-
lines.push(`- tokens: \`${
|
|
158
|
+
lines.push(`- tokens: \`${getCacheDir()}/\``);
|
|
150
159
|
lines.push(`- audit log: \`${getAuditPath()}\``);
|
|
160
|
+
lines.push(
|
|
161
|
+
`- browser sign-in: ${canOpenBrowser() ? "available" : `unavailable — ${browserUnavailableReason()}`}`,
|
|
162
|
+
);
|
|
151
163
|
lines.push("");
|
|
152
164
|
|
|
153
165
|
if (errors.length > 0) {
|