brightspace-mcp-server 3.7.12 → 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.
@@ -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);
@@ -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
- if (!hintAccepted) {
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.12",
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",