brightspace-mcp-server 3.8.10 → 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.
- package/build/auth/purdue-sso.js +39 -10
- package/build/setup.js +42 -29
- package/package.json +1 -1
package/build/auth/purdue-sso.js
CHANGED
|
@@ -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
|
-
/**
|
|
99
|
-
|
|
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
|
|
450
|
-
// commonly leaves the field on screen while
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
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.
|
|
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/build/setup.js
CHANGED
|
@@ -80,6 +80,34 @@ function ask(rl, question) {
|
|
|
80
80
|
rl.question(question, (answer) => resolve(answer.trim()));
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Apply one raw-mode stdin chunk to the password typed so far. A paste
|
|
85
|
+
* arrives as a single chunk, possibly wrapped in bracketed-paste markers and
|
|
86
|
+
* ending in a copied newline, so the chunk is read one character at a time
|
|
87
|
+
* and escape sequences (paste markers, arrow keys) never reach the password.
|
|
88
|
+
*/
|
|
89
|
+
export function readPasswordInput(password, chunk) {
|
|
90
|
+
let echo = "";
|
|
91
|
+
const text = chunk.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
92
|
+
for (const ch of text) {
|
|
93
|
+
if (ch === "\x03")
|
|
94
|
+
return { password, echo, outcome: "cancel" };
|
|
95
|
+
if (ch === "\r" || ch === "\n")
|
|
96
|
+
return { password, echo, outcome: "submit" };
|
|
97
|
+
if (ch === "\x7f" || ch === "\b") {
|
|
98
|
+
if (password.length > 0) {
|
|
99
|
+
password = [...password].slice(0, -1).join("");
|
|
100
|
+
echo += "\b \b";
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (ch < " ")
|
|
105
|
+
continue;
|
|
106
|
+
password += ch;
|
|
107
|
+
echo += "*";
|
|
108
|
+
}
|
|
109
|
+
return { password, echo, outcome: "pending" };
|
|
110
|
+
}
|
|
83
111
|
/**
|
|
84
112
|
* Prompt for a password without echoing characters to the terminal.
|
|
85
113
|
* We swap stdout.write to suppress the default echo, then print
|
|
@@ -114,37 +142,22 @@ function askPassword(prompt) {
|
|
|
114
142
|
process.stdin.setRawMode?.(true);
|
|
115
143
|
process.stdin.resume();
|
|
116
144
|
const onData = (key) => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
const input = readPasswordInput(password, key.toString("utf-8"));
|
|
146
|
+
if (input.echo)
|
|
147
|
+
origWrite(input.echo);
|
|
148
|
+
password = input.password;
|
|
149
|
+
if (input.outcome === "pending")
|
|
150
|
+
return;
|
|
151
|
+
process.stdout.write = origWrite;
|
|
152
|
+
process.stdin.setRawMode?.(false);
|
|
153
|
+
process.stdin.removeListener("data", onData);
|
|
154
|
+
rl.close();
|
|
155
|
+
if (input.outcome === "cancel") {
|
|
124
156
|
console.log("");
|
|
125
157
|
process.exit(0);
|
|
126
158
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
process.stdout.write = origWrite;
|
|
130
|
-
process.stdin.setRawMode?.(false);
|
|
131
|
-
process.stdin.removeListener("data", onData);
|
|
132
|
-
rl.close();
|
|
133
|
-
origWrite("\n");
|
|
134
|
-
resolve(password);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// Backspace
|
|
138
|
-
if (ch === "\x7f" || ch === "\b") {
|
|
139
|
-
if (password.length > 0) {
|
|
140
|
-
password = password.slice(0, -1);
|
|
141
|
-
origWrite("\b \b");
|
|
142
|
-
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
// Normal character
|
|
146
|
-
password += ch;
|
|
147
|
-
origWrite("*");
|
|
159
|
+
origWrite("\n");
|
|
160
|
+
resolve(password);
|
|
148
161
|
};
|
|
149
162
|
process.stdin.on("data", onData);
|
|
150
163
|
});
|
|
@@ -406,7 +419,7 @@ async function main() {
|
|
|
406
419
|
// Close the rl temporarily since askPassword manages its own
|
|
407
420
|
rl.close();
|
|
408
421
|
const passwordPrompt = preset
|
|
409
|
-
? `What is your ${preset.
|
|
422
|
+
? `What is your ${preset.name} password? `
|
|
410
423
|
: "What is your Brightspace password? ";
|
|
411
424
|
let password = "";
|
|
412
425
|
while (!password) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.8.
|
|
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",
|