oauth-callback 1.0.0 → 1.1.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 +123 -3
- package/dist/auth/browser-auth.d.ts +18 -0
- package/dist/auth/browser-auth.d.ts.map +1 -0
- package/dist/auth/browser-auth.test.d.ts +2 -0
- package/dist/auth/browser-auth.test.d.ts.map +1 -0
- package/dist/index.d.ts +12 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +384 -0
- package/dist/mcp-types.d.ts +75 -0
- package/dist/mcp-types.d.ts.map +1 -0
- package/dist/storage/file.d.ts +8 -0
- package/dist/storage/file.d.ts.map +1 -0
- package/dist/storage/memory.d.ts +7 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/utils/token.d.ts +11 -0
- package/dist/utils/token.d.ts.map +1 -0
- package/package.json +43 -34
- package/src/auth/browser-auth.test.ts +261 -0
- package/src/auth/browser-auth.ts +457 -0
- package/src/index.ts +23 -29
- package/src/mcp-types.ts +90 -0
- package/src/storage/file.ts +58 -0
- package/src/storage/memory.ts +30 -0
- package/src/utils/token.ts +29 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/kriasoft/oauth-callback/blob/main/LICENSE)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
A lightweight
|
|
8
|
+
A lightweight OAuth 2.0 callback handler for Node.js, Deno, and Bun with built-in browser flow and MCP SDK integration. Perfect for CLI tools, desktop applications, and development environments that need to capture OAuth authorization codes.
|
|
9
9
|
|
|
10
10
|
<div align="center">
|
|
11
11
|
<img src="https://raw.githubusercontent.com/kriasoft/oauth-callback/main/examples/notion.gif" alt="OAuth Callback Demo" width="100%" style="max-width: 800px; height: auto;">
|
|
@@ -15,10 +15,12 @@ A lightweight local HTTP server for handling OAuth 2.0 authorization callbacks i
|
|
|
15
15
|
|
|
16
16
|
- 🚀 **Multi-runtime support** - Works with Node.js 18+, Deno, and Bun
|
|
17
17
|
- 🔒 **Secure localhost-only server** for OAuth callbacks
|
|
18
|
+
- 🤖 **MCP SDK integration** - Built-in OAuth provider for Model Context Protocol
|
|
18
19
|
- ⚡ **Minimal dependencies** - Only requires `open` package
|
|
19
20
|
- 🎯 **TypeScript support** out of the box
|
|
20
21
|
- 🛡️ **Comprehensive OAuth error handling** with detailed error classes
|
|
21
22
|
- 🔄 **Automatic server cleanup** after callback
|
|
23
|
+
- 💾 **Flexible token storage** - In-memory and file-based options
|
|
22
24
|
- 🎪 **Clean success pages** with animated checkmark
|
|
23
25
|
- 🎨 **Customizable HTML templates** with placeholder support
|
|
24
26
|
- 🚦 **AbortSignal support** for programmatic cancellation
|
|
@@ -40,13 +42,21 @@ npm install oauth-callback
|
|
|
40
42
|
## Quick Start
|
|
41
43
|
|
|
42
44
|
```typescript
|
|
43
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
getAuthCode,
|
|
47
|
+
OAuthError,
|
|
48
|
+
browserAuth,
|
|
49
|
+
fileStore,
|
|
50
|
+
} from "oauth-callback";
|
|
44
51
|
|
|
45
52
|
// Simple usage
|
|
46
53
|
const result = await getAuthCode(
|
|
47
54
|
"https://example.com/oauth/authorize?client_id=xxx&redirect_uri=http://localhost:3000/callback",
|
|
48
55
|
);
|
|
49
56
|
console.log("Authorization code:", result.code);
|
|
57
|
+
|
|
58
|
+
// MCP SDK integration
|
|
59
|
+
const authProvider = browserAuth({ store: fileStore() });
|
|
50
60
|
```
|
|
51
61
|
|
|
52
62
|
## Usage Examples
|
|
@@ -124,6 +134,71 @@ const microsoftAuth = await getAuthCode(
|
|
|
124
134
|
);
|
|
125
135
|
```
|
|
126
136
|
|
|
137
|
+
### MCP SDK Integration
|
|
138
|
+
|
|
139
|
+
The `browserAuth()` function provides a drop-in OAuth provider for the Model Context Protocol SDK:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { browserAuth, inMemoryStore } from "oauth-callback";
|
|
143
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
144
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
145
|
+
|
|
146
|
+
// Create MCP-compatible OAuth provider
|
|
147
|
+
const authProvider = browserAuth({
|
|
148
|
+
port: 3000,
|
|
149
|
+
scope: "read write",
|
|
150
|
+
store: inMemoryStore(), // Or fileStore() for persistence
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Use with MCP SDK transport
|
|
154
|
+
const transport = new StreamableHTTPClientTransport(
|
|
155
|
+
new URL("https://mcp.notion.com/mcp"),
|
|
156
|
+
{ authProvider },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const client = new Client(
|
|
160
|
+
{ name: "my-app", version: "1.0.0" },
|
|
161
|
+
{ capabilities: {} },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await client.connect(transport);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Token Storage Options
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { browserAuth, inMemoryStore, fileStore } from "oauth-callback";
|
|
171
|
+
|
|
172
|
+
// Ephemeral storage (tokens lost on restart)
|
|
173
|
+
const ephemeralAuth = browserAuth({
|
|
174
|
+
store: inMemoryStore(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Persistent file storage (default: ~/.mcp/tokens.json)
|
|
178
|
+
const persistentAuth = browserAuth({
|
|
179
|
+
store: fileStore(),
|
|
180
|
+
storeKey: "my-app-tokens", // Namespace for multiple apps
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Custom file location
|
|
184
|
+
const customAuth = browserAuth({
|
|
185
|
+
store: fileStore("/path/to/tokens.json"),
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Pre-configured Client Credentials
|
|
190
|
+
|
|
191
|
+
If you have pre-registered OAuth client credentials:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const authProvider = browserAuth({
|
|
195
|
+
clientId: "your-client-id",
|
|
196
|
+
clientSecret: "your-client-secret",
|
|
197
|
+
scope: "read write",
|
|
198
|
+
store: fileStore(), // Persist tokens across sessions
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
127
202
|
### Advanced Usage
|
|
128
203
|
|
|
129
204
|
```typescript
|
|
@@ -206,6 +281,50 @@ class OAuthError extends Error {
|
|
|
206
281
|
}
|
|
207
282
|
```
|
|
208
283
|
|
|
284
|
+
### `browserAuth(options)`
|
|
285
|
+
|
|
286
|
+
Creates an MCP SDK-compatible OAuth provider for browser-based flows. Handles Dynamic Client Registration (DCR), token storage, and automatic refresh.
|
|
287
|
+
|
|
288
|
+
#### Parameters
|
|
289
|
+
|
|
290
|
+
- `options` (BrowserAuthOptions): Configuration object with:
|
|
291
|
+
- `port` (number): Port for callback server (default: 3000)
|
|
292
|
+
- `hostname` (string): Hostname to bind to (default: "localhost")
|
|
293
|
+
- `callbackPath` (string): URL path for OAuth callback (default: "/callback")
|
|
294
|
+
- `scope` (string): OAuth scopes to request
|
|
295
|
+
- `clientId` (string): Pre-registered client ID (optional)
|
|
296
|
+
- `clientSecret` (string): Pre-registered client secret (optional)
|
|
297
|
+
- `store` (TokenStore): Token storage implementation (default: inMemoryStore())
|
|
298
|
+
- `storeKey` (string): Storage key for tokens (default: "mcp-tokens")
|
|
299
|
+
- `authTimeout` (number): Authorization timeout in ms (default: 300000)
|
|
300
|
+
- `successHtml` (string): Custom success page HTML
|
|
301
|
+
- `errorHtml` (string): Custom error page HTML
|
|
302
|
+
- `onRequest` (function): Request logging callback
|
|
303
|
+
|
|
304
|
+
#### Returns
|
|
305
|
+
|
|
306
|
+
OAuthClientProvider compatible with MCP SDK transports.
|
|
307
|
+
|
|
308
|
+
### `inMemoryStore()`
|
|
309
|
+
|
|
310
|
+
Creates an ephemeral in-memory token store. Tokens are lost when the process exits.
|
|
311
|
+
|
|
312
|
+
#### Returns
|
|
313
|
+
|
|
314
|
+
TokenStore implementation for temporary token storage.
|
|
315
|
+
|
|
316
|
+
### `fileStore(filepath?)`
|
|
317
|
+
|
|
318
|
+
Creates a persistent file-based token store.
|
|
319
|
+
|
|
320
|
+
#### Parameters
|
|
321
|
+
|
|
322
|
+
- `filepath` (string): Optional custom file path (default: `~/.mcp/tokens.json`)
|
|
323
|
+
|
|
324
|
+
#### Returns
|
|
325
|
+
|
|
326
|
+
TokenStore implementation for persistent token storage.
|
|
327
|
+
|
|
209
328
|
## How It Works
|
|
210
329
|
|
|
211
330
|
1. **Server Creation**: Creates a temporary HTTP server on the specified port
|
|
@@ -279,7 +398,8 @@ This example demonstrates:
|
|
|
279
398
|
- Dynamic Client Registration (OAuth 2.0 DCR) - no pre-configured client ID/secret needed
|
|
280
399
|
- Integration with Model Context Protocol (MCP) servers
|
|
281
400
|
- Automatic client registration with the authorization server
|
|
282
|
-
- Using
|
|
401
|
+
- Using `browserAuth()` provider with MCP SDK's `StreamableHTTPClientTransport`
|
|
402
|
+
- Token persistence with `inMemoryStore()` for ephemeral sessions
|
|
283
403
|
|
|
284
404
|
## Development
|
|
285
405
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BrowserAuthOptions } from "../mcp-types";
|
|
2
|
+
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Factory for MCP SDK-compatible OAuth provider using browser flow.
|
|
5
|
+
*
|
|
6
|
+
* @param options Configuration for OAuth flow behavior
|
|
7
|
+
* @returns OAuthClientProvider for MCP SDK transport
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const transport = new StreamableHTTPClientTransport(
|
|
12
|
+
* new URL("https://mcp.notion.com/mcp"),
|
|
13
|
+
* { authProvider: browserAuth() }
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function browserAuth(options?: BrowserAuthOptions): OAuthClientProvider;
|
|
18
|
+
//# sourceMappingURL=browser-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-auth.d.ts","sourceRoot":"","sources":["../../src/auth/browser-auth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kBAAkB,EAMnB,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAQpF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,mBAAmB,CAErB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-auth.test.d.ts","sourceRoot":"","sources":["../../src/auth/browser-auth.test.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,27 +3,26 @@ import type { GetAuthCodeOptions } from "./types";
|
|
|
3
3
|
export type { CallbackResult, CallbackServer, ServerOptions } from "./server";
|
|
4
4
|
export { OAuthError } from "./errors";
|
|
5
5
|
export type { GetAuthCodeOptions } from "./types";
|
|
6
|
+
export { browserAuth } from "./auth/browser-auth";
|
|
7
|
+
export { inMemoryStore } from "./storage/memory";
|
|
8
|
+
export { fileStore } from "./storage/file";
|
|
9
|
+
export type { BrowserAuthOptions, Tokens, TokenStore } from "./mcp-types";
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
11
|
+
* Captures OAuth authorization code via localhost callback.
|
|
12
|
+
* Opens browser to auth URL, waits for provider redirect to localhost.
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param input - Either a string containing the OAuth authorization URL,
|
|
14
|
-
* or a GetAuthCodeOptions object with detailed configuration
|
|
15
|
-
* @returns Promise that resolves to CallbackResult containing the authorization code
|
|
16
|
-
* and other parameters, or rejects with OAuthError if authorization fails
|
|
17
|
-
* @throws {OAuthError} When OAuth provider returns an error (e.g., access_denied)
|
|
18
|
-
* @throws {Error} For timeout, network errors, or other unexpected failures
|
|
14
|
+
* @param input - Auth URL string or GetAuthCodeOptions with config
|
|
15
|
+
* @returns Promise<CallbackResult> with code and params
|
|
16
|
+
* @throws {OAuthError} Provider errors (access_denied, invalid_scope)
|
|
17
|
+
* @throws {Error} Timeout, network failures, port conflicts
|
|
19
18
|
*
|
|
20
19
|
* @example
|
|
21
20
|
* ```typescript
|
|
22
|
-
* // Simple
|
|
21
|
+
* // Simple
|
|
23
22
|
* const result = await getAuthCode('https://oauth.example.com/authorize?...');
|
|
24
23
|
* console.log('Code:', result.code);
|
|
25
24
|
*
|
|
26
|
-
* //
|
|
25
|
+
* // Custom port/timeout
|
|
27
26
|
* const result = await getAuthCode({
|
|
28
27
|
* authorizationUrl: 'https://oauth.example.com/authorize?...',
|
|
29
28
|
* port: 8080,
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGlD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,kBAAkB,GAAG,MAAM,GACjC,OAAO,CAAC,cAAc,CAAC,CA8DzB"}
|
package/dist/index.js
CHANGED
|
@@ -946,6 +946,387 @@ function createCallbackServer() {
|
|
|
946
946
|
}
|
|
947
947
|
return new NodeCallbackServer;
|
|
948
948
|
}
|
|
949
|
+
// src/auth/browser-auth.ts
|
|
950
|
+
import { randomBytes } from "node:crypto";
|
|
951
|
+
|
|
952
|
+
// src/utils/token.ts
|
|
953
|
+
function calculateExpiry(expiresIn) {
|
|
954
|
+
if (!expiresIn)
|
|
955
|
+
return;
|
|
956
|
+
return Date.now() + expiresIn * 1000;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/storage/memory.ts
|
|
960
|
+
function inMemoryStore() {
|
|
961
|
+
const store = new Map;
|
|
962
|
+
return {
|
|
963
|
+
async get(key) {
|
|
964
|
+
return store.get(key) ?? null;
|
|
965
|
+
},
|
|
966
|
+
async set(key, tokens) {
|
|
967
|
+
store.set(key, tokens);
|
|
968
|
+
},
|
|
969
|
+
async delete(key) {
|
|
970
|
+
store.delete(key);
|
|
971
|
+
},
|
|
972
|
+
async clear() {
|
|
973
|
+
store.clear();
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/auth/browser-auth.ts
|
|
979
|
+
function browserAuth(options = {}) {
|
|
980
|
+
return new BrowserOAuthProvider(options);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
class BrowserOAuthProvider {
|
|
984
|
+
_store;
|
|
985
|
+
_storeKey;
|
|
986
|
+
_port;
|
|
987
|
+
_hostname;
|
|
988
|
+
_callbackPath;
|
|
989
|
+
_authTimeout;
|
|
990
|
+
_usePKCE;
|
|
991
|
+
_openBrowser;
|
|
992
|
+
_clientId;
|
|
993
|
+
_clientSecret;
|
|
994
|
+
_scope;
|
|
995
|
+
_successHtml;
|
|
996
|
+
_errorHtml;
|
|
997
|
+
_onRequest;
|
|
998
|
+
_clientInfo;
|
|
999
|
+
_tokens;
|
|
1000
|
+
_codeVerifier;
|
|
1001
|
+
_pendingAuthCode;
|
|
1002
|
+
_pendingAuthState;
|
|
1003
|
+
_isExchangingCode = false;
|
|
1004
|
+
_tokensLoaded = false;
|
|
1005
|
+
_loadingTokens;
|
|
1006
|
+
_authInProgress;
|
|
1007
|
+
_refreshInProgress;
|
|
1008
|
+
constructor(options = {}) {
|
|
1009
|
+
this._store = options.store ?? inMemoryStore();
|
|
1010
|
+
this._storeKey = options.storeKey ?? "mcp-tokens";
|
|
1011
|
+
this._port = options.port ?? 3000;
|
|
1012
|
+
this._hostname = options.hostname ?? "localhost";
|
|
1013
|
+
this._callbackPath = options.callbackPath ?? "/callback";
|
|
1014
|
+
this._authTimeout = options.authTimeout ?? 300000;
|
|
1015
|
+
this._usePKCE = options.usePKCE ?? true;
|
|
1016
|
+
this._openBrowser = options.openBrowser ?? true;
|
|
1017
|
+
this._clientId = options.clientId;
|
|
1018
|
+
this._clientSecret = options.clientSecret;
|
|
1019
|
+
this._scope = options.scope;
|
|
1020
|
+
this._successHtml = options.successHtml;
|
|
1021
|
+
this._errorHtml = options.errorHtml;
|
|
1022
|
+
this._onRequest = options.onRequest;
|
|
1023
|
+
}
|
|
1024
|
+
async _ensureTokensLoaded() {
|
|
1025
|
+
if (this._tokensLoaded)
|
|
1026
|
+
return;
|
|
1027
|
+
if (!this._loadingTokens) {
|
|
1028
|
+
this._loadingTokens = this._loadStoredData();
|
|
1029
|
+
}
|
|
1030
|
+
await this._loadingTokens;
|
|
1031
|
+
}
|
|
1032
|
+
async _loadStoredData() {
|
|
1033
|
+
try {
|
|
1034
|
+
const stored = await this._store.get(this._storeKey);
|
|
1035
|
+
if (stored) {
|
|
1036
|
+
this._tokens = {
|
|
1037
|
+
access_token: stored.accessToken,
|
|
1038
|
+
token_type: "Bearer",
|
|
1039
|
+
refresh_token: stored.refreshToken,
|
|
1040
|
+
expires_in: stored.expiresAt ? Math.floor((stored.expiresAt - Date.now()) / 1000) : undefined,
|
|
1041
|
+
scope: stored.scope
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
if (this._isOAuthStore(this._store)) {
|
|
1045
|
+
const clientInfo = await this._store.getClient(this._storeKey);
|
|
1046
|
+
if (clientInfo) {
|
|
1047
|
+
this._clientInfo = {
|
|
1048
|
+
client_id: clientInfo.clientId,
|
|
1049
|
+
client_secret: clientInfo.clientSecret,
|
|
1050
|
+
client_id_issued_at: clientInfo.clientIdIssuedAt,
|
|
1051
|
+
client_secret_expires_at: clientInfo.clientSecretExpiresAt,
|
|
1052
|
+
redirect_uris: [this.redirectUrl]
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
const session = await this._store.getSession(this._storeKey);
|
|
1056
|
+
if (session) {
|
|
1057
|
+
this._codeVerifier = session.codeVerifier;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
this._tokensLoaded = true;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
console.warn("Failed to load stored data:", error);
|
|
1063
|
+
this._tokensLoaded = true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
_isOAuthStore(store) {
|
|
1067
|
+
return typeof store.getClient === "function";
|
|
1068
|
+
}
|
|
1069
|
+
get redirectUrl() {
|
|
1070
|
+
return `http://${this._hostname}:${this._port}${this._callbackPath}`;
|
|
1071
|
+
}
|
|
1072
|
+
get clientMetadata() {
|
|
1073
|
+
return {
|
|
1074
|
+
client_name: "OAuth Callback Handler",
|
|
1075
|
+
client_uri: "https://github.com/kriasoft/oauth-callback",
|
|
1076
|
+
redirect_uris: [this.redirectUrl],
|
|
1077
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1078
|
+
response_types: ["code"],
|
|
1079
|
+
scope: this._scope,
|
|
1080
|
+
token_endpoint_auth_method: this._clientSecret ? "client_secret_post" : "none"
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
async state() {
|
|
1084
|
+
const buffer = randomBytes(32);
|
|
1085
|
+
return buffer.toString("base64url");
|
|
1086
|
+
}
|
|
1087
|
+
async clientInformation() {
|
|
1088
|
+
if (this._clientId) {
|
|
1089
|
+
return {
|
|
1090
|
+
client_id: this._clientId,
|
|
1091
|
+
client_secret: this._clientSecret
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (this._clientInfo) {
|
|
1095
|
+
return {
|
|
1096
|
+
client_id: this._clientInfo.client_id,
|
|
1097
|
+
client_secret: this._clientInfo.client_secret
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
async saveClientInformation(clientInformation) {
|
|
1103
|
+
this._clientInfo = clientInformation;
|
|
1104
|
+
if (this._isOAuthStore(this._store)) {
|
|
1105
|
+
const clientInfo = {
|
|
1106
|
+
clientId: clientInformation.client_id,
|
|
1107
|
+
clientSecret: clientInformation.client_secret,
|
|
1108
|
+
clientIdIssuedAt: clientInformation.client_id_issued_at,
|
|
1109
|
+
clientSecretExpiresAt: clientInformation.client_secret_expires_at
|
|
1110
|
+
};
|
|
1111
|
+
await this._store.setClient(this._storeKey, clientInfo);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async tokens() {
|
|
1115
|
+
await this._ensureTokensLoaded();
|
|
1116
|
+
if (!this._tokens) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const stored = await this._store.get(this._storeKey);
|
|
1120
|
+
if (stored?.expiresAt) {
|
|
1121
|
+
if (Date.now() >= stored.expiresAt - 60000) {
|
|
1122
|
+
if (this._tokens.refresh_token) {
|
|
1123
|
+
try {
|
|
1124
|
+
await this._refreshTokens();
|
|
1125
|
+
return this._tokens;
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
console.warn("Token refresh failed:", error);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return this._tokens;
|
|
1135
|
+
}
|
|
1136
|
+
async saveTokens(tokens) {
|
|
1137
|
+
this._tokens = tokens;
|
|
1138
|
+
this._tokensLoaded = true;
|
|
1139
|
+
const storedTokens = {
|
|
1140
|
+
accessToken: tokens.access_token,
|
|
1141
|
+
refreshToken: tokens.refresh_token,
|
|
1142
|
+
expiresAt: tokens.expires_in ? calculateExpiry(tokens.expires_in) : undefined,
|
|
1143
|
+
scope: tokens.scope
|
|
1144
|
+
};
|
|
1145
|
+
await this._store.set(this._storeKey, storedTokens);
|
|
1146
|
+
}
|
|
1147
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
1148
|
+
if (this._authInProgress) {
|
|
1149
|
+
await this._authInProgress;
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
this._authInProgress = this._doAuthorization(authorizationUrl);
|
|
1153
|
+
try {
|
|
1154
|
+
await this._authInProgress;
|
|
1155
|
+
} finally {
|
|
1156
|
+
this._authInProgress = undefined;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
async _doAuthorization(authorizationUrl) {
|
|
1160
|
+
let lastError;
|
|
1161
|
+
const maxRetries = 2;
|
|
1162
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
1163
|
+
try {
|
|
1164
|
+
const result = await getAuthCode({
|
|
1165
|
+
authorizationUrl: authorizationUrl.href,
|
|
1166
|
+
port: this._port,
|
|
1167
|
+
hostname: this._hostname,
|
|
1168
|
+
callbackPath: this._callbackPath,
|
|
1169
|
+
timeout: this._authTimeout,
|
|
1170
|
+
openBrowser: typeof this._openBrowser === "boolean" ? this._openBrowser : true,
|
|
1171
|
+
successHtml: this._successHtml,
|
|
1172
|
+
errorHtml: this._errorHtml,
|
|
1173
|
+
onRequest: this._onRequest
|
|
1174
|
+
});
|
|
1175
|
+
this._pendingAuthCode = result.code;
|
|
1176
|
+
this._pendingAuthState = result.state;
|
|
1177
|
+
setTimeout(() => {
|
|
1178
|
+
if (this._pendingAuthCode === result.code) {
|
|
1179
|
+
this._pendingAuthCode = undefined;
|
|
1180
|
+
this._pendingAuthState = undefined;
|
|
1181
|
+
}
|
|
1182
|
+
}, this._authTimeout);
|
|
1183
|
+
return;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1186
|
+
if (error instanceof OAuthError) {
|
|
1187
|
+
throw error;
|
|
1188
|
+
}
|
|
1189
|
+
if (attempt < maxRetries) {
|
|
1190
|
+
console.warn(`Auth attempt ${attempt + 1} failed, retrying...`, error);
|
|
1191
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
throw new Error(`OAuth authorization failed after ${maxRetries + 1} attempts: ${lastError?.message}`);
|
|
1196
|
+
}
|
|
1197
|
+
async saveCodeVerifier(codeVerifier) {
|
|
1198
|
+
this._codeVerifier = codeVerifier;
|
|
1199
|
+
if (this._isOAuthStore(this._store)) {
|
|
1200
|
+
const session = {
|
|
1201
|
+
codeVerifier,
|
|
1202
|
+
state: this._pendingAuthState
|
|
1203
|
+
};
|
|
1204
|
+
await this._store.setSession(this._storeKey, session);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async codeVerifier() {
|
|
1208
|
+
if (!this._codeVerifier) {
|
|
1209
|
+
throw new Error("Code verifier not found");
|
|
1210
|
+
}
|
|
1211
|
+
return this._codeVerifier;
|
|
1212
|
+
}
|
|
1213
|
+
async invalidateCredentials(scope) {
|
|
1214
|
+
if (scope === "all" && this._isExchangingCode) {
|
|
1215
|
+
this._tokens = undefined;
|
|
1216
|
+
await this._store.delete(this._storeKey);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (this._isExchangingCode && (scope === "client" || scope === "all")) {
|
|
1220
|
+
this._isExchangingCode = false;
|
|
1221
|
+
}
|
|
1222
|
+
switch (scope) {
|
|
1223
|
+
case "all":
|
|
1224
|
+
this._clientInfo = undefined;
|
|
1225
|
+
this._tokens = undefined;
|
|
1226
|
+
this._codeVerifier = undefined;
|
|
1227
|
+
this._tokensLoaded = false;
|
|
1228
|
+
await this._store.clear();
|
|
1229
|
+
break;
|
|
1230
|
+
case "client":
|
|
1231
|
+
this._clientInfo = undefined;
|
|
1232
|
+
if (this._isOAuthStore(this._store)) {
|
|
1233
|
+
await this._store.setClient(this._storeKey, {
|
|
1234
|
+
clientId: ""
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
break;
|
|
1238
|
+
case "tokens":
|
|
1239
|
+
this._tokens = undefined;
|
|
1240
|
+
await this._store.delete(this._storeKey);
|
|
1241
|
+
break;
|
|
1242
|
+
case "verifier":
|
|
1243
|
+
this._codeVerifier = undefined;
|
|
1244
|
+
if (this._isOAuthStore(this._store)) {
|
|
1245
|
+
await this._store.setSession(this._storeKey, {});
|
|
1246
|
+
}
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async validateResourceURL(_serverUrl, _resource) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
getPendingAuthCode() {
|
|
1254
|
+
if (this._pendingAuthCode) {
|
|
1255
|
+
const result = {
|
|
1256
|
+
code: this._pendingAuthCode,
|
|
1257
|
+
state: this._pendingAuthState
|
|
1258
|
+
};
|
|
1259
|
+
this._isExchangingCode = true;
|
|
1260
|
+
this._pendingAuthCode = undefined;
|
|
1261
|
+
this._pendingAuthState = undefined;
|
|
1262
|
+
return result;
|
|
1263
|
+
}
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
async _refreshTokens() {
|
|
1267
|
+
if (this._refreshInProgress) {
|
|
1268
|
+
await this._refreshInProgress;
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
this._refreshInProgress = this._doRefreshTokens();
|
|
1272
|
+
try {
|
|
1273
|
+
await this._refreshInProgress;
|
|
1274
|
+
} finally {
|
|
1275
|
+
this._refreshInProgress = undefined;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async _doRefreshTokens() {
|
|
1279
|
+
if (!this._tokens?.refresh_token) {
|
|
1280
|
+
throw new Error("No refresh token available");
|
|
1281
|
+
}
|
|
1282
|
+
const clientInfo = await this.clientInformation();
|
|
1283
|
+
if (!clientInfo?.client_id) {
|
|
1284
|
+
throw new Error("No client information available for refresh");
|
|
1285
|
+
}
|
|
1286
|
+
throw new Error("Token refresh not yet implemented - requires token endpoint URL");
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
// src/storage/file.ts
|
|
1290
|
+
import * as fs6 from "node:fs/promises";
|
|
1291
|
+
import * as path2 from "node:path";
|
|
1292
|
+
import * as os2 from "node:os";
|
|
1293
|
+
function fileStore(filepath) {
|
|
1294
|
+
const file = filepath ?? path2.join(os2.homedir(), ".mcp", "tokens.json");
|
|
1295
|
+
async function ensureDir() {
|
|
1296
|
+
await fs6.mkdir(path2.dirname(file), { recursive: true });
|
|
1297
|
+
}
|
|
1298
|
+
async function readStore() {
|
|
1299
|
+
try {
|
|
1300
|
+
const data = await fs6.readFile(file, "utf-8");
|
|
1301
|
+
return JSON.parse(data);
|
|
1302
|
+
} catch {
|
|
1303
|
+
return {};
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async function writeStore(data) {
|
|
1307
|
+
await ensureDir();
|
|
1308
|
+
await fs6.writeFile(file, JSON.stringify(data, null, 2), "utf-8");
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
async get(key) {
|
|
1312
|
+
const store = await readStore();
|
|
1313
|
+
return store[key] ?? null;
|
|
1314
|
+
},
|
|
1315
|
+
async set(key, tokens) {
|
|
1316
|
+
const store = await readStore();
|
|
1317
|
+
store[key] = tokens;
|
|
1318
|
+
await writeStore(store);
|
|
1319
|
+
},
|
|
1320
|
+
async delete(key) {
|
|
1321
|
+
const store = await readStore();
|
|
1322
|
+
delete store[key];
|
|
1323
|
+
await writeStore(store);
|
|
1324
|
+
},
|
|
1325
|
+
async clear() {
|
|
1326
|
+
await writeStore({});
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
949
1330
|
|
|
950
1331
|
// src/index.ts
|
|
951
1332
|
async function getAuthCode(input) {
|
|
@@ -994,6 +1375,9 @@ async function getAuthCode(input) {
|
|
|
994
1375
|
}
|
|
995
1376
|
}
|
|
996
1377
|
export {
|
|
1378
|
+
inMemoryStore,
|
|
997
1379
|
getAuthCode,
|
|
1380
|
+
fileStore,
|
|
1381
|
+
browserAuth,
|
|
998
1382
|
OAuthError
|
|
999
1383
|
};
|