oauth-callback 2.1.1 → 2.2.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.
@@ -18,34 +18,30 @@ export interface ClientInfo {
18
18
  clientIdIssuedAt?: number;
19
19
  clientSecretExpiresAt?: number;
20
20
  }
21
- /**
22
- * Active OAuth flow state for crash recovery.
23
- * Preserves PKCE verifier and state across process restarts.
24
- */
25
- export interface OAuthSession {
26
- codeVerifier?: string;
27
- state?: string;
28
- }
29
21
  /**
30
22
  * Minimal storage interface for OAuth tokens.
31
- * @invariant Implementations must be thread-safe within process.
32
23
  * @invariant Keys are scoped to avoid collisions between multiple OAuth flows.
33
24
  */
34
25
  export interface TokenStore {
35
26
  get(key: string): Promise<Tokens | null>;
36
27
  set(key: string, tokens: Tokens): Promise<void>;
37
28
  delete(key: string): Promise<void>;
38
- clear(): Promise<void>;
39
29
  }
30
+ /** Brand symbol for OAuthStore type detection. */
31
+ export declare const OAuthStoreBrand: unique symbol;
40
32
  /**
41
- * Full OAuth state storage including client registration and session.
42
- * Enables recovery from crashes mid-flow and reuse of dynamic registration.
33
+ * Extended storage with client registration and PKCE verifier persistence.
34
+ * Enables crash recovery mid-flow and reuse of dynamic registration.
35
+ * @invariant Implementations must include `[OAuthStoreBrand]: true` property.
43
36
  */
44
37
  export interface OAuthStore extends TokenStore {
38
+ readonly [OAuthStoreBrand]: true;
45
39
  getClient(key: string): Promise<ClientInfo | null>;
46
40
  setClient(key: string, client: ClientInfo): Promise<void>;
47
- getSession(key: string): Promise<OAuthSession | null>;
48
- setSession(key: string, session: OAuthSession): Promise<void>;
41
+ deleteClient(key: string): Promise<void>;
42
+ getCodeVerifier(key: string): Promise<string | null>;
43
+ setCodeVerifier(key: string, verifier: string): Promise<void>;
44
+ deleteCodeVerifier(key: string): Promise<void>;
49
45
  }
50
46
  /**
51
47
  * Configuration for browser-based OAuth flows with MCP servers.
@@ -53,8 +49,16 @@ export interface OAuthStore extends TokenStore {
53
49
  * @see https://datatracker.ietf.org/doc/html/rfc8252
54
50
  */
