brightspace-mcp-server 3.8.11 → 3.8.12

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.
@@ -24,6 +24,9 @@ const FIELD_POLL_MS = 250;
24
24
  const NUMBER_MATCH_SELECTOR = "#idRichContext_DisplaySign";
25
25
  const MFA_CODE_SELECTORS = ["#idTxtBx_SAOTCC_OTC", 'input[name="otc"]'];
26
26
  const MFA_CODE_SUBMIT_SELECTORS = ["#idSubmit_SAOTCC_Continue", "#idSIButton9"];
27
+ // Entra rejects a wrong code in place: the field stays and this message appears.
28
+ const MFA_CODE_ERROR_SELECTOR = "#idSpan_SAOTCC_Error_OTC";
29
+ const MAX_MFA_CODE_ATTEMPTS = 3;
27
30
  /**
28
31
  * Entra's "Don't ask again for N days" checkbox: the number-match page's id
29
32
  * first, then the verification-code page's, then the label text for a tenant
@@ -95,8 +98,15 @@ function signInName(username, baseUrl) {
95
98
  export class PurdueSSOFlow {
96
99
  config;
97
100
  accountHintSubmitted = false;
98
- /** One authenticator code per login. See submitMfaCode. */
99
- mfaCodeSubmitted = false;
101
+ /** Authenticator codes asked for during this login. See submitMfaCode. */
102
+ mfaCodeAttempts = 0;
103
+ /**
104
+ * True for exactly one poll after a code is resubmitted in response to a
105
+ * rejection. Entra's rejection span can still be in the DOM on that very
106
+ * next poll - it hasn't re-rendered yet, so what's on screen is still the
107
+ * PREVIOUS attempt's verdict, not this one's. See submitMfaCode.
108
+ */
109
+ awaitingMfaCodeSettle = false;
100
110
  /** Set once per login, the first time Entra's MFA page is handled. */
101
111
  rememberMfa;
102
112
  duoMfa;
@@ -446,18 +456,37 @@ export class PurdueSSOFlow {
446
456
  return false;
447
457
  if (this.config.headless === false)
448
458
  return false;
449
- // Ask once per login. This runs on every two-second poll, and Microsoft
450
- // commonly leaves the field on screen while it validates, so without this
451
- // a correct code gets a second prompt on the next tick. That prompt blocks
452
- // on stdin, and the deadline is only checked between iterations, so the
453
- // five-minute budget can never fire while parked there.
454
- if (this.mfaCodeSubmitted)
455
- return false;
459
+ // Ask again only after Entra's rejection message. This runs on every
460
+ // two-second poll, and Microsoft commonly leaves the field on screen while
461
+ // it validates, so a lingering field alone would give a correct code a
462
+ // second prompt on the next tick. That prompt blocks on stdin, and the
463
+ // deadline is only checked between iterations, so the five-minute budget
464
+ // can never fire while parked there.
465
+ const isRetry = this.mfaCodeAttempts > 0;
466
+ if (isRetry) {
467
+ // The rejection span can still be the stale one from the attempt we
468
+ // just resubmitted in reaction to - Entra hasn't re-rendered yet, so
469
+ // this poll can't tell that span apart from a genuine rejection of the
470
+ // new attempt. Give it exactly one more poll (like cuny-sso.ts requiring
471
+ // the URL to change before trusting its own error state) before trusting
472
+ // the span again.
473
+ if (this.awaitingMfaCodeSettle) {
474
+ this.awaitingMfaCodeSettle = false;
475
+ return false;
476
+ }
477
+ if (!await page.locator(MFA_CODE_ERROR_SELECTOR).first().isVisible().catch(() => false))
478
+ return false;
479
+ if (this.mfaCodeAttempts >= MAX_MFA_CODE_ATTEMPTS) {
480
+ throw new BrowserAuthError(`Microsoft rejected ${this.mfaCodeAttempts} authenticator codes. Run \`${AUTH_COMMAND}\` to try again.`, "mfa_code");
481
+ }
482
+ log("WARN", "Microsoft rejected that code. Enter the current one from your authenticator app.");
483
+ }
456
484
  if (!this.config.requestMfaCode) {
457
485
  throw new UnsupportedAuthenticationError(`This MFA method requires a code. Run \`${AUTH_COMMAND}\` in a terminal to enter it.`);
458
486
  }
459
487
  await this.rememberMfaDevice(page);
460
- this.mfaCodeSubmitted = true;
488
+ this.mfaCodeAttempts += 1;
489
+ this.awaitingMfaCodeSettle = isRetry;
461
490
  const code = await this.config.requestMfaCode();
462
491
  if (!/^\d{6,8}$/.test(code))
463
492
  throw new UnsupportedAuthenticationError("The MFA code must contain 6-8 digits.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.8.11",
3
+ "version": "3.8.12",
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",