@mcp-z/client 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cjs/auth/capability-discovery.js +18 -8
- package/dist/cjs/auth/capability-discovery.js.map +1 -1
- package/dist/cjs/auth/discovery-fetch.d.cts +35 -0
- package/dist/cjs/auth/discovery-fetch.d.ts +35 -0
- package/dist/cjs/auth/discovery-fetch.js +658 -0
- package/dist/cjs/auth/discovery-fetch.js.map +1 -0
- package/dist/cjs/auth/index.d.cts +1 -0
- package/dist/cjs/auth/index.d.ts +1 -0
- package/dist/cjs/auth/index.js +7 -0
- package/dist/cjs/auth/index.js.map +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.d.cts +20 -13
- package/dist/cjs/auth/interactive-oauth-flow.d.ts +20 -13
- package/dist/cjs/auth/interactive-oauth-flow.js +48 -24
- package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/cjs/auth/oauth-callback-listener.js +4 -0
- package/dist/cjs/auth/oauth-callback-listener.js.map +1 -1
- package/dist/cjs/auth/rfc9728-discovery.d.cts +12 -25
- package/dist/cjs/auth/rfc9728-discovery.d.ts +12 -25
- package/dist/cjs/auth/rfc9728-discovery.js +59 -48
- package/dist/cjs/auth/rfc9728-discovery.js.map +1 -1
- package/dist/cjs/auth/types.d.cts +34 -4
- package/dist/cjs/auth/types.d.ts +34 -4
- package/dist/cjs/auth/types.js.map +1 -1
- package/dist/cjs/dcr/dcr-authenticator.d.cts +9 -5
- package/dist/cjs/dcr/dcr-authenticator.d.ts +9 -5
- package/dist/cjs/dcr/dcr-authenticator.js +436 -43
- package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
- package/dist/cjs/dcr/dynamic-client-registrar.d.cts +7 -18
- package/dist/cjs/dcr/dynamic-client-registrar.d.ts +7 -18
- package/dist/cjs/dcr/dynamic-client-registrar.js +18 -25
- package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +7 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/url-utils.js +2 -2
- package/dist/cjs/lib/url-utils.js.map +1 -1
- package/dist/esm/auth/capability-discovery.js +17 -7
- package/dist/esm/auth/capability-discovery.js.map +1 -1
- package/dist/esm/auth/discovery-fetch.d.ts +35 -0
- package/dist/esm/auth/discovery-fetch.js +184 -0
- package/dist/esm/auth/discovery-fetch.js.map +1 -0
- package/dist/esm/auth/index.d.ts +1 -0
- package/dist/esm/auth/index.js +1 -0
- package/dist/esm/auth/index.js.map +1 -1
- package/dist/esm/auth/interactive-oauth-flow.d.ts +20 -13
- package/dist/esm/auth/interactive-oauth-flow.js +50 -23
- package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/esm/auth/oauth-callback-listener.js +4 -0
- package/dist/esm/auth/oauth-callback-listener.js.map +1 -1
- package/dist/esm/auth/rfc9728-discovery.d.ts +12 -25
- package/dist/esm/auth/rfc9728-discovery.js +49 -56
- package/dist/esm/auth/rfc9728-discovery.js.map +1 -1
- package/dist/esm/auth/types.d.ts +34 -4
- package/dist/esm/auth/types.js.map +1 -1
- package/dist/esm/dcr/dcr-authenticator.d.ts +9 -5
- package/dist/esm/dcr/dcr-authenticator.js +92 -39
- package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
- package/dist/esm/dcr/dynamic-client-registrar.d.ts +7 -18
- package/dist/esm/dcr/dynamic-client-registrar.js +19 -23
- package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/url-utils.js +2 -2
- package/dist/esm/lib/url-utils.js.map +1 -1
- package/package.json +3 -8
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Exports public API for OAuth authentication only (DCR moved to ../dcr/)
|
|
4
4
|
*/
|
|
5
5
|
export { probeAuthCapabilities } from './capability-discovery.js';
|
|
6
|
+
export { DiscoveryFetchError, isLoopbackUrl } from './discovery-fetch.js';
|
|
6
7
|
export { InteractiveOAuthFlow } from './interactive-oauth-flow.js';
|
|
7
8
|
export type { OAuthCallbackListenerOptions } from './oauth-callback-listener.js';
|
|
8
9
|
export { OAuthCallbackListener } from './oauth-callback-listener.js';
|
package/dist/cjs/auth/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Exports public API for OAuth authentication only (DCR moved to ../dcr/)
|
|
4
4
|
*/
|
|
5
5
|
export { probeAuthCapabilities } from './capability-discovery.js';
|
|
6
|
+
export { DiscoveryFetchError, isLoopbackUrl } from './discovery-fetch.js';
|
|
6
7
|
export { InteractiveOAuthFlow } from './interactive-oauth-flow.js';
|
|
7
8
|
export type { OAuthCallbackListenerOptions } from './oauth-callback-listener.js';
|
|
8
9
|
export { OAuthCallbackListener } from './oauth-callback-listener.js';
|
package/dist/cjs/auth/index.js
CHANGED
|
@@ -12,17 +12,24 @@ function _export(target, all) {
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
_export(exports, {
|
|
15
|
+
get DiscoveryFetchError () {
|
|
16
|
+
return _discoveryfetchts.DiscoveryFetchError;
|
|
17
|
+
},
|
|
15
18
|
get InteractiveOAuthFlow () {
|
|
16
19
|
return _interactiveoauthflowts.InteractiveOAuthFlow;
|
|
17
20
|
},
|
|
18
21
|
get OAuthCallbackListener () {
|
|
19
22
|
return _oauthcallbacklistenerts.OAuthCallbackListener;
|
|
20
23
|
},
|
|
24
|
+
get isLoopbackUrl () {
|
|
25
|
+
return _discoveryfetchts.isLoopbackUrl;
|
|
26
|
+
},
|
|
21
27
|
get probeAuthCapabilities () {
|
|
22
28
|
return _capabilitydiscoveryts.probeAuthCapabilities;
|
|
23
29
|
}
|
|
24
30
|
});
|
|
25
31
|
var _capabilitydiscoveryts = require("./capability-discovery.js");
|
|
32
|
+
var _discoveryfetchts = require("./discovery-fetch.js");
|
|
26
33
|
var _interactiveoauthflowts = require("./interactive-oauth-flow.js");
|
|
27
34
|
var _oauthcallbacklistenerts = require("./oauth-callback-listener.js");
|
|
28
35
|
/* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/index.ts"],"sourcesContent":["/**\n * Authentication Module\n * Exports public API for OAuth authentication only (DCR moved to ../dcr/)\n */\n\nexport { probeAuthCapabilities } from './capability-discovery.ts';\nexport { InteractiveOAuthFlow } from './interactive-oauth-flow.ts';\nexport type { OAuthCallbackListenerOptions } from './oauth-callback-listener.ts';\nexport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nexport type { AuthCapabilities, CallbackResult, OAuthFlowOptions, TokenSet } from './types.ts';\n"],"names":["InteractiveOAuthFlow","OAuthCallbackListener","probeAuthCapabilities"],"mappings":"AAAA;;;CAGC;;;;;;;;;;;QAGQA;eAAAA,4CAAoB;;QAEpBC;eAAAA,8CAAqB;;
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/index.ts"],"sourcesContent":["/**\n * Authentication Module\n * Exports public API for OAuth authentication only (DCR moved to ../dcr/)\n */\n\nexport { probeAuthCapabilities } from './capability-discovery.ts';\nexport { DiscoveryFetchError, isLoopbackUrl } from './discovery-fetch.ts';\nexport { InteractiveOAuthFlow } from './interactive-oauth-flow.ts';\nexport type { OAuthCallbackListenerOptions } from './oauth-callback-listener.ts';\nexport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nexport type { AuthCapabilities, CallbackResult, OAuthFlowOptions, TokenSet } from './types.ts';\n"],"names":["DiscoveryFetchError","InteractiveOAuthFlow","OAuthCallbackListener","isLoopbackUrl","probeAuthCapabilities"],"mappings":"AAAA;;;CAGC;;;;;;;;;;;QAGQA;eAAAA,qCAAmB;;QACnBC;eAAAA,4CAAoB;;QAEpBC;eAAAA,8CAAqB;;QAHAC;eAAAA,+BAAa;;QADlCC;eAAAA,4CAAqB;;;qCAAQ;gCACa;sCACd;uCAEC"}
|
|
@@ -29,27 +29,34 @@ export declare class InteractiveOAuthFlow {
|
|
|
29
29
|
* 'https://example.com/oauth/token',
|
|
30
30
|
* 'client-id',
|
|
31
31
|
* 'client-secret',
|
|
32
|
-
* { port, scopes: ['read', 'write'] }
|
|
32
|
+
* { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }
|
|
33
33
|
* );
|
|
34
34
|
*/
|
|
35
35
|
performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet>;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Rejects an authorization response that was not minted by the issuer
|
|
38
|
+
* discovered before the flow started (RFC 9207 authorization-server mix-up).
|
|
39
|
+
*/
|
|
40
|
+
private assertResponseIssuer;
|
|
41
|
+
/**
|
|
42
|
+
* Exchanges an authorization code for access and refresh tokens.
|
|
43
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
44
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
45
|
+
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
39
46
|
*/
|
|
40
47
|
private exchangeCodeForTokens;
|
|
41
48
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
46
|
-
* @param
|
|
47
|
-
* @param
|
|
48
|
-
* @
|
|
49
|
-
*
|
|
50
|
-
* @throws Error if refresh fails
|
|
49
|
+
* Refreshes an access token using a refresh token.
|
|
50
|
+
* @param tokenEndpoint - OAuth token endpoint URL.
|
|
51
|
+
* @param refreshToken - Refresh token from a previous token set.
|
|
52
|
+
* @param clientId - OAuth client ID.
|
|
53
|
+
* @param clientSecret - OAuth client secret.
|
|
54
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
55
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
56
|
+
* @returns New token set with a refreshed access token.
|
|
57
|
+
* @throws Error if refresh fails.
|
|
51
58
|
*/
|
|
52
|
-
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string): Promise<TokenSet>;
|
|
59
|
+
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, resource: string, allowLoopback?: boolean): Promise<TokenSet>;
|
|
53
60
|
/**
|
|
54
61
|
* Open browser to authorization URL
|
|
55
62
|
* Uses platform-specific command to open default browser
|
|
@@ -29,27 +29,34 @@ export declare class InteractiveOAuthFlow {
|
|
|
29
29
|
* 'https://example.com/oauth/token',
|
|
30
30
|
* 'client-id',
|
|
31
31
|
* 'client-secret',
|
|
32
|
-
* { port, scopes: ['read', 'write'] }
|
|
32
|
+
* { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }
|
|
33
33
|
* );
|
|
34
34
|
*/
|
|
35
35
|
performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet>;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Rejects an authorization response that was not minted by the issuer
|
|
38
|
+
* discovered before the flow started (RFC 9207 authorization-server mix-up).
|
|
39
|
+
*/
|
|
40
|
+
private assertResponseIssuer;
|
|
41
|
+
/**
|
|
42
|
+
* Exchanges an authorization code for access and refresh tokens.
|
|
43
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
44
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
45
|
+
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
39
46
|
*/
|
|
40
47
|
private exchangeCodeForTokens;
|
|
41
48
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
46
|
-
* @param
|
|
47
|
-
* @param
|
|
48
|
-
* @
|
|
49
|
-
*
|
|
50
|
-
* @throws Error if refresh fails
|
|
49
|
+
* Refreshes an access token using a refresh token.
|
|
50
|
+
* @param tokenEndpoint - OAuth token endpoint URL.
|
|
51
|
+
* @param refreshToken - Refresh token from a previous token set.
|
|
52
|
+
* @param clientId - OAuth client ID.
|
|
53
|
+
* @param clientSecret - OAuth client secret.
|
|
54
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
55
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
56
|
+
* @returns New token set with a refreshed access token.
|
|
57
|
+
* @throws Error if refresh fails.
|
|
51
58
|
*/
|
|
52
|
-
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string): Promise<TokenSet>;
|
|
59
|
+
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, resource: string, allowLoopback?: boolean): Promise<TokenSet>;
|
|
53
60
|
/**
|
|
54
61
|
* Open browser to authorization URL
|
|
55
62
|
* Uses platform-specific command to open default browser
|
|
@@ -13,6 +13,7 @@ Object.defineProperty(exports, "InteractiveOAuthFlow", {
|
|
|
13
13
|
});
|
|
14
14
|
var _nodechild_process = /*#__PURE__*/ _interop_require_wildcard(require("node:child_process"));
|
|
15
15
|
var _loggerts = require("../utils/logger.js");
|
|
16
|
+
var _discoveryfetchts = require("./discovery-fetch.js");
|
|
16
17
|
var _oauthcallbacklistenerts = require("./oauth-callback-listener.js");
|
|
17
18
|
var _pkcets = require("./pkce.js");
|
|
18
19
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
|
|
@@ -207,11 +208,11 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
207
208
|
* 'https://example.com/oauth/token',
|
|
208
209
|
* 'client-id',
|
|
209
210
|
* 'client-secret',
|
|
210
|
-
* { port, scopes: ['read', 'write'] }
|
|
211
|
+
* { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }
|
|
211
212
|
* );
|
|
212
213
|
*/ _proto.performAuthFlow = function performAuthFlow(authorizationEndpoint, tokenEndpoint, clientId, clientSecret, options) {
|
|
213
214
|
return _async_to_generator(function() {
|
|
214
|
-
var _options_logger, logger, callbackListener, pkce, redirectUri, authUrl, timeout, result, tokens, error;
|
|
215
|
+
var _options_logger, logger, callbackListener, pkce, _options_allowLoopback, redirectUri, authUrl, timeout, result, tokens, error;
|
|
215
216
|
return _ts_generator(this, function(_state) {
|
|
216
217
|
switch(_state.label){
|
|
217
218
|
case 0:
|
|
@@ -256,10 +257,8 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
256
257
|
if (options.scopes && options.scopes.length > 0) {
|
|
257
258
|
authUrl.searchParams.set('scope', options.scopes.join(' '));
|
|
258
259
|
}
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
authUrl.searchParams.set('resource', options.resource);
|
|
262
|
-
}
|
|
260
|
+
// Audience-bind the request to the resource server (RFC 8707)
|
|
261
|
+
authUrl.searchParams.set('resource', options.resource);
|
|
263
262
|
// Add PKCE parameters if generated (RFC 7636)
|
|
264
263
|
if (pkce) {
|
|
265
264
|
authUrl.searchParams.set('code_challenge', pkce.codeChallenge);
|
|
@@ -295,9 +294,10 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
295
294
|
];
|
|
296
295
|
case 7:
|
|
297
296
|
result = _state.sent();
|
|
297
|
+
this.assertResponseIssuer(result.iss, options, logger);
|
|
298
298
|
return [
|
|
299
299
|
4,
|
|
300
|
-
this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, pkce === null || pkce === void 0 ? void 0 : pkce.codeVerifier)
|
|
300
|
+
this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, options.resource, (_options_allowLoopback = options.allowLoopback) !== null && _options_allowLoopback !== void 0 ? _options_allowLoopback : false, pkce === null || pkce === void 0 ? void 0 : pkce.codeVerifier)
|
|
301
301
|
];
|
|
302
302
|
case 8:
|
|
303
303
|
tokens = _state.sent();
|
|
@@ -329,9 +329,26 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
329
329
|
}).call(this);
|
|
330
330
|
};
|
|
331
331
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*/ _proto.
|
|
332
|
+
* Rejects an authorization response that was not minted by the issuer
|
|
333
|
+
* discovered before the flow started (RFC 9207 authorization-server mix-up).
|
|
334
|
+
*/ _proto.assertResponseIssuer = function assertResponseIssuer(iss, options, logger) {
|
|
335
|
+
if (iss !== undefined) {
|
|
336
|
+
if (iss !== options.issuer) {
|
|
337
|
+
throw new Error("Authorization response issuer mismatch: got '".concat(iss, "', expected '").concat(options.issuer, "' - refusing to redeem the authorization code"));
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (options.authorizationResponseIssSupported) {
|
|
342
|
+
throw new Error("Authorization server '".concat(options.issuer, "' advertises authorization_response_iss_parameter_supported but omitted 'iss' - refusing to redeem the authorization code"));
|
|
343
|
+
}
|
|
344
|
+
logger.debug("⚠️ Authorization response carried no 'iss' and '".concat(options.issuer, "' does not advertise support for it (RFC 9207)"));
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Exchanges an authorization code for access and refresh tokens.
|
|
348
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
349
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
350
|
+
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
351
|
+
*/ _proto.exchangeCodeForTokens = function exchangeCodeForTokens(tokenEndpoint, code, clientId, clientSecret, redirectUri, resource, allowLoopback, codeVerifier) {
|
|
335
352
|
return _async_to_generator(function() {
|
|
336
353
|
var params, response, errorText, data, tokenSet;
|
|
337
354
|
return _ts_generator(this, function(_state) {
|
|
@@ -342,7 +359,8 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
342
359
|
code: code,
|
|
343
360
|
redirect_uri: redirectUri,
|
|
344
361
|
client_id: clientId,
|
|
345
|
-
client_secret: clientSecret
|
|
362
|
+
client_secret: clientSecret,
|
|
363
|
+
resource: resource
|
|
346
364
|
});
|
|
347
365
|
// Add PKCE code verifier if provided (RFC 7636)
|
|
348
366
|
if (codeVerifier) {
|
|
@@ -350,7 +368,7 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
350
368
|
}
|
|
351
369
|
return [
|
|
352
370
|
4,
|
|
353
|
-
|
|
371
|
+
(0, _discoveryfetchts.discoveryFetch)(tokenEndpoint, {
|
|
354
372
|
method: 'POST',
|
|
355
373
|
headers: {
|
|
356
374
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -358,6 +376,8 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
358
376
|
Connection: 'close'
|
|
359
377
|
},
|
|
360
378
|
body: params
|
|
379
|
+
}, 'token endpoint', {
|
|
380
|
+
allowLoopback: allowLoopback
|
|
361
381
|
})
|
|
362
382
|
];
|
|
363
383
|
case 1:
|
|
@@ -402,16 +422,17 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
402
422
|
})();
|
|
403
423
|
};
|
|
404
424
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
* @param
|
|
408
|
-
* @param
|
|
409
|
-
* @param
|
|
410
|
-
* @param
|
|
411
|
-
* @
|
|
412
|
-
*
|
|
413
|
-
* @throws Error if refresh fails
|
|
414
|
-
*/ _proto.refreshTokens = function refreshTokens(tokenEndpoint, refreshToken, clientId, clientSecret) {
|
|
425
|
+
* Refreshes an access token using a refresh token.
|
|
426
|
+
* @param tokenEndpoint - OAuth token endpoint URL.
|
|
427
|
+
* @param refreshToken - Refresh token from a previous token set.
|
|
428
|
+
* @param clientId - OAuth client ID.
|
|
429
|
+
* @param clientSecret - OAuth client secret.
|
|
430
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
431
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
432
|
+
* @returns New token set with a refreshed access token.
|
|
433
|
+
* @throws Error if refresh fails.
|
|
434
|
+
*/ _proto.refreshTokens = function refreshTokens(tokenEndpoint, refreshToken, clientId, clientSecret, resource) {
|
|
435
|
+
var allowLoopback = arguments.length > 5 && arguments[5] !== void 0 ? arguments[5] : false;
|
|
415
436
|
return _async_to_generator(function() {
|
|
416
437
|
var response, errorText, data, tokenSet;
|
|
417
438
|
return _ts_generator(this, function(_state) {
|
|
@@ -419,7 +440,7 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
419
440
|
case 0:
|
|
420
441
|
return [
|
|
421
442
|
4,
|
|
422
|
-
|
|
443
|
+
(0, _discoveryfetchts.discoveryFetch)(tokenEndpoint, {
|
|
423
444
|
method: 'POST',
|
|
424
445
|
headers: {
|
|
425
446
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -430,8 +451,11 @@ var InteractiveOAuthFlow = /*#__PURE__*/ function() {
|
|
|
430
451
|
grant_type: 'refresh_token',
|
|
431
452
|
refresh_token: refreshToken,
|
|
432
453
|
client_id: clientId,
|
|
433
|
-
client_secret: clientSecret
|
|
454
|
+
client_secret: clientSecret,
|
|
455
|
+
resource: resource
|
|
434
456
|
})
|
|
457
|
+
}, 'token endpoint', {
|
|
458
|
+
allowLoopback: allowLoopback
|
|
435
459
|
})
|
|
436
460
|
];
|
|
437
461
|
case 1:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/interactive-oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Authorization Flow Handler\n * Manages browser-based OAuth flows and token exchange with PKCE support\n */\n\nimport * as child_process from 'node:child_process';\nimport { logger as defaultLogger } from '../utils/logger.ts';\nimport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nimport { generatePkce } from './pkce.ts';\nimport type { OAuthFlowOptions, PkceParams, TokenSet } from './types.ts';\n\n/**\n * OAuth token response from token endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * InteractiveOAuthFlow manages the complete OAuth authorization code flow\n */\nexport class InteractiveOAuthFlow {\n /**\n * Perform OAuth authorization code flow\n *\n * @param authorizationEndpoint - OAuth authorization endpoint URL\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @param options - Flow options (port is required - use get-port to find available port)\n * @returns Token set with access and refresh tokens\n *\n * @throws Error if flow fails or times out\n *\n * @example\n * import getPort from 'get-port';\n *\n * const flow = new InteractiveOAuthFlow();\n * const port = await getPort();\n * const tokens = await flow.performAuthFlow(\n * 'https://example.com/oauth/authorize',\n * 'https://example.com/oauth/token',\n * 'client-id',\n * 'client-secret',\n * { port, scopes: ['read', 'write'] }\n * );\n */\n async performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet> {\n const logger = options.logger ?? defaultLogger;\n const callbackListener = new OAuthCallbackListener({ port: options.port, logger });\n\n // Generate PKCE parameters if requested (RFC 7636)\n let pkce: PkceParams | undefined;\n if (options.pkce) {\n logger.debug('🔐 Generating PKCE parameters...');\n pkce = await generatePkce();\n }\n\n try {\n // Start callback server\n await callbackListener.start();\n\n // Build redirect URI\n const redirectUri = options.redirectUri || `http://localhost:${options.port}/callback`;\n\n // Build authorization URL\n const authUrl = new URL(authorizationEndpoint);\n authUrl.searchParams.set('client_id', clientId);\n authUrl.searchParams.set('redirect_uri', redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n\n if (options.scopes && options.scopes.length > 0) {\n authUrl.searchParams.set('scope', options.scopes.join(' '));\n }\n\n // Add resource parameter if specified (RFC 8707)\n if (options.resource) {\n authUrl.searchParams.set('resource', options.resource);\n }\n\n // Add PKCE parameters if generated (RFC 7636)\n if (pkce) {\n authUrl.searchParams.set('code_challenge', pkce.codeChallenge);\n authUrl.searchParams.set('code_challenge_method', pkce.codeChallengeMethod);\n }\n\n // Open browser or print URL for headless mode\n if (options.headless) {\n logger.info('🔗 Please visit this URL to authorize:');\n logger.info(authUrl.toString());\n logger.info('Waiting for callback...');\n } else {\n logger.debug('🌐 Opening browser for OAuth authorization...');\n // Try to open browser (requires 'open' package or native command)\n await this.openBrowser(authUrl.toString());\n }\n\n // Wait for callback with timeout\n const timeout = options.timeout || (options.headless ? 60000 : 300000);\n const result = await callbackListener.waitForCallback(timeout);\n\n // Exchange authorization code for tokens (with PKCE verifier if used)\n const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, pkce?.codeVerifier);\n\n return tokens;\n } catch (error) {\n logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));\n throw error;\n } finally {\n // Always close callback server\n await callbackListener.stop();\n }\n }\n\n /**\n * Exchange authorization code for access and refresh tokens\n * @param codeVerifier - Optional PKCE code verifier (RFC 7636)\n */\n private async exchangeCodeForTokens(tokenEndpoint: string, code: string, clientId: string, clientSecret: string, redirectUri: string, codeVerifier?: string): Promise<TokenSet> {\n const params = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n // Add PKCE code verifier if provided (RFC 7636)\n if (codeVerifier) {\n params.set('code_verifier', codeVerifier);\n }\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: params,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || '',\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param refreshToken - Refresh token from previous token set\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @returns New token set with refreshed access token\n *\n * @throws Error if refresh fails\n */\n async refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string): Promise<TokenSet> {\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token refresh response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken, // Reuse old refresh token if not provided\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Open browser to authorization URL\n * Uses platform-specific command to open default browser\n */\n private async openBrowser(url: string): Promise<void> {\n // Determine platform-specific command\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', url];\n } else {\n // Linux and others\n command = 'xdg-open';\n args = [url];\n }\n\n // Spawn browser process\n const child = child_process.spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n }\n}\n"],"names":["InteractiveOAuthFlow","performAuthFlow","authorizationEndpoint","tokenEndpoint","clientId","clientSecret","options","logger","callbackListener","pkce","redirectUri","authUrl","timeout","result","tokens","error","defaultLogger","OAuthCallbackListener","port","debug","generatePkce","start","URL","searchParams","set","scopes","length","join","resource","codeChallenge","codeChallengeMethod","headless","info","toString","openBrowser","waitForCallback","exchangeCodeForTokens","code","codeVerifier","Error","message","String","stop","params","response","errorText","data","tokenSet","URLSearchParams","grant_type","redirect_uri","client_id","client_secret","fetch","method","headers","Accept","Connection","body","ok","text","status","json","access_token","accessToken","refreshToken","refresh_token","expiresAt","Date","now","expires_in","scope","split","refreshTokens","url","platform","command","args","child","process","child_process","spawn","detached","stdio","unref"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/interactive-oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Authorization Flow Handler\n * Manages browser-based OAuth flows and token exchange with PKCE support\n */\n\nimport * as child_process from 'node:child_process';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { discoveryFetch } from './discovery-fetch.ts';\nimport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nimport { generatePkce } from './pkce.ts';\nimport type { OAuthFlowOptions, PkceParams, TokenSet } from './types.ts';\n\n/**\n * OAuth token response from token endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * InteractiveOAuthFlow manages the complete OAuth authorization code flow\n */\nexport class InteractiveOAuthFlow {\n /**\n * Perform OAuth authorization code flow\n *\n * @param authorizationEndpoint - OAuth authorization endpoint URL\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @param options - Flow options (port is required - use get-port to find available port)\n * @returns Token set with access and refresh tokens\n *\n * @throws Error if flow fails or times out\n *\n * @example\n * import getPort from 'get-port';\n *\n * const flow = new InteractiveOAuthFlow();\n * const port = await getPort();\n * const tokens = await flow.performAuthFlow(\n * 'https://example.com/oauth/authorize',\n * 'https://example.com/oauth/token',\n * 'client-id',\n * 'client-secret',\n * { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }\n * );\n */\n async performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet> {\n const logger = options.logger ?? defaultLogger;\n const callbackListener = new OAuthCallbackListener({ port: options.port, logger });\n\n // Generate PKCE parameters if requested (RFC 7636)\n let pkce: PkceParams | undefined;\n if (options.pkce) {\n logger.debug('🔐 Generating PKCE parameters...');\n pkce = await generatePkce();\n }\n\n try {\n // Start callback server\n await callbackListener.start();\n\n // Build redirect URI\n const redirectUri = options.redirectUri || `http://localhost:${options.port}/callback`;\n\n // Build authorization URL\n const authUrl = new URL(authorizationEndpoint);\n authUrl.searchParams.set('client_id', clientId);\n authUrl.searchParams.set('redirect_uri', redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n\n if (options.scopes && options.scopes.length > 0) {\n authUrl.searchParams.set('scope', options.scopes.join(' '));\n }\n\n // Audience-bind the request to the resource server (RFC 8707)\n authUrl.searchParams.set('resource', options.resource);\n\n // Add PKCE parameters if generated (RFC 7636)\n if (pkce) {\n authUrl.searchParams.set('code_challenge', pkce.codeChallenge);\n authUrl.searchParams.set('code_challenge_method', pkce.codeChallengeMethod);\n }\n\n // Open browser or print URL for headless mode\n if (options.headless) {\n logger.info('🔗 Please visit this URL to authorize:');\n logger.info(authUrl.toString());\n logger.info('Waiting for callback...');\n } else {\n logger.debug('🌐 Opening browser for OAuth authorization...');\n // Try to open browser (requires 'open' package or native command)\n await this.openBrowser(authUrl.toString());\n }\n\n // Wait for callback with timeout\n const timeout = options.timeout || (options.headless ? 60000 : 300000);\n const result = await callbackListener.waitForCallback(timeout);\n\n this.assertResponseIssuer(result.iss, options, logger);\n\n // Exchange authorization code for tokens (with PKCE verifier if used)\n const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, options.resource, options.allowLoopback ?? false, pkce?.codeVerifier);\n\n return tokens;\n } catch (error) {\n logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));\n throw error;\n } finally {\n // Always close callback server\n await callbackListener.stop();\n }\n }\n\n /**\n * Rejects an authorization response that was not minted by the issuer\n * discovered before the flow started (RFC 9207 authorization-server mix-up).\n */\n private assertResponseIssuer(iss: string | undefined, options: OAuthFlowOptions, logger: Logger): void {\n if (iss !== undefined) {\n if (iss !== options.issuer) {\n throw new Error(`Authorization response issuer mismatch: got '${iss}', expected '${options.issuer}' - refusing to redeem the authorization code`);\n }\n return;\n }\n\n if (options.authorizationResponseIssSupported) {\n throw new Error(`Authorization server '${options.issuer}' advertises authorization_response_iss_parameter_supported but omitted 'iss' - refusing to redeem the authorization code`);\n }\n\n logger.debug(`⚠️ Authorization response carried no 'iss' and '${options.issuer}' does not advertise support for it (RFC 9207)`);\n }\n\n /**\n * Exchanges an authorization code for access and refresh tokens.\n * @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.\n * @param codeVerifier - Optional PKCE code verifier (RFC 7636).\n */\n private async exchangeCodeForTokens(tokenEndpoint: string, code: string, clientId: string, clientSecret: string, redirectUri: string, resource: string, allowLoopback: boolean, codeVerifier?: string): Promise<TokenSet> {\n const params = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n resource,\n });\n\n // Add PKCE code verifier if provided (RFC 7636)\n if (codeVerifier) {\n params.set('code_verifier', codeVerifier);\n }\n\n // tokenEndpoint is remote-controlled discovery data; discoveryFetch blocks\n // a private/internal target before the client secret is sent to it.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: params,\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || '',\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param tokenEndpoint - OAuth token endpoint URL.\n * @param refreshToken - Refresh token from a previous token set.\n * @param clientId - OAuth client ID.\n * @param clientSecret - OAuth client secret.\n * @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.\n * @returns New token set with a refreshed access token.\n * @throws Error if refresh fails.\n */\n async refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, resource: string, allowLoopback = false): Promise<TokenSet> {\n // See exchangeCodeForTokens - tokenEndpoint is remote-controlled discovery data.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n resource,\n }),\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token refresh response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken, // Reuse old refresh token if not provided\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Open browser to authorization URL\n * Uses platform-specific command to open default browser\n */\n private async openBrowser(url: string): Promise<void> {\n // Determine platform-specific command\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', url];\n } else {\n // Linux and others\n command = 'xdg-open';\n args = [url];\n }\n\n // Spawn browser process\n const child = child_process.spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n }\n}\n"],"names":["InteractiveOAuthFlow","performAuthFlow","authorizationEndpoint","tokenEndpoint","clientId","clientSecret","options","logger","callbackListener","pkce","redirectUri","authUrl","timeout","result","tokens","error","defaultLogger","OAuthCallbackListener","port","debug","generatePkce","start","URL","searchParams","set","scopes","length","join","resource","codeChallenge","codeChallengeMethod","headless","info","toString","openBrowser","waitForCallback","assertResponseIssuer","iss","exchangeCodeForTokens","code","allowLoopback","codeVerifier","Error","message","String","stop","undefined","issuer","authorizationResponseIssSupported","params","response","errorText","data","tokenSet","URLSearchParams","grant_type","redirect_uri","client_id","client_secret","discoveryFetch","method","headers","Accept","Connection","body","ok","text","status","json","access_token","accessToken","refreshToken","refresh_token","expiresAt","Date","now","expires_in","scope","split","refreshTokens","url","platform","command","args","child","process","child_process","spawn","detached","stdio","unref"],"mappings":"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"}
|
|
@@ -211,6 +211,7 @@ var OAuthCallbackListener = /*#__PURE__*/ function() {
|
|
|
211
211
|
*/ _proto.handleCallback = function handleCallback(url, res) {
|
|
212
212
|
var code = url.searchParams.get('code');
|
|
213
213
|
var state = url.searchParams.get('state');
|
|
214
|
+
var iss = url.searchParams.get('iss');
|
|
214
215
|
var error = url.searchParams.get('error');
|
|
215
216
|
var errorDescription = url.searchParams.get('error_description');
|
|
216
217
|
// Handle OAuth errors
|
|
@@ -249,6 +250,9 @@ var OAuthCallbackListener = /*#__PURE__*/ function() {
|
|
|
249
250
|
if (state) {
|
|
250
251
|
result.state = state;
|
|
251
252
|
}
|
|
253
|
+
if (iss) {
|
|
254
|
+
result.iss = iss;
|
|
255
|
+
}
|
|
252
256
|
this.resolveCallback(result);
|
|
253
257
|
}
|
|
254
258
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>${errorMessage}</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["OAuthCallbackListener","options","port","logger","defaultLogger","start","listen","debug","Promise","resolve","reject","server","http","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl"],"mappings":"AAAA;;;CAGC;;;;+BAmBYA;;;eAAAA;;;+DAjBI;wBACoC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgB9C,IAAA,AAAMA,sCAAN;;aAAMA,sBAQCC,OAAqC;gCARtCD;YAUKC;QADd,IAAI,CAACC,IAAI,GAAGD,QAAQC,IAAI;QACxB,IAAI,CAACC,MAAM,IAAGF,kBAAAA,QAAQE,MAAM,cAAdF,6BAAAA,kBAAkBG,gBAAa;;iBAVpCJ;IAaX;;;GAGC,GACD,OAAMK,KAGL,GAHD,SAAMA;;;;;wBACJ;;4BAAM,IAAI,CAACC,MAAM,CAAC,IAAI,CAACJ,IAAI;;;wBAA3B;wBACA,IAAI,CAACC,MAAM,CAACI,KAAK,CAAC,AAAC,mDAA4D,OAAV,IAAI,CAACL,IAAI,EAAC;;;;;;QACjF;;IAEA;;GAEC,GACD,OAAQI,MAcP,GAdD,SAAQA,OAAOJ,IAAY;;QACzB,OAAO,IAAIM,QAAQ,SAACC,SAASC;YAC3B,MAAKC,MAAM,GAAGC,iBAAI,CAACC,YAAY,CAAC,SAACC,KAAKC;gBACpC,MAAKC,aAAa,CAACF,KAAKC;YAC1B;YAEA,MAAKJ,MAAM,CAACM,EAAE,CAAC,SAAS,SAACC;gBACvBR,OAAOQ;YACT;YAEA,MAAKP,MAAM,CAACL,MAAM,CAACJ,MAAM;gBACvBO;YACF;QACF;IACF;IAEA;;GAEC,GACD,OAAQO,aASP,GATD,SAAQA,cAAcF,GAAyB,EAAEC,GAAwB;QACvE,IAAMI,MAAM,IAAIC,IAAIN,IAAIK,GAAG,IAAI,IAAI,AAAC,oBAA6B,OAAV,IAAI,CAACjB,IAAI;QAEhE,IAAIiB,IAAIE,QAAQ,KAAK,aAAa;YAChC,IAAI,CAACC,cAAc,CAACH,KAAKJ;QAC3B,OAAO;YACLA,IAAIQ,SAAS,CAAC,KAAK;gBAAE,gBAAgB;YAAa;YAClDR,IAAIS,GAAG,CAAC;QACV;IACF;IAEA;;GAEC,GACD,OAAQF,
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const iss = url.searchParams.get('iss');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>${errorMessage}</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n if (iss) {\n result.iss = iss;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["OAuthCallbackListener","options","port","logger","defaultLogger","start","listen","debug","Promise","resolve","reject","server","http","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","iss","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl"],"mappings":"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"}
|
|
@@ -4,37 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
11
|
-
* Probes .well-known/oauth-protected-resource endpoint
|
|
7
|
+
* Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`
|
|
8
|
+
* header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}
|
|
16
|
-
*
|
|
17
|
-
* @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)
|
|
18
|
-
* @returns ProtectedResourceMetadata if discovered, null otherwise
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com
|
|
22
|
-
* const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');
|
|
23
|
-
* // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
|
|
10
|
+
* @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).
|
|
11
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
24
12
|
*/
|
|
25
13
|
export declare function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null>;
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)
|
|
31
|
-
* @returns AuthorizationServerMetadata if discovered, null otherwise
|
|
15
|
+
* Discovers RFC 8414 Authorization Server Metadata at
|
|
16
|
+
* `.well-known/oauth-authorization-server`, path-local variant first.
|
|
32
17
|
*
|
|
33
|
-
* @
|
|
34
|
-
*
|
|
35
|
-
*
|
|
18
|
+
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).
|
|
19
|
+
* @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.
|
|
20
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
36
21
|
*/
|
|
37
|
-
export declare function discoverAuthorizationServerMetadata(authServerUrl: string
|
|
22
|
+
export declare function discoverAuthorizationServerMetadata(authServerUrl: string, options?: {
|
|
23
|
+
allowLoopback?: boolean;
|
|
24
|
+
}): Promise<AuthorizationServerMetadata | null>;
|
|
38
25
|
/**
|
|
39
26
|
* Discover OAuth Authorization Server Issuer from resource response (RFC 9207)
|
|
40
27
|
*
|
|
@@ -4,37 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
11
|
-
* Probes .well-known/oauth-protected-resource endpoint
|
|
7
|
+
* Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`
|
|
8
|
+
* header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}
|
|
16
|
-
*
|
|
17
|
-
* @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)
|
|
18
|
-
* @returns ProtectedResourceMetadata if discovered, null otherwise
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com
|
|
22
|
-
* const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');
|
|
23
|
-
* // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
|
|
10
|
+
* @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).
|
|
11
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
24
12
|
*/
|
|
25
13
|
export declare function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null>;
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)
|
|
31
|
-
* @returns AuthorizationServerMetadata if discovered, null otherwise
|
|
15
|
+
* Discovers RFC 8414 Authorization Server Metadata at
|
|
16
|
+
* `.well-known/oauth-authorization-server`, path-local variant first.
|
|
32
17
|
*
|
|
33
|
-
* @
|
|
34
|
-
*
|
|
35
|
-
*
|
|
18
|
+
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).
|
|
19
|
+
* @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.
|
|
20
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
36
21
|
*/
|
|
37
|
-
export declare function discoverAuthorizationServerMetadata(authServerUrl: string
|
|
22
|
+
export declare function discoverAuthorizationServerMetadata(authServerUrl: string, options?: {
|
|
23
|
+
allowLoopback?: boolean;
|
|
24
|
+
}): Promise<AuthorizationServerMetadata | null>;
|
|
38
25
|
/**
|
|
39
26
|
* Discover OAuth Authorization Server Issuer from resource response (RFC 9207)
|
|
40
27
|
*
|