mcp-oauth-provider 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +668 -0
- package/dist/__tests__/config.test.js +56 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration.test.js +341 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/oauth-flow.test.js +201 -0
- package/dist/__tests__/oauth-flow.test.js.map +1 -0
- package/dist/__tests__/server.test.js +271 -0
- package/dist/__tests__/server.test.js.map +1 -0
- package/dist/__tests__/storage.test.js +256 -0
- package/dist/__tests__/storage.test.js.map +1 -0
- package/dist/client/config.js +30 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/factory.js +16 -0
- package/dist/client/factory.js.map +1 -0
- package/dist/client/index.js +237 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/oauth-flow.js +73 -0
- package/dist/client/oauth-flow.js.map +1 -0
- package/dist/client/storage.js +237 -0
- package/dist/client/storage.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/server/callback.js +164 -0
- package/dist/server/callback.js.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/templates.js +245 -0
- package/dist/server/templates.js.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/config.test.ts +78 -0
- package/src/__tests__/integration.test.ts +398 -0
- package/src/__tests__/oauth-flow.test.ts +276 -0
- package/src/__tests__/server.test.ts +391 -0
- package/src/__tests__/storage.test.ts +329 -0
- package/src/client/config.ts +134 -0
- package/src/client/factory.ts +19 -0
- package/src/client/index.ts +361 -0
- package/src/client/oauth-flow.ts +115 -0
- package/src/client/storage.ts +335 -0
- package/src/index.ts +31 -0
- package/src/server/callback.ts +257 -0
- package/src/server/index.ts +21 -0
- package/src/server/templates.ts +271 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
/**
|
2
|
+
* Default OAuth client metadata
|
3
|
+
*/ export const DEFAULT_CLIENT_METADATA = {
|
4
|
+
redirect_uris: [],
|
5
|
+
grant_types: [
|
6
|
+
'authorization_code',
|
7
|
+
'refresh_token'
|
8
|
+
],
|
9
|
+
response_types: [
|
10
|
+
'code'
|
11
|
+
],
|
12
|
+
token_endpoint_auth_method: 'client_secret_post',
|
13
|
+
scope: 'openid profile email',
|
14
|
+
client_name: 'MCP OAuth Client',
|
15
|
+
client_uri: 'https://github.com/modelcontextprotocol/typescript-sdk'
|
16
|
+
};
|
17
|
+
/**
|
18
|
+
* Generate a random session ID
|
19
|
+
*/ export function generateSessionId() {
|
20
|
+
return crypto.randomUUID();
|
21
|
+
}
|
22
|
+
/**
|
23
|
+
* Generate a random state parameter for CSRF protection
|
24
|
+
*/ export function generateState() {
|
25
|
+
const array = new Uint8Array(32);
|
26
|
+
crypto.getRandomValues(array);
|
27
|
+
return Array.from(array, (byte)=>byte.toString(16).padStart(2, '0')).join('');
|
28
|
+
}
|
29
|
+
|
30
|
+
//# sourceMappingURL=config.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["../../src/client/config.ts"],"sourcesContent":["import type {\n OAuthClientInformation,\n OAuthClientInformationFull,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js';\n\nexport type {\n OAuthClientInformation,\n OAuthClientInformationFull,\n OAuthClientMetadata,\n OAuthTokens,\n};\n\n/**\n * Storage adapter interface for persisting OAuth data\n */\nexport interface StorageAdapter {\n /**\n * Get a value by key\n */\n get(key: string): Promise<string | undefined> | string | undefined;\n\n /**\n * Set a value by key\n */\n set(key: string, value: string): Promise<void> | void;\n\n /**\n * Delete a value by key\n */\n delete(key: string): Promise<void> | void;\n}\n\n/**\n * Configuration for OAuth client provider\n */\nexport interface OAuthConfig {\n /**\n * OAuth client ID (can be provided or dynamically registered)\n */\n clientId?: string;\n\n /**\n * OAuth client secret (optional for public clients)\n */\n clientSecret?: string;\n\n /**\n * Redirect URI for OAuth callbacks\n */\n redirectUri: string;\n\n /**\n * OAuth scope to request\n */\n scope?: string;\n\n /**\n * Session identifier for this OAuth client instance\n */\n sessionId?: string;\n\n /**\n * Storage adapter for persisting OAuth data\n */\n storage?: StorageAdapter;\n\n /**\n * OAuth client metadata for registration\n */\n clientMetadata?: Partial<OAuthClientMetadata>;\n\n /**\n * OAuth tokens (can be provided statically or loaded from storage)\n */\n tokens?: OAuthTokens;\n\n /**\n * Token refresh configuration\n */\n tokenRefresh?: {\n /**\n * Maximum number of retry attempts for token refresh\n */\n maxRetries?: number;\n\n /**\n * Delay between retry attempts in milliseconds\n */\n retryDelay?: number;\n };\n\n /**\n * Authorization server metadata (can be provided or obtained during auth flow)\n */\n authorizationServerMetadata?: {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n [key: string]: unknown;\n };\n}\n\n/**\n * Default OAuth client metadata\n */\nexport const DEFAULT_CLIENT_METADATA: OAuthClientMetadata = {\n redirect_uris: [],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n token_endpoint_auth_method: 'client_secret_post',\n scope: 'openid profile email',\n client_name: 'MCP OAuth Client',\n client_uri: 'https://github.com/modelcontextprotocol/typescript-sdk',\n};\n\n/**\n * Generate a random session ID\n */\nexport function generateSessionId(): string {\n return crypto.randomUUID();\n}\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nexport function generateState(): string {\n const array = new Uint8Array(32);\n\n crypto.getRandomValues(array);\n\n return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');\n}\n"],"names":["DEFAULT_CLIENT_METADATA","redirect_uris","grant_types","response_types","token_endpoint_auth_method","scope","client_name","client_uri","generateSessionId","crypto","randomUUID","generateState","array","Uint8Array","getRandomValues","Array","from","byte","toString","padStart","join"],"mappings":"AAwGA;;CAEC,GACD,OAAO,MAAMA,0BAA+C;IAC1DC,eAAe,EAAE;IACjBC,aAAa;QAAC;QAAsB;KAAgB;IACpDC,gBAAgB;QAAC;KAAO;IACxBC,4BAA4B;IAC5BC,OAAO;IACPC,aAAa;IACbC,YAAY;AACd,EAAE;AAEF;;CAEC,GACD,OAAO,SAASC;IACd,OAAOC,OAAOC,UAAU;AAC1B;AAEA;;CAEC,GACD,OAAO,SAASC;IACd,MAAMC,QAAQ,IAAIC,WAAW;IAE7BJ,OAAOK,eAAe,CAACF;IAEvB,OAAOG,MAAMC,IAAI,CAACJ,OAAOK,CAAAA,OAAQA,KAAKC,QAAQ,CAAC,IAAIC,QAAQ,CAAC,GAAG,MAAMC,IAAI,CAAC;AAC5E"}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { MCPOAuthClientProvider } from './index.js';
|
2
|
+
/**
|
3
|
+
* Factory function to create an OAuth client provider
|
4
|
+
*
|
5
|
+
* @example
|
6
|
+
* ```typescript
|
7
|
+
* const provider = createOAuthProvider({
|
8
|
+
* redirectUri: 'http://localhost:8080/callback',
|
9
|
+
* scope: 'openid profile email',
|
10
|
+
* });
|
11
|
+
* ```
|
12
|
+
*/ export function createOAuthProvider(config) {
|
13
|
+
return new MCPOAuthClientProvider(config);
|
14
|
+
}
|
15
|
+
|
16
|
+
//# sourceMappingURL=factory.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["../../src/client/factory.ts"],"sourcesContent":["import type { OAuthConfig } from './config.js';\nimport { MCPOAuthClientProvider } from './index.js';\n\n/**\n * Factory function to create an OAuth client provider\n *\n * @example\n * ```typescript\n * const provider = createOAuthProvider({\n * redirectUri: 'http://localhost:8080/callback',\n * scope: 'openid profile email',\n * });\n * ```\n */\nexport function createOAuthProvider(\n config: OAuthConfig\n): MCPOAuthClientProvider {\n return new MCPOAuthClientProvider(config);\n}\n"],"names":["MCPOAuthClientProvider","createOAuthProvider","config"],"mappings":"AACA,SAASA,sBAAsB,QAAQ,aAAa;AAEpD;;;;;;;;;;CAUC,GACD,OAAO,SAASC,oBACdC,MAAmB;IAEnB,OAAO,IAAIF,uBAAuBE;AACpC"}
|
@@ -0,0 +1,237 @@
|
|
1
|
+
import { _ as _define_property } from "@swc/helpers/_/_define_property";
|
2
|
+
import { DEFAULT_CLIENT_METADATA, generateSessionId, generateState } from './config.js';
|
3
|
+
import { areTokensExpired, refreshTokensWithRetry } from './oauth-flow.js';
|
4
|
+
import { MemoryStorage, OAuthStorage } from './storage.js';
|
5
|
+
export class MCPOAuthClientProvider {
|
6
|
+
/**
|
7
|
+
* The URL to redirect the user agent to after authorization
|
8
|
+
*/ get redirectUrl() {
|
9
|
+
return this.config.redirectUri;
|
10
|
+
}
|
11
|
+
/**
|
12
|
+
* Metadata about this OAuth client
|
13
|
+
*/ get clientMetadata() {
|
14
|
+
return this.config.clientMetadata;
|
15
|
+
}
|
16
|
+
/**
|
17
|
+
* Returns a OAuth2 state parameter for CSRF protection
|
18
|
+
*/ async state() {
|
19
|
+
return generateState();
|
20
|
+
}
|
21
|
+
/**
|
22
|
+
* Loads information about this OAuth client
|
23
|
+
* Config credentials are initialized into storage, so all reads go through storage
|
24
|
+
*/ async clientInformation() {
|
25
|
+
return await this.oauthStorage.getClientInfo();
|
26
|
+
}
|
27
|
+
/**
|
28
|
+
* Saves client information after dynamic registration
|
29
|
+
*/ async saveClientInformation(clientInformation) {
|
30
|
+
this.cachedClientInformation = clientInformation;
|
31
|
+
await this.oauthStorage.saveClientInfo(clientInformation);
|
32
|
+
}
|
33
|
+
/**
|
34
|
+
* Loads any existing OAuth tokens for the current session
|
35
|
+
* Automatically refreshes tokens if they're expired or about to expire (within 5 minutes)
|
36
|
+
* Requires authorizationServerMetadata to be set (from auth flow) for automatic refresh
|
37
|
+
*/ async tokens() {
|
38
|
+
const currentTokens = await this.oauthStorage.getTokens();
|
39
|
+
if (!currentTokens) {
|
40
|
+
return undefined;
|
41
|
+
}
|
42
|
+
// Check if tokens are expired or about to expire (within 5 minutes)
|
43
|
+
const needsRefresh = areTokensExpired(currentTokens, undefined, 300);
|
44
|
+
// If tokens need refresh and we have the server metadata and refresh token, refresh automatically
|
45
|
+
if (needsRefresh && currentTokens.refresh_token && this.authorizationServerMetadata?.token_endpoint) {
|
46
|
+
try {
|
47
|
+
// Use the token endpoint from authorization server metadata
|
48
|
+
const newTokens = await this.refreshTokens();
|
49
|
+
return newTokens;
|
50
|
+
} catch (error) {
|
51
|
+
// If refresh fails, return the current tokens and let the caller handle it
|
52
|
+
// This prevents breaking existing flows that might handle refresh differently
|
53
|
+
return currentTokens;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
return currentTokens;
|
57
|
+
}
|
58
|
+
/**
|
59
|
+
* Refresh tokens using helper function
|
60
|
+
* Uses the token endpoint from authorizationServerMetadata
|
61
|
+
*
|
62
|
+
* @returns New OAuth tokens
|
63
|
+
* @throws Error if no authorization server metadata is available
|
64
|
+
*/ async refreshTokens() {
|
65
|
+
if (!this.authorizationServerMetadata?.token_endpoint) {
|
66
|
+
throw new Error('No authorization server metadata available. Cannot refresh tokens without token_endpoint.');
|
67
|
+
}
|
68
|
+
const { maxRetries, retryDelay } = this.config.tokenRefresh;
|
69
|
+
const currentTokens = await this.oauthStorage.getTokens();
|
70
|
+
if (!currentTokens?.refresh_token) {
|
71
|
+
throw new Error('No refresh token available');
|
72
|
+
}
|
73
|
+
const clientInfo = await this.clientInformation();
|
74
|
+
if (!clientInfo) {
|
75
|
+
throw new Error('No client information available');
|
76
|
+
}
|
77
|
+
try {
|
78
|
+
// Use helper function for retry logic with token endpoint
|
79
|
+
const newTokens = await refreshTokensWithRetry(this.authorizationServerMetadata.token_endpoint, clientInfo, currentTokens.refresh_token, this.addClientAuthentication, maxRetries, retryDelay);
|
80
|
+
// Save the new tokens
|
81
|
+
await this.oauthStorage.saveTokens(newTokens);
|
82
|
+
return newTokens;
|
83
|
+
} catch (error) {
|
84
|
+
// All retries failed, invalidate tokens
|
85
|
+
await this.invalidateCredentials('tokens');
|
86
|
+
throw error;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
/**
|
90
|
+
* Loads any existing OAuth tokens for the current session (without auto-refresh)
|
91
|
+
* Use this if you want to check tokens without triggering a refresh
|
92
|
+
*/ async getStoredTokens() {
|
93
|
+
return this.oauthStorage.getTokens();
|
94
|
+
}
|
95
|
+
/**
|
96
|
+
* Stores new OAuth tokens for the current session
|
97
|
+
*/ async saveTokens(tokens) {
|
98
|
+
await this.oauthStorage.saveTokens(tokens);
|
99
|
+
}
|
100
|
+
/**
|
101
|
+
* Invoked to redirect the user agent to the given URL to begin the authorization flow
|
102
|
+
*/ async redirectToAuthorization(authorizationUrl) {
|
103
|
+
// In a real implementation, this would open a browser or redirect the user
|
104
|
+
// For now, we'll let the caller handle the redirect by throwing an error with the URL
|
105
|
+
// In a Bun environment, we could automatically open the browser:
|
106
|
+
if (typeof Bun !== 'undefined') {
|
107
|
+
try {
|
108
|
+
await Bun.$`open ${authorizationUrl.toString()}`;
|
109
|
+
return;
|
110
|
+
} catch {
|
111
|
+
// Fallback if 'open' command is not available
|
112
|
+
}
|
113
|
+
}
|
114
|
+
// If we can't automatically open, throw an error with the URL for the caller to handle
|
115
|
+
throw new Error(`Please navigate to: ${authorizationUrl.toString()}`);
|
116
|
+
}
|
117
|
+
/**
|
118
|
+
* Saves a PKCE code verifier for the current session
|
119
|
+
*/ async saveCodeVerifier(codeVerifier) {
|
120
|
+
await this.oauthStorage.saveCodeVerifier(codeVerifier);
|
121
|
+
}
|
122
|
+
/**
|
123
|
+
* Loads the PKCE code verifier for the current session
|
124
|
+
*/ async codeVerifier() {
|
125
|
+
const verifier = await this.oauthStorage.getCodeVerifier();
|
126
|
+
if (!verifier) {
|
127
|
+
throw new Error('No code verifier found for current session');
|
128
|
+
}
|
129
|
+
return verifier;
|
130
|
+
}
|
131
|
+
/**
|
132
|
+
* Validates the resource URL for OAuth requests
|
133
|
+
*/ async validateResourceURL(serverUrl, resource) {
|
134
|
+
// Simple validation - in a real implementation you might want more sophisticated logic
|
135
|
+
if (resource) {
|
136
|
+
try {
|
137
|
+
return new URL(resource);
|
138
|
+
} catch {
|
139
|
+
throw new Error(`Invalid resource URL: ${resource}`);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
// Default to server URL if no specific resource is provided
|
143
|
+
return new URL(serverUrl);
|
144
|
+
}
|
145
|
+
/**
|
146
|
+
* Invalidates stored credentials based on the specified scope
|
147
|
+
*/ async invalidateCredentials(scope) {
|
148
|
+
switch(scope){
|
149
|
+
case 'all':
|
150
|
+
await this.oauthStorage.clearAll();
|
151
|
+
break;
|
152
|
+
case 'client':
|
153
|
+
await this.oauthStorage.clearClientInfo();
|
154
|
+
break;
|
155
|
+
case 'tokens':
|
156
|
+
await this.oauthStorage.clearTokens();
|
157
|
+
break;
|
158
|
+
case 'verifier':
|
159
|
+
await this.oauthStorage.clearCodeVerifier();
|
160
|
+
break;
|
161
|
+
}
|
162
|
+
}
|
163
|
+
/**
|
164
|
+
* Get the current session ID
|
165
|
+
*/ getSessionId() {
|
166
|
+
return this.sessionId;
|
167
|
+
}
|
168
|
+
/**
|
169
|
+
* Get the OAuth storage helper
|
170
|
+
*/ getOAuthStorage() {
|
171
|
+
return this.oauthStorage;
|
172
|
+
}
|
173
|
+
/**
|
174
|
+
* Clear the current session
|
175
|
+
*/ async clearSession() {
|
176
|
+
await this.oauthStorage.clearSession();
|
177
|
+
}
|
178
|
+
constructor(config){
|
179
|
+
_define_property(this, "config", void 0);
|
180
|
+
_define_property(this, "oauthStorage", void 0);
|
181
|
+
_define_property(this, "sessionId", void 0);
|
182
|
+
_define_property(this, "cachedClientInformation", void 0);
|
183
|
+
_define_property(this, "authorizationServerMetadata", void 0);
|
184
|
+
/**
|
185
|
+
* Adds custom client authentication to OAuth token requests
|
186
|
+
*/ _define_property(this, "addClientAuthentication", (headers, params, url, metadata)=>{
|
187
|
+
const clientInfo = this.cachedClientInformation;
|
188
|
+
this.authorizationServerMetadata = metadata;
|
189
|
+
if (!clientInfo) {
|
190
|
+
throw new Error('No client information available for authentication');
|
191
|
+
}
|
192
|
+
// Use client_secret_post method by default
|
193
|
+
params.set('client_id', clientInfo.client_id);
|
194
|
+
if (clientInfo.client_secret) {
|
195
|
+
params.set('client_secret', clientInfo.client_secret);
|
196
|
+
}
|
197
|
+
});
|
198
|
+
this.sessionId = config.sessionId ?? generateSessionId();
|
199
|
+
const storage = config.storage ?? new MemoryStorage();
|
200
|
+
// Set authorization server metadata if provided
|
201
|
+
if (config.authorizationServerMetadata) {
|
202
|
+
this.authorizationServerMetadata = config.authorizationServerMetadata;
|
203
|
+
}
|
204
|
+
this.oauthStorage = new OAuthStorage(storage, this.sessionId, {
|
205
|
+
staticClientInfo: config.clientId ? {
|
206
|
+
client_id: config.clientId,
|
207
|
+
client_secret: config.clientSecret
|
208
|
+
} : undefined,
|
209
|
+
initialTokens: config.tokens
|
210
|
+
});
|
211
|
+
// Merge with defaults
|
212
|
+
this.config = {
|
213
|
+
clientId: config.clientId ?? '',
|
214
|
+
clientSecret: config.clientSecret ?? undefined,
|
215
|
+
redirectUri: config.redirectUri,
|
216
|
+
scope: config.scope ?? 'read write',
|
217
|
+
sessionId: this.sessionId,
|
218
|
+
storage,
|
219
|
+
tokens: config.tokens,
|
220
|
+
clientMetadata: {
|
221
|
+
...DEFAULT_CLIENT_METADATA,
|
222
|
+
...config.clientMetadata ?? {},
|
223
|
+
redirect_uris: [
|
224
|
+
config.redirectUri
|
225
|
+
],
|
226
|
+
scope: config.scope ?? DEFAULT_CLIENT_METADATA.scope
|
227
|
+
},
|
228
|
+
tokenRefresh: {
|
229
|
+
maxRetries: 3,
|
230
|
+
retryDelay: 1000,
|
231
|
+
...config.tokenRefresh ?? {}
|
232
|
+
}
|
233
|
+
};
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
//# sourceMappingURL=index.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts"],"sourcesContent":["import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';\nimport { AuthorizationServerMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';\nimport type {\n OAuthClientInformation,\n OAuthClientInformationFull,\n OAuthClientMetadata,\n OAuthConfig,\n OAuthTokens,\n} from './config.js';\nimport {\n DEFAULT_CLIENT_METADATA,\n generateSessionId,\n generateState,\n} from './config.js';\nimport { areTokensExpired, refreshTokensWithRetry } from './oauth-flow.js';\nimport { MemoryStorage, OAuthStorage } from './storage.js';\n\n/**\n * Options for token refresh\n */\ninterface TokenRefreshConfig {\n maxRetries: number;\n retryDelay: number;\n}\n\n/**\n * MCP OAuth Client Provider implementation with automatic token refresh\n */\ninterface ProcessedOAuthConfig\n extends Omit<\n Required<OAuthConfig>,\n 'clientSecret' | 'tokens' | 'authorizationServerMetadata'\n > {\n clientSecret?: string;\n tokens?: OAuthTokens;\n clientMetadata: OAuthClientMetadata;\n tokenRefresh: TokenRefreshConfig;\n}\n\nexport class MCPOAuthClientProvider implements OAuthClientProvider {\n private readonly config: ProcessedOAuthConfig;\n private readonly oauthStorage: OAuthStorage;\n private readonly sessionId: string;\n private cachedClientInformation?: OAuthClientInformationFull;\n\n public authorizationServerMetadata?: AuthorizationServerMetadata;\n\n constructor(config: OAuthConfig) {\n this.sessionId = config.sessionId ?? generateSessionId();\n const storage = config.storage ?? new MemoryStorage();\n\n // Set authorization server metadata if provided\n if (config.authorizationServerMetadata) {\n this.authorizationServerMetadata =\n config.authorizationServerMetadata as AuthorizationServerMetadata;\n }\n\n this.oauthStorage = new OAuthStorage(storage, this.sessionId, {\n staticClientInfo: config.clientId\n ? {\n client_id: config.clientId,\n client_secret: config.clientSecret,\n }\n : undefined,\n initialTokens: config.tokens,\n });\n\n // Merge with defaults\n this.config = {\n clientId: config.clientId ?? '',\n clientSecret: config.clientSecret ?? undefined,\n redirectUri: config.redirectUri,\n scope: config.scope ?? 'read write',\n sessionId: this.sessionId,\n storage,\n tokens: config.tokens,\n clientMetadata: {\n ...DEFAULT_CLIENT_METADATA,\n ...(config.clientMetadata ?? {}),\n redirect_uris: [config.redirectUri], // Always required\n scope: config.scope ?? DEFAULT_CLIENT_METADATA.scope,\n },\n tokenRefresh: {\n maxRetries: 3,\n retryDelay: 1000,\n ...(config.tokenRefresh ?? {}),\n },\n };\n }\n\n /**\n * The URL to redirect the user agent to after authorization\n */\n get redirectUrl(): string | URL {\n return this.config.redirectUri;\n }\n\n /**\n * Metadata about this OAuth client\n */\n get clientMetadata(): OAuthClientMetadata {\n return this.config.clientMetadata;\n }\n\n /**\n * Returns a OAuth2 state parameter for CSRF protection\n */\n async state(): Promise<string> {\n return generateState();\n }\n\n /**\n * Loads information about this OAuth client\n * Config credentials are initialized into storage, so all reads go through storage\n */\n async clientInformation(): Promise<OAuthClientInformation | undefined> {\n return await this.oauthStorage.getClientInfo();\n }\n\n /**\n * Saves client information after dynamic registration\n */\n async saveClientInformation(\n clientInformation: OAuthClientInformationFull\n ): Promise<void> {\n this.cachedClientInformation = clientInformation;\n\n await this.oauthStorage.saveClientInfo(clientInformation);\n }\n\n /**\n * Loads any existing OAuth tokens for the current session\n * Automatically refreshes tokens if they're expired or about to expire (within 5 minutes)\n * Requires authorizationServerMetadata to be set (from auth flow) for automatic refresh\n */\n async tokens(): Promise<OAuthTokens | undefined> {\n const currentTokens = await this.oauthStorage.getTokens();\n\n if (!currentTokens) {\n return undefined;\n }\n\n // Check if tokens are expired or about to expire (within 5 minutes)\n const needsRefresh = areTokensExpired(currentTokens, undefined, 300);\n\n // If tokens need refresh and we have the server metadata and refresh token, refresh automatically\n if (\n needsRefresh &&\n currentTokens.refresh_token &&\n this.authorizationServerMetadata?.token_endpoint\n ) {\n try {\n // Use the token endpoint from authorization server metadata\n const newTokens = await this.refreshTokens();\n\n return newTokens;\n } catch (error) {\n // If refresh fails, return the current tokens and let the caller handle it\n // This prevents breaking existing flows that might handle refresh differently\n return currentTokens;\n }\n }\n\n return currentTokens;\n }\n\n /**\n * Refresh tokens using helper function\n * Uses the token endpoint from authorizationServerMetadata\n *\n * @returns New OAuth tokens\n * @throws Error if no authorization server metadata is available\n */\n async refreshTokens(): Promise<OAuthTokens> {\n if (!this.authorizationServerMetadata?.token_endpoint) {\n throw new Error(\n 'No authorization server metadata available. Cannot refresh tokens without token_endpoint.'\n );\n }\n\n const { maxRetries, retryDelay } = this.config.tokenRefresh;\n\n const currentTokens = await this.oauthStorage.getTokens();\n\n if (!currentTokens?.refresh_token) {\n throw new Error('No refresh token available');\n }\n\n const clientInfo = await this.clientInformation();\n\n if (!clientInfo) {\n throw new Error('No client information available');\n }\n\n try {\n // Use helper function for retry logic with token endpoint\n const newTokens = await refreshTokensWithRetry(\n this.authorizationServerMetadata.token_endpoint,\n clientInfo,\n currentTokens.refresh_token,\n this.addClientAuthentication,\n maxRetries,\n retryDelay\n );\n\n // Save the new tokens\n await this.oauthStorage.saveTokens(newTokens);\n\n return newTokens;\n } catch (error) {\n // All retries failed, invalidate tokens\n await this.invalidateCredentials('tokens');\n throw error;\n }\n }\n\n /**\n * Loads any existing OAuth tokens for the current session (without auto-refresh)\n * Use this if you want to check tokens without triggering a refresh\n */\n async getStoredTokens(): Promise<OAuthTokens | undefined> {\n return this.oauthStorage.getTokens();\n }\n\n /**\n * Stores new OAuth tokens for the current session\n */\n async saveTokens(tokens: OAuthTokens): Promise<void> {\n await this.oauthStorage.saveTokens(tokens);\n }\n\n /**\n * Invoked to redirect the user agent to the given URL to begin the authorization flow\n */\n async redirectToAuthorization(authorizationUrl: URL): Promise<void> {\n // In a real implementation, this would open a browser or redirect the user\n // For now, we'll let the caller handle the redirect by throwing an error with the URL\n\n // In a Bun environment, we could automatically open the browser:\n if (typeof Bun !== 'undefined') {\n try {\n await Bun.$`open ${authorizationUrl.toString()}`;\n\n return;\n } catch {\n // Fallback if 'open' command is not available\n }\n }\n\n // If we can't automatically open, throw an error with the URL for the caller to handle\n throw new Error(`Please navigate to: ${authorizationUrl.toString()}`);\n }\n\n /**\n * Saves a PKCE code verifier for the current session\n */\n async saveCodeVerifier(codeVerifier: string): Promise<void> {\n await this.oauthStorage.saveCodeVerifier(codeVerifier);\n }\n\n /**\n * Loads the PKCE code verifier for the current session\n */\n async codeVerifier(): Promise<string> {\n const verifier = await this.oauthStorage.getCodeVerifier();\n\n if (!verifier) {\n throw new Error('No code verifier found for current session');\n }\n\n return verifier;\n }\n\n /**\n * Adds custom client authentication to OAuth token requests\n */\n addClientAuthentication = (\n headers: Headers,\n params: URLSearchParams,\n url: string | URL,\n metadata?: AuthorizationServerMetadata\n ): void => {\n const clientInfo = this.cachedClientInformation;\n\n this.authorizationServerMetadata = metadata;\n\n if (!clientInfo) {\n throw new Error('No client information available for authentication');\n }\n\n // Use client_secret_post method by default\n params.set('client_id', clientInfo.client_id);\n\n if (clientInfo.client_secret) {\n params.set('client_secret', clientInfo.client_secret);\n }\n };\n\n /**\n * Validates the resource URL for OAuth requests\n */\n async validateResourceURL(\n serverUrl: string | URL,\n resource?: string\n ): Promise<URL | undefined> {\n // Simple validation - in a real implementation you might want more sophisticated logic\n if (resource) {\n try {\n return new URL(resource);\n } catch {\n throw new Error(`Invalid resource URL: ${resource}`);\n }\n }\n\n // Default to server URL if no specific resource is provided\n return new URL(serverUrl);\n }\n\n /**\n * Invalidates stored credentials based on the specified scope\n */\n async invalidateCredentials(\n scope: 'all' | 'client' | 'tokens' | 'verifier'\n ): Promise<void> {\n switch (scope) {\n case 'all':\n await this.oauthStorage.clearAll();\n break;\n case 'client':\n await this.oauthStorage.clearClientInfo();\n break;\n case 'tokens':\n await this.oauthStorage.clearTokens();\n break;\n case 'verifier':\n await this.oauthStorage.clearCodeVerifier();\n break;\n }\n }\n\n /**\n * Get the current session ID\n */\n getSessionId(): string {\n return this.sessionId;\n }\n\n /**\n * Get the OAuth storage helper\n */\n getOAuthStorage(): OAuthStorage {\n return this.oauthStorage;\n }\n\n /**\n * Clear the current session\n */\n async clearSession(): Promise<void> {\n await this.oauthStorage.clearSession();\n }\n}\n"],"names":["DEFAULT_CLIENT_METADATA","generateSessionId","generateState","areTokensExpired","refreshTokensWithRetry","MemoryStorage","OAuthStorage","MCPOAuthClientProvider","redirectUrl","config","redirectUri","clientMetadata","state","clientInformation","oauthStorage","getClientInfo","saveClientInformation","cachedClientInformation","saveClientInfo","tokens","currentTokens","getTokens","undefined","needsRefresh","refresh_token","authorizationServerMetadata","token_endpoint","newTokens","refreshTokens","error","Error","maxRetries","retryDelay","tokenRefresh","clientInfo","addClientAuthentication","saveTokens","invalidateCredentials","getStoredTokens","redirectToAuthorization","authorizationUrl","Bun","$","toString","saveCodeVerifier","codeVerifier","verifier","getCodeVerifier","validateResourceURL","serverUrl","resource","URL","scope","clearAll","clearClientInfo","clearTokens","clearCodeVerifier","getSessionId","sessionId","getOAuthStorage","clearSession","headers","params","url","metadata","set","client_id","client_secret","storage","staticClientInfo","clientId","clientSecret","initialTokens","redirect_uris"],"mappings":"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"}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
/**
|
2
|
+
* OAuth Flow Helpers
|
3
|
+
*
|
4
|
+
* Pure utility functions for OAuth operations
|
5
|
+
*/ import { refreshAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';
|
6
|
+
/**
|
7
|
+
* Check if tokens are expired or about to expire
|
8
|
+
*
|
9
|
+
* @param tokens - OAuth tokens to check
|
10
|
+
* @param tokenExpiryTime - Tracked expiry timestamp (if available)
|
11
|
+
* @param bufferSeconds - Number of seconds before expiry to consider expired (default: 300 = 5 minutes)
|
12
|
+
* @returns true if tokens are expired or will expire soon
|
13
|
+
*/ export function areTokensExpired(tokens, tokenExpiryTime, bufferSeconds = 300) {
|
14
|
+
if (!tokens) {
|
15
|
+
return true;
|
16
|
+
}
|
17
|
+
// If we have a tracked expiry time, use it (most accurate)
|
18
|
+
if (tokenExpiryTime) {
|
19
|
+
const now = Date.now();
|
20
|
+
return now >= tokenExpiryTime - bufferSeconds * 1000;
|
21
|
+
}
|
22
|
+
// If no expiry info, assume tokens are valid
|
23
|
+
if (tokens.expires_in === undefined) {
|
24
|
+
return false;
|
25
|
+
}
|
26
|
+
// Check if tokens will expire within the buffer period
|
27
|
+
// Note: expires_in is now derived from stored expires_at, so it's always accurate
|
28
|
+
return tokens.expires_in <= bufferSeconds;
|
29
|
+
}
|
30
|
+
/**
|
31
|
+
* Refresh OAuth tokens with retry logic
|
32
|
+
*
|
33
|
+
* @param serverUrl - Authorization server URL
|
34
|
+
* @param clientInfo - Client information
|
35
|
+
* @param refreshToken - Current refresh token
|
36
|
+
* @param addClientAuth - Client authentication function
|
37
|
+
* @param maxRetries - Maximum retry attempts (default: 3)
|
38
|
+
* @param retryDelay - Base delay between retries in ms (default: 1000)
|
39
|
+
* @param fetchFn - Optional custom fetch function
|
40
|
+
* @returns New OAuth tokens
|
41
|
+
* @throws Error if refresh fails after all retries
|
42
|
+
*/ export async function refreshTokensWithRetry(serverUrl, clientInfo, refreshToken, addClientAuth, maxRetries = 3, retryDelay = 1000, fetchFn) {
|
43
|
+
let lastError;
|
44
|
+
/* eslint-disable no-await-in-loop */ for(let attempt = 0; attempt < maxRetries; attempt++){
|
45
|
+
try {
|
46
|
+
// Attempt token refresh using SDK function
|
47
|
+
const newTokens = await refreshAuthorization(serverUrl, {
|
48
|
+
clientInformation: clientInfo,
|
49
|
+
refreshToken,
|
50
|
+
addClientAuthentication: addClientAuth,
|
51
|
+
fetchFn
|
52
|
+
});
|
53
|
+
return newTokens;
|
54
|
+
} catch (error) {
|
55
|
+
lastError = error;
|
56
|
+
// If this isn't the last attempt, wait before retrying
|
57
|
+
if (attempt < maxRetries - 1) {
|
58
|
+
await new Promise((resolve)=>setTimeout(resolve, retryDelay * (attempt + 1)));
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
/* eslint-enable no-await-in-loop */ throw new Error(`Token refresh failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
|
63
|
+
}
|
64
|
+
/**
|
65
|
+
* Calculate token expiry timestamp
|
66
|
+
*
|
67
|
+
* @param expiresIn - Token lifetime in seconds
|
68
|
+
* @returns Expiry timestamp in milliseconds
|
69
|
+
*/ export function calculateTokenExpiry(expiresIn) {
|
70
|
+
return Date.now() + expiresIn * 1000;
|
71
|
+
}
|
72
|
+
|
73
|
+
//# sourceMappingURL=oauth-flow.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["../../src/client/oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Flow Helpers\n *\n * Pure utility functions for OAuth operations\n */\n\nimport { refreshAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';\nimport type {\n AuthorizationServerMetadata,\n OAuthClientInformation,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js';\n\n/**\n * Check if tokens are expired or about to expire\n *\n * @param tokens - OAuth tokens to check\n * @param tokenExpiryTime - Tracked expiry timestamp (if available)\n * @param bufferSeconds - Number of seconds before expiry to consider expired (default: 300 = 5 minutes)\n * @returns true if tokens are expired or will expire soon\n */\nexport function areTokensExpired(\n tokens: OAuthTokens | undefined,\n tokenExpiryTime?: number,\n bufferSeconds = 300\n): boolean {\n if (!tokens) {\n return true;\n }\n\n // If we have a tracked expiry time, use it (most accurate)\n if (tokenExpiryTime) {\n const now = Date.now();\n\n return now >= tokenExpiryTime - bufferSeconds * 1000;\n }\n\n // If no expiry info, assume tokens are valid\n if (tokens.expires_in === undefined) {\n return false;\n }\n\n // Check if tokens will expire within the buffer period\n // Note: expires_in is now derived from stored expires_at, so it's always accurate\n return tokens.expires_in <= bufferSeconds;\n}\n\n/**\n * Refresh OAuth tokens with retry logic\n *\n * @param serverUrl - Authorization server URL\n * @param clientInfo - Client information\n * @param refreshToken - Current refresh token\n * @param addClientAuth - Client authentication function\n * @param maxRetries - Maximum retry attempts (default: 3)\n * @param retryDelay - Base delay between retries in ms (default: 1000)\n * @param fetchFn - Optional custom fetch function\n * @returns New OAuth tokens\n * @throws Error if refresh fails after all retries\n */\nexport async function refreshTokensWithRetry(\n serverUrl: string | URL,\n clientInfo: OAuthClientInformation,\n refreshToken: string,\n addClientAuth?: (\n headers: Headers,\n params: URLSearchParams,\n url: string | URL,\n metadata?: AuthorizationServerMetadata\n ) => void | Promise<void>,\n maxRetries = 3,\n retryDelay = 1000,\n fetchFn?: typeof fetch\n): Promise<OAuthTokens> {\n let lastError: Error | undefined;\n\n /* eslint-disable no-await-in-loop */\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n // Attempt token refresh using SDK function\n const newTokens = await refreshAuthorization(serverUrl, {\n clientInformation: clientInfo,\n refreshToken,\n addClientAuthentication: addClientAuth,\n fetchFn,\n });\n\n return newTokens;\n } catch (error) {\n lastError = error as Error;\n\n // If this isn't the last attempt, wait before retrying\n if (attempt < maxRetries - 1) {\n await new Promise(resolve =>\n setTimeout(resolve, retryDelay * (attempt + 1))\n );\n }\n }\n }\n /* eslint-enable no-await-in-loop */\n\n throw new Error(\n `Token refresh failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`\n );\n}\n\n/**\n * Calculate token expiry timestamp\n *\n * @param expiresIn - Token lifetime in seconds\n * @returns Expiry timestamp in milliseconds\n */\nexport function calculateTokenExpiry(expiresIn: number): number {\n return Date.now() + expiresIn * 1000;\n}\n"],"names":["refreshAuthorization","areTokensExpired","tokens","tokenExpiryTime","bufferSeconds","now","Date","expires_in","undefined","refreshTokensWithRetry","serverUrl","clientInfo","refreshToken","addClientAuth","maxRetries","retryDelay","fetchFn","lastError","attempt","newTokens","clientInformation","addClientAuthentication","error","Promise","resolve","setTimeout","Error","message","calculateTokenExpiry","expiresIn"],"mappings":"AAAA;;;;CAIC,GAED,SAASA,oBAAoB,QAAQ,2CAA2C;AAOhF;;;;;;;CAOC,GACD,OAAO,SAASC,iBACdC,MAA+B,EAC/BC,eAAwB,EACxBC,gBAAgB,GAAG;IAEnB,IAAI,CAACF,QAAQ;QACX,OAAO;IACT;IAEA,2DAA2D;IAC3D,IAAIC,iBAAiB;QACnB,MAAME,MAAMC,KAAKD,GAAG;QAEpB,OAAOA,OAAOF,kBAAkBC,gBAAgB;IAClD;IAEA,6CAA6C;IAC7C,IAAIF,OAAOK,UAAU,KAAKC,WAAW;QACnC,OAAO;IACT;IAEA,uDAAuD;IACvD,kFAAkF;IAClF,OAAON,OAAOK,UAAU,IAAIH;AAC9B;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeK,uBACpBC,SAAuB,EACvBC,UAAkC,EAClCC,YAAoB,EACpBC,aAKyB,EACzBC,aAAa,CAAC,EACdC,aAAa,IAAI,EACjBC,OAAsB;IAEtB,IAAIC;IAEJ,mCAAmC,GACnC,IAAK,IAAIC,UAAU,GAAGA,UAAUJ,YAAYI,UAAW;QACrD,IAAI;YACF,2CAA2C;YAC3C,MAAMC,YAAY,MAAMnB,qBAAqBU,WAAW;gBACtDU,mBAAmBT;gBACnBC;gBACAS,yBAAyBR;gBACzBG;YACF;YAEA,OAAOG;QACT,EAAE,OAAOG,OAAO;YACdL,YAAYK;YAEZ,uDAAuD;YACvD,IAAIJ,UAAUJ,aAAa,GAAG;gBAC5B,MAAM,IAAIS,QAAQC,CAAAA,UAChBC,WAAWD,SAAST,aAAcG,CAAAA,UAAU,CAAA;YAEhD;QACF;IACF;IACA,kCAAkC,GAElC,MAAM,IAAIQ,MACR,CAAC,2BAA2B,EAAEZ,WAAW,WAAW,EAAEG,WAAWU,WAAW,iBAAiB;AAEjG;AAEA;;;;;CAKC,GACD,OAAO,SAASC,qBAAqBC,SAAiB;IACpD,OAAOvB,KAAKD,GAAG,KAAKwB,YAAY;AAClC"}
|
@@ -0,0 +1,237 @@
|
|
1
|
+
import { _ as _define_property } from "@swc/helpers/_/_define_property";
|
2
|
+
/**
|
3
|
+
* Calculate expires_in from expires_at timestamp
|
4
|
+
*/ function calculateExpiresIn(expiresAt) {
|
5
|
+
if (!expiresAt) {
|
6
|
+
return undefined;
|
7
|
+
}
|
8
|
+
const now = Date.now();
|
9
|
+
const expiresInMs = expiresAt - now;
|
10
|
+
const expiresInSeconds = Math.floor(expiresInMs / 1000);
|
11
|
+
// Return 0 if already expired, otherwise return remaining seconds
|
12
|
+
return Math.max(0, expiresInSeconds);
|
13
|
+
}
|
14
|
+
/**
|
15
|
+
* Calculate expires_at from expires_in
|
16
|
+
*/ function calculateExpiresAt(expiresIn) {
|
17
|
+
return Date.now() + (expiresIn || 0) * 1000;
|
18
|
+
}
|
19
|
+
/**
|
20
|
+
* In-memory storage adapter for OAuth data
|
21
|
+
* Suitable for development and testing, but data is lost when process exits
|
22
|
+
*/ export class MemoryStorage {
|
23
|
+
async get(key) {
|
24
|
+
return this.data.get(key);
|
25
|
+
}
|
26
|
+
async set(key, value) {
|
27
|
+
this.data.set(key, value);
|
28
|
+
}
|
29
|
+
async delete(key) {
|
30
|
+
this.data.delete(key);
|
31
|
+
}
|
32
|
+
/**
|
33
|
+
* Clear all data (useful for testing)
|
34
|
+
*/ clear() {
|
35
|
+
this.data.clear();
|
36
|
+
}
|
37
|
+
constructor(){
|
38
|
+
_define_property(this, "data", new Map());
|
39
|
+
}
|
40
|
+
}
|
41
|
+
/**
|
42
|
+
* File-based storage adapter for OAuth data
|
43
|
+
* Persists data to the filesystem for longer-term storage
|
44
|
+
*/ export class FileStorage {
|
45
|
+
getFilePath(key) {
|
46
|
+
// Sanitize key for filename
|
47
|
+
const sanitized = key.replace(/[^a-zA-Z0-9-_]/g, '_');
|
48
|
+
return `${this.basePath}/${sanitized}.json`;
|
49
|
+
}
|
50
|
+
async ensureDirectory() {
|
51
|
+
try {
|
52
|
+
await Bun.write(`${this.basePath}/.gitkeep`, '');
|
53
|
+
} catch {
|
54
|
+
// Directory creation will happen automatically with Bun.write
|
55
|
+
}
|
56
|
+
}
|
57
|
+
async get(key) {
|
58
|
+
try {
|
59
|
+
const file = Bun.file(this.getFilePath(key));
|
60
|
+
const exists = await file.exists();
|
61
|
+
if (!exists) {
|
62
|
+
return undefined;
|
63
|
+
}
|
64
|
+
return await file.text();
|
65
|
+
} catch {
|
66
|
+
return undefined;
|
67
|
+
}
|
68
|
+
}
|
69
|
+
async set(key, value) {
|
70
|
+
await this.ensureDirectory();
|
71
|
+
await Bun.write(this.getFilePath(key), value);
|
72
|
+
}
|
73
|
+
async delete(key) {
|
74
|
+
try {
|
75
|
+
await Bun.$`rm -f ${this.getFilePath(key)}`;
|
76
|
+
} catch {
|
77
|
+
// File might not exist, ignore error
|
78
|
+
}
|
79
|
+
}
|
80
|
+
/**
|
81
|
+
* Clear all data (useful for testing)
|
82
|
+
*/ async clear() {
|
83
|
+
try {
|
84
|
+
await Bun.$`rm -rf ${this.basePath}`;
|
85
|
+
} catch {
|
86
|
+
// Directory might not exist, ignore error
|
87
|
+
}
|
88
|
+
}
|
89
|
+
constructor(basePath = './oauth-data'){
|
90
|
+
_define_property(this, "basePath", void 0);
|
91
|
+
this.basePath = basePath;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
/**
|
95
|
+
* Create a storage adapter based on the provided configuration
|
96
|
+
*/ export function createStorageAdapter(type, options) {
|
97
|
+
switch(type){
|
98
|
+
case 'file':
|
99
|
+
return new FileStorage(options?.path);
|
100
|
+
case 'memory':
|
101
|
+
default:
|
102
|
+
return new MemoryStorage();
|
103
|
+
}
|
104
|
+
}
|
105
|
+
/**
|
106
|
+
* Helper class to manage OAuth data with the simplified storage adapter
|
107
|
+
* Handles initialization from config and provides a unified storage interface
|
108
|
+
*/ export class OAuthStorage {
|
109
|
+
/**
|
110
|
+
* Initialize storage with config values if provided
|
111
|
+
* This ensures config tokens are written to storage on first use
|
112
|
+
*/ async initialize() {
|
113
|
+
if (this.initialized) {
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
this.initialized = true;
|
117
|
+
// Initialize tokens if provided and not already in storage
|
118
|
+
// (tokens are one-time initialization, storage takes over after that)
|
119
|
+
if (this.options?.initialTokens) {
|
120
|
+
const existing = await this.storage.get(`tokens:${this.sessionId}`);
|
121
|
+
if (!existing) {
|
122
|
+
// Convert expires_in to expires_at before storing
|
123
|
+
const storedTokens = {
|
124
|
+
access_token: this.options.initialTokens.access_token,
|
125
|
+
token_type: this.options.initialTokens.token_type,
|
126
|
+
refresh_token: this.options.initialTokens.refresh_token,
|
127
|
+
scope: this.options.initialTokens.scope
|
128
|
+
};
|
129
|
+
// Only set expires_at if expires_in is provided
|
130
|
+
if (this.options.initialTokens.expires_in !== undefined) {
|
131
|
+
storedTokens.expires_at = calculateExpiresAt(this.options.initialTokens.expires_in);
|
132
|
+
}
|
133
|
+
await this.storage.set(`tokens:${this.sessionId}`, JSON.stringify(storedTokens));
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
async saveTokens(tokens) {
|
138
|
+
await this.initialize();
|
139
|
+
// Convert expires_in to expires_at for storage
|
140
|
+
const storedTokens = {
|
141
|
+
access_token: tokens.access_token,
|
142
|
+
token_type: tokens.token_type,
|
143
|
+
refresh_token: tokens.refresh_token,
|
144
|
+
scope: tokens.scope
|
145
|
+
};
|
146
|
+
// Only set expires_at if expires_in is provided
|
147
|
+
if (tokens.expires_in !== undefined) {
|
148
|
+
storedTokens.expires_at = calculateExpiresAt(tokens.expires_in);
|
149
|
+
}
|
150
|
+
await this.storage.set(`tokens:${this.sessionId}`, JSON.stringify(storedTokens));
|
151
|
+
}
|
152
|
+
async getTokens() {
|
153
|
+
await this.initialize();
|
154
|
+
const data = await this.storage.get(`tokens:${this.sessionId}`);
|
155
|
+
if (!data) {
|
156
|
+
return undefined;
|
157
|
+
}
|
158
|
+
const storedTokens = JSON.parse(data);
|
159
|
+
// Convert expires_at back to expires_in for the OAuthTokens interface
|
160
|
+
const tokens = {
|
161
|
+
access_token: storedTokens.access_token,
|
162
|
+
token_type: storedTokens.token_type
|
163
|
+
};
|
164
|
+
// Only include optional fields if they exist
|
165
|
+
if (storedTokens.refresh_token !== undefined) {
|
166
|
+
tokens.refresh_token = storedTokens.refresh_token;
|
167
|
+
}
|
168
|
+
if (storedTokens.scope !== undefined) {
|
169
|
+
tokens.scope = storedTokens.scope;
|
170
|
+
}
|
171
|
+
if (storedTokens.expires_at !== undefined) {
|
172
|
+
tokens.expires_in = calculateExpiresIn(storedTokens.expires_at);
|
173
|
+
}
|
174
|
+
return tokens;
|
175
|
+
}
|
176
|
+
async clearTokens() {
|
177
|
+
await this.initialize();
|
178
|
+
await this.storage.delete(`tokens:${this.sessionId}`);
|
179
|
+
}
|
180
|
+
async saveClientInfo(clientInfo) {
|
181
|
+
await this.initialize();
|
182
|
+
await this.storage.set('client_info', JSON.stringify(clientInfo));
|
183
|
+
}
|
184
|
+
async getClientInfo() {
|
185
|
+
await this.initialize();
|
186
|
+
// Static client info from config ALWAYS takes precedence
|
187
|
+
// (client credentials are like API keys - they don't change)
|
188
|
+
if (this.options?.staticClientInfo?.client_id) {
|
189
|
+
return this.options.staticClientInfo;
|
190
|
+
}
|
191
|
+
const data = await this.storage.get('client_info');
|
192
|
+
return data ? JSON.parse(data) : undefined;
|
193
|
+
}
|
194
|
+
async clearClientInfo() {
|
195
|
+
await this.initialize();
|
196
|
+
await this.storage.delete('client_info');
|
197
|
+
}
|
198
|
+
async saveCodeVerifier(verifier) {
|
199
|
+
await this.initialize();
|
200
|
+
await this.storage.set(`verifier:${this.sessionId}`, verifier);
|
201
|
+
}
|
202
|
+
async getCodeVerifier() {
|
203
|
+
await this.initialize();
|
204
|
+
return this.storage.get(`verifier:${this.sessionId}`);
|
205
|
+
}
|
206
|
+
async clearCodeVerifier() {
|
207
|
+
await this.initialize();
|
208
|
+
await this.storage.delete(`verifier:${this.sessionId}`);
|
209
|
+
}
|
210
|
+
async clearSession() {
|
211
|
+
await this.initialize();
|
212
|
+
await Promise.all([
|
213
|
+
this.clearTokens(),
|
214
|
+
this.clearCodeVerifier()
|
215
|
+
]);
|
216
|
+
}
|
217
|
+
async clearAll() {
|
218
|
+
await this.initialize();
|
219
|
+
await Promise.all([
|
220
|
+
this.clearTokens(),
|
221
|
+
this.clearCodeVerifier(),
|
222
|
+
this.clearClientInfo()
|
223
|
+
]);
|
224
|
+
}
|
225
|
+
constructor(storage, sessionId, options){
|
226
|
+
_define_property(this, "storage", void 0);
|
227
|
+
_define_property(this, "sessionId", void 0);
|
228
|
+
_define_property(this, "options", void 0);
|
229
|
+
_define_property(this, "initialized", void 0);
|
230
|
+
this.storage = storage;
|
231
|
+
this.sessionId = sessionId;
|
232
|
+
this.options = options;
|
233
|
+
this.initialized = false;
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
//# sourceMappingURL=storage.js.map
|