brightspace-mcp-server 3.7.11 → 3.7.13
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 +1 -1
- package/build/auth/auth-runner.js +7 -2
- package/build/auth/browser-auth.js +29 -0
- package/build/auth/purdue-sso.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ There is nothing to log into first. Ask for your grades and the sign-in happens
|
|
|
99
99
|
|
|
100
100
|
Returning the next day normally requires no action. The server renews short-lived API tokens over HTTPS using the saved Brightspace session. If that session ends, a browser restores your saved Microsoft session and tries silent SSO. Approval and code-based modes stay headless; when an automatic run needs a code, run the auth command below to enter it securely in the terminal.
|
|
101
101
|
|
|
102
|
-
If visible-browser mode is configured, the window stays open for up to five minutes so you can finish credentials and MFA manually when automatic sign-in cannot continue. Rerunning setup preserves your previous hidden or visible choice as the prompt default.
|
|
102
|
+
If visible-browser mode is configured, the auth command opens a window that stays open for up to five minutes so you can finish credentials and MFA manually when automatic sign-in cannot continue. Background recovery during a tool call always runs headless so it never opens a browser over your work; set `D2L_HEADLESS=false` in the server's environment to let it open one too. Rerunning setup preserves your previous hidden or visible choice as the prompt default.
|
|
103
103
|
|
|
104
104
|
Your school's policy controls when MFA is required. There is no local 24-hour cutoff, and the server no longer discards browser state after one hour. A network outage preserves the saved session and returns a temporary error.
|
|
105
105
|
|
|
@@ -111,7 +111,8 @@ function forwardLines(stream, emit) {
|
|
|
111
111
|
* re-authenticate when the current session has expired.
|
|
112
112
|
*
|
|
113
113
|
* The child inherits the parent's resolved environment and working directory,
|
|
114
|
-
* so both processes read the same account configuration and .env file.
|
|
114
|
+
* so both processes read the same account configuration and .env file. It
|
|
115
|
+
* runs headless unless D2L_HEADLESS is set explicitly.
|
|
115
116
|
*
|
|
116
117
|
* run() settles as soon as the child reports an MFA challenge (with or
|
|
117
118
|
* without a number to display) rather than waiting for the child to exit —
|
|
@@ -241,9 +242,13 @@ export class AuthRunner {
|
|
|
241
242
|
const child = spawn(process.execPath, // use the same Node binary
|
|
242
243
|
[this.scriptPath, "--automatic"], {
|
|
243
244
|
cwd: process.cwd(),
|
|
244
|
-
|
|
245
|
+
// Background recovery never opens a browser over the user's work
|
|
246
|
+
// unless D2L_HEADLESS explicitly asks for one; the saved
|
|
247
|
+
// visible-browser preference applies to manual sign-in only.
|
|
248
|
+
env: { D2L_HEADLESS: "true", ...process.env },
|
|
245
249
|
stdio: ["ignore", "pipe", "pipe"],
|
|
246
250
|
detached: process.platform !== "win32",
|
|
251
|
+
windowsHide: true,
|
|
247
252
|
});
|
|
248
253
|
let timedOut = false;
|
|
249
254
|
// Whether the caller-facing promise above (resolve/reject) has
|
|
@@ -30,6 +30,11 @@ const SILENT_SSO = {
|
|
|
30
30
|
kmsiCheckbox: "#KmsiCheckboxField",
|
|
31
31
|
kmsiTitle: "Stay signed in?",
|
|
32
32
|
kmsiSubmit: "#idSIButton9",
|
|
33
|
+
// Microsoft's passwordless approval view ("Approve sign in request", or
|
|
34
|
+
// "Request wasn't sent" once a push fails) offers the password instead. The
|
|
35
|
+
// text fallback covers a tenant that renames the id.
|
|
36
|
+
passwordSwitch: "#idA_PWD_SwitchToPassword",
|
|
37
|
+
passwordSwitchText: "Use your password instead",
|
|
33
38
|
};
|
|
34
39
|
export class BrowserAuthTransportError extends Error {
|
|
35
40
|
code = "AUTH_TRANSPORT";
|
|
@@ -338,6 +343,7 @@ export class BrowserAuth {
|
|
|
338
343
|
const deadline = Date.now() + SILENT_SSO_TIMEOUT_MS;
|
|
339
344
|
let accountHintAttempted = false;
|
|
340
345
|
let passwordPromptPolls = 0;
|
|
346
|
+
let passwordSwitchAttempted = false;
|
|
341
347
|
do {
|
|
342
348
|
if (await this.hasLiveSession(page))
|
|
343
349
|
return true;
|
|
@@ -359,6 +365,22 @@ export class BrowserAuth {
|
|
|
359
365
|
log("INFO", "The identity provider requires a login challenge");
|
|
360
366
|
return false;
|
|
361
367
|
}
|
|
368
|
+
const passwordSwitch = await this.passwordSignInOption(page);
|
|
369
|
+
if (passwordSwitch) {
|
|
370
|
+
// The approval view has no field or MFA title this poll recognizes, so
|
|
371
|
+
// without leaving it the poll would wait out its whole budget. Switch
|
|
372
|
+
// once when a password is saved; the password page then settles the
|
|
373
|
+
// poll like any other credential prompt.
|
|
374
|
+
if (!passwordSwitchAttempted && this.ssoFlow.hasCredentials()) {
|
|
375
|
+
passwordSwitchAttempted = true;
|
|
376
|
+
await passwordSwitch.click().catch(() => { });
|
|
377
|
+
log("DEBUG", "Chose the password sign-in option over passwordless approval");
|
|
378
|
+
await page.waitForTimeout(SILENT_SSO_POLL_MS);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
log("INFO", "The identity provider requires a password sign-in");
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
362
384
|
if (await this.isAnyOnScreen(page, SILENT_SSO.credentialFields)) {
|
|
363
385
|
passwordPromptPolls += 1;
|
|
364
386
|
// Entra can briefly expose a password-shaped control while its email
|
|
@@ -390,6 +412,13 @@ export class BrowserAuth {
|
|
|
390
412
|
return await this.isAnyOnScreen(page, SILENT_SSO.emailFields) ||
|
|
391
413
|
await this.isAnyOnScreen(page, SILENT_SSO.credentialFields);
|
|
392
414
|
}
|
|
415
|
+
/** The visible "Use your password instead" control, when Microsoft offers one. */
|
|
416
|
+
async passwordSignInOption(page) {
|
|
417
|
+
if (await this.isOnScreen(page, SILENT_SSO.passwordSwitch))
|
|
418
|
+
return page.locator(SILENT_SSO.passwordSwitch).first();
|
|
419
|
+
const byText = page.getByText(SILENT_SSO.passwordSwitchText).first();
|
|
420
|
+
return await byText.isVisible().catch(() => false) ? byText : null;
|
|
421
|
+
}
|
|
393
422
|
/** One definition of the MFA pages the shared authentication loop supports. */
|
|
394
423
|
async hasMfaChallenge(page) {
|
|
395
424
|
return isDuoPrompt(page) || await this.isAnyOnScreen(page, SILENT_SSO.mfaChallenges);
|
package/build/auth/purdue-sso.js
CHANGED
|
@@ -174,7 +174,11 @@ export class PurdueSSOFlow {
|
|
|
174
174
|
// and then blames a missing password field.
|
|
175
175
|
const hintAccepted = this.accountHintSubmitted && !await this.anyVisible(page, EMAIL_SELECTORS);
|
|
176
176
|
this.accountHintSubmitted = false;
|
|
177
|
-
|
|
177
|
+
// Microsoft can also know the account without a hint from this login: a
|
|
178
|
+
// remembered account's passwordless approval view, once awaitSilentSSO
|
|
179
|
+
// takes "Use your password instead", leads to a password page with no
|
|
180
|
+
// username field at all.
|
|
181
|
+
if (!hintAccepted && !await this.isPasswordOnlyPage(page)) {
|
|
178
182
|
const email = signInName(this.config.username, this.config.baseUrl);
|
|
179
183
|
if (!await this.fillWhenReady(page, EMAIL_SELECTORS, email)) {
|
|
180
184
|
throw new UnsupportedAuthenticationError("The identity provider's username field did not appear. Automatic sign-in cannot continue.");
|
|
@@ -253,6 +257,10 @@ export class PurdueSSOFlow {
|
|
|
253
257
|
await page.waitForTimeout(FIELD_POLL_MS);
|
|
254
258
|
return await this.anyVisible(page, PASSWORD_SELECTORS);
|
|
255
259
|
}
|
|
260
|
+
/** A stable password field with no username field beside it. */
|
|
261
|
+
async isPasswordOnlyPage(page) {
|
|
262
|
+
return await this.hasCoVisiblePassword(page) && !await this.anyVisible(page, EMAIL_SELECTORS);
|
|
263
|
+
}
|
|
256
264
|
async hasPostCredentialChallenge(page) {
|
|
257
265
|
return this.duoMfa.isChallenge(page) || await this.anyVisible(page, [
|
|
258
266
|
NUMBER_MATCH_SELECTOR,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.13",
|
|
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",
|