brightspace-mcp-server 1.6.3 → 3.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.
Files changed (127) hide show
  1. package/README.md +43 -35
  2. package/build/api/client.d.ts +37 -10
  3. package/build/api/client.d.ts.map +1 -1
  4. package/build/api/client.js +158 -100
  5. package/build/api/client.js.map +1 -1
  6. package/build/api/errors.d.ts +4 -0
  7. package/build/api/errors.d.ts.map +1 -1
  8. package/build/api/errors.js +7 -0
  9. package/build/api/errors.js.map +1 -1
  10. package/build/auth/auth-cooldown.d.ts +24 -0
  11. package/build/auth/auth-cooldown.d.ts.map +1 -0
  12. package/build/auth/auth-cooldown.js +60 -0
  13. package/build/auth/auth-cooldown.js.map +1 -0
  14. package/build/auth/auth-lock.d.ts +7 -0
  15. package/build/auth/auth-lock.d.ts.map +1 -0
  16. package/build/auth/auth-lock.js +162 -0
  17. package/build/auth/auth-lock.js.map +1 -0
  18. package/build/auth/auth-runner.d.ts +30 -9
  19. package/build/auth/auth-runner.d.ts.map +1 -1
  20. package/build/auth/auth-runner.js +171 -56
  21. package/build/auth/auth-runner.js.map +1 -1
  22. package/build/auth/browser-auth.d.ts +19 -87
  23. package/build/auth/browser-auth.d.ts.map +1 -1
  24. package/build/auth/browser-auth.js +266 -614
  25. package/build/auth/browser-auth.js.map +1 -1
  26. package/build/auth/browser-state-store.d.ts +19 -0
  27. package/build/auth/browser-state-store.d.ts.map +1 -0
  28. package/build/auth/browser-state-store.js +101 -0
  29. package/build/auth/browser-state-store.js.map +1 -0
  30. package/build/auth/credential-store.d.ts +29 -0
  31. package/build/auth/credential-store.d.ts.map +1 -0
  32. package/build/auth/credential-store.js +233 -0
  33. package/build/auth/credential-store.js.map +1 -0
  34. package/build/auth/encrypted-store.d.ts +25 -0
  35. package/build/auth/encrypted-store.d.ts.map +1 -0
  36. package/build/auth/encrypted-store.js +61 -0
  37. package/build/auth/encrypted-store.js.map +1 -0
  38. package/build/auth/legacy-profile.d.ts +3 -0
  39. package/build/auth/legacy-profile.d.ts.map +1 -0
  40. package/build/auth/legacy-profile.js +41 -0
  41. package/build/auth/legacy-profile.js.map +1 -0
  42. package/build/auth/legacy-state.d.ts +14 -0
  43. package/build/auth/legacy-state.d.ts.map +1 -0
  44. package/build/auth/legacy-state.js +107 -0
  45. package/build/auth/legacy-state.js.map +1 -0
  46. package/build/auth/purdue-sso.d.ts +25 -36
  47. package/build/auth/purdue-sso.d.ts.map +1 -1
  48. package/build/auth/purdue-sso.js +210 -191
  49. package/build/auth/purdue-sso.js.map +1 -1
  50. package/build/auth/session-store.d.ts +16 -38
  51. package/build/auth/session-store.d.ts.map +1 -1
  52. package/build/auth/session-store.js +157 -152
  53. package/build/auth/session-store.js.map +1 -1
  54. package/build/auth/sso-flow.d.ts +16 -4
  55. package/build/auth/sso-flow.d.ts.map +1 -1
  56. package/build/auth/sso-flow.js +24 -2
  57. package/build/auth/sso-flow.js.map +1 -1
  58. package/build/auth/suny-sso.d.ts +6 -2
  59. package/build/auth/suny-sso.d.ts.map +1 -1
  60. package/build/auth/suny-sso.js +17 -24
  61. package/build/auth/suny-sso.js.map +1 -1
  62. package/build/auth/token-manager.d.ts +10 -3
  63. package/build/auth/token-manager.d.ts.map +1 -1
  64. package/build/auth/token-manager.js +62 -26
  65. package/build/auth/token-manager.js.map +1 -1
  66. package/build/auth/token-mint.d.ts +2 -2
  67. package/build/auth/token-mint.d.ts.map +1 -1
  68. package/build/auth/token-mint.js +31 -9
  69. package/build/auth/token-mint.js.map +1 -1
  70. package/build/auth-cli.d.ts +1 -7
  71. package/build/auth-cli.d.ts.map +1 -1
  72. package/build/auth-cli.js +71 -70
  73. package/build/auth-cli.js.map +1 -1
  74. package/build/index.js +20 -64
  75. package/build/index.js.map +1 -1
  76. package/build/setup.js +44 -19
  77. package/build/setup.js.map +1 -1
  78. package/build/tools/tool-helpers.d.ts +7 -1
  79. package/build/tools/tool-helpers.d.ts.map +1 -1
  80. package/build/tools/tool-helpers.js +96 -11
  81. package/build/tools/tool-helpers.js.map +1 -1
  82. package/build/types/index.d.ts +6 -0
  83. package/build/types/index.d.ts.map +1 -1
  84. package/build/utils/browser-install.d.ts +23 -0
  85. package/build/utils/browser-install.d.ts.map +1 -0
  86. package/build/utils/browser-install.js +33 -0
  87. package/build/utils/browser-install.js.map +1 -0
  88. package/build/utils/commands.d.ts +26 -0
  89. package/build/utils/commands.d.ts.map +1 -0
  90. package/build/utils/commands.js +26 -0
  91. package/build/utils/commands.js.map +1 -0
  92. package/build/utils/config-store.d.ts +1 -0
  93. package/build/utils/config-store.d.ts.map +1 -1
  94. package/build/utils/config-store.js +3 -0
  95. package/build/utils/config-store.js.map +1 -1
  96. package/build/utils/config.d.ts +2 -1
  97. package/build/utils/config.d.ts.map +1 -1
  98. package/build/utils/config.js +32 -10
  99. package/build/utils/config.js.map +1 -1
  100. package/build/utils/download-errors.d.ts +45 -0
  101. package/build/utils/download-errors.d.ts.map +1 -0
  102. package/build/utils/download-errors.js +45 -0
  103. package/build/utils/download-errors.js.map +1 -0
  104. package/build/utils/download-helpers.d.ts.map +1 -1
  105. package/build/utils/download-helpers.js +2 -1
  106. package/build/utils/download-helpers.js.map +1 -1
  107. package/build/utils/file-validator.d.ts +1 -1
  108. package/build/utils/file-validator.d.ts.map +1 -1
  109. package/build/utils/file-validator.js +47 -7
  110. package/build/utils/file-validator.js.map +1 -1
  111. package/build/utils/install-sites.d.ts +61 -0
  112. package/build/utils/install-sites.d.ts.map +1 -0
  113. package/build/utils/install-sites.js +207 -0
  114. package/build/utils/install-sites.js.map +1 -0
  115. package/build/utils/secure-config.d.ts +6 -0
  116. package/build/utils/secure-config.d.ts.map +1 -0
  117. package/build/utils/secure-config.js +57 -0
  118. package/build/utils/secure-config.js.map +1 -0
  119. package/build/utils/self-update.d.ts +61 -0
  120. package/build/utils/self-update.d.ts.map +1 -0
  121. package/build/utils/self-update.js +92 -0
  122. package/build/utils/self-update.js.map +1 -0
  123. package/build/utils/update-checker.d.ts +85 -7
  124. package/build/utils/update-checker.d.ts.map +1 -1
  125. package/build/utils/update-checker.js +141 -36
  126. package/build/utils/update-checker.js.map +1 -1
  127. package/package.json +4 -2
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
3
+ *
4
+ * Recognising a missing Playwright browser.
5
+ *
6
+ * This package's postinstall downloads Chromium. npm skips postinstall under
7
+ * `--ignore-scripts` and under the newer allow-scripts policy, both of which
8
+ * are common on global installs, so the package can land without a browser and
9
+ * fail only later, at first sign-in.
10
+ *
11
+ * Playwright's own message does say what to run, but by the time it has been
12
+ * wrapped in BrowserAuthError and then flattened into a generic
13
+ * "Authentication failed" by the auth runner, the actionable command is gone.
14
+ * Matching the error lets us keep the fix attached to the failure.
15
+ *
16
+ * Matching reactively rather than probing for the executable up front keeps us
17
+ * off Playwright's internals, and means no false positives: this only ever
18
+ * fires after a launch has genuinely failed.
19
+ */
20
+ import { CLEAR_NPX_CACHE_COMMAND } from "./commands.js";
21
+ export const PLAYWRIGHT_INSTALL_HINT = "Chromium is not installed, so sign-in cannot start. Your package manager " +
22
+ "skipped this package's postinstall script, which is what normally downloads it " +
23
+ "(common with --ignore-scripts or an allow-scripts policy). Run: " +
24
+ "npx playwright install chromium" +
25
+ `. If that does not help, reinstall and clear caches with ${CLEAR_NPX_CACHE_COMMAND}.`;
26
+ /** True when an error is Playwright reporting that no browser binary exists. */
27
+ export function isMissingBrowserError(error) {
28
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
29
+ if (!message)
30
+ return false;
31
+ return /Executable doesn't exist|playwright install|Failed to launch.*because executable doesn't exist/i.test(message);
32
+ }
33
+ //# sourceMappingURL=browser-install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-install.js","sourceRoot":"","sources":["../../src/utils/browser-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAExD,MAAM,CAAC,MAAM,uBAAuB,GAClC,2EAA2E;IAC3E,iFAAiF;IACjF,kEAAkE;IAClE,iCAAiC;IACjC,4DAA4D,uBAAuB,GAAG,CAAC;AAEzF,gFAAgF;AAChF,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,MAAM,OAAO,GACX,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAClF,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,OAAO,iGAAiG,CAAC,IAAI,CAC3G,OAAO,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
3
+ *
4
+ * Canonical command strings shown to users.
5
+ *
6
+ * Every npx command here is pinned to @latest deliberately. Invoking npx with
7
+ * a bare package name, no version tag, prefers a binary already on PATH, so on
8
+ * a machine with an old global install it silently runs that stale copy
9
+ * instead of fetching the current release. That is exactly how a healthy
10
+ * v2.0.0 server ended up printing instructions that executed v1.2.6, whose
11
+ * sign-in flow no longer worked. Pinning makes every printed command
12
+ * self-updating.
13
+ *
14
+ * Import these instead of writing the command inline. A release test greps the
15
+ * source for untagged forms and fails the build if one reappears.
16
+ */
17
+ export declare const PACKAGE_NAME = "brightspace-mcp-server";
18
+ /** Re-authenticate. Always runs the current published release. */
19
+ export declare const AUTH_COMMAND = "npx -y brightspace-mcp-server@latest auth";
20
+ /** Re-run the setup wizard. Always runs the current published release. */
21
+ export declare const SETUP_COMMAND = "npx -y brightspace-mcp-server@latest setup";
22
+ /** Bring a global install up to date. The only fix for a stale `npm i -g` copy. */
23
+ export declare const GLOBAL_INSTALL_COMMAND = "npm install -g brightspace-mcp-server@latest";
24
+ /** Drop cached npx copies so the next `@latest` genuinely refetches. */
25
+ export declare const CLEAR_NPX_CACHE_COMMAND = "npx clear-npx-cache";
26
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/utils/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,YAAY,2BAA2B,CAAC;AAErD,kEAAkE;AAClE,eAAO,MAAM,YAAY,8CAAuC,CAAC;AAEjE,0EAA0E;AAC1E,eAAO,MAAM,aAAa,+CAAwC,CAAC;AAEnE,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,iDAA0C,CAAC;AAE9E,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,wBAAwB,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
3
+ *
4
+ * Canonical command strings shown to users.
5
+ *
6
+ * Every npx command here is pinned to @latest deliberately. Invoking npx with
7
+ * a bare package name, no version tag, prefers a binary already on PATH, so on
8
+ * a machine with an old global install it silently runs that stale copy
9
+ * instead of fetching the current release. That is exactly how a healthy
10
+ * v2.0.0 server ended up printing instructions that executed v1.2.6, whose
11
+ * sign-in flow no longer worked. Pinning makes every printed command
12
+ * self-updating.
13
+ *
14
+ * Import these instead of writing the command inline. A release test greps the
15
+ * source for untagged forms and fails the build if one reappears.
16
+ */
17
+ export const PACKAGE_NAME = "brightspace-mcp-server";
18
+ /** Re-authenticate. Always runs the current published release. */
19
+ export const AUTH_COMMAND = `npx -y ${PACKAGE_NAME}@latest auth`;
20
+ /** Re-run the setup wizard. Always runs the current published release. */
21
+ export const SETUP_COMMAND = `npx -y ${PACKAGE_NAME}@latest setup`;
22
+ /** Bring a global install up to date. The only fix for a stale `npm i -g` copy. */
23
+ export const GLOBAL_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}@latest`;
24
+ /** Drop cached npx copies so the next `@latest` genuinely refetches. */
25
+ export const CLEAR_NPX_CACHE_COMMAND = "npx clear-npx-cache";
26
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/utils/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAErD,kEAAkE;AAClE,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,YAAY,cAAc,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,YAAY,eAAe,CAAC;AAEnE,mFAAmF;AACnF,MAAM,CAAC,MAAM,sBAAsB,GAAG,kBAAkB,YAAY,SAAS,CAAC;AAE9E,wEAAwE;AACxE,MAAM,CAAC,MAAM,uBAAuB,GAAG,qBAAqB,CAAC"}
@@ -7,6 +7,7 @@
7
7
  export interface ConfigStoreData {
8
8
  baseUrl?: string;
9
9
  username?: string;
10
+ /** Read only for migration from v1. Never written by saveConfigStore. */
10
11
  password?: string;
11
12
  campus?: string;
12
13
  sessionDir?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../../src/utils/config-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAKD,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,wBAAgB,eAAe,IAAI,eAAe,CAGjD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAU7D;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
1
+ {"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../../src/utils/config-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAKD,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,wBAAgB,eAAe,IAAI,eAAe,CAGjD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAa7D;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
@@ -17,6 +17,9 @@ export function loadConfigStore() {
17
17
  return JSON.parse(raw);
18
18
  }
19
19
  export function saveConfigStore(config) {
20
+ if (config.password !== undefined) {
21
+ throw new Error("Passwords must be saved in the native credential store before saving configuration.");
22
+ }
20
23
  const isWindows = process.platform === "win32";
21
24
  if (!fs.existsSync(CONFIG_DIR)) {
22
25
  fs.mkdirSync(CONFIG_DIR, { recursive: true, ...(isWindows ? {} : { mode: 0o700 }) });
@@ -1 +1 @@
1
- {"version":3,"file":"config-store.js","sourceRoot":"","sources":["../../src/utils/config-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAgBxD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,kBAAkB,CAAC,CAAC;AAC/D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEzD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,mBAAmB,CACjB,WAAW,EACX,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EACtC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CACjC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"config-store.js","sourceRoot":"","sources":["../../src/utils/config-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAiBxD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,kBAAkB,CAAC,CAAC;AAC/D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEzD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,qFAAqF,CAAC,CAAC;IACzG,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,mBAAmB,CACjB,WAAW,EACX,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EACtC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CACjC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -4,6 +4,7 @@
4
4
  * Licensed under MIT — see LICENSE file for details.
5
5
  */
6
6
  import type { AppConfig } from "../types/index.js";
7
- export declare function loadConfig(): AppConfig;
7
+ export declare function loadConfig(): Promise<AppConfig>;
8
+ export declare function accountSessionDirectory(root: string, baseUrl: string, username?: string): string;
8
9
  export type { AppConfig };
9
10
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,wBAAgB,UAAU,IAAI,SAAS,CAyDtC;AASD,YAAY,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAKnD,wBAAsB,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAsErD;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAIhG;AASD,YAAY,EAAE,SAAS,EAAE,CAAC"}
@@ -5,8 +5,13 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import * as os from "node:os";
8
+ import { createHash } from "node:crypto";
9
+ import dotenv from "dotenv";
8
10
  import { configStoreExists, loadConfigStore } from "./config-store.js";
9
- export function loadConfig() {
11
+ import { resolveStoredPassword } from "./secure-config.js";
12
+ import { migrateLegacyState } from "../auth/legacy-state.js";
13
+ export async function loadConfig() {
14
+ dotenv.config({ quiet: true });
10
15
  const store = configStoreExists() ? loadConfigStore() : null;
11
16
  if (store) {
12
17
  console.error("[config] Loaded base config from ~/.brightspace-mcp/config.json");
@@ -15,16 +20,15 @@ export function loadConfig() {
15
20
  console.error("[config] No config.json found, using environment variables");
16
21
  }
17
22
  // Resolve sessionDir: env > store > default
18
- const sessionDir = process.env.D2L_SESSION_DIR
23
+ const sessionRoot = process.env.D2L_SESSION_DIR
19
24
  ? expandTilde(process.env.D2L_SESSION_DIR)
20
25
  : store?.sessionDir
21
26
  ? expandTilde(store.sessionDir)
22
27
  : path.join(os.homedir(), ".d2l-session");
23
- // Resolve headless: env > store > default (false)
24
- let headless = store?.headless ?? false;
25
- if (process.env.D2L_HEADLESS !== undefined) {
26
- headless = process.env.D2L_HEADLESS === "true";
27
- }
28
+ // Code-entry and other interactive MFA methods need a visible browser.
29
+ const headless = process.env.D2L_HEADLESS !== undefined
30
+ ? process.env.D2L_HEADLESS !== "false"
31
+ : store?.headless ?? true;
28
32
  // Resolve tokenTtl: env > store > default (3600)
29
33
  const tokenTtl = process.env.D2L_TOKEN_TTL
30
34
  ? parseInt(process.env.D2L_TOKEN_TTL, 10)
@@ -42,13 +46,25 @@ export function loadConfig() {
42
46
  if (process.env.D2L_ACTIVE_ONLY !== undefined) {
43
47
  activeOnly = process.env.D2L_ACTIVE_ONLY !== 'false';
44
48
  }
49
+ const configuredUrl = new URL(process.env.D2L_BASE_URL || store?.baseUrl || "https://purdue.brightspace.com");
50
+ if (configuredUrl.protocol !== "https:" || configuredUrl.username || configuredUrl.password) {
51
+ throw new Error("The Brightspace URL must be an HTTPS school URL without embedded credentials.");
52
+ }
53
+ const baseUrl = configuredUrl.origin;
54
+ const username = process.env.D2L_USERNAME || store?.username;
55
+ const password = await resolveStoredPassword(baseUrl, username, store);
56
+ // A new account must never inherit another account's cookies, even at the same school.
57
+ const sessionDir = accountSessionDirectory(sessionRoot, baseUrl, username);
58
+ const legacyMigration = sessionDir !== sessionRoot ? await migrateLegacyState(sessionRoot) : undefined;
45
59
  return {
46
- baseUrl: process.env.D2L_BASE_URL || store?.baseUrl || "https://purdue.brightspace.com",
60
+ baseUrl,
47
61
  sessionDir,
62
+ sessionRoot,
63
+ legacyBrowserStateMigrated: legacyMigration?.browserState === "encrypted",
48
64
  tokenTtl,
49
65
  headless,
50
- username: process.env.D2L_USERNAME || store?.username,
51
- password: process.env.D2L_PASSWORD || store?.password,
66
+ username,
67
+ password,
52
68
  campus: process.env.D2L_CAMPUS || store?.campus,
53
69
  courseFilter: {
54
70
  includeCourseIds,
@@ -57,6 +73,12 @@ export function loadConfig() {
57
73
  },
58
74
  };
59
75
  }
76
+ export function accountSessionDirectory(root, baseUrl, username) {
77
+ if (!username)
78
+ return root;
79
+ const account = createHash("sha256").update(JSON.stringify([new URL(baseUrl).origin, username])).digest("hex");
80
+ return path.join(root, "accounts", account);
81
+ }
60
82
  function expandTilde(filePath) {
61
83
  if (filePath.startsWith("~")) {
62
84
  return path.join(os.homedir(), filePath.slice(1));
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Purdue Brightspace MCP Server
3
+ * Copyright (c) 2026 Rohan Muppa. All rights reserved.
4
+ * Licensed under MIT — see LICENSE file for details.
5
+ *
6
+ * Typed download failures.
7
+ *
8
+ * A download that failed validation used to throw a plain Error, which
9
+ * sanitizeError has no branch for, so every one of them reached the user as
10
+ * "An unexpected error occurred. Please try again." That is the least useful
11
+ * sentence available, and it hid a real bug for as long as the tool has
12
+ * existed: no legacy Office file could ever be downloaded.
13
+ *
14
+ * The guidance keyed off `kind` follows the same contract as the auth table:
15
+ * the text is chosen from a closed set this package controls, never built from
16
+ * a caught message. That matters here more than on the auth path, because a
17
+ * download error message can embed a MIME type and a filename that both
18
+ * originate from remote HTTP headers or remote file bytes.
19
+ */
20
+ /** Closed set of ways a download can fail before the bytes reach disk. */
21
+ export type DownloadFailureKind = "unsupportedType" | "undetectableType" | "badFilename" | "pathTraversal";
22
+ export declare class DownloadError extends Error {
23
+ readonly kind: DownloadFailureKind;
24
+ /**
25
+ * One short fact worth showing the user, currently only a detected MIME
26
+ * type. Rendered only after isSafeDetail approves it, so a crafted
27
+ * Content-Disposition header or file body cannot put prose in a response.
28
+ */
29
+ readonly detail?: string | undefined;
30
+ constructor(kind: DownloadFailureKind, message: string,
31
+ /**
32
+ * One short fact worth showing the user, currently only a detected MIME
33
+ * type. Rendered only after isSafeDetail approves it, so a crafted
34
+ * Content-Disposition header or file body cannot put prose in a response.
35
+ */
36
+ detail?: string | undefined);
37
+ }
38
+ /**
39
+ * True only for something shaped like a bare MIME type.
40
+ *
41
+ * This is the narrowest shape that still answers "what actually went wrong",
42
+ * and it admits no spaces, so no free text, path, or response body can pass.
43
+ */
44
+ export declare function isSafeDetail(value: string): boolean;
45
+ //# sourceMappingURL=download-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download-errors.d.ts","sourceRoot":"","sources":["../../src/utils/download-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,MAAM,MAAM,mBAAmB,GAC3B,iBAAiB,GACjB,kBAAkB,GAClB,aAAa,GACb,eAAe,CAAC;AAEpB,qBAAa,aAAc,SAAQ,KAAK;aAEpB,IAAI,EAAE,mBAAmB;IAEzC;;;;OAIG;aACa,MAAM,CAAC,EAAE,MAAM;gBAPf,IAAI,EAAE,mBAAmB,EACzC,OAAO,EAAE,MAAM;IACf;;;;OAIG;IACa,MAAM,CAAC,EAAE,MAAM,YAAA;CAKlC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Purdue Brightspace MCP Server
3
+ * Copyright (c) 2026 Rohan Muppa. All rights reserved.
4
+ * Licensed under MIT — see LICENSE file for details.
5
+ *
6
+ * Typed download failures.
7
+ *
8
+ * A download that failed validation used to throw a plain Error, which
9
+ * sanitizeError has no branch for, so every one of them reached the user as
10
+ * "An unexpected error occurred. Please try again." That is the least useful
11
+ * sentence available, and it hid a real bug for as long as the tool has
12
+ * existed: no legacy Office file could ever be downloaded.
13
+ *
14
+ * The guidance keyed off `kind` follows the same contract as the auth table:
15
+ * the text is chosen from a closed set this package controls, never built from
16
+ * a caught message. That matters here more than on the auth path, because a
17
+ * download error message can embed a MIME type and a filename that both
18
+ * originate from remote HTTP headers or remote file bytes.
19
+ */
20
+ export class DownloadError extends Error {
21
+ kind;
22
+ detail;
23
+ constructor(kind, message,
24
+ /**
25
+ * One short fact worth showing the user, currently only a detected MIME
26
+ * type. Rendered only after isSafeDetail approves it, so a crafted
27
+ * Content-Disposition header or file body cannot put prose in a response.
28
+ */
29
+ detail) {
30
+ super(message);
31
+ this.kind = kind;
32
+ this.detail = detail;
33
+ this.name = "DownloadError";
34
+ }
35
+ }
36
+ /**
37
+ * True only for something shaped like a bare MIME type.
38
+ *
39
+ * This is the narrowest shape that still answers "what actually went wrong",
40
+ * and it admits no spaces, so no free text, path, or response body can pass.
41
+ */
42
+ export function isSafeDetail(value) {
43
+ return value.length <= 64 && /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/.test(value);
44
+ }
45
+ //# sourceMappingURL=download-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download-errors.js","sourceRoot":"","sources":["../../src/utils/download-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,MAAM,OAAO,aAAc,SAAQ,KAAK;IAEpB;IAOA;IARlB,YACkB,IAAyB,EACzC,OAAe;IACf;;;;OAIG;IACa,MAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QATC,SAAI,GAAJ,IAAI,CAAqB;QAOzB,WAAM,GAAN,MAAM,CAAS;QAG/B,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,KAAK,CAAC,MAAM,IAAI,EAAE,IAAI,8CAA8C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC1F,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"download-helpers.d.ts","sourceRoot":"","sources":["../../src/utils/download-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CA8BjB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAiDxD"}
1
+ {"version":3,"file":"download-helpers.d.ts","sourceRoot":"","sources":["../../src/utils/download-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CA8BjB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkDxD"}
@@ -72,7 +72,8 @@ export async function secureDownload(options) {
72
72
  }
73
73
  log("DEBUG", `secureDownload: file size ${size} bytes (within limit)`);
74
74
  // Validate file type via magic bytes
75
- const { mime } = await validateFileType(data, allowedTypes);
75
+ // The filename decides which legacy Office format a CFB container is.
76
+ const { mime } = await validateFileType(data, allowedTypes, filename);
76
77
  log("DEBUG", `secureDownload: file type validated as ${mime}`);
77
78
  // Validate download path (prevent path traversal)
78
79
  const validatedPath = validateDownloadPath(targetDir, filename);
@@ -1 +1 @@
1
- {"version":3,"file":"download-helpers.js","sourceRoot":"","sources":["../../src/utils/download-helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"download-helpers.js","sourceRoot":"","sources":["../../src/utils/download-helpers.ts"],"names":[],"mappings":"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"}
@@ -31,7 +31,7 @@ export declare function validateDownloadPath(baseDir: string, filename: string):
31
31
  * @returns Detected MIME type and extension
32
32
  * @throws Error if file type not allowed
33
33
  */
34
- export declare function validateFileType(buffer: Buffer, allowedTypes?: string[]): Promise<{
34
+ export declare function validateFileType(buffer: Buffer, allowedTypes?: string[], filename?: string): Promise<{
35
35
  mime: string;
36
36
  ext: string;
37
37
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"file-validator.d.ts","sourceRoot":"","sources":["../../src/utils/file-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAmB,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,EA4BtC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAwBR;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,MAAM,EAAuB,GAC1C,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCxC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAQrD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAM1E"}
1
+ {"version":3,"file":"file-validator.d.ts","sourceRoot":"","sources":["../../src/utils/file-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA+BH;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAmB,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,EA4BtC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CA+BR;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,MAAM,EAAuB,EAC3C,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAwDxC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAQrD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAM1E"}
@@ -6,6 +6,30 @@
6
6
  import path from "node:path";
7
7
  import { fileTypeFromBuffer } from "file-type";
8
8
  import sanitizeFilename from "sanitize-filename";
9
+ import { DownloadError } from "./download-errors.js";
10
+ /**
11
+ * file-type reports every OLE2 Compound File Binary container as
12
+ * application/x-cfb. It does not read the CFB directory, so it cannot tell a
13
+ * .doc from a .xls from an .msi. The three legacy Office entries in
14
+ * ALLOWED_MIME_TYPES were therefore unreachable and every legacy Office
15
+ * download failed with a generic error.
16
+ *
17
+ * Allowing application/x-cfb outright is not the fix: .msi installers are the
18
+ * same container, and letting one through under a .doc name is exactly what a
19
+ * magic-byte allowlist exists to prevent. Reconcile against the declared
20
+ * extension instead, which admits the formats the allowlist already intended
21
+ * and still refuses everything else.
22
+ */
23
+ const CFB_MIME = "application/x-cfb";
24
+ const CFB_EXTENSION_MIMES = {
25
+ ".doc": "application/msword",
26
+ ".dot": "application/msword",
27
+ ".xls": "application/vnd.ms-excel",
28
+ ".xlt": "application/vnd.ms-excel",
29
+ ".ppt": "application/vnd.ms-powerpoint",
30
+ ".pot": "application/vnd.ms-powerpoint",
31
+ ".pps": "application/vnd.ms-powerpoint",
32
+ };
9
33
  /**
10
34
  * Maximum file size for downloads (50 MB).
11
35
  * Prevents memory exhaustion from malicious large file requests.
@@ -53,12 +77,20 @@ export const ALLOWED_MIME_TYPES = [
53
77
  * @throws Error if path traversal detected
54
78
  */
55
79
  export function validateDownloadPath(baseDir, filename) {
56
- // Decode URL-encoded characters
57
- const decoded = decodeURIComponent(filename);
80
+ // A filename carrying a bare '%' (say "100% Final.doc") makes
81
+ // decodeURIComponent throw URIError. Brightspace supplies these names, so
82
+ // that is remote input, and the raw name is the right fallback.
83
+ let decoded;
84
+ try {
85
+ decoded = decodeURIComponent(filename);
86
+ }
87
+ catch {
88
+ decoded = filename;
89
+ }
58
90
  // Sanitize filename (removes path separators, null bytes, etc.)
59
91
  const sanitized = sanitizeFilename(decoded);
60
92
  if (!sanitized || sanitized.length === 0) {
61
- throw new Error("Invalid filename after sanitization");
93
+ throw new DownloadError("badFilename", "Invalid filename after sanitization");
62
94
  }
63
95
  // Resolve full path
64
96
  const fullPath = path.resolve(baseDir, sanitized);
@@ -66,7 +98,7 @@ export function validateDownloadPath(baseDir, filename) {
66
98
  // Verify resolved path is within base directory
67
99
  if (!fullPath.startsWith(resolvedBase + path.sep) &&
68
100
  fullPath !== resolvedBase) {
69
- throw new Error("Path traversal detected");
101
+ throw new DownloadError("pathTraversal", "Path traversal detected");
70
102
  }
71
103
  return fullPath;
72
104
  }
@@ -79,12 +111,20 @@ export function validateDownloadPath(baseDir, filename) {
79
111
  * @returns Detected MIME type and extension
80
112
  * @throws Error if file type not allowed
81
113
  */
82
- export async function validateFileType(buffer, allowedTypes = ALLOWED_MIME_TYPES) {
114
+ export async function validateFileType(buffer, allowedTypes = ALLOWED_MIME_TYPES, filename) {
83
115
  // Try magic byte detection first
84
116
  const detected = await fileTypeFromBuffer(buffer);
85
117
  if (detected) {
118
+ if (detected.mime === CFB_MIME) {
119
+ const ext = filename ? path.extname(filename).toLowerCase() : "";
120
+ const resolved = CFB_EXTENSION_MIMES[ext];
121
+ if (resolved && allowedTypes.includes(resolved)) {
122
+ return { mime: resolved, ext: ext.slice(1) };
123
+ }
124
+ throw new DownloadError("unsupportedType", `Compound File Binary with extension '${ext || "none"}' is not an allowed Office format`, CFB_MIME);
125
+ }
86
126
  if (!allowedTypes.includes(detected.mime)) {
87
- throw new Error(`File type '${detected.mime}' not allowed. Allowed types: ${allowedTypes.join(", ")}`);
127
+ throw new DownloadError("unsupportedType", `File type '${detected.mime}' not allowed`, detected.mime);
88
128
  }
89
129
  return { mime: detected.mime, ext: detected.ext };
90
130
  }
@@ -109,7 +149,7 @@ export async function validateFileType(buffer, allowedTypes = ALLOWED_MIME_TYPES
109
149
  }
110
150
  }
111
151
  }
112
- throw new Error("Could not determine file type or type not allowed");
152
+ throw new DownloadError("undetectableType", "Could not determine file type or type not allowed");
113
153
  }
114
154
  /**
115
155
  * Validate content ID is a positive integer.
@@ -1 +1 @@
1
- {"version":3,"file":"file-validator.js","sourceRoot":"","sources":["../../src/utils/file-validator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-validator.js","sourceRoot":"","sources":["../../src/utils/file-validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
3
+ *
4
+ * Find every copy of this package on the machine and report disagreement.
5
+ *
6
+ * A single machine can easily hold several copies at different versions: one
7
+ * per Node version under a version manager, one per npx cache entry, plus a
8
+ * local checkout. They are installed at different times and nothing keeps them
9
+ * in step. The failure that motivated this had a v2.0.0 MCP server running
10
+ * happily while the auth command typed in a shell resolved to v1.2.6, whose
11
+ * sign-in flow no longer worked, with no way to see the mismatch.
12
+ *
13
+ * This scan needs no network, so unlike the registry check it still works
14
+ * offline, and it catches the problem before anything fails rather than after.
15
+ *
16
+ * It reads package.json files and resolves symlinks. It never spawns a
17
+ * process, never runs npm, and never writes anything.
18
+ */
19
+ export interface InstallSite {
20
+ kind: "global" | "npx-cache" | "self";
21
+ /** The package directory, i.e. .../node_modules/brightspace-mcp-server */
22
+ dir: string;
23
+ version: string | null;
24
+ /** The PATH entry that resolves here, when a shell command points at it. */
25
+ binPath?: string;
26
+ isSelf: boolean;
27
+ }
28
+ export interface InstallScanDeps {
29
+ env?: NodeJS.ProcessEnv;
30
+ platform?: NodeJS.Platform;
31
+ execPath?: string;
32
+ home?: string;
33
+ /** The running package directory, excluded from "other copies". */
34
+ selfDir?: string;
35
+ readVersion?: (packageJsonPath: string) => Promise<string | null>;
36
+ listDir?: (dir: string) => Promise<string[]>;
37
+ realpath?: (path: string) => Promise<string>;
38
+ }
39
+ /**
40
+ * Every copy of this package we can find, including the running one.
41
+ * Never throws; anything unreadable is simply omitted.
42
+ */
43
+ export declare function scanInstallSites(deps?: InstallScanDeps): Promise<InstallSite[]>;
44
+ /**
45
+ * Scan and format in one step. Returns null when there is nothing to say, and
46
+ * never throws, so callers can treat it as advisory.
47
+ *
48
+ * Honors D2L_NO_UPDATE_CHECK, which switches off every version-related notice.
49
+ */
50
+ export declare function detectSkew(runningVersion: string, deps?: InstallScanDeps): Promise<string | null>;
51
+ /**
52
+ * A warning about copies that disagree with the running version, or null when
53
+ * there is nothing worth saying.
54
+ *
55
+ * Deliberately quiet: it only speaks when another copy exists at a *different*
56
+ * version, since a machine with several copies all in step is fine. Copies a
57
+ * shell command actually resolves to are reported first, because those are the
58
+ * ones a user can trip over.
59
+ */
60
+ export declare function formatSkewNotice(sites: InstallSite[], runningVersion: string, home?: string): string | null;
61
+ //# sourceMappingURL=install-sites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-sites.d.ts","sourceRoot":"","sources":["../../src/utils/install-sites.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAaH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;IACtC,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9C;AAgFD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAsEzF;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,cAAc,EAAE,MAAM,EACtB,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAAE,EACpB,cAAc,EAAE,MAAM,EACtB,IAAI,GAAE,MAAkB,GACvB,MAAM,GAAG,IAAI,CA4Bf"}