brightspace-mcp-server 5.1.42 → 5.1.44
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.
|
@@ -13,17 +13,19 @@ import { AUTH_COMMAND } from "../utils/commands.js";
|
|
|
13
13
|
import { dependencyRepairHint, PLAYWRIGHT_INSTALL_HINT } from "../utils/browser-install.js";
|
|
14
14
|
import { devActivity, newRecoveryId, RECOVERY_ID_ENV } from "../utils/dev-activity.js";
|
|
15
15
|
import { parsePhaseMarker } from "./auth-phases.js";
|
|
16
|
-
import { AUTOMATIC_PENDING_MARKER, MFA_HELD_MARKER, RELAYED_CHALLENGE_MARKER, RELAYED_STARTING_MARKER, authLockPath, challengeRelayedAt, relayChallenge } from "./mfa-challenge.js";
|
|
16
|
+
import { AUTOMATIC_PENDING_MARKER, MFA_HELD_MARKER, MFA_MAX_WAIT_MS, RELAYED_CHALLENGE_MARKER, RELAYED_STARTING_MARKER, authLockPath, challengeRelayedAt, relayChallenge } from "./mfa-challenge.js";
|
|
17
17
|
import { inspectLockOwner, liveAutomaticOwner } from "./auth-lock.js";
|
|
18
18
|
/**
|
|
19
19
|
* Timeout for the auth process. It has to outlast the child's own MFA wait,
|
|
20
|
-
* which is
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* which is up to MFA_MAX_WAIT_MS (15 minutes: five for the first number and five
|
|
21
|
+
* more for each of the two resent ones, see purdue-sso.ts) plus the browser
|
|
22
|
+
* launch and sign-in pages before MFA starts. A shorter parent budget would
|
|
23
|
+
* kill the child in the middle of a sign-in the user was still completing
|
|
24
|
+
* (the 8-minute budget cut a resent number's window short). run() itself usually
|
|
23
25
|
* returns long before this fires — the moment the child reports an MFA
|
|
24
26
|
* challenge — but this timer keeps bounding the background child regardless.
|
|
25
27
|
*/
|
|
26
|
-
const AUTH_TIMEOUT_MS =
|
|
28
|
+
const AUTH_TIMEOUT_MS = MFA_MAX_WAIT_MS + 3 * 60 * 1000;
|
|
27
29
|
const KILL_GRACE_MS = 5000;
|
|
28
30
|
/**
|
|
29
31
|
* How long a caller keeps waiting for the background sign-in once the
|
|
@@ -65,8 +67,8 @@ const ABANDON_MS = MFA_POLL_MS;
|
|
|
65
67
|
const RELAY_POLL_MS = 1000;
|
|
66
68
|
/**
|
|
67
69
|
* How long a relayed challenge stays "already told to a caller" for the rule
|
|
68
|
-
* that only a RETRY waits on another process's sign-in.
|
|
69
|
-
* minutes, so a number reported longer ago than that belongs to a sign-in that
|
|
70
|
+
* that only a RETRY waits on another process's sign-in. One number's MFA window is five
|
|
71
|
+
* minutes (a resent number is a new number with a new window), so a number reported longer ago than that belongs to a sign-in that
|
|
70
72
|
* is over; the same digits showing up again are a new challenge.
|
|
71
73
|
*/
|
|
72
74
|
const RELAY_TOLD_MS = 5 * 60 * 1000;
|
|
@@ -16,6 +16,21 @@ import * as fs from "node:fs/promises";
|
|
|
16
16
|
import { statSync } from "node:fs";
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import { CHALLENGE_FILE } from "./auth-lock.js";
|
|
19
|
+
/**
|
|
20
|
+
* How long one Microsoft number-match request stays approvable: a person has
|
|
21
|
+
* to find their phone, unlock it and read a prompt, and Microsoft expires the
|
|
22
|
+
* request itself at about this mark.
|
|
23
|
+
*/
|
|
24
|
+
export const MFA_APPROVAL_WINDOW_MS = 5 * 60 * 1000;
|
|
25
|
+
/** Times a sign-in may ask Microsoft for a fresh number after one expires or is denied. */
|
|
26
|
+
export const MFA_MAX_RESENDS = 2;
|
|
27
|
+
/**
|
|
28
|
+
* The longest a sign-in waits on MFA: the first number's window plus a full
|
|
29
|
+
* window for each resent number (each resend restarts the clock, see
|
|
30
|
+
* purdue-sso.ts handleMFA). 15 minutes. Anything that bounds the sign-in
|
|
31
|
+
* from outside has to outlast this.
|
|
32
|
+
*/
|
|
33
|
+
export const MFA_MAX_WAIT_MS = MFA_APPROVAL_WINDOW_MS * (1 + MFA_MAX_RESENDS);
|
|
19
34
|
/** Same bound the auth child's stdout marker enforces: 1-3 digits and nothing else. */
|
|
20
35
|
const NUMBER_MATCH = /^\d{1,3}$/;
|
|
21
36
|
export function authLockPath(sessionDir) {
|
package/build/auth/purdue-sso.js
CHANGED
|
@@ -9,6 +9,7 @@ import { devActivity } from "../utils/dev-activity.js";
|
|
|
9
9
|
import { AutomaticCodeAuthenticationError, MfaApprovalError, UnsupportedAuthenticationError } from "./sso-flow.js";
|
|
10
10
|
import { DuoMfaHandler } from "./duo-mfa.js";
|
|
11
11
|
import { AUTH_COMMAND } from "../utils/commands.js";
|
|
12
|
+
import { MFA_APPROVAL_WINDOW_MS, MFA_MAX_RESENDS } from "./mfa-challenge.js";
|
|
12
13
|
import { generateTotp, secondsUntilFreshCode } from "./totp.js";
|
|
13
14
|
// Entra names its username field type=email/loginfmt; Shibboleth portals (USC's
|
|
14
15
|
// login.usc.edu among them) use the protocol's j_username.
|
|
@@ -66,16 +67,20 @@ const REMEMBER_MFA_LOG = {
|
|
|
66
67
|
/** How often to look for the number while waiting on MFA. */
|
|
67
68
|
const NUMBER_MATCH_POLL_MS = 2000;
|
|
68
69
|
/** A person has to find their phone, unlock it, and read a prompt. */
|
|
69
|
-
const MFA_TIMEOUT_MS =
|
|
70
|
+
const MFA_TIMEOUT_MS = MFA_APPROVAL_WINDOW_MS;
|
|
70
71
|
/**
|
|
71
|
-
* Entra's number-match request itself expires (
|
|
72
|
-
*
|
|
73
|
-
* can tap Deny. Either way Entra stops showing the
|
|
74
|
-
* send another request; without asking for one
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* Entra's number-match request itself expires (observed live at about the
|
|
73
|
+
* same 5 minutes as MFA_TIMEOUT_MS, 2026-11-08; earlier tenants showed 1-2
|
|
74
|
+
* minutes), or the user can tap Deny. Either way Entra stops showing the
|
|
75
|
+
* number and offers a way to send another request; without asking for one
|
|
76
|
+
* this flow just polls a dead request until the deadline. Each resend gives
|
|
77
|
+
* the new number its own full MFA_TIMEOUT_MS (the deadline moves to
|
|
78
|
+
* resend time + MFA_TIMEOUT_MS), because a resend made at the 5-minute mark
|
|
79
|
+
* left the new number about four seconds. Bounded, so the total is at most
|
|
80
|
+
* (1 + MAX_RESENDS) * MFA_TIMEOUT_MS (MFA_MAX_WAIT_MS, 15 minutes) and a
|
|
81
|
+
* tenant that never clears this screen still ends instead of resending forever.
|
|
77
82
|
*/
|
|
78
|
-
const MAX_RESENDS =
|
|
83
|
+
const MAX_RESENDS = MFA_MAX_RESENDS;
|
|
79
84
|
/**
|
|
80
85
|
* EXPECTED, not verified against a live tenant from here: Entra's "Send
|
|
81
86
|
* another request" control on the number-match timeout/denied view.
|
|
@@ -412,7 +417,7 @@ export class PurdueSSOFlow {
|
|
|
412
417
|
throw new UnsupportedAuthenticationError("A school URL is required to verify authentication.");
|
|
413
418
|
}
|
|
414
419
|
const startedAt = Date.now();
|
|
415
|
-
|
|
420
|
+
let deadline = startedAt + MFA_TIMEOUT_MS;
|
|
416
421
|
let challenged = false;
|
|
417
422
|
/** True once onMfaHeld has reported this login. */
|
|
418
423
|
let heldReported = false;
|
|
@@ -565,9 +570,9 @@ export class PurdueSSOFlow {
|
|
|
565
570
|
resendNotFoundWarned = false;
|
|
566
571
|
}
|
|
567
572
|
// Fix 2: Entra's number-match request itself times out (or the user
|
|
568
|
-
// taps Deny)
|
|
573
|
+
// taps Deny), at the latest about when the budget above runs out. Once that has
|
|
569
574
|
// happened, ask Entra for another one instead of polling a dead
|
|
570
|
-
// request until
|
|
575
|
+
// request until the deadline; readNumberMatch on the next poll
|
|
571
576
|
// picks up the fresh number, and the re-announce logic above tells
|
|
572
577
|
// the caller about it.
|
|
573
578
|
if (manualRequired && sawNumber && number === null && new URL(page.url()).hostname === "login.microsoftonline.com") {
|
|
@@ -581,6 +586,8 @@ export class PurdueSSOFlow {
|
|
|
581
586
|
onResend: (clickedAt) => {
|
|
582
587
|
resendCount += 1;
|
|
583
588
|
resendLastClickAt = clickedAt;
|
|
589
|
+
// The new number gets its own full approval window; never shorten.
|
|
590
|
+
deadline = Math.max(deadline, clickedAt + MFA_TIMEOUT_MS);
|
|
584
591
|
},
|
|
585
592
|
});
|
|
586
593
|
}
|
|
@@ -637,7 +644,7 @@ export class PurdueSSOFlow {
|
|
|
637
644
|
}
|
|
638
645
|
const clickedAt = Date.now();
|
|
639
646
|
state.onResend(clickedAt);
|
|
640
|
-
log("WARN", `Number-match request expired; sent another request (${state.resendCount + 1} of ${MAX_RESENDS}).`);
|
|
647
|
+
log("WARN", `Number-match request expired; sent another request (${state.resendCount + 1} of ${MAX_RESENDS}). The new number gets its own ${MFA_TIMEOUT_MS / 60000} minutes.`);
|
|
641
648
|
await control.click().catch(() => { });
|
|
642
649
|
return state.resendNotFoundWarned;
|
|
643
650
|
}
|
package/build/auth/sso-flow.js
CHANGED
|
@@ -39,7 +39,7 @@ export class MfaApprovalError extends BrowserAuthError {
|
|
|
39
39
|
*/
|
|
40
40
|
numberMatch;
|
|
41
41
|
constructor(cause, numberMatch) {
|
|
42
|
-
super(`MFA approval failed or timed out
|
|
42
|
+
super(`MFA approval failed or timed out: no approval within 5 minutes of the last number shown. Run ${AUTH_COMMAND} to retry.`, "mfa_approval", cause);
|
|
43
43
|
this.name = "MfaApprovalError";
|
|
44
44
|
this.numberMatch = numberMatch;
|
|
45
45
|
}
|
package/build/setup/clients.js
CHANGED
|
@@ -13,16 +13,77 @@ import * as path from "node:path";
|
|
|
13
13
|
import { writeFileAtomicSync } from "../utils/atomic-write.js";
|
|
14
14
|
import { classifyRegistration, cliMcpClients, inspectCliMcpClient, isCliAvailable, registerCliMcpClient, serverCommand, } from "../utils/mcp-client-cli.js";
|
|
15
15
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
16
|
+
const CLAUDE_DESKTOP_CONFIG = "claude_desktop_config.json";
|
|
17
|
+
const USER_DATA_LEFT_MARKER = ".user-data-left.";
|
|
18
|
+
function listDirs(dir) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function mtimeMs(file) {
|
|
27
|
+
try {
|
|
28
|
+
return fs.statSync(file).mtimeMs;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Newer Windows builds of Claude Desktop (the Microsoft Store package) keep
|
|
36
|
+
* their data outside the package, in %LOCALAPPDATA%\Claude-Data\<package>\,
|
|
37
|
+
* and no longer read %APPDATA%\Claude. They leave a
|
|
38
|
+
* `.user-data-left.<package>` file behind in %APPDATA%\Claude to say so.
|
|
39
|
+
* Writing to the old folder there registers the server in a file the app
|
|
40
|
+
* never reads, so Claude Code works and chats in the app do not.
|
|
41
|
+
*
|
|
42
|
+
* Returns the folder the app reads, or null when there is no Claude-Data
|
|
43
|
+
* folder (an install that still uses %APPDATA%\Claude). With several package
|
|
44
|
+
* folders, the one the marker names wins, then the one whose config the app
|
|
45
|
+
* wrote most recently (it rewrites the file as preferences change).
|
|
46
|
+
*/
|
|
47
|
+
export function findRelocatedClaudeDataDir(home = os.homedir(), env = process.env) {
|
|
48
|
+
const roaming = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
49
|
+
const local = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
50
|
+
const root = path.join(local, "Claude-Data");
|
|
51
|
+
const packages = listDirs(root);
|
|
52
|
+
if (packages.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
let markers = [];
|
|
55
|
+
try {
|
|
56
|
+
markers = fs.readdirSync(path.join(roaming, "Claude"))
|
|
57
|
+
.filter((name) => name.startsWith(USER_DATA_LEFT_MARKER))
|
|
58
|
+
.map((name) => name.slice(USER_DATA_LEFT_MARKER.length).toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// No old folder at all is fine: the app may never have used it.
|
|
62
|
+
}
|
|
63
|
+
const marked = packages.find((name) => markers.includes(name.toLowerCase()));
|
|
64
|
+
if (marked)
|
|
65
|
+
return path.join(root, marked);
|
|
66
|
+
const withConfig = packages
|
|
67
|
+
.map((name) => ({ name, mtime: mtimeMs(path.join(root, name, CLAUDE_DESKTOP_CONFIG)) }))
|
|
68
|
+
.filter((p) => p.mtime >= 0)
|
|
69
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
70
|
+
if (withConfig.length > 0)
|
|
71
|
+
return path.join(root, withConfig[0].name);
|
|
72
|
+
return packages.length === 1 ? path.join(root, packages[0]) : null;
|
|
73
|
+
}
|
|
16
74
|
export function getClaudeDesktopConfigPath(platform = os.platform(), home = os.homedir(), env = process.env) {
|
|
17
75
|
if (platform === "darwin") {
|
|
18
|
-
return path.join(home, "Library", "Application Support", "Claude",
|
|
76
|
+
return path.join(home, "Library", "Application Support", "Claude", CLAUDE_DESKTOP_CONFIG);
|
|
19
77
|
}
|
|
20
78
|
if (platform === "win32") {
|
|
79
|
+
const relocated = findRelocatedClaudeDataDir(home, env);
|
|
80
|
+
if (relocated)
|
|
81
|
+
return path.join(relocated, CLAUDE_DESKTOP_CONFIG);
|
|
21
82
|
const appdata = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
22
|
-
return path.join(appdata, "Claude",
|
|
83
|
+
return path.join(appdata, "Claude", CLAUDE_DESKTOP_CONFIG);
|
|
23
84
|
}
|
|
24
85
|
if (platform === "linux") {
|
|
25
|
-
return path.join(home, ".config", "Claude",
|
|
86
|
+
return path.join(home, ".config", "Claude", CLAUDE_DESKTOP_CONFIG);
|
|
26
87
|
}
|
|
27
88
|
return null;
|
|
28
89
|
}
|
|
@@ -114,7 +114,8 @@ const MFA_SAME_PROMPT = "Calling again doesn't send a new prompt, and neither do
|
|
|
114
114
|
"keeps waiting, for up to 5 minutes, on the prompt already sent.";
|
|
115
115
|
/** Entra number match only: purdue-sso.ts asks for another request when one expires or is denied. */
|
|
116
116
|
const MFA_NEW_NUMBER = "If Microsoft's number expires or is denied, the sign-in asks Microsoft for a new one by itself " +
|
|
117
|
-
"and the next answer shows it
|
|
117
|
+
"and the next answer shows it, with its own 5 minutes to approve (at most twice, so a sign-in can run up " +
|
|
118
|
+
"to 15 minutes in all); always give the user the latest number.";
|
|
118
119
|
/**
|
|
119
120
|
* Why a missed prompt cannot be skipped. Microsoft keeps a prompt open for up
|
|
120
121
|
* to 5 minutes and refuses another for the same account until then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.44",
|
|
4
4
|
"mcpName": "io.github.RohanMuppa/brightspace",
|
|
5
5
|
"description": "MCP server for Brightspace (D2L). Check grades, due dates, assignments, announcements, syllabus, rosters and more via Claude, ChatGPT, Cursor, Windsurf, or any MCP client.",
|
|
6
6
|
"type": "module",
|