brightspace-mcp-server 1.6.3 → 2.0.0
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 +34 -7
- package/build/api/client.d.ts +5 -3
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +71 -79
- package/build/api/client.js.map +1 -1
- package/build/api/errors.d.ts +4 -0
- package/build/api/errors.d.ts.map +1 -1
- package/build/api/errors.js +7 -0
- package/build/api/errors.js.map +1 -1
- package/build/auth/auth-cooldown.d.ts +16 -0
- package/build/auth/auth-cooldown.d.ts.map +1 -0
- package/build/auth/auth-cooldown.js +47 -0
- package/build/auth/auth-cooldown.js.map +1 -0
- package/build/auth/auth-lock.d.ts +7 -0
- package/build/auth/auth-lock.d.ts.map +1 -0
- package/build/auth/auth-lock.js +114 -0
- package/build/auth/auth-lock.js.map +1 -0
- package/build/auth/auth-runner.d.ts +17 -7
- package/build/auth/auth-runner.d.ts.map +1 -1
- package/build/auth/auth-runner.js +122 -26
- package/build/auth/auth-runner.js.map +1 -1
- package/build/auth/browser-auth.d.ts +17 -86
- package/build/auth/browser-auth.d.ts.map +1 -1
- package/build/auth/browser-auth.js +256 -615
- package/build/auth/browser-auth.js.map +1 -1
- package/build/auth/browser-state-store.d.ts +19 -0
- package/build/auth/browser-state-store.d.ts.map +1 -0
- package/build/auth/browser-state-store.js +101 -0
- package/build/auth/browser-state-store.js.map +1 -0
- package/build/auth/credential-store.d.ts +29 -0
- package/build/auth/credential-store.d.ts.map +1 -0
- package/build/auth/credential-store.js +233 -0
- package/build/auth/credential-store.js.map +1 -0
- package/build/auth/encrypted-store.d.ts +25 -0
- package/build/auth/encrypted-store.d.ts.map +1 -0
- package/build/auth/encrypted-store.js +61 -0
- package/build/auth/encrypted-store.js.map +1 -0
- package/build/auth/legacy-profile.d.ts +3 -0
- package/build/auth/legacy-profile.d.ts.map +1 -0
- package/build/auth/legacy-profile.js +41 -0
- package/build/auth/legacy-profile.js.map +1 -0
- package/build/auth/legacy-state.d.ts +14 -0
- package/build/auth/legacy-state.d.ts.map +1 -0
- package/build/auth/legacy-state.js +107 -0
- package/build/auth/legacy-state.js.map +1 -0
- package/build/auth/purdue-sso.d.ts +18 -36
- package/build/auth/purdue-sso.d.ts.map +1 -1
- package/build/auth/purdue-sso.js +165 -192
- package/build/auth/purdue-sso.js.map +1 -1
- package/build/auth/session-store.d.ts +16 -38
- package/build/auth/session-store.d.ts.map +1 -1
- package/build/auth/session-store.js +157 -152
- package/build/auth/session-store.js.map +1 -1
- package/build/auth/sso-flow.d.ts +14 -3
- package/build/auth/sso-flow.d.ts.map +1 -1
- package/build/auth/sso-flow.js +16 -1
- package/build/auth/sso-flow.js.map +1 -1
- package/build/auth/suny-sso.d.ts +3 -2
- package/build/auth/suny-sso.d.ts.map +1 -1
- package/build/auth/suny-sso.js +15 -24
- package/build/auth/suny-sso.js.map +1 -1
- package/build/auth/token-manager.d.ts +10 -3
- package/build/auth/token-manager.d.ts.map +1 -1
- package/build/auth/token-manager.js +62 -26
- package/build/auth/token-manager.js.map +1 -1
- package/build/auth/token-mint.d.ts +2 -2
- package/build/auth/token-mint.d.ts.map +1 -1
- package/build/auth/token-mint.js +31 -9
- package/build/auth/token-mint.js.map +1 -1
- package/build/auth-cli.d.ts +1 -7
- package/build/auth-cli.d.ts.map +1 -1
- package/build/auth-cli.js +29 -73
- package/build/auth-cli.js.map +1 -1
- package/build/index.js +8 -4
- package/build/index.js.map +1 -1
- package/build/setup.js +14 -8
- package/build/setup.js.map +1 -1
- package/build/types/index.d.ts +6 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/utils/config-store.d.ts +1 -0
- package/build/utils/config-store.d.ts.map +1 -1
- package/build/utils/config-store.js +3 -0
- package/build/utils/config-store.js.map +1 -1
- package/build/utils/config.d.ts +2 -1
- package/build/utils/config.d.ts.map +1 -1
- package/build/utils/config.js +30 -10
- package/build/utils/config.js.map +1 -1
- package/build/utils/secure-config.d.ts +6 -0
- package/build/utils/secure-config.d.ts.map +1 -0
- package/build/utils/secure-config.js +57 -0
- package/build/utils/secure-config.js.map +1 -0
- package/package.json +4 -2
|
@@ -1,192 +1,197 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Brightspace MCP Server
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
-
* Licensed under MIT
|
|
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";
|
|
8
|
-
import * as fsSync from "node:fs";
|
|
9
8
|
import * as path from "node:path";
|
|
10
9
|
import * as os from "node:os";
|
|
11
10
|
import { SessionStoreError } from "../utils/errors.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { acquireProcessLock, AuthenticationInProgressError } from "./auth-lock.js";
|
|
12
|
+
import { NativeCredentialStoreError } from "./credential-store.js";
|
|
13
|
+
import { decrypt, readEncryptedRecord, saveEncryptedRecord, trashFile } from "./encrypted-store.js";
|
|
14
14
|
const DEFAULT_SESSION_DIR = path.join(os.homedir(), ".d2l-session");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
function validTenantOrigin(origin) {
|
|
16
|
+
if (origin === undefined)
|
|
17
|
+
return true;
|
|
18
|
+
if (typeof origin !== "string")
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(origin);
|
|
22
|
+
return parsed.protocol === "https:" && parsed.origin === origin;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function validToken(value) {
|
|
29
|
+
const token = value;
|
|
30
|
+
return !!token && typeof token.accessToken === "string" && Number.isFinite(token.capturedAt) && Number.isFinite(token.expiresAt)
|
|
31
|
+
&& (token.source === "browser" || token.source === "cache")
|
|
32
|
+
&& (token.cookieHeader === undefined || typeof token.cookieHeader === "string")
|
|
33
|
+
&& (token.csrfToken === undefined || typeof token.csrfToken === "string")
|
|
34
|
+
&& validTenantOrigin(token.tenantOrigin);
|
|
35
|
+
}
|
|
36
|
+
function equalToken(a, b) {
|
|
37
|
+
return a === b || (!!a && !!b && a.accessToken === b.accessToken && a.capturedAt === b.capturedAt
|
|
38
|
+
&& a.expiresAt === b.expiresAt && a.source === b.source && a.cookieHeader === b.cookieHeader && a.csrfToken === b.csrfToken && a.tenantOrigin === b.tenantOrigin);
|
|
39
|
+
}
|
|
40
|
+
/** AES-GCM session persistence with a random key held by the native secret store. */
|
|
27
41
|
export class SessionStore {
|
|
42
|
+
options;
|
|
28
43
|
sessionDir;
|
|
29
44
|
sessionFilePath;
|
|
30
|
-
constructor(sessionDir) {
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
45
|
+
constructor(sessionDir = DEFAULT_SESSION_DIR, options = {}) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.sessionDir = sessionDir;
|
|
48
|
+
this.sessionFilePath = path.join(sessionDir, "session.json");
|
|
33
49
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
async withWriteLock(operation) {
|
|
51
|
+
await fs.mkdir(this.sessionDir, { recursive: true, mode: 0o700 });
|
|
52
|
+
let release;
|
|
53
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
54
|
+
try {
|
|
55
|
+
release = await acquireProcessLock(path.join(this.sessionDir, ".session-write.lock"));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (!(error instanceof AuthenticationInProgressError) || attempt === 99)
|
|
60
|
+
throw error;
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
40
64
|
try {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const isWindows = process.platform === "win32";
|
|
46
|
-
fsSync.mkdirSync(this.sessionDir, {
|
|
47
|
-
recursive: true,
|
|
48
|
-
...(isWindows ? {} : { mode: 0o700 }),
|
|
49
|
-
});
|
|
50
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
51
|
-
fsSync.writeFileSync(saltPath, salt, {
|
|
52
|
-
...(isWindows ? {} : { mode: 0o600 }),
|
|
53
|
-
});
|
|
54
|
-
return salt;
|
|
65
|
+
return await operation();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await release?.();
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// with a full MFA login. The per-install random salt already provides the
|
|
67
|
-
// uniqueness the hostname was there for, and it is stable.
|
|
68
|
-
const username = os.userInfo().username;
|
|
69
|
-
const salt = this.getOrCreateSalt();
|
|
70
|
-
// Use scrypt to derive a 32-byte key (256 bits for AES-256)
|
|
71
|
-
return crypto.scryptSync(username, salt, 32);
|
|
71
|
+
async readFile() {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(await fs.readFile(this.sessionFilePath, "utf8"));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code === "ENOENT")
|
|
77
|
+
return null;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
async decode(record) {
|
|
82
|
+
let token;
|
|
83
|
+
if (record.version === 2) {
|
|
84
|
+
token = await readEncryptedRecord(this.sessionDir, record, "session", this.options);
|
|
85
|
+
}
|
|
86
|
+
else if (record.version === 1) {
|
|
87
|
+
// Never generate a salt when loading old data. That would make recovery harder.
|
|
88
|
+
const salt = await fs.readFile(path.join(this.sessionDir, "salt"));
|
|
89
|
+
const username = os.userInfo().username;
|
|
90
|
+
let lastError;
|
|
91
|
+
for (const material of [username, username + os.hostname()]) {
|
|
92
|
+
try {
|
|
93
|
+
token = JSON.parse(decrypt(record.encrypted, crypto.scryptSync(material, salt, 32)));
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
lastError = error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!token)
|
|
101
|
+
throw lastError;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new Error("Unsupported saved session version.");
|
|
105
|
+
}
|
|
106
|
+
if (!validToken(token))
|
|
107
|
+
throw new Error("Invalid saved session contents.");
|
|
108
|
+
return token;
|
|
89
109
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
async loadUnlocked() {
|
|
111
|
+
const record = await this.readFile();
|
|
112
|
+
if (!record)
|
|
113
|
+
return null;
|
|
114
|
+
const token = await this.decode(record);
|
|
115
|
+
if (record.version === 1)
|
|
116
|
+
await this.saveUnlocked(token);
|
|
117
|
+
return token;
|
|
118
|
+
}
|
|
119
|
+
async saveUnlocked(token) {
|
|
120
|
+
if (!validToken(token))
|
|
121
|
+
throw new Error("Invalid session token.");
|
|
122
|
+
await saveEncryptedRecord(this.sessionDir, this.sessionFilePath, "session", token, this.options);
|
|
123
|
+
}
|
|
124
|
+
storeError(action, error) {
|
|
125
|
+
if (error instanceof NativeCredentialStoreError || error instanceof AuthenticationInProgressError)
|
|
126
|
+
throw error;
|
|
127
|
+
throw new SessionStoreError(`Failed to ${action} session. Existing session data was preserved.`, error instanceof Error ? error : new Error(String(error)));
|
|
103
128
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Save token to disk with encryption.
|
|
106
|
-
* Creates session directory if it doesn't exist.
|
|
107
|
-
*/
|
|
108
129
|
async save(token) {
|
|
109
130
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
await this.withWriteLock(async () => {
|
|
132
|
+
const current = await this.readFile();
|
|
133
|
+
if (current)
|
|
134
|
+
await this.decode(current);
|
|
135
|
+
await this.saveUnlocked(token);
|
|
115
136
|
});
|
|
116
|
-
const plaintext = JSON.stringify(token);
|
|
117
|
-
const encrypted = this.encrypt(plaintext);
|
|
118
|
-
const sessionFile = {
|
|
119
|
-
version: SESSION_VERSION,
|
|
120
|
-
encrypted,
|
|
121
|
-
createdAt: Date.now(),
|
|
122
|
-
expiresAt: token.expiresAt,
|
|
123
|
-
};
|
|
124
|
-
// Staged and renamed, so the auth CLI and a running server can never
|
|
125
|
-
// leave each other a half-written file.
|
|
126
|
-
await writeFileAtomic(this.sessionFilePath, JSON.stringify(sessionFile, null, 2), isWindows ? {} : { mode: 0o600 });
|
|
127
|
-
log("DEBUG", `Session saved to ${this.sessionFilePath}`);
|
|
128
137
|
}
|
|
129
138
|
catch (error) {
|
|
130
|
-
|
|
131
|
-
log("ERROR", `Failed to save session: ${err.message}`);
|
|
132
|
-
throw new SessionStoreError("Failed to save session", err);
|
|
139
|
+
this.storeError("save", error);
|
|
133
140
|
}
|
|
134
141
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Load token from disk with decryption.
|
|
137
|
-
* Returns null if file doesn't exist or is corrupted (graceful degradation).
|
|
138
|
-
*/
|
|
142
|
+
/** Missing files return null. Damaged or inaccessible secrets fail without MFA. */
|
|
139
143
|
async load() {
|
|
140
144
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await fs.access(this.sessionFilePath);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
log("DEBUG", "No session file found");
|
|
145
|
+
const record = await this.readFile();
|
|
146
|
+
if (!record)
|
|
147
147
|
return null;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const sessionFile = JSON.parse(fileContent);
|
|
152
|
-
// Decrypt token data
|
|
153
|
-
const plaintext = this.decrypt(sessionFile.encrypted);
|
|
154
|
-
const token = JSON.parse(plaintext);
|
|
155
|
-
log("DEBUG", `Session loaded from ${this.sessionFilePath}`);
|
|
156
|
-
return token;
|
|
148
|
+
if (record.version === 1)
|
|
149
|
+
return await this.withWriteLock(() => this.loadUnlocked());
|
|
150
|
+
return await this.decode(record);
|
|
157
151
|
}
|
|
158
152
|
catch (error) {
|
|
159
|
-
|
|
160
|
-
log("WARN", `Failed to load session: ${err.message}`);
|
|
161
|
-
// A session this install cannot read is never going to become readable:
|
|
162
|
-
// it was written by an older build whose key derivation differed, or the
|
|
163
|
-
// file is damaged. Removing it means the next run does one clean login
|
|
164
|
-
// instead of logging the same warning on every start forever.
|
|
165
|
-
try {
|
|
166
|
-
await fs.rm(this.sessionFilePath, { force: true });
|
|
167
|
-
log("DEBUG", "Removed the unreadable session file, a fresh login is needed");
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// Nothing more to do: the caller re-authenticates either way.
|
|
171
|
-
}
|
|
172
|
-
// Return null instead of throwing - graceful degradation
|
|
173
|
-
return null;
|
|
153
|
+
this.storeError("load", error);
|
|
174
154
|
}
|
|
175
155
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Clear session by deleting the session file.
|
|
178
|
-
*/
|
|
179
156
|
async clear() {
|
|
180
157
|
try {
|
|
181
|
-
await
|
|
182
|
-
|
|
158
|
+
await this.withWriteLock(async () => {
|
|
159
|
+
const current = await this.readFile();
|
|
160
|
+
if (!current)
|
|
161
|
+
return;
|
|
162
|
+
await this.decode(current);
|
|
163
|
+
await trashFile(this.sessionFilePath, this.options);
|
|
164
|
+
});
|
|
183
165
|
}
|
|
184
166
|
catch (error) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
this.storeError("clear", error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Prevent a slow mint from replacing credentials saved by a newer login. */
|
|
171
|
+
async saveIfCurrent(token, expected) {
|
|
172
|
+
try {
|
|
173
|
+
return await this.withWriteLock(async () => {
|
|
174
|
+
if (!equalToken(await this.loadUnlocked(), expected))
|
|
175
|
+
return false;
|
|
176
|
+
await this.saveUnlocked(token);
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
this.storeError("save", error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async clearIfCurrent(expected) {
|
|
185
|
+
try {
|
|
186
|
+
return await this.withWriteLock(async () => {
|
|
187
|
+
if (!equalToken(await this.loadUnlocked(), expected))
|
|
188
|
+
return false;
|
|
189
|
+
await trashFile(this.sessionFilePath, this.options);
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.storeError("clear", error);
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/auth/session-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/auth/session-store.ts"],"names":[],"mappings":"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"}
|
package/build/auth/sso-flow.d.ts
CHANGED
|
@@ -5,14 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Page } from "playwright";
|
|
7
7
|
import type { AppConfig } from "../types/index.js";
|
|
8
|
+
import { BrowserAuthError } from "../utils/errors.js";
|
|
9
|
+
export declare class UnsupportedAuthenticationError extends BrowserAuthError {
|
|
10
|
+
readonly code = "AUTH_UNSUPPORTED";
|
|
11
|
+
constructor(message: string, cause?: Error);
|
|
12
|
+
}
|
|
13
|
+
export declare class MfaApprovalError extends BrowserAuthError {
|
|
14
|
+
readonly code = "AUTH_MFA_FAILED";
|
|
15
|
+
constructor(cause?: Error);
|
|
16
|
+
}
|
|
8
17
|
/** The browser login sequence for one institution's identity provider. */
|
|
9
18
|
export interface SSOFlow {
|
|
19
|
+
/** Pass known school and campus selectors without entering credentials. */
|
|
20
|
+
prepareLogin?(page: Page): Promise<void>;
|
|
21
|
+
/** Submit only the public account name so a saved IdP session can resume. */
|
|
22
|
+
identifyAccount?(page: Page): Promise<boolean>;
|
|
10
23
|
/** True when saved credentials allow an automated sign-in attempt. */
|
|
11
24
|
hasCredentials(): boolean;
|
|
12
|
-
/** Drive the sign-in form
|
|
25
|
+
/** Drive the supported headless sign-in form, surfacing MFA in terminal logs. */
|
|
13
26
|
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
27
|
}
|
|
17
28
|
/**
|
|
18
29
|
* Pick the login sequence for the configured Brightspace host. Schools whose
|
|
@@ -1 +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;
|
|
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;AAGnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,qBAAa,8BAA+B,SAAQ,gBAAgB;IAClE,QAAQ,CAAC,IAAI,sBAAsB;gBACvB,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK;CAI3C;AAED,qBAAa,gBAAiB,SAAQ,gBAAgB;IACpD,QAAQ,CAAC,IAAI,qBAAqB;gBACtB,KAAK,CAAC,EAAE,KAAK;CAI1B;AAED,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,2EAA2E;IAC3E,YAAY,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,6EAA6E;IAC7E,eAAe,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,sEAAsE;IACtE,cAAc,IAAI,OAAO,CAAC;IAC1B,iFAAiF;IACjF,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAQxD"}
|
package/build/auth/sso-flow.js
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { PurdueSSOFlow } from "./purdue-sso.js";
|
|
7
7
|
import { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
|
|
8
|
+
import { BrowserAuthError } from "../utils/errors.js";
|
|
9
|
+
export class UnsupportedAuthenticationError extends BrowserAuthError {
|
|
10
|
+
code = "AUTH_UNSUPPORTED";
|
|
11
|
+
constructor(message, cause) {
|
|
12
|
+
super(message, "headless_login", cause);
|
|
13
|
+
this.name = "UnsupportedAuthenticationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class MfaApprovalError extends BrowserAuthError {
|
|
17
|
+
code = "AUTH_MFA_FAILED";
|
|
18
|
+
constructor(cause) {
|
|
19
|
+
super("MFA approval failed or timed out after 5 minutes. Run brightspace-auth to retry.", "mfa_approval", cause);
|
|
20
|
+
this.name = "MfaApprovalError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
/**
|
|
9
24
|
* Pick the login sequence for the configured Brightspace host. Schools whose
|
|
10
25
|
* identity provider needs extra steps get their own handler here; everything
|
|
@@ -12,7 +27,7 @@ import { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
|
|
|
12
27
|
* CAS, and Microsoft Entra forms.
|
|
13
28
|
*/
|
|
14
29
|
export function createSSOFlow(config) {
|
|
15
|
-
const credentials = { username: config.username, password: config.password };
|
|
30
|
+
const credentials = { username: config.username, password: config.password, baseUrl: config.baseUrl };
|
|
16
31
|
if (isSunyBrightspace(config.baseUrl)) {
|
|
17
32
|
return new SunySSOFlow({ ...credentials, campus: config.campus });
|
|
18
33
|
}
|
|
@@ -1 +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;
|
|
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;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,OAAO,8BAA+B,SAAQ,gBAAgB;IACzD,IAAI,GAAG,kBAAkB,CAAC;IACnC,YAAY,OAAe,EAAE,KAAa;QACxC,KAAK,CAAC,OAAO,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,gCAAgC,CAAC;IAC/C,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,gBAAgB;IAC3C,IAAI,GAAG,iBAAiB,CAAC;IAClC,YAAY,KAAa;QACvB,KAAK,CAAC,kFAAkF,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC;QACjH,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAcD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,MAAiB;IAC7C,MAAM,WAAW,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;IAEtG,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"}
|
package/build/auth/suny-sso.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Brightspace MCP Server
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
-
* Licensed under MIT
|
|
4
|
+
* Licensed under MIT : see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import type { Page } from "playwright";
|
|
7
7
|
interface SunySSOConfig {
|
|
@@ -26,8 +26,9 @@ export declare class SunySSOFlow {
|
|
|
26
26
|
private defaultFlow;
|
|
27
27
|
constructor(config: SunySSOConfig);
|
|
28
28
|
hasCredentials(): boolean;
|
|
29
|
+
prepareLogin(page: Page): Promise<void>;
|
|
30
|
+
identifyAccount(page: Page): Promise<boolean>;
|
|
29
31
|
login(page: Page): Promise<boolean>;
|
|
30
|
-
manualLogin(page: Page): Promise<boolean>;
|
|
31
32
|
/**
|
|
32
33
|
* Brightspace's own campus selector renders its buttons in a shadow DOM, so
|
|
33
34
|
* go straight to the SAML endpoint that selector would have reached.
|
|
@@ -1 +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;
|
|
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;AAevC,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;IASjC,cAAc,IAAI,OAAO;IAInB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvC,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7C,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAUzC;;;OAGG;YACW,cAAc;IAY5B;;;OAGG;YACW,YAAY;IAmC1B;;;;OAIG;IACH,OAAO,CAAC,UAAU;CAUnB"}
|
package/build/auth/suny-sso.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Brightspace MCP Server
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
-
* Licensed under MIT
|
|
4
|
+
* Licensed under MIT : see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import { PurdueSSOFlow } from "./purdue-sso.js";
|
|
7
7
|
import { log } from "../utils/logger.js";
|
|
8
|
+
import { UnsupportedAuthenticationError } from "./sso-flow.js";
|
|
8
9
|
/** SUNY campuses share one Brightspace tenant behind one Shibboleth IdP. */
|
|
9
10
|
const SUNY_BRIGHTSPACE_HOST = "mylearning.suny.edu";
|
|
10
11
|
const SUNY_IDP_ENTITY_ID = "https://idm.suny.edu/shibboleth/idp/";
|
|
@@ -51,34 +52,29 @@ export class SunySSOFlow {
|
|
|
51
52
|
this.defaultFlow = new PurdueSSOFlow({
|
|
52
53
|
username: config.username,
|
|
53
54
|
password: config.password,
|
|
55
|
+
baseUrl: `https://${SUNY_BRIGHTSPACE_HOST}`,
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
58
|
hasCredentials() {
|
|
57
59
|
return this.defaultFlow.hasCredentials();
|
|
58
60
|
}
|
|
61
|
+
async prepareLogin(page) {
|
|
62
|
+
await this.startSamlLogin(page);
|
|
63
|
+
await this.selectCampus(page);
|
|
64
|
+
}
|
|
65
|
+
async identifyAccount(page) {
|
|
66
|
+
return this.defaultFlow.identifyAccount(page);
|
|
67
|
+
}
|
|
59
68
|
async login(page) {
|
|
60
69
|
try {
|
|
61
70
|
await this.startSamlLogin(page);
|
|
62
71
|
await this.selectCampus(page);
|
|
63
72
|
}
|
|
64
73
|
catch (error) {
|
|
65
|
-
|
|
66
|
-
return false;
|
|
74
|
+
throw new UnsupportedAuthenticationError("SUNY campus selection could not complete headlessly. Run brightspace-mcp-server setup --suny and select a campus.", error);
|
|
67
75
|
}
|
|
68
76
|
return this.defaultFlow.login(page);
|
|
69
77
|
}
|
|
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
78
|
/**
|
|
83
79
|
* Brightspace's own campus selector renders its buttons in a shadow DOM, so
|
|
84
80
|
* go straight to the SAML endpoint that selector would have reached.
|
|
@@ -88,7 +84,7 @@ export class SunySSOFlow {
|
|
|
88
84
|
if (!currentUrl.includes("/d2l/login"))
|
|
89
85
|
return;
|
|
90
86
|
const origin = new URL(currentUrl).origin;
|
|
91
|
-
log("INFO", "Campus selector detected
|
|
87
|
+
log("INFO", "Campus selector detected : navigating directly to SUNY's SAML endpoint");
|
|
92
88
|
await page.goto(`${origin}/d2l/lp/auth/saml/initiate-login?entityId=${SUNY_IDP_ENTITY_ID}`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
93
89
|
}
|
|
94
90
|
/**
|
|
@@ -104,7 +100,7 @@ export class SunySSOFlow {
|
|
|
104
100
|
await page.waitForSelector(SELECTORS.campusSelect, { timeout: 45000 });
|
|
105
101
|
}
|
|
106
102
|
catch {
|
|
107
|
-
log("DEBUG", "No SUNY campus dropdown found
|
|
103
|
+
log("DEBUG", "No SUNY campus dropdown found : continuing to the sign-in form");
|
|
108
104
|
return;
|
|
109
105
|
}
|
|
110
106
|
const options = await page.$$eval(`${SELECTORS.campusSelect} option`, (nodes) => nodes
|
|
@@ -114,16 +110,11 @@ export class SunySSOFlow {
|
|
|
114
110
|
}))
|
|
115
111
|
.filter((option) => option.value !== ""));
|
|
116
112
|
if (!this.config.campus) {
|
|
117
|
-
|
|
118
|
-
log("INFO", "Set one with: brightspace-mcp-server setup --suny");
|
|
119
|
-
return;
|
|
113
|
+
throw new UnsupportedAuthenticationError("No SUNY campus configured. Run brightspace-mcp-server setup --suny.");
|
|
120
114
|
}
|
|
121
115
|
const match = this.findCampus(options);
|
|
122
116
|
if (!match) {
|
|
123
|
-
|
|
124
|
-
log("INFO", `Available: ${options.map((o) => o.label).join(", ")}`);
|
|
125
|
-
log("WARN", "Choose your campus in the browser window to continue.");
|
|
126
|
-
return;
|
|
117
|
+
throw new UnsupportedAuthenticationError(`Configured campus did not match SUNY. Available campuses: ${options.map((o) => o.label).join(", ")}`);
|
|
127
118
|
}
|
|
128
119
|
log("INFO", `Selecting campus ${match.label} (${match.value})`);
|
|
129
120
|
await page.selectOption(SELECTORS.campusSelect, match.value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suny-sso.js","sourceRoot":"","sources":["../../src/auth/suny-sso.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"suny-sso.js","sourceRoot":"","sources":["../../src/auth/suny-sso.ts"],"names":[],"mappings":"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"}
|