brightspace-mcp-server 1.2.6 → 1.5.1
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 +44 -13
- package/build/api/cache.d.ts +1 -1
- package/build/api/cache.js +1 -1
- package/build/api/client.d.ts +1 -1
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +25 -3
- package/build/api/client.js.map +1 -1
- package/build/api/errors.d.ts +2 -2
- package/build/api/errors.js +2 -2
- package/build/api/index.d.ts +1 -1
- package/build/api/index.js +1 -1
- package/build/api/rate-limiter.d.ts +2 -2
- package/build/api/rate-limiter.js +2 -2
- package/build/api/types.d.ts +1 -1
- package/build/api/types.js +1 -1
- package/build/api/version-discovery.d.ts +2 -2
- package/build/api/version-discovery.js +2 -2
- package/build/auth/auth-runner.d.ts +2 -2
- package/build/auth/auth-runner.d.ts.map +1 -1
- package/build/auth/auth-runner.js +61 -12
- package/build/auth/auth-runner.js.map +1 -1
- package/build/auth/browser-auth.d.ts +35 -1
- package/build/auth/browser-auth.d.ts.map +1 -1
- package/build/auth/browser-auth.js +219 -28
- package/build/auth/browser-auth.js.map +1 -1
- package/build/auth/index.d.ts +5 -2
- package/build/auth/index.d.ts.map +1 -1
- package/build/auth/index.js +4 -2
- package/build/auth/index.js.map +1 -1
- package/build/auth/purdue-sso.d.ts +30 -1
- package/build/auth/purdue-sso.d.ts.map +1 -1
- package/build/auth/purdue-sso.js +144 -17
- package/build/auth/purdue-sso.js.map +1 -1
- package/build/auth/session-store.d.ts +1 -1
- package/build/auth/session-store.js +1 -1
- package/build/auth/sso-flow.d.ts +24 -0
- package/build/auth/sso-flow.d.ts.map +1 -0
- package/build/auth/sso-flow.js +21 -0
- package/build/auth/sso-flow.js.map +1 -0
- package/build/auth/suny-sso.d.ts +49 -0
- package/build/auth/suny-sso.d.ts.map +1 -0
- package/build/auth/suny-sso.js +146 -0
- package/build/auth/suny-sso.js.map +1 -0
- package/build/auth/token-manager.d.ts +29 -2
- package/build/auth/token-manager.d.ts.map +1 -1
- package/build/auth/token-manager.js +83 -4
- package/build/auth/token-manager.js.map +1 -1
- package/build/auth/token-mint.d.ts +32 -0
- package/build/auth/token-mint.d.ts.map +1 -0
- package/build/auth/token-mint.js +71 -0
- package/build/auth/token-mint.js.map +1 -0
- package/build/auth-cli.d.ts +1 -1
- package/build/auth-cli.js +9 -4
- package/build/auth-cli.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +6 -2
- package/build/index.js.map +1 -1
- package/build/setup.d.ts +2 -2
- package/build/setup.js +31 -5
- package/build/setup.js.map +1 -1
- package/build/tools/download-file.d.ts +6 -2
- package/build/tools/download-file.d.ts.map +1 -1
- package/build/tools/download-file.js +27 -7
- package/build/tools/download-file.js.map +1 -1
- package/build/tools/get-announcements.d.ts +65 -2
- package/build/tools/get-announcements.d.ts.map +1 -1
- package/build/tools/get-announcements.js +69 -25
- package/build/tools/get-announcements.js.map +1 -1
- package/build/tools/get-assignments.d.ts +8 -2
- package/build/tools/get-assignments.d.ts.map +1 -1
- package/build/tools/get-assignments.js +76 -17
- package/build/tools/get-assignments.js.map +1 -1
- package/build/tools/get-classlist-emails.d.ts +2 -2
- package/build/tools/get-classlist-emails.js +2 -2
- package/build/tools/get-course-content.d.ts +2 -2
- package/build/tools/get-course-content.js +2 -2
- package/build/tools/get-discussions.d.ts +1 -1
- package/build/tools/get-discussions.js +1 -1
- package/build/tools/get-my-courses.d.ts +2 -2
- package/build/tools/get-my-courses.d.ts.map +1 -1
- package/build/tools/get-my-courses.js +10 -4
- package/build/tools/get-my-courses.js.map +1 -1
- package/build/tools/get-my-grades.d.ts +1 -1
- package/build/tools/get-my-grades.js +1 -1
- package/build/tools/get-roster.d.ts +2 -2
- package/build/tools/get-roster.js +2 -2
- package/build/tools/get-syllabus.d.ts +1 -1
- package/build/tools/get-syllabus.js +1 -1
- package/build/tools/get-upcoming-due-dates.d.ts +2 -2
- package/build/tools/get-upcoming-due-dates.d.ts.map +1 -1
- package/build/tools/get-upcoming-due-dates.js +121 -45
- package/build/tools/get-upcoming-due-dates.js.map +1 -1
- package/build/tools/index.d.ts +2 -2
- package/build/tools/index.js +2 -2
- package/build/tools/schemas.d.ts +3 -3
- package/build/tools/schemas.d.ts.map +1 -1
- package/build/tools/schemas.js +11 -5
- package/build/tools/schemas.js.map +1 -1
- package/build/tools/tool-helpers.d.ts +1 -1
- package/build/tools/tool-helpers.js +1 -1
- package/build/types/index.d.ts +12 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js +2 -2
- package/build/update.d.ts +1 -1
- package/build/update.js +1 -1
- package/build/utils/config-store.d.ts +3 -2
- package/build/utils/config-store.d.ts.map +1 -1
- package/build/utils/config-store.js +2 -2
- package/build/utils/config-store.js.map +1 -1
- package/build/utils/config.d.ts +2 -2
- package/build/utils/config.d.ts.map +1 -1
- package/build/utils/config.js +3 -2
- package/build/utils/config.js.map +1 -1
- package/build/utils/course-filter.d.ts +2 -2
- package/build/utils/course-filter.js +2 -2
- package/build/utils/deep-links.d.ts +15 -0
- package/build/utils/deep-links.d.ts.map +1 -0
- package/build/utils/deep-links.js +31 -0
- package/build/utils/deep-links.js.map +1 -0
- package/build/utils/download-helpers.d.ts +2 -2
- package/build/utils/download-helpers.js +2 -2
- package/build/utils/errors.d.ts +2 -2
- package/build/utils/errors.js +2 -2
- package/build/utils/file-validator.d.ts +2 -2
- package/build/utils/file-validator.d.ts.map +1 -1
- package/build/utils/file-validator.js +21 -11
- package/build/utils/file-validator.js.map +1 -1
- package/build/utils/html-converter.d.ts +2 -2
- package/build/utils/html-converter.js +2 -2
- package/build/utils/logger.d.ts +2 -2
- package/build/utils/logger.js +2 -2
- package/build/utils/pdf-extractor.d.ts +2 -2
- package/build/utils/pdf-extractor.js +2 -2
- package/package.json +1 -1
package/build/auth/purdue-sso.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purdue Brightspace MCP Server
|
|
3
|
-
* Copyright (c)
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
4
|
* Licensed under MIT — see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import { BrowserAuthError } from "../utils/errors.js";
|
|
@@ -10,7 +10,28 @@ const SELECTORS = {
|
|
|
10
10
|
passwordInput: "input#password",
|
|
11
11
|
submitButton: 'button[name="_eventId_proceed"]',
|
|
12
12
|
staySignedInYes: "input[type=submit][value='Yes']",
|
|
13
|
+
// Purdue's proceed button, D2L's own, and the generic submit used by Entra
|
|
14
|
+
// (input#idSIButton9) and most other identity providers.
|
|
15
|
+
formSubmit: 'button[name="_eventId_proceed"], button.d2l-button, input[type="submit"]',
|
|
13
16
|
};
|
|
17
|
+
// Entra labels the account-name button "Next" and only shows "Sign in" on the
|
|
18
|
+
// view that follows. Both are input#idSIButton9, so the label tells them apart.
|
|
19
|
+
const ACCOUNT_NAME_BUTTON = /^next$/i;
|
|
20
|
+
/**
|
|
21
|
+
* Entra's number-match digits. The tenant shows a two-digit number that has to
|
|
22
|
+
* be typed into Microsoft Authenticator, and nothing else on the machine
|
|
23
|
+
* reveals it, so a headless run stalls forever unless this is scraped and
|
|
24
|
+
* logged. Plain DOM text, no OCR.
|
|
25
|
+
*/
|
|
26
|
+
const NUMBER_MATCH_SELECTOR = "#idRichContext_DisplaySign";
|
|
27
|
+
/** How often to look for the number while waiting on MFA. */
|
|
28
|
+
const NUMBER_MATCH_POLL_MS = 2000;
|
|
29
|
+
/** A person has to find their phone, unlock it, and read a prompt. */
|
|
30
|
+
const MFA_TIMEOUT_MS = 5 * 60 * 1000;
|
|
31
|
+
/** Visible text of a button, or the value of a submit input. */
|
|
32
|
+
async function controlLabel(handle) {
|
|
33
|
+
return handle.evaluate((el) => (el.value || el.textContent || "").trim());
|
|
34
|
+
}
|
|
14
35
|
export class PurdueSSOFlow {
|
|
15
36
|
config;
|
|
16
37
|
constructor(config) {
|
|
@@ -31,7 +52,7 @@ export class PurdueSSOFlow {
|
|
|
31
52
|
*/
|
|
32
53
|
async login(page) {
|
|
33
54
|
try {
|
|
34
|
-
log("INFO", "Starting
|
|
55
|
+
log("INFO", "Starting SSO login flow");
|
|
35
56
|
// Step 1: Handle campus selector on purdue.brightspace.com/d2l/login
|
|
36
57
|
await this.handleCampusSelector(page);
|
|
37
58
|
// Step 2: Enter username + password on sso.purdue.edu (Shibboleth)
|
|
@@ -73,7 +94,7 @@ export class PurdueSSOFlow {
|
|
|
73
94
|
}
|
|
74
95
|
async handleCampusSelector(page) {
|
|
75
96
|
const currentUrl = page.url();
|
|
76
|
-
if (currentUrl.includes("/d2l/login")) {
|
|
97
|
+
if (currentUrl.includes("purdue.brightspace.com") && currentUrl.includes("/d2l/login")) {
|
|
77
98
|
// Campus selector buttons are inside a shadow DOM — navigate directly
|
|
78
99
|
// to Purdue's Shibboleth SAML endpoint instead of clicking them
|
|
79
100
|
const baseUrl = new URL(currentUrl).origin;
|
|
@@ -84,8 +105,11 @@ export class PurdueSSOFlow {
|
|
|
84
105
|
}
|
|
85
106
|
async enterCredentials(page) {
|
|
86
107
|
try {
|
|
87
|
-
log("DEBUG", "Waiting for
|
|
88
|
-
|
|
108
|
+
log("DEBUG", "Waiting for login form");
|
|
109
|
+
// Wait for either Purdue's username or Albany's userName (or typical email fields)
|
|
110
|
+
// Use a shorter timeout so it falls back to manual login quickly if unrecognized
|
|
111
|
+
const usernameSelector = 'input#username, input#userName, input[type="email"]';
|
|
112
|
+
await page.waitForSelector(usernameSelector, { timeout: 10000 });
|
|
89
113
|
if (!this.config.username) {
|
|
90
114
|
throw new BrowserAuthError("Username is required for SSO login", "credentials");
|
|
91
115
|
}
|
|
@@ -93,39 +117,142 @@ export class PurdueSSOFlow {
|
|
|
93
117
|
throw new BrowserAuthError("Password is required for SSO login", "credentials");
|
|
94
118
|
}
|
|
95
119
|
log("INFO", "Entering credentials");
|
|
96
|
-
await
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
const usernameField = await this.findVisible(page, usernameSelector);
|
|
121
|
+
if (usernameField) {
|
|
122
|
+
await usernameField.fill(this.config.username);
|
|
123
|
+
}
|
|
124
|
+
// Microsoft Entra puts a password box on its account-name page as well,
|
|
125
|
+
// but the button there only advances to the next view. Submit the account
|
|
126
|
+
// name first so the password lands on the page that actually signs in.
|
|
127
|
+
await this.submitAccountName(page);
|
|
128
|
+
const passwordSelector = 'input#password, input[type="password"]';
|
|
129
|
+
const passwordField = await this.findVisible(page, passwordSelector);
|
|
130
|
+
if (passwordField) {
|
|
131
|
+
await passwordField.fill(this.config.password);
|
|
132
|
+
}
|
|
133
|
+
const submitButton = await this.findVisible(page, SELECTORS.formSubmit);
|
|
134
|
+
if (submitButton) {
|
|
135
|
+
await submitButton.click();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Fallback: just hit Enter on the password field
|
|
139
|
+
await passwordField?.press('Enter');
|
|
140
|
+
}
|
|
99
141
|
await page.waitForLoadState("networkidle");
|
|
100
142
|
}
|
|
101
143
|
catch (error) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new BrowserAuthError("Failed to enter credentials", "credentials", error);
|
|
144
|
+
log("WARN", "Automated credentials entry failed, will fallback to manual login.", error);
|
|
145
|
+
throw error;
|
|
105
146
|
}
|
|
106
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a control only when it is actually on screen.
|
|
150
|
+
*
|
|
151
|
+
* Entra is a single-page app: the account-name view stays in the DOM once
|
|
152
|
+
* the password view replaces it, so a plain page.$() returns the hidden
|
|
153
|
+
* first-page input or button that precedes the live one in document order.
|
|
154
|
+
* Filling or clicking those stalls on Playwright's actionability wait.
|
|
155
|
+
*/
|
|
156
|
+
async findVisible(page, selector) {
|
|
157
|
+
for (const handle of await page.$$(selector)) {
|
|
158
|
+
if (await handle.isVisible())
|
|
159
|
+
return handle;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Advance Microsoft Entra's account-name page.
|
|
165
|
+
*
|
|
166
|
+
* That page carries a password box of its own, and Playwright reports it as
|
|
167
|
+
* visible, so presence cannot tell it apart from a single-page form. The
|
|
168
|
+
* button label can: it reads "Next" there and "Sign in" on the view that
|
|
169
|
+
* follows. Filling the password against the account-name page leaves the
|
|
170
|
+
* browser parked with both fields populated and nothing submitted.
|
|
171
|
+
*/
|
|
172
|
+
async submitAccountName(page) {
|
|
173
|
+
const submit = await this.findVisible(page, SELECTORS.formSubmit);
|
|
174
|
+
if (!submit)
|
|
175
|
+
return;
|
|
176
|
+
if (!ACCOUNT_NAME_BUTTON.test(await controlLabel(submit)))
|
|
177
|
+
return;
|
|
178
|
+
log("INFO", "Account-name page detected — submitting account name");
|
|
179
|
+
await submit.click();
|
|
180
|
+
// The sign-in view has arrived once the button stops saying "Next".
|
|
181
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
182
|
+
await page.waitForTimeout(500);
|
|
183
|
+
const current = await this.findVisible(page, SELECTORS.formSubmit);
|
|
184
|
+
if (current && !ACCOUNT_NAME_BUTTON.test(await controlLabel(current)))
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
log("WARN", "Sign-in page did not appear after submitting the account name");
|
|
188
|
+
}
|
|
107
189
|
async handleMFA(page) {
|
|
190
|
+
// The number is only on screen while MFA is pending, so the scrape has to
|
|
191
|
+
// run alongside the wait rather than before or after it.
|
|
192
|
+
const pending = { done: false };
|
|
193
|
+
let release = () => { };
|
|
194
|
+
const released = new Promise((resolve) => {
|
|
195
|
+
release = resolve;
|
|
196
|
+
});
|
|
197
|
+
const watcher = this.watchNumberMatch(page, pending, released);
|
|
108
198
|
try {
|
|
109
199
|
log("WARN", "Waiting for Microsoft MFA approval on your device...");
|
|
110
|
-
log("INFO", "Timeout:
|
|
111
|
-
log("INFO", "
|
|
200
|
+
log("INFO", "Timeout: 5 minutes");
|
|
201
|
+
log("INFO", "Approve the sign-in request in Microsoft Authenticator. If it shows a number, it is printed below.");
|
|
112
202
|
// Wait for MFA approval by watching for the post-MFA redirect.
|
|
113
203
|
// Using waitForURL instead of networkidle because networkidle fires
|
|
114
204
|
// after 500ms of no network activity, which can happen while the MFA
|
|
115
|
-
// page UI finishes loading but before the user approves the
|
|
205
|
+
// page UI finishes loading but before the user approves the request.
|
|
116
206
|
await page.waitForURL((url) => {
|
|
117
207
|
const href = url.toString();
|
|
118
208
|
return href.includes("/d2l/") ||
|
|
119
209
|
href.includes("kmsi") ||
|
|
120
210
|
href.includes("/sso/") ||
|
|
121
211
|
href.includes("SAMLResponse");
|
|
122
|
-
}, { timeout:
|
|
123
|
-
log("INFO", `MFA completed
|
|
212
|
+
}, { timeout: MFA_TIMEOUT_MS });
|
|
213
|
+
log("INFO", `MFA completed - redirected to: ${page.url()}`);
|
|
124
214
|
}
|
|
125
215
|
catch (error) {
|
|
126
|
-
throw new BrowserAuthError("MFA approval timed out after
|
|
216
|
+
throw new BrowserAuthError("MFA approval timed out after 5 minutes", "mfa_approval", error);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
pending.done = true;
|
|
220
|
+
release();
|
|
221
|
+
await watcher;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Log Entra's number-match digits until the MFA wait is over.
|
|
226
|
+
*
|
|
227
|
+
* Only a CHANGED value is logged: Entra re-mints the number whenever the
|
|
228
|
+
* user asks for a new request, and repeating the same digits every two
|
|
229
|
+
* seconds would bury everything else in the log.
|
|
230
|
+
*/
|
|
231
|
+
async watchNumberMatch(page, pending, released) {
|
|
232
|
+
let last = null;
|
|
233
|
+
while (!pending.done) {
|
|
234
|
+
const number = await this.readNumberMatch(page);
|
|
235
|
+
if (number && number !== last) {
|
|
236
|
+
last = number;
|
|
237
|
+
log("WARN", `Number match: ${number}. Enter it in Microsoft Authenticator.`);
|
|
238
|
+
}
|
|
239
|
+
// Racing the release keeps the wait from outliving the MFA it watches.
|
|
240
|
+
await Promise.race([
|
|
241
|
+
page.waitForTimeout(NUMBER_MATCH_POLL_MS).catch(() => { }),
|
|
242
|
+
released,
|
|
243
|
+
]);
|
|
127
244
|
}
|
|
128
245
|
}
|
|
246
|
+
/** The digits on screen, or null when Entra is not showing any. */
|
|
247
|
+
async readNumberMatch(page) {
|
|
248
|
+
const sign = page.locator(NUMBER_MATCH_SELECTOR).first();
|
|
249
|
+
// isVisible answers immediately rather than waiting out a timeout, so the
|
|
250
|
+
// runs that never show a number keep the poll on its two-second rhythm.
|
|
251
|
+
if (!(await sign.isVisible().catch(() => false)))
|
|
252
|
+
return null;
|
|
253
|
+
const text = await sign.textContent().catch(() => null);
|
|
254
|
+
return text?.trim() || null;
|
|
255
|
+
}
|
|
129
256
|
async handleStaySignedIn(page) {
|
|
130
257
|
try {
|
|
131
258
|
log("DEBUG", "Checking for 'Stay signed in?' prompt");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purdue-sso.js","sourceRoot":"","sources":["../../src/auth/purdue-sso.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAEzC,MAAM,SAAS,GAAG;IAChB,aAAa,EAAE,gBAAgB;IAC/B,aAAa,EAAE,gBAAgB;IAC/B,YAAY,EAAE,iCAAiC;IAC/C,eAAe,EAAE,iCAAiC;
|
|
1
|
+
{"version":3,"file":"purdue-sso.js","sourceRoot":"","sources":["../../src/auth/purdue-sso.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purdue Brightspace MCP Server
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
-
* Licensed under
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import type { TokenData } from "../types/index.js";
|
|
7
7
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purdue Brightspace MCP Server
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
-
* Licensed under
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import * as crypto from "node:crypto";
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import type { Page } from "playwright";
|
|
7
|
+
import type { AppConfig } from "../types/index.js";
|
|
8
|
+
/** The browser login sequence for one institution's identity provider. */
|
|
9
|
+
export interface SSOFlow {
|
|
10
|
+
/** True when saved credentials allow an automated sign-in attempt. */
|
|
11
|
+
hasCredentials(): boolean;
|
|
12
|
+
/** Drive the sign-in form. Resolves false on timeout so the caller can fall back. */
|
|
13
|
+
login(page: Page): Promise<boolean>;
|
|
14
|
+
/** Wait for the user to sign in themselves in a visible browser. */
|
|
15
|
+
manualLogin(page: Page): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pick the login sequence for the configured Brightspace host. Schools whose
|
|
19
|
+
* identity provider needs extra steps get their own handler here; everything
|
|
20
|
+
* else uses the default flow, which already covers the common Shibboleth,
|
|
21
|
+
* CAS, and Microsoft Entra forms.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createSSOFlow(config: AppConfig): SSOFlow;
|
|
24
|
+
//# sourceMappingURL=sso-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sso-flow.d.ts","sourceRoot":"","sources":["../../src/auth/sso-flow.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAInD,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,sEAAsE;IACtE,cAAc,IAAI,OAAO,CAAC;IAC1B,qFAAqF;IACrF,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,oEAAoE;IACpE,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAQxD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { PurdueSSOFlow } from "./purdue-sso.js";
|
|
7
|
+
import { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
|
|
8
|
+
/**
|
|
9
|
+
* Pick the login sequence for the configured Brightspace host. Schools whose
|
|
10
|
+
* identity provider needs extra steps get their own handler here; everything
|
|
11
|
+
* else uses the default flow, which already covers the common Shibboleth,
|
|
12
|
+
* CAS, and Microsoft Entra forms.
|
|
13
|
+
*/
|
|
14
|
+
export function createSSOFlow(config) {
|
|
15
|
+
const credentials = { username: config.username, password: config.password };
|
|
16
|
+
if (isSunyBrightspace(config.baseUrl)) {
|
|
17
|
+
return new SunySSOFlow({ ...credentials, campus: config.campus });
|
|
18
|
+
}
|
|
19
|
+
return new PurdueSSOFlow(credentials);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=sso-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sso-flow.js","sourceRoot":"","sources":["../../src/auth/sso-flow.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAY/D;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,MAAiB;IAC7C,MAAM,WAAW,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;IAE7E,IAAI,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,WAAW,CAAC,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,IAAI,aAAa,CAAC,WAAW,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import type { Page } from "playwright";
|
|
7
|
+
interface SunySSOConfig {
|
|
8
|
+
username?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
/** Campus name or numeric code, matched against SUNY's own dropdown. */
|
|
11
|
+
campus?: string;
|
|
12
|
+
}
|
|
13
|
+
/** True for the shared SUNY Brightspace instance. */
|
|
14
|
+
export declare function isSunyBrightspace(baseUrl: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Login flow for the shared SUNY Brightspace instance.
|
|
17
|
+
*
|
|
18
|
+
* SUNY puts two extra pages in front of the campus identity provider: the
|
|
19
|
+
* Brightspace campus selector, whose buttons sit in a shadow DOM, and SUNY's
|
|
20
|
+
* own campus dropdown at idm.suny.edu. This handler clears both, then hands
|
|
21
|
+
* off to the default flow, which already knows the Shibboleth, CAS, and
|
|
22
|
+
* Microsoft Entra sign-in forms the individual campuses use.
|
|
23
|
+
*/
|
|
24
|
+
export declare class SunySSOFlow {
|
|
25
|
+
private config;
|
|
26
|
+
private defaultFlow;
|
|
27
|
+
constructor(config: SunySSOConfig);
|
|
28
|
+
hasCredentials(): boolean;
|
|
29
|
+
login(page: Page): Promise<boolean>;
|
|
30
|
+
manualLogin(page: Page): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Brightspace's own campus selector renders its buttons in a shadow DOM, so
|
|
33
|
+
* go straight to the SAML endpoint that selector would have reached.
|
|
34
|
+
*/
|
|
35
|
+
private startSamlLogin;
|
|
36
|
+
/**
|
|
37
|
+
* Answer SUNY's "Select Campus" dropdown. Without a configured campus the
|
|
38
|
+
* page is left for the user, who is already at the browser for MFA.
|
|
39
|
+
*/
|
|
40
|
+
private selectCampus;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the configured campus against SUNY's live option list, so no copy
|
|
43
|
+
* of the campus table has to be kept in this repo. Matches a numeric code or
|
|
44
|
+
* a campus name, exactly first and then as a prefix.
|
|
45
|
+
*/
|
|
46
|
+
private findCampus;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=suny-sso.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suny-sso.d.ts","sourceRoot":"","sources":["../../src/auth/suny-sso.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAcvC,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAOD,qDAAqD;AACrD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM1D;AAcD;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,WAAW,CAAgB;gBAEvB,MAAM,EAAE,aAAa;IAQjC,cAAc,IAAI,OAAO;IAInB,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAWnC,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAY/C;;;OAGG;YACW,cAAc;IAY5B;;;OAGG;YACW,YAAY;IAwC1B;;;;OAIG;IACH,OAAO,CAAC,UAAU;CAUnB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { PurdueSSOFlow } from "./purdue-sso.js";
|
|
7
|
+
import { log } from "../utils/logger.js";
|
|
8
|
+
/** SUNY campuses share one Brightspace tenant behind one Shibboleth IdP. */
|
|
9
|
+
const SUNY_BRIGHTSPACE_HOST = "mylearning.suny.edu";
|
|
10
|
+
const SUNY_IDP_ENTITY_ID = "https://idm.suny.edu/shibboleth/idp/";
|
|
11
|
+
const SUNY_IDM_HOST = "idm.suny.edu";
|
|
12
|
+
const SELECTORS = {
|
|
13
|
+
campusSelect: "select#campus",
|
|
14
|
+
campusSubmit: '#selectionForm button[type="submit"]',
|
|
15
|
+
};
|
|
16
|
+
/** True for the shared SUNY Brightspace instance. */
|
|
17
|
+
export function isSunyBrightspace(baseUrl) {
|
|
18
|
+
try {
|
|
19
|
+
return new URL(baseUrl).hostname.toLowerCase() === SUNY_BRIGHTSPACE_HOST;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* SUNY carries its own hostname inside the redirect parameters it round-trips,
|
|
27
|
+
* so compare the host rather than searching the whole URL.
|
|
28
|
+
*/
|
|
29
|
+
function isSunyIdp(url) {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(url).hostname.toLowerCase() === SUNY_IDM_HOST;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Login flow for the shared SUNY Brightspace instance.
|
|
39
|
+
*
|
|
40
|
+
* SUNY puts two extra pages in front of the campus identity provider: the
|
|
41
|
+
* Brightspace campus selector, whose buttons sit in a shadow DOM, and SUNY's
|
|
42
|
+
* own campus dropdown at idm.suny.edu. This handler clears both, then hands
|
|
43
|
+
* off to the default flow, which already knows the Shibboleth, CAS, and
|
|
44
|
+
* Microsoft Entra sign-in forms the individual campuses use.
|
|
45
|
+
*/
|
|
46
|
+
export class SunySSOFlow {
|
|
47
|
+
config;
|
|
48
|
+
defaultFlow;
|
|
49
|
+
constructor(config) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.defaultFlow = new PurdueSSOFlow({
|
|
52
|
+
username: config.username,
|
|
53
|
+
password: config.password,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
hasCredentials() {
|
|
57
|
+
return this.defaultFlow.hasCredentials();
|
|
58
|
+
}
|
|
59
|
+
async login(page) {
|
|
60
|
+
try {
|
|
61
|
+
await this.startSamlLogin(page);
|
|
62
|
+
await this.selectCampus(page);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
log("ERROR", "SUNY campus selection failed", error);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return this.defaultFlow.login(page);
|
|
69
|
+
}
|
|
70
|
+
async manualLogin(page) {
|
|
71
|
+
try {
|
|
72
|
+
// Clear the shadow-DOM selector and, when configured, the campus
|
|
73
|
+
// dropdown, so the user lands directly on their campus sign-in page.
|
|
74
|
+
await this.startSamlLogin(page);
|
|
75
|
+
await this.selectCampus(page);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
log("DEBUG", "Could not pre-fill SUNY campus selection", error);
|
|
79
|
+
}
|
|
80
|
+
return this.defaultFlow.manualLogin(page);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Brightspace's own campus selector renders its buttons in a shadow DOM, so
|
|
84
|
+
* go straight to the SAML endpoint that selector would have reached.
|
|
85
|
+
*/
|
|
86
|
+
async startSamlLogin(page) {
|
|
87
|
+
const currentUrl = page.url();
|
|
88
|
+
if (!currentUrl.includes("/d2l/login"))
|
|
89
|
+
return;
|
|
90
|
+
const origin = new URL(currentUrl).origin;
|
|
91
|
+
log("INFO", "Campus selector detected — navigating directly to SUNY's SAML endpoint");
|
|
92
|
+
await page.goto(`${origin}/d2l/lp/auth/saml/initiate-login?entityId=${SUNY_IDP_ENTITY_ID}`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Answer SUNY's "Select Campus" dropdown. Without a configured campus the
|
|
96
|
+
* page is left for the user, who is already at the browser for MFA.
|
|
97
|
+
*/
|
|
98
|
+
async selectCampus(page) {
|
|
99
|
+
if (!isSunyIdp(page.url()))
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
// idm.suny.edu sits behind a bot check, so the form can take a few
|
|
103
|
+
// seconds to appear even on a fast connection.
|
|
104
|
+
await page.waitForSelector(SELECTORS.campusSelect, { timeout: 45000 });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
log("DEBUG", "No SUNY campus dropdown found — continuing to the sign-in form");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const options = await page.$$eval(`${SELECTORS.campusSelect} option`, (nodes) => nodes
|
|
111
|
+
.map((node) => ({
|
|
112
|
+
value: node.value,
|
|
113
|
+
label: (node.textContent ?? "").trim(),
|
|
114
|
+
}))
|
|
115
|
+
.filter((option) => option.value !== ""));
|
|
116
|
+
if (!this.config.campus) {
|
|
117
|
+
log("WARN", "No campus configured — choose your campus in the browser window.");
|
|
118
|
+
log("INFO", "Set one with: brightspace-mcp-server setup --suny");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const match = this.findCampus(options);
|
|
122
|
+
if (!match) {
|
|
123
|
+
log("WARN", `Campus "${this.config.campus}" did not match any SUNY campus.`);
|
|
124
|
+
log("INFO", `Available: ${options.map((o) => o.label).join(", ")}`);
|
|
125
|
+
log("WARN", "Choose your campus in the browser window to continue.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
log("INFO", `Selecting campus ${match.label} (${match.value})`);
|
|
129
|
+
await page.selectOption(SELECTORS.campusSelect, match.value);
|
|
130
|
+
await page.click(SELECTORS.campusSubmit);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the configured campus against SUNY's live option list, so no copy
|
|
134
|
+
* of the campus table has to be kept in this repo. Matches a numeric code or
|
|
135
|
+
* a campus name, exactly first and then as a prefix.
|
|
136
|
+
*/
|
|
137
|
+
findCampus(options) {
|
|
138
|
+
const wanted = this.config.campus?.trim().toLowerCase();
|
|
139
|
+
if (!wanted)
|
|
140
|
+
return undefined;
|
|
141
|
+
return (options.find((option) => option.value.toLowerCase() === wanted) ??
|
|
142
|
+
options.find((option) => option.label.toLowerCase() === wanted) ??
|
|
143
|
+
options.find((option) => option.label.toLowerCase().startsWith(wanted)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=suny-sso.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suny-sso.js","sourceRoot":"","sources":["../../src/auth/suny-sso.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purdue Brightspace MCP Server
|
|
3
|
-
* Copyright (c)
|
|
4
|
-
* Licensed under
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import type { TokenData } from "../types/index.js";
|
|
7
|
+
import { mintAccessToken } from "./token-mint.js";
|
|
8
|
+
export interface TokenManagerOptions {
|
|
9
|
+
sessionDir?: string;
|
|
10
|
+
/** Tenant base URL. Without it the cookie mint is not attempted. */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Lifetime in seconds stamped on a minted token. */
|
|
13
|
+
tokenTtl?: number;
|
|
14
|
+
/** Injection seam for tests. */
|
|
15
|
+
mint?: typeof mintAccessToken;
|
|
16
|
+
}
|
|
7
17
|
/**
|
|
8
18
|
* TokenManager manages token lifecycle with in-memory caching and disk persistence.
|
|
9
19
|
* Handles expiry detection with a configurable refresh buffer.
|
|
@@ -11,13 +21,30 @@ import type { TokenData } from "../types/index.js";
|
|
|
11
21
|
export declare class TokenManager {
|
|
12
22
|
private cachedToken;
|
|
13
23
|
private readonly sessionStore;
|
|
24
|
+
private readonly baseUrl?;
|
|
25
|
+
private readonly tokenTtl;
|
|
26
|
+
private readonly mint;
|
|
27
|
+
/** Single in-flight mint, so concurrent callers share one request. */
|
|
28
|
+
private mintInFlight;
|
|
14
29
|
constructor(sessionDir?: string);
|
|
30
|
+
constructor(options: TokenManagerOptions);
|
|
15
31
|
/**
|
|
16
32
|
* Get the current token if valid, otherwise null.
|
|
17
33
|
* Checks memory cache first, then loads from disk if needed.
|
|
18
34
|
* Returns null if token is expired or within refresh buffer.
|
|
19
35
|
*/
|
|
20
36
|
getToken(): Promise<TokenData | null>;
|
|
37
|
+
/**
|
|
38
|
+
* First candidate that carries both the cookie header and the XSRF token,
|
|
39
|
+
* or null when neither can be used to mint.
|
|
40
|
+
*/
|
|
41
|
+
private pickMintable;
|
|
42
|
+
/**
|
|
43
|
+
* Mint a fresh token from the stale one's session material, collapsing
|
|
44
|
+
* concurrent callers onto a single request.
|
|
45
|
+
*/
|
|
46
|
+
private mintFromSession;
|
|
47
|
+
private runMint;
|
|
21
48
|
/**
|
|
22
49
|
* Set a new token, caching in memory and persisting to disk.
|
|
23
50
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAYlD,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,IAAI,CAAC,EAAE,OAAO,eAAe,CAAC;CAC/B;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,sEAAsE;IACtE,OAAO,CAAC,YAAY,CAA0C;gBAElD,UAAU,CAAC,EAAE,MAAM;gBACnB,OAAO,EAAE,mBAAmB;IAaxC;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA2B3C;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB;;;OAGG;YACW,eAAe;YAaf,OAAO;IAkCrB;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAMjC;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO;IAiBlC;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;CAIvC"}
|