msteams-mcp 0.30.1 → 0.30.2
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.
|
@@ -63,6 +63,11 @@ export declare function writeSessionState(state: SessionState): void;
|
|
|
63
63
|
* Deletes the session state file.
|
|
64
64
|
*/
|
|
65
65
|
export declare function clearSessionState(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Deletes everything stored locally: browser profile (cookies, MSAL caches,
|
|
68
|
+
* every account ever signed in), session state and token cache.
|
|
69
|
+
*/
|
|
70
|
+
export declare function clearAllLocalData(): void;
|
|
66
71
|
/**
|
|
67
72
|
* Gets the age of the session state in hours.
|
|
68
73
|
*/
|
|
@@ -86,6 +86,13 @@ export function clearSessionState() {
|
|
|
86
86
|
fs.unlinkSync(SESSION_STATE_PATH);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Deletes everything stored locally: browser profile (cookies, MSAL caches,
|
|
91
|
+
* every account ever signed in), session state and token cache.
|
|
92
|
+
*/
|
|
93
|
+
export function clearAllLocalData() {
|
|
94
|
+
fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
95
|
+
}
|
|
89
96
|
/**
|
|
90
97
|
* Gets the age of the session state in hours.
|
|
91
98
|
*/
|
package/dist/tools/auth-tools.js
CHANGED
|
@@ -5,12 +5,12 @@ import { createRequire } from 'module';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const pkg = require('../../package.json');
|
|
8
|
-
import { hasSessionState, isSessionLikelyExpired,
|
|
8
|
+
import { hasSessionState, isSessionLikelyExpired, clearAllLocalData, } from '../auth/session-store.js';
|
|
9
9
|
import { getSubstrateTokenStatus, getMessageAuthStatus, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, } from '../auth/token-extractor.js';
|
|
10
10
|
import { refreshTokensViaHttp } from '../auth/token-refresh-http.js';
|
|
11
11
|
import { createBrowserContext, closeBrowser } from '../browser/context.js';
|
|
12
12
|
import * as log from '../utils/logger.js';
|
|
13
|
-
import { resetAssignmentsAvailability } from '../utils/auth-guards.js';
|
|
13
|
+
import { resetAssignmentsAvailability, clearRegionCache } from '../utils/auth-guards.js';
|
|
14
14
|
import { ensureAuthenticated } from '../browser/auth.js';
|
|
15
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
// Schemas
|
|
@@ -29,7 +29,7 @@ const loginToolDefinition = {
|
|
|
29
29
|
properties: {
|
|
30
30
|
forceNew: {
|
|
31
31
|
type: 'boolean',
|
|
32
|
-
description: '
|
|
32
|
+
description: 'Delete all previous login data (browser profile, cookies, saved session, every previously signed-in account) and sign in from scratch in a visible browser. Use to switch accounts or recover a broken session (default: false)',
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
35
|
},
|
|
@@ -76,36 +76,32 @@ async function handleLogin(input) {
|
|
|
76
76
|
// A fresh login may have changed what the account can access (e.g. new consent)
|
|
77
77
|
resetAssignmentsAvailability();
|
|
78
78
|
if (input.forceNew) {
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Start from nothing: no profile, cookies, MSAL cache or session from any
|
|
80
|
+
// earlier account can survive to be mixed into the new login.
|
|
81
|
+
clearAllLocalData();
|
|
82
|
+
clearRegionCache();
|
|
83
|
+
return loginInVisibleBrowser();
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
minutesRemaining: tokenStatus.minutesRemaining,
|
|
94
|
-
},
|
|
85
|
+
// Fast path: if tokens are still valid, skip browser entirely
|
|
86
|
+
const tokenStatus = getSubstrateTokenStatus();
|
|
87
|
+
if (tokenStatus.hasToken && tokenStatus.minutesRemaining >= TOKEN_VALID_THRESHOLD_MINUTES) {
|
|
88
|
+
await fillCoreCredentialGaps();
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
data: {
|
|
92
|
+
message: `Already authenticated. Token valid for ${tokenStatus.minutesRemaining} more minutes.`,
|
|
93
|
+
tokenStatus: {
|
|
94
|
+
expiresAt: tokenStatus.expiresAt,
|
|
95
|
+
minutesRemaining: tokenStatus.minutesRemaining,
|
|
95
96
|
},
|
|
96
|
-
}
|
|
97
|
-
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
98
99
|
}
|
|
99
100
|
// Headless-first strategy:
|
|
100
101
|
// The persistent browser profile retains Microsoft's long-lived session cookies,
|
|
101
|
-
// so headless SSO can succeed even without a session-state file.
|
|
102
|
-
// headless first — even for forceNew. Most recovery scenarios complete silently.
|
|
102
|
+
// so headless SSO can succeed even without a session-state file.
|
|
103
103
|
const headless = await createBrowserContext({ headless: true });
|
|
104
104
|
try {
|
|
105
|
-
if (input.forceNew) {
|
|
106
|
-
// Clear persistent profile cookies to force fresh authentication
|
|
107
|
-
await headless.context.clearCookies();
|
|
108
|
-
}
|
|
109
105
|
await ensureAuthenticated(headless.page, headless.context, (msg) => log.info('login:headless', msg), false, // No overlay in headless
|
|
110
106
|
true // Headless mode - throw immediately if user interaction required
|
|
111
107
|
);
|
|
@@ -122,7 +118,10 @@ async function handleLogin(input) {
|
|
|
122
118
|
log.warn('login:headless', `Headless SSO failed, falling back to visible browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
119
|
await closeBrowser(headless, false).catch(() => { });
|
|
124
120
|
}
|
|
125
|
-
|
|
121
|
+
return loginInVisibleBrowser();
|
|
122
|
+
}
|
|
123
|
+
/** Opens a visible browser for the user to sign in, then saves the session. */
|
|
124
|
+
async function loginInVisibleBrowser() {
|
|
126
125
|
const visible = await createBrowserContext({ headless: false });
|
|
127
126
|
try {
|
|
128
127
|
await ensureAuthenticated(visible.page, visible.context, (msg) => log.info('login', msg));
|
|
@@ -2,8 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
vi.mock('../auth/session-store.js', () => ({
|
|
3
3
|
hasSessionState: vi.fn(() => true),
|
|
4
4
|
isSessionLikelyExpired: vi.fn(() => false),
|
|
5
|
-
|
|
6
|
-
clearTokenCache: vi.fn(),
|
|
5
|
+
clearAllLocalData: vi.fn(),
|
|
7
6
|
}));
|
|
8
7
|
vi.mock('../auth/token-extractor.js', () => ({
|
|
9
8
|
getSubstrateTokenStatus: vi.fn(),
|
|
@@ -16,8 +15,9 @@ vi.mock('../auth/token-extractor.js', () => ({
|
|
|
16
15
|
vi.mock('../auth/token-refresh-http.js', () => ({ refreshTokensViaHttp: vi.fn() }));
|
|
17
16
|
vi.mock('../browser/context.js', () => ({ createBrowserContext: vi.fn(), closeBrowser: vi.fn() }));
|
|
18
17
|
vi.mock('../browser/auth.js', () => ({ ensureAuthenticated: vi.fn() }));
|
|
19
|
-
vi.mock('../utils/auth-guards.js', () => ({ resetAssignmentsAvailability: vi.fn() }));
|
|
18
|
+
vi.mock('../utils/auth-guards.js', () => ({ resetAssignmentsAvailability: vi.fn(), clearRegionCache: vi.fn() }));
|
|
20
19
|
import { loginTool } from './auth-tools.js';
|
|
20
|
+
import { clearAllLocalData } from '../auth/session-store.js';
|
|
21
21
|
import { getSubstrateTokenStatus, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, } from '../auth/token-extractor.js';
|
|
22
22
|
import { refreshTokensViaHttp } from '../auth/token-refresh-http.js';
|
|
23
23
|
import { createBrowserContext, closeBrowser } from '../browser/context.js';
|
|
@@ -49,12 +49,26 @@ describe('teams_login core credential top-up', () => {
|
|
|
49
49
|
});
|
|
50
50
|
it('tops up after a forced browser login leaves the CSA token behind', async () => {
|
|
51
51
|
sessionMissingCsaToken();
|
|
52
|
-
vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: {
|
|
52
|
+
vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: {} });
|
|
53
53
|
vi.mocked(closeBrowser).mockResolvedValue(undefined);
|
|
54
54
|
const result = await loginTool.handler({ forceNew: true });
|
|
55
55
|
expect(result.success).toBe(true);
|
|
56
56
|
expect(refreshTokensViaHttp).toHaveBeenCalledOnce();
|
|
57
57
|
});
|
|
58
|
+
it('forceNew wipes all previous login data and signs in from scratch in a visible browser', async () => {
|
|
59
|
+
sessionMissingCsaToken();
|
|
60
|
+
vi.mocked(extractCsaToken).mockReturnValue('csa');
|
|
61
|
+
vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: {} });
|
|
62
|
+
vi.mocked(closeBrowser).mockResolvedValue(undefined);
|
|
63
|
+
await loginTool.handler({ forceNew: true });
|
|
64
|
+
expect(clearAllLocalData).toHaveBeenCalledOnce();
|
|
65
|
+
expect(vi.mocked(clearAllLocalData).mock.invocationCallOrder[0])
|
|
66
|
+
.toBeLessThan(vi.mocked(createBrowserContext).mock.invocationCallOrder[0]);
|
|
67
|
+
// Valid-looking tokens from the old session must not short-circuit the login,
|
|
68
|
+
// and a wiped profile has nothing for headless SSO to use.
|
|
69
|
+
expect(createBrowserContext).toHaveBeenCalledOnce();
|
|
70
|
+
expect(createBrowserContext).toHaveBeenCalledWith({ headless: false });
|
|
71
|
+
});
|
|
58
72
|
it('still reports success when the top-up fails', async () => {
|
|
59
73
|
sessionMissingCsaToken();
|
|
60
74
|
vi.mocked(refreshTokensViaHttp).mockResolvedValue({
|