55
51
  export interface BrowserAuthOptions {
56
- /** Pre-registered OAuth client credentials. Omit for dynamic registration. */
52
+ /**
53
+ * Pre-registered OAuth client ID. Omit to use dynamic client registration.
54
+ * When provided, takes precedence over any DCR-obtained client.
55
+ */
57
56
  clientId?: string;
57
+ /**
58
+ * Pre-registered client secret (for confidential clients).
59
+ * Determines auth method for token requests: `client_secret_post` if set, `none` otherwise.
60
+ * This is fixed at construction - DCR-obtained secrets don't change the auth method.
61
+ */
58
62
  clientSecret?: string;
59
63
  scope?: string;
60
64
  /** Local callback server config. Must match redirect_uri in client registration. */
@@ -72,5 +76,11 @@ export interface BrowserAuthOptions {
72
76
  errorHtml?: string;
73
77
  /** Request inspection callback for debugging OAuth flows. */
74
78
  onRequest?: (req: Request) => void;
79
+ /**
80
+ * Authorization server base URL (issuer) for token endpoint discovery.
81
+ * Pass the origin (e.g., `https://auth.example.com`), not `/token`.
82
+ * Defaults to the authorization URL origin. Discovery failures are non-fatal.
83
+ */
84
+ authServerUrl?: string | URL;
75
85
  }
76
86
  //# sourceMappingURL=mcp-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-types.d.ts","sourceRoot":"","sources":["../src/mcp-types.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACtD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;4FACwF;IACxF,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAElC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC"}
1
+ {"version":3,"file":"mcp-types.d.ts","sourceRoot":"","sources":["../src/mcp-types.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe,EAAE,OAAO,MAA6B,CAAC;AAEnE;;;;GAIG;AACH,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,QAAQ,CAAC,CAAC,eAAe,CAAC,EAAE,IAAI,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;4FACwF;IACxF,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAElC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAEnC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAC9B"}
package/dist/mcp.d.ts CHANGED
@@ -7,5 +7,6 @@
7
7
  export { browserAuth } from "./auth/browser-auth";
8
8
  export { inMemoryStore } from "./storage/memory";
9
9
  export { fileStore } from "./storage/file";
10
- export type { BrowserAuthOptions, Tokens, TokenStore, ClientInfo, OAuthSession, OAuthStore, } from "./mcp-types";
10
+ export { OAuthStoreBrand } from "./mcp-types";
11
+ export type { BrowserAuthOptions, Tokens, TokenStore, ClientInfo, OAuthStore, } from "./mcp-types";
11
12
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,YAAY,EACV,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,UAAU,EACV,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EACV,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC"}
package/dist/mcp.js CHANGED
@@ -74,11 +74,11 @@ var init_is_inside_container = __esm(() => {
74
74
  });
75
75
 
76
76
  // node_modules/is-wsl/index.js
77
- import process from "node:process";
77
+ import process2 from "node:process";
78
78
  import os2 from "node:os";
79
79
  import fs4 from "node:fs";
80
80
  var isWsl = () => {
81
- if (process.platform !== "linux") {
81
+ if (process2.platform !== "linux") {
82
82
  return false;
83
83
  }
84
84
  if (os2.release().toLowerCase().includes("microsoft")) {
@@ -95,15 +95,15 @@ var isWsl = () => {
95
95
  }, is_wsl_default;
96
96
  var init_is_wsl = __esm(() => {
97
97
  init_is_inside_container();
98
- is_wsl_default = process.env.__IS_WSL_TEST__ ? isWsl : isWsl();
98
+ is_wsl_default = process2.env.__IS_WSL_TEST__ ? isWsl : isWsl();
99
99
  });
100
100
 
101
101
  // node_modules/powershell-utils/index.js
102
- import process2 from "node:process";
102
+ import process3 from "node:process";
103
103
  import { Buffer } from "node:buffer";
104
104
  import { promisify } from "node:util";
105
105
  import childProcess from "node:child_process";
106
- var execFile, powerShellPath = () => `${process2.env.SYSTEMROOT || process2.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, executePowerShell = async (command, options = {}) => {
106
+ var execFile, powerShellPath = () => `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, executePowerShell = async (command, options = {}) => {
107
107
  const {
108
108
  powerShellPath: psPath,
109
109
  ...execFileOptions
@@ -233,10 +233,10 @@ function defineLazyProperty(object, propertyName, valueGetter) {
233
233
 
234
234
  // node_modules/default-browser-id/index.js
235
235
  import { promisify as promisify3 } from "node:util";
236
- import process3 from "node:process";
236
+ import process4 from "node:process";
237
237
  import { execFile as execFile3 } from "node:child_process";
238
238
  async function defaultBrowserId() {
239
- if (process3.platform !== "darwin") {
239
+ if (process4.platform !== "darwin") {
240
240
  throw new Error("macOS only");
241
241
  }
242
242
  const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
@@ -249,11 +249,11 @@ var init_default_browser_id = __esm(() => {
249
249
  });
250
250
 
251
251
  // node_modules/run-applescript/index.js
252
- import process4 from "node:process";
252
+ import process5 from "node:process";
253
253
  import { promisify as promisify4 } from "node:util";
254
254
  import { execFile as execFile4, execFileSync } from "node:child_process";
255
255
  async function runAppleScript(script, { humanReadableOutput = true } = {}) {
256
- if (process4.platform !== "darwin") {
256
+ if (process5.platform !== "darwin") {
257
257
  throw new Error("macOS only");
258
258
  }
259
259
  const outputArguments = humanReadableOutput ? [] : ["-ss"];
@@ -323,21 +323,21 @@ var init_windows = __esm(() => {
323
323
 
324
324
  // node_modules/default-browser/index.js
325
325
  import { promisify as promisify6 } from "node:util";
326
- import process5 from "node:process";
326
+ import process6 from "node:process";
327
327
  import { execFile as execFile6 } from "node:child_process";
328
328
  async function defaultBrowser2() {
329
- if (process5.platform === "darwin") {
329
+ if (process6.platform === "darwin") {
330
330
  const id = await defaultBrowserId();
331
331
  const name = await bundleName(id);
332
332
  return { name, id };
333
333
  }
334
- if (process5.platform === "linux") {
334
+ if (process6.platform === "linux") {
335
335
  const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
336
336
  const id = stdout.trim();
337
337
  const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
338
338
  return { name, id };
339
339
  }
340
- if (process5.platform === "win32") {
340
+ if (process6.platform === "win32") {
341
341
  return defaultBrowser();
342
342
  }
343
343
  throw new Error("Only macOS, Linux, and Windows are supported");
@@ -352,10 +352,10 @@ var init_default_browser = __esm(() => {
352
352
  });
353
353
 
354
354
  // node_modules/is-in-ssh/index.js
355
- import process6 from "node:process";
355
+ import process7 from "node:process";
356
356
  var isInSsh, is_in_ssh_default;
357
357
  var init_is_in_ssh = __esm(() => {
358
- isInSsh = Boolean(process6.env.SSH_CONNECTION || process6.env.SSH_CLIENT || process6.env.SSH_TTY);
358
+ isInSsh = Boolean(process7.env.SSH_CONNECTION || process7.env.SSH_CLIENT || process7.env.SSH_TTY);
359
359
  is_in_ssh_default = isInSsh;
360
360
  });
361
361
 
@@ -366,7 +366,7 @@ __export(exports_open, {
366
366
  default: () => open_default,
367
367
  apps: () => apps
368
368
  });
369
- import process7 from "node:process";
369
+ import process8 from "node:process";
370
370
  import path2 from "node:path";
371
371
  import { fileURLToPath } from "node:url";
372
372
  import childProcess3 from "node:child_process";
@@ -537,7 +537,7 @@ var fallbackAttemptSymbol, __dirname2, localXdgOpenPath, platform, arch, tryEach
537
537
  await fs6.access(localXdgOpenPath, fsConstants2.X_OK);
538
538
  exeLocalXdgOpen = true;
539
539
  } catch {}
540
- const useSystemXdgOpen = process7.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
540
+ const useSystemXdgOpen = process8.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
541
541
  command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
542
542
  }
543
543
  if (appArguments.length > 0) {
@@ -624,7 +624,7 @@ var init_open = __esm(() => {
624
624
  fallbackAttemptSymbol = Symbol("fallbackAttempt");
625
625
  __dirname2 = import.meta.url ? path2.dirname(fileURLToPath(import.meta.url)) : "";
626
626
  localXdgOpenPath = path2.join(__dirname2, "xdg-open");
627
- ({ platform, arch } = process7);
627
+ ({ platform, arch } = process8);
628
628
  apps = {
629
629
  browser: "browser",
630
630
  browserPrivate: "browserPrivate"
@@ -672,6 +672,9 @@ var init_open = __esm(() => {
672
672
  // src/auth/browser-auth.ts
673
673
  import { randomBytes } from "node:crypto";
674
674
 
675
+ // src/mcp-types.ts
676
+ var OAuthStoreBrand = Symbol("OAuthStore");
677
+
675
678
  // src/utils/token.ts
676
679
  function calculateExpiry(expiresIn) {
677
680
  if (!expiresIn)
@@ -691,9 +694,6 @@ function inMemoryStore() {
691
694
  },
692
695
  async delete(key) {
693
696
  store.delete(key);
694
- },
695
- async clear() {
696
- store.clear();
697
697
  }
698
698
  };
699
699
  }
@@ -982,7 +982,9 @@ function fileStore(filepath) {
982
982
  }
983
983
  async function writeStore(data) {
984
984
  await ensureDir();
985
- await fs.writeFile(file, JSON.stringify(data, null, 2), "utf-8");
985
+ const tmp = `${file}.tmp.${process.pid}`;
986
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2), "utf-8");
987
+ await fs.rename(tmp, file);
986
988
  }
987
989
  return {
988
990
  async get(key) {
@@ -998,9 +1000,6 @@ function fileStore(filepath) {
998
1000
  const store = await readStore();
999
1001
  delete store[key];
1000
1002
  await writeStore(store);
1001
- },
1002
- async clear() {
1003
- await writeStore({});
1004
1003
  }
1005
1004
  };
1006
1005
  }
@@ -1049,6 +1048,10 @@ async function getAuthCode(input) {
1049
1048
  }
1050
1049
 
1051
1050
  // src/auth/browser-auth.ts
1051
+ import {
1052
+ exchangeAuthorization,
1053
+ discoverAuthorizationServerMetadata
1054
+ } from "@modelcontextprotocol/sdk/client/auth.js";
1052
1055
  function browserAuth(options = {}) {
1053
1056
  return new BrowserOAuthProvider(options);
1054
1057
  }
@@ -1060,6 +1063,7 @@ class BrowserOAuthProvider {
1060
1063
  _hostname;
1061
1064
  _callbackPath;
1062
1065
  _authTimeout;
1066
+ _redirectUrl;
1063
1067
  _launch;
1064
1068
  _clientId;
1065
1069
  _clientSecret;
@@ -1067,12 +1071,11 @@ class BrowserOAuthProvider {
1067
1071
  _successHtml;
1068
1072
  _errorHtml;
1069
1073
  _onRequest;
1074
+ _authServerUrl;
1070
1075
  _clientInfo;
1071
1076
  _tokens;
1077
+ _expiresAt;
1072
1078
  _codeVerifier;
1073
- _pendingAuthCode;
1074
- _pendingAuthState;
1075
- _isExchangingCode = false;
1076
1079
  _tokensLoaded = false;
1077
1080
  _loadingTokens;
1078
1081
  _authInProgress;
@@ -1083,6 +1086,7 @@ class BrowserOAuthProvider {
1083
1086
  this._hostname = options.hostname ?? "localhost";
1084
1087
  this._callbackPath = options.callbackPath ?? "/callback";
1085
1088
  this._authTimeout = options.authTimeout ?? 300000;
1089
+ this._redirectUrl = `http://${this._hostname}:${this._port}${this._callbackPath}`;
1086
1090
  this._launch = options.launch;
1087
1091
  this._clientId = options.clientId;
1088
1092
  this._clientSecret = options.clientSecret;
@@ -1090,6 +1094,7 @@ class BrowserOAuthProvider {
1090
1094
  this._successHtml = options.successHtml;
1091
1095
  this._errorHtml = options.errorHtml;
1092
1096
  this._onRequest = options.onRequest;
1097
+ this._authServerUrl = options.authServerUrl ? new URL(options.authServerUrl) : undefined;
1093
1098
  }
1094
1099
  async _ensureTokensLoaded() {
1095
1100
  if (this._tokensLoaded)
@@ -1103,28 +1108,30 @@ class BrowserOAuthProvider {
1103
1108
  try {
1104
1109
  const stored = await this._store.get(this._storeKey);
1105
1110
  if (stored) {
1111
+ this._expiresAt = stored.expiresAt;
1106
1112
  this._tokens = {
1107
1113
  access_token: stored.accessToken,
1108
1114
  token_type: "Bearer",
1109
1115
  refresh_token: stored.refreshToken,
1110
- expires_in: stored.expiresAt ? Math.floor((stored.expiresAt - Date.now()) / 1000) : undefined,
1111
1116
  scope: stored.scope
1112
1117
  };
1113
1118
  }
1114
1119
  if (this._isOAuthStore(this._store)) {
1115
- const clientInfo = await this._store.getClient(this._storeKey);
1116
- if (clientInfo?.clientId) {
1117
- this._clientInfo = {
1118
- client_id: clientInfo.clientId,
1119
- client_secret: clientInfo.clientSecret,
1120
- client_id_issued_at: clientInfo.clientIdIssuedAt,
1121
- client_secret_expires_at: clientInfo.clientSecretExpiresAt,
1122
- redirect_uris: [this.redirectUrl]
1123
- };
1120
+ if (!this._clientId) {
1121
+ const clientInfo = await this._store.getClient(this._storeKey);
1122
+ if (clientInfo?.clientId) {
1123
+ this._clientInfo = {
1124
+ client_id: clientInfo.clientId,
1125
+ client_secret: clientInfo.clientSecret,
1126
+ client_id_issued_at: clientInfo.clientIdIssuedAt,
1127
+ client_secret_expires_at: clientInfo.clientSecretExpiresAt,
1128
+ redirect_uris: [this.redirectUrl]
1129
+ };
1130
+ }
1124
1131
  }
1125
- const session = await this._store.getSession(this._storeKey);
1126
- if (session?.codeVerifier) {
1127
- this._codeVerifier = session.codeVerifier;
1132
+ const verifier = await this._store.getCodeVerifier(this._storeKey);
1133
+ if (verifier) {
1134
+ this._codeVerifier = verifier;
1128
1135
  }
1129
1136
  }
1130
1137
  this._tokensLoaded = true;
@@ -1133,17 +1140,17 @@ class BrowserOAuthProvider {
1133
1140
  }
1134
1141
  }
1135
1142
  _isOAuthStore(store) {
1136
- return typeof store.getClient === "function" && typeof store.setClient === "function" && typeof store.getSession === "function";
1143
+ return OAuthStoreBrand in store;
1137
1144
  }
1138
1145
  get redirectUrl() {
1139
- return `http://${this._hostname}:${this._port}${this._callbackPath}`;
1146
+ return this._redirectUrl;
1140
1147
  }
1141
1148
  get clientMetadata() {
1142
1149
  return {
1143
1150
  client_name: "OAuth Callback Handler",
1144
1151
  client_uri: "https://github.com/kriasoft/oauth-callback",
1145
1152
  redirect_uris: [this.redirectUrl],
1146
- grant_types: ["authorization_code", "refresh_token"],
1153
+ grant_types: ["authorization_code"],
1147
1154
  response_types: ["code"],
1148
1155
  scope: this._scope,
1149
1156
  token_endpoint_auth_method: this._clientSecret ? "client_secret_post" : "none"
@@ -1185,19 +1192,19 @@ class BrowserOAuthProvider {
1185
1192
  if (!this._tokens) {
1186
1193
  return;
1187
1194
  }
1188
- const stored = await this._store.get(this._storeKey);
1189
- if (stored?.expiresAt && Date.now() >= stored.expiresAt - 60000) {
1195
+ if (this._expiresAt && Date.now() >= this._expiresAt - 60000) {
1190
1196
  return;
1191
1197
  }
1192
1198
  return this._tokens;
1193
1199
  }
1194
1200
  async saveTokens(tokens) {
1195
1201
  this._tokens = tokens;
1202
+ this._expiresAt = tokens.expires_in ? calculateExpiry(tokens.expires_in) : undefined;
1196
1203
  this._tokensLoaded = true;
1197
1204
  const storedTokens = {
1198
1205
  accessToken: tokens.access_token,
1199
1206
  refreshToken: tokens.refresh_token,
1200
- expiresAt: tokens.expires_in ? calculateExpiry(tokens.expires_in) : undefined,
1207
+ expiresAt: this._expiresAt,
1201
1208
  scope: tokens.scope
1202
1209
  };
1203
1210
  await this._store.set(this._storeKey, storedTokens);
@@ -1207,14 +1214,14 @@ class BrowserOAuthProvider {
1207
1214
  await this._authInProgress;
1208
1215
  return;
1209
1216
  }
1210
- this._authInProgress = this._doAuthorization(authorizationUrl);
1217
+ this._authInProgress = this._completeAuthorizationFlow(authorizationUrl);
1211
1218
  try {
1212
1219
  await this._authInProgress;
1213
1220
  } finally {
1214
1221
  this._authInProgress = undefined;
1215
1222
  }
1216
1223
  }
1217
- async _doAuthorization(authorizationUrl) {
1224
+ async _completeAuthorizationFlow(authorizationUrl) {
1218
1225
  const baseOptions = {
1219
1226
  port: this._port,
1220
1227
  hostname: this._hostname,
@@ -1229,23 +1236,49 @@ class BrowserOAuthProvider {
1229
1236
  authorizationUrl: authorizationUrl.href,
1230
1237
  launch: this._launch
1231
1238
  } : baseOptions);
1232
- this._pendingAuthCode = result.code;
1233
- this._pendingAuthState = result.state;
1234
- setTimeout(() => {
1235
- if (this._pendingAuthCode === result.code) {
1236
- this._pendingAuthCode = undefined;
1237
- this._pendingAuthState = undefined;
1239
+ if (!result.code) {
1240
+ throw new Error("No authorization code received");
1241
+ }
1242
+ const expectedState = authorizationUrl.searchParams.get("state");
1243
+ if (expectedState && result.state !== expectedState) {
1244
+ throw new Error("OAuth state mismatch - possible CSRF attack");
1245
+ }
1246
+ await this._exchangeCodeForTokens(authorizationUrl, result.code);
1247
+ }
1248
+ async _exchangeCodeForTokens(authorizationUrl, code) {
1249
+ const authServerUrl = this._authServerUrl ?? new URL("/", authorizationUrl.origin);
1250
+ const metadata = await discoverAuthorizationServerMetadata(authServerUrl).catch(() => {
1251
+ return;
1252
+ });
1253
+ const clientInfo = await this.clientInformation();
1254
+ if (!clientInfo) {
1255
+ throw new Error("Client information required for token exchange. " + "Provide clientId in options or ensure DCR succeeded.");
1256
+ }
1257
+ if (!this._codeVerifier) {
1258
+ throw new Error("Code verifier required for token exchange");
1259
+ }
1260
+ let tokens;
1261
+ try {
1262
+ tokens = await exchangeAuthorization(authServerUrl, {
1263
+ metadata,
1264
+ clientInformation: clientInfo,
1265
+ authorizationCode: code,
1266
+ codeVerifier: this._codeVerifier,
1267
+ redirectUri: this.redirectUrl
1268
+ });
1269
+ } catch (error) {
1270
+ if (!this._authServerUrl && !metadata) {
1271
+ const msg = error instanceof Error ? error.message : String(error);
1272
+ throw new Error(`Token exchange failed: ${msg}. ` + `If the token endpoint differs from ${authorizationUrl.origin}, set authServerUrl explicitly.`);
1238
1273
  }
1239
- }, this._authTimeout);
1274
+ throw error;
1275
+ }
1276
+ await this.saveTokens(tokens);
1240
1277
  }
1241
1278
  async saveCodeVerifier(codeVerifier) {
1242
1279
  this._codeVerifier = codeVerifier;
1243
1280
  if (this._isOAuthStore(this._store)) {
1244
- const session = {
1245
- codeVerifier,
1246
- state: this._pendingAuthState
1247
- };
1248
- await this._store.setSession(this._storeKey, session);
1281
+ await this._store.setCodeVerifier(this._storeKey, codeVerifier);
1249
1282
  }
1250
1283
  }
1251
1284
  async codeVerifier() {
@@ -1255,36 +1288,34 @@ class BrowserOAuthProvider {
1255
1288
  return this._codeVerifier;
1256
1289
  }
1257
1290
  async invalidateCredentials(scope) {
1258
- if (scope === "all" && this._isExchangingCode) {
1259
- this._tokens = undefined;
1260
- await this._store.delete(this._storeKey);
1261
- return;
1262
- }
1263
- if (this._isExchangingCode && (scope === "client" || scope === "all")) {
1264
- this._isExchangingCode = false;
1265
- }
1266
1291
  switch (scope) {
1267
1292
  case "all":
1268
1293
  this._clientInfo = undefined;
1269
1294
  this._tokens = undefined;
1295
+ this._expiresAt = undefined;
1270
1296
  this._codeVerifier = undefined;
1271
1297
  this._tokensLoaded = false;
1272
- await this._store.clear();
1298
+ await this._store.delete(this._storeKey);
1299
+ if (this._isOAuthStore(this._store)) {
1300
+ await this._store.deleteClient(this._storeKey);
1301
+ await this._store.deleteCodeVerifier(this._storeKey);
1302
+ }
1273
1303
  break;
1274
1304
  case "client":
1275
1305
  this._clientInfo = undefined;
1276
1306
  if (this._isOAuthStore(this._store)) {
1277
- await this._store.setClient(this._storeKey, { clientId: "" });
1307
+ await this._store.deleteClient(this._storeKey);
1278
1308
  }
1279
1309
  break;
1280
1310
  case "tokens":
1281
1311
  this._tokens = undefined;
1312
+ this._expiresAt = undefined;
1282
1313
  await this._store.delete(this._storeKey);
1283
1314
  break;
1284
1315
  case "verifier":
1285
1316
  this._codeVerifier = undefined;
1286
1317
  if (this._isOAuthStore(this._store)) {
1287
- await this._store.setSession(this._storeKey, {});
1318
+ await this._store.deleteCodeVerifier(this._storeKey);
1288
1319
  }
1289
1320
  break;
1290
1321
  }
@@ -1292,22 +1323,10 @@ class BrowserOAuthProvider {
1292
1323
  async validateResourceURL(_serverUrl, _resource) {
1293
1324
  return;
1294
1325
  }
1295
- getPendingAuthCode() {
1296
- if (this._pendingAuthCode) {
1297
- const result = {
1298
- code: this._pendingAuthCode,
1299
- state: this._pendingAuthState
1300
- };
1301
- this._isExchangingCode = true;
1302
- this._pendingAuthCode = undefined;
1303
- this._pendingAuthState = undefined;
1304
- return result;
1305
- }
1306
- return;
1307
- }
1308
1326
  }
1309
1327
  export {
1310
1328
  inMemoryStore,
1311
1329
  fileStore,
1312
- browserAuth
1330
+ browserAuth,
1331
+ OAuthStoreBrand
1313
1332
  };
@@ -1,8 +1,8 @@
1
1
  import type { TokenStore } from "../mcp-types";
2
2
  /**
3
3
  * Persistent file-based token storage.
4
+ * Not safe for concurrent access across multiple processes.
4
5
  * Default: ~/.mcp/tokens.json
5
- * WARNING: Not safe for concurrent access across processes.
6
6
  */
7
7
  export declare function fileStore(filepath?: string): TokenStore;
8
8
  //# sourceMappingURL=file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/storage/file.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CA4CvD"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/storage/file.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAyCvD"}
@@ -1,7 +1,7 @@
1
1
  import type { TokenStore } from "../mcp-types";
2
2
  /**
3
3
  * Ephemeral in-memory token storage.
4
- * Tokens lost on process restart. Safe for concurrent access within process.
4
+ * Tokens lost on process restart.
5
5
  */
6
6
  export declare function inMemoryStore(): TokenStore;
7
7
  //# sourceMappingURL=memory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/storage/memory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAoB1C"}
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/storage/memory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAgB1C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth-callback",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Lightweight OAuth 2.0 callback handler for Node.js, Deno, and Bun with built-in browser flow and MCP SDK integration",
5
5
  "keywords": [
6
6
  "oauth",
@@ -104,7 +104,7 @@
104
104
  },
105
105
  "scripts": {
106
106
  "build:templates": "bun run templates/build.ts",
107
- "build": "bun run build:templates && bun build ./src/index.ts --outdir=./dist --target=node && bun build ./src/mcp.ts --outdir=./dist --target=node && tsc --declaration --emitDeclarationOnly --outDir ./dist",
107
+ "build": "bun run build:templates && bun build ./src/index.ts --outdir=./dist --target=node && bun build ./src/mcp.ts --outdir=./dist --target=node --external=@modelcontextprotocol/sdk && tsc --declaration --emitDeclarationOnly --outDir ./dist",
108
108
  "clean": "rm -rf dist",
109
109
  "test": "bun test",
110
110
  "test:login": "bun run scripts/test-login.ts",