brightspace-mcp-server 5.1.43 → 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 five minutes: a person has to find their phone, unlock it, and read
21
- * a number off the screen. A shorter parent budget would kill the child in the
22
- * middle of a sign-in the user was still completing. run() itself usually
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 = 8 * 60 * 1000;
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. The MFA window is five
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) {
@@ -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 = 5 * 60 * 1000;
70
+ const MFA_TIMEOUT_MS = MFA_APPROVAL_WINDOW_MS;
70
71
  /**
71
- * Entra's number-match request itself expires (typically ~1-2 minutes) well
72
- * before the 5-minute MFA_TIMEOUT_MS this flow allows overall, or the user
73
- * can tap Deny. Either way Entra stops showing the number and offers a way to
74
- * send another request; without asking for one this flow just polls a dead
75
- * request until MFA_TIMEOUT_MS. Bounded so a tenant that never clears this
76
- * screen still hits the ordinary timeout instead of resending forever.
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 = 2;
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
- const deadline = startedAt + MFA_TIMEOUT_MS;
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) well before the 5-minute budget above. Once that has
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 MFA_TIMEOUT_MS; readNumberMatch on the next poll
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
  }
@@ -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 after 5 minutes. Run ${AUTH_COMMAND} to retry.`, "mfa_approval", cause);
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
  }
@@ -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; always give the user the latest number.";
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.43",
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",