@onyxsecurity/mcp-gateway 2.1.21 → 2.1.22

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.
@@ -1,4 +1,4 @@
1
- import{OAuthError as e,SSEClientTransport as t,StreamableHTTPClientTransport as n,discoverAuthorizationServerMetadata as r,discoverOAuthProtectedResourceMetadata as i,exchangeAuthorization as a,normalizeUrl as o,refreshAuthorization as s,registerClient as c,startAuthorization as l}from"./normalizeUrl-BrUSzVK4.js";import{config as u}from"./env-CQqjlDR9.js";import{logger as d}from"./logger-CWTMh2Co.js";import{findProviderForUrl as f}from"./providers-BCTTGfb7.js";import{platform as p}from"node:os";import{URL as m}from"node:url";import{createServer as h}from"node:http";import{execFile as g}from"node:child_process";async function _(e){let t=e.toString(),n=p(),r=x(t);return new Promise((e,i)=>{let a,o;if(n===`win32`){let e=`Start-Process ${y(t)}`,n=v(e);a=b(),o=[`-NoProfile`,`-NonInteractive`,`-ExecutionPolicy`,`Bypass`,`-EncodedCommand`,n]}else n===`darwin`?(a=`open`,o=[t]):(a=`xdg-open`,o=[t]);g(a,o,(t,o,s)=>{if(t){let e={command:a,os:n,stderr:s,url:r};d.warn(`Failed to open browser automatically`,{...e,error:t.message});let o=Error(`Failed to open browser: ${t.message} (command: ${a}, os: ${n}, url: ${r})`);o.cause=t,i(o)}else d.debug(`Browser opened successfully`,{url:r}),e()})})}function v(e){return Buffer.from(e,`utf16le`).toString(`base64`)}function y(e){return`'${e.replaceAll(`'`,`''`)}'`}function b(){return`${process.env.SYSTEMROOT||process.env.windir||`C:\\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`}function x(e){try{let t=new URL(e);return t.search?`${t.protocol}//${t.host}${t.pathname}?[REDACTED]`:`${t.protocol}//${t.host}${t.pathname}`}catch{return`[INVALID_URL]`}}const S={CALLBACK_FAILED:`mcp_auth_callback_failed`,DCR_FAILED:`mcp_auth_dcr_failed`,REFRESH_FAILED:`mcp_auth_refresh_failed`,SERVER_METADATA_FAILED:`mcp_auth_server_metadata_failed`,STATE_MISMATCH:`mcp_auth_state_mismatch`,TOKEN_EXCHANGE_FAILED:`mcp_auth_token_exchange_failed`},C=49152;function w(){return Math.floor(Math.random()*(65535-C+1))+C}function T(e,t){return new Promise((n,r)=>{let i=t=>{e.removeListener(`listening`,a),r(t)},a=()=>{e.removeListener(`error`,i),n()};e.once(`error`,i),e.once(`listening`,a),e.listen(t,`127.0.0.1`)})}async function E(e={}){let{maxAttempts:t=5,preferredPort:n,serverUrl:r,timeoutMs:i=3e5}=e,a=r?k(r):void 0,o,s,c={},l=new Promise((e,t)=>{o=e,s=t}),u,f=h((e,t)=>{if(e.url===`/favicon.ico`){t.writeHead(404),t.end();return}if(!e.url?.startsWith(`/callback`)){t.writeHead(404),t.end(`Not Found`);return}try{let n=new m(e.url,`http://localhost:${u}`),r=n.searchParams.get(`code`),i=n.searchParams.get(`error`),l=n.searchParams.get(`error_description`),p=n.searchParams.get(`state`);if(i){d.error(`OAuth authorization error`,{error:i,errorDescription:l,eventType:S.CALLBACK_FAILED,...a?{serverUrl:a}:{}}),t.writeHead(400,{"Content-Type":`text/html`}),t.end(O(i,l||void 0)),clearTimeout(c.id),s(Error(`OAuth authorization failed: ${i}${l?` - ${l}`:``}`));return}if(!r){d.error(`OAuth callback missing authorization code`,{eventType:S.CALLBACK_FAILED,reason:`missing_code`,...a?{serverUrl:a}:{}}),t.writeHead(400,{"Content-Type":`text/html`}),t.end(O(`missing_code`,`No authorization code was provided`)),clearTimeout(c.id),s(Error(`OAuth callback missing authorization code`));return}d.info(`OAuth authorization code received`,{codePrefix:`${r.substring(0,10)}...`,hasState:!!p}),t.writeHead(200,{"Content-Type":`text/html`}),t.end(`<!DOCTYPE html>
1
+ import{OAuthError as e,SSEClientTransport as t,StreamableHTTPClientTransport as n,discoverAuthorizationServerMetadata as r,discoverOAuthProtectedResourceMetadata as i,exchangeAuthorization as a,normalizeUrl as o,refreshAuthorization as s,registerClient as c,startAuthorization as l}from"./normalizeUrl-BrUSzVK4.js";import{config as u}from"./env-CoFKttme.js";import{logger as d}from"./logger-BLiPNNW4.js";import{findProviderForUrl as f}from"./providers-B5VTNpa-.js";import{platform as p}from"node:os";import{URL as m}from"node:url";import{createServer as h}from"node:http";import{execFile as g}from"node:child_process";async function _(e){let t=e.toString(),n=p(),r=x(t);return new Promise((e,i)=>{let a,o;if(n===`win32`){let e=`Start-Process ${y(t)}`,n=v(e);a=b(),o=[`-NoProfile`,`-NonInteractive`,`-ExecutionPolicy`,`Bypass`,`-EncodedCommand`,n]}else n===`darwin`?(a=`open`,o=[t]):(a=`xdg-open`,o=[t]);g(a,o,(t,o,s)=>{if(t){let e={command:a,os:n,stderr:s,url:r};d.warn(`Failed to open browser automatically`,{...e,error:t.message});let o=Error(`Failed to open browser: ${t.message} (command: ${a}, os: ${n}, url: ${r})`);o.cause=t,i(o)}else d.debug(`Browser opened successfully`,{url:r}),e()})})}function v(e){return Buffer.from(e,`utf16le`).toString(`base64`)}function y(e){return`'${e.replaceAll(`'`,`''`)}'`}function b(){return`${process.env.SYSTEMROOT||process.env.windir||`C:\\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`}function x(e){try{let t=new URL(e);return t.search?`${t.protocol}//${t.host}${t.pathname}?[REDACTED]`:`${t.protocol}//${t.host}${t.pathname}`}catch{return`[INVALID_URL]`}}const S={CALLBACK_FAILED:`mcp_auth_callback_failed`,DCR_FAILED:`mcp_auth_dcr_failed`,REFRESH_FAILED:`mcp_auth_refresh_failed`,SERVER_METADATA_FAILED:`mcp_auth_server_metadata_failed`,STATE_MISMATCH:`mcp_auth_state_mismatch`,TOKEN_EXCHANGE_FAILED:`mcp_auth_token_exchange_failed`},C=49152;function w(){return Math.floor(Math.random()*(65535-C+1))+C}function T(e,t){return new Promise((n,r)=>{let i=t=>{e.removeListener(`listening`,a),r(t)},a=()=>{e.removeListener(`error`,i),n()};e.once(`error`,i),e.once(`listening`,a),e.listen(t,`127.0.0.1`)})}async function E(e={}){let{maxAttempts:t=5,preferredPort:n,serverUrl:r,timeoutMs:i=3e5}=e,a=r?k(r):void 0,o,s,c={},l=new Promise((e,t)=>{o=e,s=t}),u,f=h((e,t)=>{if(e.url===`/favicon.ico`){t.writeHead(404),t.end();return}if(!e.url?.startsWith(`/callback`)){t.writeHead(404),t.end(`Not Found`);return}try{let n=new m(e.url,`http://localhost:${u}`),r=n.searchParams.get(`code`),i=n.searchParams.get(`error`),l=n.searchParams.get(`error_description`),p=n.searchParams.get(`state`);if(i){d.error(`OAuth authorization error`,{error:i,errorDescription:l,eventType:S.CALLBACK_FAILED,...a?{serverUrl:a}:{}}),t.writeHead(400,{"Content-Type":`text/html`}),t.end(O(i,l||void 0)),clearTimeout(c.id),s(Error(`OAuth authorization failed: ${i}${l?` - ${l}`:``}`));return}if(!r){d.error(`OAuth callback missing authorization code`,{eventType:S.CALLBACK_FAILED,reason:`missing_code`,...a?{serverUrl:a}:{}}),t.writeHead(400,{"Content-Type":`text/html`}),t.end(O(`missing_code`,`No authorization code was provided`)),clearTimeout(c.id),s(Error(`OAuth callback missing authorization code`));return}d.info(`OAuth authorization code received`,{codePrefix:`${r.substring(0,10)}...`,hasState:!!p}),t.writeHead(200,{"Content-Type":`text/html`}),t.end(`<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -133,4 +133,4 @@ import{OAuthError as e,SSEClientTransport as t,StreamableHTTPClientTransport as
133
133
  </div>
134
134
  </body>
135
135
  </html>`}function k(e){try{let t=new m(e);return t.username=``,t.password=``,t.search=``,t.hash=``,t.toString()}catch{return e}}var A=class extends Error{constructor(e,t){let n=t instanceof Error?t.message:String(t);super(`Auth server metadata discovery failed for ${e}: ${n}`),this.authServerUrl=e,this.cause=t,this.name=`AuthServerDiscoveryError`}};async function j(e,t={}){let{logger:n,signal:a}=t,o=a?M(a):void 0;n?.debug(`Discovering OAuth protected resource metadata...`);let s;try{s=o?await i(e,void 0,o):await i(e),n?.info(`Discovered protected resource metadata`,{authorizationServers:s?.authorization_servers,resource:s?.resource,scopesSupported:s?.scopes_supported})}catch(e){n?.warn(`Could not discover protected resource metadata, will try direct discovery`,{error:String(e)})}let c;s?.authorization_servers?.length?(c=new URL(s.authorization_servers[0]),n?.debug(`Using authorization server from resource metadata`,{authServerUrl:c.toString()})):(c=e,n?.debug(`Using server URL as authorization server`,{authServerUrl:c.toString()})),n?.debug(`Discovering authorization server metadata...`);let l;try{l=o?await r(c,{fetchFn:o}):await r(c)}catch(e){throw new A(c,e)}if(!l&&c.pathname!==`/`){let e=new URL(c.origin);n?.debug(`Auth server metadata absent at path-aware well-knowns, retrying at origin root`,{authServerUrl:c.toString(),rootUrl:e.toString()});try{l=o?await r(e,{fetchFn:o}):await r(e)}catch(t){if(a?.aborted)throw new A(e,t);n?.warn(`Origin-root auth server metadata fallback failed, treating metadata as absent`,{error:String(t),rootUrl:e.toString()})}l?(c=e,n?.info(`Origin-root auth server metadata fallback succeeded`,{authServerUrl:c.toString()})):n?.debug(`Origin-root auth server metadata fallback found nothing`,{rootUrl:e.toString()})}return l&&n?.info(`Discovered authorization server metadata`,{authorizationEndpoint:l.authorization_endpoint,issuer:l.issuer,registrationEndpoint:l.registration_endpoint,tokenEndpoint:l.token_endpoint}),{authMetadata:l,authServerUrl:c,resourceMetadata:s}}function M(e){return(t,n)=>{let r=n?.signal,i=r?AbortSignal.any([e,r]):e;return fetch(t,{...n,signal:i})}}function N(e){return F(e,`invalid_client`,new WeakSet)}function P(e){return F(e,`invalid_grant`,new WeakSet)}function F(t,n,r){if(t==null)return!1;if(t instanceof e)return t.errorCode===n;if(typeof t==`object`){if(r.has(t))return!1;r.add(t);let e=t.error;if(typeof e==`string`&&e===n)return!0;for(let e of[`response`,`data`,`body`,`cause`]){let i=t[e];if(i!==void 0&&F(i,n,r))return!0}}return t instanceof Error&&typeof t.message==`string`?RegExp(`"error"\\s*:\\s*"${n}"`).test(t.message)||RegExp(`\\berror\\s*=\\s*${n}\\b`).test(t.message)||RegExp(`\\berror\\s*:\\s*${n}\\b`).test(t.message):!1}const I={"claude-code":`Claude Code (mcp-gateway)`,codex:`Codex`,copilot:`Visual Studio Code`,cursor:`Cursor`,opencode:`OpenCode`},L={copilot:`https://code.visualstudio.com`},R={cursor:{"mcp.figma.com":`cursor://anysphere.cursor-mcp/oauth/callback`}};function z(){return U(u.clientAppName)}function B(){return W(u.clientAppName)}function V(e,t){return G(u.clientAppName,e,t)}async function H(e,t={}){let{credentialStore:n,scope:r,timeoutMs:i}=t,s=o(e),u=new URL(s);if(d.info(`Starting OAuth flow`,{serverUrl:s}),n){let e=await Y(u,n);if(e)return d.info(`Using existing OAuth credentials`,{clientId:e.clientInfo.client_id,serverUrl:s}),e}d.debug(`Starting OAuth callback server...`);let p=await E({serverUrl:s,timeoutMs:i}),m=p.callbackUrl;d.info(`OAuth callback server started`,{callbackUrl:m,port:p.port});try{let e=f(u);if(e)return d.info(`Using pre-configured OAuth provider`,{providerId:e.id,providerName:e.name}),await J(u,e,p,n,r);let i,o,h;try{({authMetadata:i,authServerUrl:o,resourceMetadata:h}=await j(u,{logger:d}))}catch(e){throw d.error(`Auth server metadata discovery failed`,{error:String(e),eventType:S.SERVER_METADATA_FAILED,serverUrl:s,...e instanceof A?{authServer:e.authServerUrl.toString()}:{}}),e}if(!i)throw d.error(`Auth server metadata absent (server may not support OAuth 2.0)`,{authServer:o.toString(),eventType:S.SERVER_METADATA_FAILED,reason:`metadata_absent`,serverUrl:s}),Error(`Could not discover OAuth metadata from ${o}. The server may not support OAuth 2.0 or the metadata endpoint is not accessible.`);let g=h?.scopes_supported,v=g?.includes(`offline_access`)??!1,y=r??(v?g?.join(` `):void 0);y&&!r?d.info(`Defaulting OAuth scope to the resource's advertised scopes_supported`,{scope:y,serverUrl:s}):!r&&g?.length&&d.debug(`Resource advertises scopes but not offline_access; requesting no scope`,{advertised:g,serverUrl:s});let b,x=n?await n.load(s):void 0,C=x?.clientInfo?.redirect_uris||[];if(x?.clientInfo&&C.includes(m)&&x?.clientInfo)d.info(`Reusing existing DCR client registration`,{clientId:x.clientInfo.client_id,clientName:x.clientInfo.client_name,redirectUri:m}),b=x.clientInfo;else{x?.clientInfo&&d.info(`Existing DCR client has different redirect_uri, re-registering`,{existingRedirectUris:C,newRedirectUri:m});let e=t.clientMetadata?.client_uri??B(),r={client_name:t.clientMetadata?.client_name||z(),...e&&{client_uri:e},grant_types:t.clientMetadata?.grant_types||[`authorization_code`,`refresh_token`],redirect_uris:V(u,m),response_types:t.clientMetadata?.response_types||[`code`],token_endpoint_auth_method:t.clientMetadata?.token_endpoint_auth_method||`none`,...y&&{scope:y}};if(d.debug(`Client metadata for DCR`,{clientMetadata:r}),!i.registration_endpoint)throw d.error(`DCR registration unsupported by auth server`,{authServer:o.toString(),eventType:S.DCR_FAILED,reason:`no_registration_endpoint`,serverUrl:s}),Error(`Authorization server does not support Dynamic Client Registration. No registration_endpoint found in metadata.`);d.info(`Registering client via DCR...`,{registrationEndpoint:i.registration_endpoint});try{b=await c(o,{clientMetadata:r,metadata:i})}catch(e){throw d.error(`DCR registration failed`,{authServer:o.toString(),error:String(e),eventType:S.DCR_FAILED,registrationEndpoint:i.registration_endpoint,serverUrl:s}),e}d.info(`Client registered successfully via DCR`,{clientId:b.client_id,clientName:b.client_name,clientSecretExpiresAt:b.client_secret_expires_at}),n&&await n.update(s,{clientInfo:b})}let w=crypto.randomUUID();d.debug(`Starting authorization flow...`);let{authorizationUrl:T,codeVerifier:E}=await l(o,{clientInformation:b,metadata:i,redirectUrl:m,scope:y,state:w});await _(T),d.debug(`Waiting for OAuth callback...`);let D=await p.waitForCallback();if(D.state!==w)throw d.error(`OAuth state mismatch — possible CSRF attack`,{authServer:o.toString(),eventType:S.STATE_MISMATCH,serverUrl:s}),Error(`OAuth state mismatch - possible CSRF attack`);d.info(`Authorization code received`,{codePrefix:`${D.code.substring(0,10)}...`}),d.debug(`Exchanging authorization code for tokens...`);let O;try{O=await a(o,{authorizationCode:D.code,clientInformation:b,codeVerifier:E,metadata:i,redirectUri:m})}catch(e){throw d.error(`Token exchange failed`,{authServer:o.toString(),clientId:b.client_id,error:String(e),eventType:S.TOKEN_EXCHANGE_FAILED,grantType:`authorization_code`,serverUrl:s}),e}if(d.info(`OAuth tokens obtained successfully`,{expiresIn:O.expires_in,grantedScope:O.scope,hasRefreshToken:!!O.refresh_token,requestedScope:y,tokenType:O.token_type}),n){let e=new Date().toISOString();await n.update(s,{tokens:O,tokensObtainedAt:e}),d.info(`OAuth credentials persisted to disk`,{serverUrl:s})}return{clientInfo:b,fromCache:!1,tokens:O}}finally{await p.close()}}function U(e){let t=e?.trim().toLowerCase();return t&&Object.hasOwn(I,t)?I[t]:`MCP Gateway`}function W(e){let t=e?.trim().toLowerCase();if(t)return Object.hasOwn(L,t)?L[t]:void 0}function G(e,t,n){let r=e?.trim().toLowerCase();if(!r)return[n];let i=R[r];if(!i)return[n];let a;try{a=(t instanceof URL?t:new URL(t)).hostname.toLowerCase()}catch{return[n]}let o=i[a];return o?[o,n]:[n]}async function K(e,t){let n=typeof e==`string`?new URL(e):e,r=await t.load(n.toString());if(!(!r?.clientInfo||!r.tokens?.refresh_token))return X(n,r,t)}async function q(e,t,n,r){let i=e.tokenEndpoint,a=e.tokenEndpointAuthMethod||(e.clientSecret?`client_secret_post`:`none`),o=new URLSearchParams;o.set(`grant_type`,`authorization_code`),o.set(`code`,t),o.set(`redirect_uri`,n),r&&o.set(`code_verifier`,r);let s={Accept:`application/json`,"Content-Type":`application/x-www-form-urlencoded`};a===`client_secret_basic`&&e.clientSecret?s.Authorization=`Basic ${Buffer.from(`${e.clientId}:${e.clientSecret}`).toString(`base64`)}`:a===`client_secret_post`&&e.clientSecret?(o.set(`client_id`,e.clientId),o.set(`client_secret`,e.clientSecret)):o.set(`client_id`,e.clientId),d.debug(`Sending token request`,{authMethod:a,providerId:e.id,tokenEndpoint:i});let c=await fetch(i,{body:o.toString(),headers:s,method:`POST`});if(!c.ok){let t=await c.text();throw d.error(`Token exchange failed`,{authMethod:a,error:t,eventType:S.TOKEN_EXCHANGE_FAILED,grantType:`authorization_code`,httpStatus:c.status,providerId:e.id,tokenEndpoint:i}),Error(`Token exchange failed: ${c.status} ${t}`)}let l=c.headers.get(`content-type`)||``,u;if(l.includes(`application/json`))u=await c.json();else if(l.includes(`application/x-www-form-urlencoded`)||l.includes(`text/plain`)){let e=await c.text(),t=new URLSearchParams(e);u=Object.fromEntries(t.entries())}else{let e=await c.text();try{u=JSON.parse(e)}catch{let t=new URLSearchParams(e);u=Object.fromEntries(t.entries())}}return typeof u.expires_in==`string`&&(u.expires_in=parseInt(u.expires_in,10)),{access_token:u.access_token,expires_in:u.expires_in,refresh_token:u.refresh_token,scope:u.scope,token_type:u.token_type||`bearer`}}async function J(e,t,n,r,i){let a=e.toString(),o=n.callbackUrl,s={client_id:t.clientId,client_name:t.name,client_secret:t.clientSecret,redirect_uris:[o],token_endpoint_auth_method:t.tokenEndpointAuthMethod||(t.clientSecret?`client_secret_post`:`none`)};d.info(`Using pre-configured OAuth client`,{clientId:t.clientId,hasClientSecret:!!t.clientSecret,providerId:t.id,tokenEndpointAuthMethod:s.token_endpoint_auth_method});let c=i||t.scopes?.join(` `),l=t.usePkce!==!1,u,f,p;if(l){let{generateCodeChallenge:e,generateCodeVerifier:t}=await import(`./pkce-CYE2WffE.js`);u=t(),f=await e(u),p=`S256`}let m=new URL(t.authorizationEndpoint);m.searchParams.set(`client_id`,t.clientId),m.searchParams.set(`redirect_uri`,o),m.searchParams.set(`response_type`,`code`),c&&m.searchParams.set(`scope`,c);let h=crypto.randomUUID();m.searchParams.set(`state`,h),f&&p&&(m.searchParams.set(`code_challenge`,f),m.searchParams.set(`code_challenge_method`,p)),await _(m),d.debug(`Waiting for OAuth callback...`);let g=await n.waitForCallback();if(g.state!==h)throw d.error(`OAuth state mismatch — possible CSRF attack`,{eventType:S.STATE_MISMATCH,providerId:t.id,serverUrl:a}),Error(`OAuth state mismatch - possible CSRF attack`);d.info(`Authorization code received`,{codePrefix:`${g.code.substring(0,10)}...`,providerId:t.id}),d.debug(`Exchanging authorization code for tokens...`);let v=await q(t,g.code,o,u);if(d.info(`OAuth tokens obtained successfully`,{expiresIn:v.expires_in,hasRefreshToken:!!v.refresh_token,providerId:t.id,tokenType:v.token_type}),r){let e=new Date().toISOString();await r.update(a,{clientInfo:s,tokens:v,tokensObtainedAt:e}),d.info(`OAuth credentials persisted to disk`,{providerId:t.id,serverUrl:a})}return{clientInfo:s,fromCache:!1,tokens:v}}async function Y(e,t){let n=await t.load(e.toString());if(!n){d.debug(`No stored credentials found`,{serverUrl:e.toString()});return}if(!n.clientInfo){d.debug(`Stored credentials have no client info`,{serverUrl:e.toString()});return}if(t.hasValidAccessToken(n)&&n.tokens)return d.info(`Using cached OAuth credentials with valid access token`,{clientId:n.clientInfo.client_id,serverUrl:e.toString()}),{clientInfo:n.clientInfo,fromCache:!0,tokens:n.tokens};if(t.hasRefreshToken(n)){let r=await X(e,n,t);if(r)return r}d.debug(`Stored credentials are expired and no valid refresh token`,{serverUrl:e.toString()})}async function X(e,t,n){if(!(!t.clientInfo||!t.tokens?.refresh_token)){d.info(`Attempting to refresh expired access token`,{clientId:t.clientInfo.client_id,serverUrl:e.toString()});try{let r=new AbortController,i=setTimeout(()=>r.abort(),15e3),a,o;try{({authMetadata:a,authServerUrl:o}=await j(e,{logger:d,signal:r.signal}))}finally{clearTimeout(i)}let c=await s(o,{clientInformation:t.clientInfo,metadata:a,refreshToken:t.tokens.refresh_token});d.info(`Successfully refreshed access token`,{clientId:t.clientInfo.client_id,expiresIn:c.expires_in,grantedScope:c.scope,hasNewRefreshToken:!!c.refresh_token});let l=c.refresh_token?c:{...c,refresh_token:t.tokens.refresh_token},u=new Date().toISOString();return await n.update(e.toString(),{tokens:l,tokensObtainedAt:u}),{clientInfo:t.clientInfo,fromCache:!0,tokens:l}}catch(r){let i=P(r),a=N(r),o=i&&a,s=i&&!o,c=a&&!o;d.warn(o?`Refresh failed with both invalid_client and invalid_grant; ambiguous, retaining credentials`:c?`Client registration rejected by OAuth server (invalid_client); clearing stored client and tokens`:s?`Refresh token rejected by OAuth server (invalid_grant); clearing stored tokens`:`Token refresh failed transiently; retaining refresh_token for the next attempt`,{ambiguous:o,clientId:t.clientInfo.client_id,error:String(r),eventType:S.REFRESH_FAILED,invalidClient:c,invalidGrant:s,serverUrl:e.toString()}),c?await n.update(e.toString(),{clientInfo:void 0,tokens:void 0,tokensObtainedAt:void 0}):s&&await n.update(e.toString(),{tokens:void 0,tokensObtainedAt:void 0});return}}}const Z=[`streamable-http`,`sse`];async function Q(e,t,n,r){let{result:i,transportType:a}=await $(async i=>{let a=e(),o=ee(t,i,n,r);return await a.connect(o),a});return{client:i,transportType:a}}function ee(e,r,i,a){let o=new URL(e);return r===`streamable-http`?new n(o,{fetch:a,requestInit:i}):new t(o,{fetch:a,requestInit:i})}function te(e){if(!e||typeof e!=`object`)return!1;let t=e.status,n=e.code,r=e.message;return!!(t===404||t===405||n===404||n===405||r&&/404|not found|405|method not allowed/i.test(String(r)))}async function $(e){let t=[];for(let n of Z)try{return{result:await e(n),transportType:n}}catch(e){if(te(e)){d.info(`${n} not supported, trying next transport...`),t.push({err:e,transport:n});continue}throw e}let n=t.map(({err:e,transport:t})=>{let n=e?.status;if(n!==void 0)return`${t}: ${n}`;let r=e instanceof Error?e.message:String(e);return`${t}: ${r}`}).join(`, `),r=t.length>0?Error(`all transports returned not-supported (${n})`):Error(`No compatible transport found`);throw r.cause=t,r}export{A as AuthServerDiscoveryError,Q as connectWithTransportFallback,j as discoverAuthServer,z as getDcrClientName,B as getDcrClientUri,V as getDcrRedirectUris,H as performOAuthFlow,K as tryRefreshCachedToken,$ as walkTransports};
136
- //# sourceMappingURL=transportDetection-JaimbEAP.js.map
136
+ //# sourceMappingURL=transportDetection-0rgX1zGT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transportDetection-JaimbEAP.js","names":["command: string","args: string[]","resolveCallback: (result: CallbackServerResult) => void","rejectCallback: (error: Error) => void","timeoutRef: { id?: ReturnType<typeof setTimeout> }","boundPort: number","URL","attemptedPorts: number[]","lastError: Error | undefined","portToTry: number","authServerUrl: URL","cause: unknown","resourceMetadata: OAuthProtectedResourceMetadata | undefined","authMetadata: Awaited<ReturnType<typeof discoverAuthorizationServerMetadata>>","DCR_CLIENT_NAME_BY_APP: Record<string, string>","DCR_CLIENT_URI_BY_APP: Record<string, string>","DECOY_REDIRECT_URI_BY_APP_AND_HOST: Record<string, Record<string, string>>","authMetadata: Awaited<ReturnType<typeof discoverAuthServer>>[\"authMetadata\"]","authServerUrl: URL","resourceMetadata: OAuthProtectedResourceMetadata | undefined","clientInfo: OAuthClientInformationFull","clientMetadata: OAuthClientMetadata","hostname: string","hostname","headers: Record<string, string>","tokenData: Record<string, unknown>","codeVerifier: string | undefined","codeChallenge: string | undefined","codeChallengeMethod: string | undefined","mergedTokens: OAuthTokens","TRANSPORT_PRIORITY: TransportType[]","client","transportType","failures: Array<{ err: unknown; transport: TransportType }>"],"sources":["../src/auth/browser.ts","../src/auth/events.ts","../src/auth/callbackServer.ts","../src/auth/discovery.ts","../src/auth/errors.ts","../src/auth/oauthFlow.ts","../src/lib/transportDetection.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { platform } from \"node:os\";\n\nimport { logger } from \"../lib/logger.js\";\n\n/**\n * Opens the given URL in the user's default browser.\n *\n * Cross-platform support:\n * - macOS: uses `open`\n * - Windows: uses PowerShell Start-Process with Base64-encoded command (injection-safe)\n * - Linux: uses `xdg-open`\n *\n * @param url The URL to open\n * @returns A promise that resolves when the browser command is executed\n */\nexport async function openBrowser(url: string | URL): Promise<void> {\n const urlString = url.toString();\n const os = platform();\n const redactedUrl = redactUrlForLogging(urlString);\n\n return new Promise((resolve, reject) => {\n let command: string;\n let args: string[];\n\n if (os === \"win32\") {\n // Windows - use PowerShell with Base64-encoded command to prevent injection\n // This is the same approach used by the popular 'open' npm package.\n // The command is Base64-encoded (UTF-16LE) so no shell metacharacters can escape.\n const psCommand = `Start-Process ${escapePowerShellArgument(urlString)}`;\n const encodedCommand = encodePowerShellCommand(psCommand);\n\n command = getPowerShellPath();\n args = [\"-NoProfile\", \"-NonInteractive\", \"-ExecutionPolicy\", \"Bypass\", \"-EncodedCommand\", encodedCommand];\n } else if (os === \"darwin\") {\n // macOS\n command = \"open\";\n args = [urlString];\n } else {\n // Linux and others - use xdg-open\n command = \"xdg-open\";\n args = [urlString];\n }\n\n execFile(command, args, (error, _stdout, stderr) => {\n if (error) {\n const errorContext = {\n command,\n os,\n stderr,\n url: redactedUrl,\n };\n\n logger.warn(\"Failed to open browser automatically\", {\n ...errorContext,\n error: error.message,\n });\n\n const launchError = new Error(\n `Failed to open browser: ${error.message} (command: ${command}, os: ${os}, url: ${redactedUrl})`\n );\n launchError.cause = error;\n reject(launchError);\n } else {\n logger.debug(\"Browser opened successfully\", { url: redactedUrl });\n resolve();\n }\n });\n });\n}\n\n/**\n * Encodes a PowerShell command as Base64 UTF-16LE for use with -EncodedCommand.\n * This is the safest way to pass commands to PowerShell as it prevents any\n * shell metacharacter interpretation.\n */\nfunction encodePowerShellCommand(command: string): string {\n // PowerShell's -EncodedCommand expects UTF-16LE encoded Base64\n const buffer = Buffer.from(command, \"utf16le\");\n return buffer.toString(\"base64\");\n}\n\n/**\n * Escapes a string for use as a PowerShell single-quoted string argument.\n * Single quotes inside the string are escaped by doubling them.\n */\nfunction escapePowerShellArgument(value: string): string {\n return `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\n/**\n * Gets the path to Windows PowerShell.\n * PowerShell is included by default since Windows 7 SP1 and Windows Server 2008 R2.\n */\nfunction getPowerShellPath(): string {\n const systemRoot = process.env.SYSTEMROOT || process.env.windir || \"C:\\\\Windows\";\n return `${systemRoot}\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe`;\n}\n\n/**\n * Redacts sensitive query parameters from a URL for safe logging.\n * Preserves the base URL structure while hiding potentially sensitive data.\n *\n * @param url The URL to redact\n * @returns A redacted version of the URL safe for logging\n */\nfunction redactUrlForLogging(url: string): string {\n try {\n const parsed = new URL(url);\n if (parsed.search) {\n return `${parsed.protocol}//${parsed.host}${parsed.pathname}?[REDACTED]`;\n }\n return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n } catch {\n // If URL parsing fails, return a generic redacted string\n return \"[INVALID_URL]\";\n }\n}\n","/**\n * Structured `eventType` values emitted from the OAuth code paths in this\n * package. groundcover monitors filter on these strings, so renames here are\n * breaking changes for the monitoring pipeline — bump the monitor YAMLs in\n * the infrastructure repo when you change anything below.\n *\n * Naming convention: `mcp_auth_*_failed` for unrecoverable errors; the\n * `mcp_auth_performed` happy-path event lives in src/bin/main.ts because it\n * is emitted by the proxy boot code, not by this auth subpackage.\n */\nexport const AuthEventType = {\n /** OAuth callback server received error= or no code, or failed to parse. */\n CALLBACK_FAILED: \"mcp_auth_callback_failed\",\n /** registerClient threw or auth server has no registration_endpoint. */\n DCR_FAILED: \"mcp_auth_dcr_failed\",\n /** Refresh-token grant rejected by the auth server. */\n REFRESH_FAILED: \"mcp_auth_refresh_failed\",\n /**\n * `discoverAuthorizationServerMetadata` threw, or returned an empty\n * document. NOTE: resource-metadata soft-fails are NOT emitted as failed\n * events — the SDK silently falls back to using the server URL as the\n * auth server, which is expected for some MCP servers.\n */\n SERVER_METADATA_FAILED: \"mcp_auth_server_metadata_failed\",\n /** OAuth state parameter mismatch — possible CSRF, definitely a failure. */\n STATE_MISMATCH: \"mcp_auth_state_mismatch\",\n /** Authorization-code or refresh-token POST to /token failed (non-2xx). */\n TOKEN_EXCHANGE_FAILED: \"mcp_auth_token_exchange_failed\",\n} as const;\n\nexport type AuthEventType = (typeof AuthEventType)[keyof typeof AuthEventType];\n","import { createServer, type Server } from \"node:http\";\nimport { URL } from \"node:url\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { AuthEventType } from \"./events.js\";\n\n/**\n * Port range for OAuth callback server (IANA dynamic/private ports)\n */\nconst MIN_PORT = 49152;\nconst MAX_PORT = 65535;\nconst MAX_PORT_ATTEMPTS = 5;\n\nexport interface CallbackServerOptions {\n /**\n * Maximum number of port binding attempts before giving up.\n * Default: 5\n */\n maxAttempts?: number;\n /**\n * Preferred port to use. If unavailable, random ports will be tried.\n */\n preferredPort?: number;\n /**\n * MCP server URL whose OAuth flow this callback is serving. Threaded\n * through to the callback's error logs so groundcover monitors can\n * attribute mcp_auth_callback_failed events to a specific remote.\n * Optional — when omitted, the field is simply absent from the logs.\n */\n serverUrl?: string;\n /**\n * Timeout in milliseconds to wait for the callback.\n * Default: 5 minutes (300000ms)\n */\n timeoutMs?: number;\n}\n\nexport interface CallbackServerResult {\n /**\n * The authorization code received from the OAuth server.\n */\n code: string;\n /**\n * The state parameter, if provided.\n */\n state?: string;\n}\n\n/**\n * Generates a random port number in the dynamic/private port range.\n */\nfunction getRandomPort(): number {\n return Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT;\n}\n\n/**\n * Attempts to start the HTTP server on the given port.\n * Returns a promise that resolves with the server if successful, or rejects if the port is in use.\n */\nfunction tryListenOnPort(server: Server, port: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const onError = (err: NodeJS.ErrnoException) => {\n server.removeListener(\"listening\", onListening);\n reject(err);\n };\n\n const onListening = () => {\n server.removeListener(\"error\", onError);\n resolve();\n };\n\n server.once(\"error\", onError);\n server.once(\"listening\", onListening);\n server.listen(port, \"127.0.0.1\");\n });\n}\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * HTML page shown to the user after successful authorization.\n */\nconst SUCCESS_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Authorization Successful</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n color: #fff;\n }\n .container {\n text-align: center;\n padding: 2rem;\n }\n .checkmark {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: #10b981;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 1.5rem;\n animation: pop 0.3s ease-out;\n }\n .checkmark svg {\n width: 40px;\n height: 40px;\n stroke: white;\n stroke-width: 3;\n }\n h1 {\n margin: 0 0 0.5rem;\n font-size: 1.75rem;\n font-weight: 600;\n }\n p {\n margin: 0;\n color: #a0aec0;\n font-size: 1rem;\n }\n @keyframes pop {\n 0% { transform: scale(0); }\n 50% { transform: scale(1.1); }\n 100% { transform: scale(1); }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"checkmark\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M5 13l4 4L19 7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n </div>\n <script>setTimeout(() => window.close(), 3000);</script>\n</body>\n</html>`;\n\n/**\n * Starts a local HTTP server to receive the OAuth callback.\n * Uses random port selection with retry logic to handle port conflicts.\n *\n * @returns A promise that resolves with the port number and a function to wait for the callback.\n */\nexport async function startCallbackServer(options: CallbackServerOptions = {}): Promise<{\n callbackUrl: string;\n close: () => Promise<void>;\n port: number;\n waitForCallback: () => Promise<CallbackServerResult>;\n}> {\n const { maxAttempts = MAX_PORT_ATTEMPTS, preferredPort, serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n // Strip userinfo + query params before logging — a configured MCP URL\n // can carry tokens or basic-auth credentials, and these errors get\n // persisted to logs/alerts. Keep the protocol+host+path so the log is\n // still useful for triage.\n const safeServerUrl = serverUrl ? sanitizeServerUrl(serverUrl) : undefined;\n\n let resolveCallback: (result: CallbackServerResult) => void;\n let rejectCallback: (error: Error) => void;\n // Using an object to hold the timeout ID so it can be set after server creation\n // but still referenced in the close() function\n const timeoutRef: { id?: ReturnType<typeof setTimeout> } = {};\n\n const callbackPromise = new Promise<CallbackServerResult>((resolve, reject) => {\n resolveCallback = resolve;\n rejectCallback = reject;\n });\n\n // Track the actual bound port for use in request handler\n let boundPort: number;\n\n const server = createServer((req, res) => {\n // Ignore favicon requests\n if (req.url === \"/favicon.ico\") {\n res.writeHead(404);\n res.end();\n return;\n }\n\n // Only handle the callback path\n if (!req.url?.startsWith(\"/callback\")) {\n res.writeHead(404);\n res.end(\"Not Found\");\n return;\n }\n\n try {\n const parsedUrl = new URL(req.url, `http://localhost:${boundPort}`);\n const code = parsedUrl.searchParams.get(\"code\");\n const error = parsedUrl.searchParams.get(\"error\");\n const errorDescription = parsedUrl.searchParams.get(\"error_description\");\n const state = parsedUrl.searchParams.get(\"state\");\n\n if (error) {\n logger.error(\"OAuth authorization error\", {\n error,\n errorDescription,\n eventType: AuthEventType.CALLBACK_FAILED,\n ...(safeServerUrl ? { serverUrl: safeServerUrl } : {}),\n });\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(getErrorHtml(error, errorDescription || undefined));\n clearTimeout(timeoutRef.id);\n rejectCallback(\n new Error(`OAuth authorization failed: ${error}${errorDescription ? ` - ${errorDescription}` : \"\"}`)\n );\n return;\n }\n\n if (!code) {\n logger.error(\"OAuth callback missing authorization code\", {\n eventType: AuthEventType.CALLBACK_FAILED,\n reason: \"missing_code\",\n ...(safeServerUrl ? { serverUrl: safeServerUrl } : {}),\n });\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(getErrorHtml(\"missing_code\", \"No authorization code was provided\"));\n clearTimeout(timeoutRef.id);\n rejectCallback(new Error(\"OAuth callback missing authorization code\"));\n return;\n }\n\n logger.info(\"OAuth authorization code received\", {\n codePrefix: `${code.substring(0, 10)}...`,\n hasState: !!state,\n });\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(SUCCESS_HTML);\n\n clearTimeout(timeoutRef.id);\n resolveCallback({ code, state: state || undefined });\n\n // Close server after a short delay to ensure response is sent\n setTimeout(() => server.close(), 1000);\n } catch (err) {\n logger.error(\"Error processing OAuth callback\", {\n error: String(err),\n eventType: AuthEventType.CALLBACK_FAILED,\n reason: \"callback_processing_error\",\n ...(safeServerUrl ? { serverUrl: safeServerUrl } : {}),\n });\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n clearTimeout(timeoutRef.id);\n rejectCallback(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n // Set up timeout (now that server exists)\n timeoutRef.id = setTimeout(() => {\n rejectCallback(new Error(`OAuth callback timeout after ${timeoutMs}ms`));\n server.close();\n }, timeoutMs);\n\n // Try to bind to a port with retry logic\n const attemptedPorts: number[] = [];\n let lastError: Error | undefined;\n\n while (attemptedPorts.length < maxAttempts) {\n // First attempt uses preferred port if provided, otherwise random\n let portToTry: number;\n if (attemptedPorts.length === 0 && preferredPort !== undefined) {\n portToTry = preferredPort;\n } else {\n // Generate a random port that hasn't been tried yet\n portToTry = getRandomPort();\n while (attemptedPorts.includes(portToTry)) {\n portToTry = getRandomPort();\n }\n }\n attemptedPorts.push(portToTry);\n\n try {\n await tryListenOnPort(server, portToTry);\n boundPort = portToTry;\n const callbackUrl = `http://localhost:${boundPort}/callback`;\n logger.info(\"OAuth callback server started\", { attempt: attemptedPorts.length, callbackUrl, port: boundPort });\n\n return {\n callbackUrl,\n close: () =>\n new Promise<void>((resolve) => {\n clearTimeout(timeoutRef.id);\n server.close(() => resolve());\n // Browsers hold the keep-alive socket open after the OAuth success\n // page loads. Without forcing existing connections closed, Node's\n // server.close() waits indefinitely for them to drain and the\n // OAuth flow hangs after persisting tokens.\n server.closeAllConnections();\n }),\n port: boundPort,\n waitForCallback: () => callbackPromise,\n };\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === \"EADDRINUSE\") {\n logger.debug(\"Port in use, trying another port\", {\n attempt: attemptedPorts.length,\n maxAttempts,\n port: portToTry,\n });\n lastError = new Error(`Port ${portToTry} is already in use`);\n } else {\n // For non-EADDRINUSE errors, clean up and throw immediately\n clearTimeout(timeoutRef.id);\n server.close();\n throw err;\n }\n }\n }\n\n // All attempts failed\n clearTimeout(timeoutRef.id);\n server.close();\n throw new Error(\n `Failed to start OAuth callback server after ${maxAttempts} attempts. ` +\n `Tried ports: ${attemptedPorts.join(\", \")}. Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\n/**\n * HTML page shown to the user when authorization fails.\n */\nfunction getErrorHtml(error: string, description?: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Authorization Failed</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n color: #fff;\n }\n .container {\n text-align: center;\n padding: 2rem;\n max-width: 400px;\n }\n .error-icon {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: #ef4444;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 1.5rem;\n }\n .error-icon svg {\n width: 40px;\n height: 40px;\n stroke: white;\n stroke-width: 3;\n }\n h1 {\n margin: 0 0 0.5rem;\n font-size: 1.75rem;\n font-weight: 600;\n }\n .error-code {\n color: #f87171;\n font-family: monospace;\n margin-bottom: 0.5rem;\n }\n p {\n margin: 0;\n color: #a0aec0;\n font-size: 1rem;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"error-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M6 18L18 6M6 6l12 12\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Authorization Failed</h1>\n <p class=\"error-code\">${escapeHtml(error)}</p>\n ${description ? `<p>${escapeHtml(description)}</p>` : \"\"}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Strip credentials and query params from a URL before logging. Keeps\n * protocol + host + path so the log is still useful for triage. Returns\n * the original string when parsing fails (better to over-log than to\n * silently drop the attribution field).\n */\nfunction sanitizeServerUrl(url: string): string {\n try {\n const u = new URL(url);\n u.username = \"\";\n u.password = \"\";\n u.search = \"\";\n u.hash = \"\";\n return u.toString();\n } catch {\n return url;\n }\n}\n","import {\n discoverAuthorizationServerMetadata,\n discoverOAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type { OAuthProtectedResourceMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nexport interface DiscoverAuthServerOptions {\n /**\n * Optional logger. When provided, emits the same debug/info/warn lines that\n * `performOAuthFlow` used to emit inline. When omitted, discovery runs\n * silently.\n */\n logger?: DiscoveryLogger;\n /**\n * Optional AbortSignal, threaded into both SDK discovery calls via a fetch\n * wrapper. When omitted, the SDK calls are made with no fetch override —\n * preserving the existing `performOAuthFlow` call shape exactly.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal logging surface used by the discovery helper. Matches the shape of\n * `../lib/logger.ts`'s singleton so callers can pass it directly. Omit to run\n * discovery silently (the probe's call pattern).\n */\nexport interface DiscoveryLogger {\n debug(message: string, meta?: Record<string, unknown>): void;\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nexport interface DiscoveryResult {\n /** Auth-server metadata. May be undefined — caller decides how to respond. */\n authMetadata: Awaited<ReturnType<typeof discoverAuthorizationServerMetadata>>;\n /**\n * The chosen auth server URL: the first entry of `resourceMetadata.authorization_servers`\n * when present, otherwise `serverUrl` itself. When metadata was only found\n * via the origin-root fallback, this is the origin root of that URL.\n */\n authServerUrl: URL;\n /** Protected-resource metadata. Undefined when the document is missing or lookup soft-failed. */\n resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n}\n\n/**\n * Thrown when `discoverAuthorizationServerMetadata` fails. Preserves the\n * computed `authServerUrl` so callers can include it in error messages /\n * probe results — the pre-refactor inline code had it in scope naturally.\n */\nexport class AuthServerDiscoveryError extends Error {\n constructor(\n public readonly authServerUrl: URL,\n public readonly cause: unknown\n ) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n super(`Auth server metadata discovery failed for ${authServerUrl}: ${msg}`);\n this.name = \"AuthServerDiscoveryError\";\n }\n}\n\n/**\n * Runs the OAuth metadata discovery sequence shared by the full OAuth flow and\n * the install-time DCR probe:\n *\n * 1. Look up the protected-resource metadata (soft-fails on error).\n * 2. Pick the auth server URL — the first `authorization_servers` entry, or\n * `serverUrl` as a fallback.\n * 3. Look up the auth-server metadata. Throws iff the SDK call itself throws;\n * does NOT throw when the SDK returns undefined (caller decides).\n * 4. If step 3 found nothing and the auth server URL has a path, retry the\n * lookup once at the origin root (see inline comment) — on success,\n * `authServerUrl` in the result is the origin root.\n *\n * No policy decisions baked in — both callers (`performOAuthFlow` and `probeOne`)\n * apply their own handling to the result.\n */\nexport async function discoverAuthServer(\n serverUrl: URL,\n options: DiscoverAuthServerOptions = {}\n): Promise<DiscoveryResult> {\n const { logger, signal } = options;\n const fetchFn = signal ? signaledFetch(signal) : undefined;\n\n logger?.debug(\"Discovering OAuth protected resource metadata...\");\n let resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n try {\n // Preserve the exact SDK call arity used by performOAuthFlow when no\n // signal is provided (single-arg form) — some SDKs treat an explicit\n // `undefined` differently from an omitted arg.\n resourceMetadata = fetchFn\n ? await discoverOAuthProtectedResourceMetadata(serverUrl, undefined, fetchFn)\n : await discoverOAuthProtectedResourceMetadata(serverUrl);\n logger?.info(\"Discovered protected resource metadata\", {\n authorizationServers: resourceMetadata?.authorization_servers,\n resource: resourceMetadata?.resource,\n scopesSupported: resourceMetadata?.scopes_supported,\n });\n } catch (err) {\n logger?.warn(\"Could not discover protected resource metadata, will try direct discovery\", {\n error: String(err),\n });\n }\n\n let authServerUrl: URL;\n if (resourceMetadata?.authorization_servers?.length) {\n authServerUrl = new URL(resourceMetadata.authorization_servers[0]);\n logger?.debug(\"Using authorization server from resource metadata\", {\n authServerUrl: authServerUrl.toString(),\n });\n } else {\n authServerUrl = serverUrl;\n logger?.debug(\"Using server URL as authorization server\", {\n authServerUrl: authServerUrl.toString(),\n });\n }\n\n logger?.debug(\"Discovering authorization server metadata...\");\n let authMetadata: Awaited<ReturnType<typeof discoverAuthorizationServerMetadata>>;\n try {\n authMetadata = fetchFn\n ? await discoverAuthorizationServerMetadata(authServerUrl, { fetchFn })\n : await discoverAuthorizationServerMetadata(authServerUrl);\n } catch (err) {\n throw new AuthServerDiscoveryError(authServerUrl, err);\n }\n\n // The SDK's buildDiscoveryUrls only probes path-aware well-knowns for URLs\n // with a path (e.g. /.well-known/oauth-authorization-server/v1/mcp) — it\n // never tries the origin root. Older SDKs did, and servers like Atlassian's\n // remote MCP publish auth-server metadata ONLY at the root\n // /.well-known/oauth-authorization-server (with no RFC 9728\n // protected-resource metadata to redirect us there). Restore the root\n // fallback: when the path-aware lookup came back absent and the auth server\n // URL has a path, retry once against the origin root. Never fires when\n // metadata was already found.\n if (!authMetadata && authServerUrl.pathname !== \"/\") {\n const rootUrl = new URL(authServerUrl.origin);\n logger?.debug(\"Auth server metadata absent at path-aware well-knowns, retrying at origin root\", {\n authServerUrl: authServerUrl.toString(),\n rootUrl: rootUrl.toString(),\n });\n try {\n authMetadata = fetchFn\n ? await discoverAuthorizationServerMetadata(rootUrl, { fetchFn })\n : await discoverAuthorizationServerMetadata(rootUrl);\n } catch (err) {\n // Caller-initiated cancellation is not evidence about the server: the\n // root check never finished, so reporting \"absent\" here would let a\n // timeout masquerade as a definitive no-OAuth verdict (which the probe\n // caches permanently). Rethrow so cancellation stays a failure.\n // Checked via signal.aborted rather than error-name sniffing — robust\n // across AbortError / TimeoutError / custom abort reasons.\n if (signal?.aborted) {\n throw new AuthServerDiscoveryError(rootUrl, err);\n }\n // Otherwise the path-aware attempt already concluded \"absent\" and this\n // retry is a best-effort extra probe — a transport error here must not\n // upgrade that verdict into a thrown discovery failure (callers would\n // surface it as transient instead of \"no OAuth metadata\"). Swallow and\n // keep the absent outcome, matching pre-fallback behavior exactly.\n logger?.warn(\"Origin-root auth server metadata fallback failed, treating metadata as absent\", {\n error: String(err),\n rootUrl: rootUrl.toString(),\n });\n }\n if (authMetadata) {\n // Callers put authServerUrl in error messages and log fields — point it\n // at the URL that actually served the metadata.\n authServerUrl = rootUrl;\n logger?.info(\"Origin-root auth server metadata fallback succeeded\", {\n authServerUrl: authServerUrl.toString(),\n });\n } else {\n logger?.debug(\"Origin-root auth server metadata fallback found nothing\", {\n rootUrl: rootUrl.toString(),\n });\n }\n }\n\n if (authMetadata) {\n logger?.info(\"Discovered authorization server metadata\", {\n authorizationEndpoint: authMetadata.authorization_endpoint,\n issuer: authMetadata.issuer,\n registrationEndpoint: authMetadata.registration_endpoint,\n tokenEndpoint: authMetadata.token_endpoint,\n });\n }\n\n return { authMetadata, authServerUrl, resourceMetadata };\n}\n\n/** Wraps global fetch so the SDK inherits a caller-supplied AbortSignal. */\nfunction signaledFetch(probeSignal: AbortSignal): typeof fetch {\n return (input, init) => {\n const sdkSignal = init?.signal;\n const combined =\n sdkSignal ? AbortSignal.any([probeSignal, sdkSignal]) : probeSignal;\n return fetch(input, { ...init, signal: combined });\n };\n}\n","import { OAuthError } from \"@modelcontextprotocol/sdk/server/auth/errors.js\";\n\n/**\n * Spec-defined RFC 6749 §5.2 error codes this module classifies. The string\n * comparisons are intentionally case-sensitive lowercase: RFC 6749 §5.2\n * defines `error` as a case-sensitive ASCII token and the spec values are\n * always lowercase. Non-compliant servers that return `INVALID_GRANT` are out\n * of scope — being permissive on case would risk false positives on\n * similarly-cased non-OAuth errors.\n */\ntype FatalOAuthErrorCode = \"invalid_client\" | \"invalid_grant\";\n\n/**\n * Detects the spec-defined `invalid_client` response (RFC 6749 §5.2): client\n * authentication failed — the auth server no longer recognizes the client_id\n * the tokens were minted under (DCR registrations can be purged or expire).\n *\n * A dead client is as fatal as a dead grant, but wider: no token-endpoint call\n * AND no authorization redirect can ever succeed with it, so callers should\n * discard the stored client registration along with the tokens rather than\n * retrying — a retry loop against a purged client never converges.\n *\n * Same conservative-unwrap semantics as `isInvalidGrantError`: returns false\n * on anything ambiguous.\n */\nexport function isInvalidClientError(error: unknown): boolean {\n return checkOAuthErrorCode(error, \"invalid_client\", new WeakSet());\n}\n\n/**\n * Detects whether an error from `refreshAuthorization` (or any OAuth token-endpoint\n * call) represents the spec-defined `invalid_grant` response.\n *\n * Per RFC 6749 §5.2, `invalid_grant` is the only error code that means the\n * refresh_token itself is dead. All other errors (network failures, timeouts,\n * 5xx, rate limits, etc.) are transient or unrelated to the refresh_token's\n * validity and MUST NOT trigger destructive credential cleanup.\n *\n * Returns false on anything ambiguous — the safer default, since wrongly\n * classifying a transient error as `invalid_grant` would wipe a perfectly good\n * refresh_token and force the user through a browser re-auth.\n */\nexport function isInvalidGrantError(error: unknown): boolean {\n return checkOAuthErrorCode(error, \"invalid_grant\", new WeakSet());\n}\n\n/**\n * Recursively unwraps the common shapes a token-endpoint failure can arrive\n * in, testing for one specific RFC 6749 §5.2 error code. The MCP SDK's\n * `refreshAuthorization` typically throws an `OAuthError` subclass when the\n * response was a well-formed RFC 6749 error body, but non-spec-compliant\n * servers, intermediate proxies, and downstream wrappers can produce:\n *\n * - plain `{ error: \"invalid_grant\" }`\n * - `{ response: { body: { error: \"invalid_grant\" }}}` (some SDK wraps)\n * - `{ data: { error: \"invalid_grant\" }}` (other SDK wraps)\n * - an `Error` whose `.cause` carries any of the above (Node.js error chain)\n * - an `Error` whose `.message` mentions the code as last resort\n *\n * The `seen` set prevents infinite recursion on cyclic `.cause` chains\n * (rare but possible).\n */\nfunction checkOAuthErrorCode(\n error: unknown,\n code: FatalOAuthErrorCode,\n seen: WeakSet<object>\n): boolean {\n if (error == null) return false;\n\n // SDK OAuth error class (covers the happy path)\n if (error instanceof OAuthError) {\n return error.errorCode === code;\n }\n\n // Cycle guard for nested .cause / .response / .data traversal\n if (typeof error === \"object\") {\n if (seen.has(error)) return false;\n seen.add(error);\n\n // Plain RFC 6749 error body\n const errField = (error as { error?: unknown }).error;\n if (typeof errField === \"string\" && errField === code) {\n return true;\n }\n\n // Common SDK wrap shapes — each may itself contain the error body\n for (const key of [\"response\", \"data\", \"body\", \"cause\"] as const) {\n const inner = (error as Record<string, unknown>)[key];\n if (inner !== undefined && checkOAuthErrorCode(inner, code, seen)) return true;\n }\n }\n\n // Message-based fallback as the LAST resort. Some SDK wrappers stringify\n // the RFC 6749 error envelope into the Error's `message` instead of\n // preserving structured `.cause` / `.response` / `.data`. Recognize those\n // cases — but ONLY when the message looks like a serialized error envelope,\n // not when the code merely appears as a token somewhere in the prose\n // (e.g., enum listings or negation: `\"expected one of: invalid_grant, ...\"`\n // or `\"not the invalid_grant code\"`). False positives here are expensive:\n // they wipe perfectly good credentials. Require the textual shape of an\n // actual error assignment: a quoted JSON field, an `error=<code>`\n // form-encoded assignment, or an `error: <code>` colon assignment.\n if (error instanceof Error && typeof error.message === \"string\") {\n return (\n new RegExp(`\"error\"\\\\s*:\\\\s*\"${code}\"`).test(error.message) ||\n new RegExp(`\\\\berror\\\\s*=\\\\s*${code}\\\\b`).test(error.message) ||\n new RegExp(`\\\\berror\\\\s*:\\\\s*${code}\\\\b`).test(error.message)\n );\n }\n\n return false;\n}\n","import {\n exchangeAuthorization,\n extractResourceMetadataUrl,\n refreshAuthorization,\n registerClient,\n startAuthorization,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type {\n OAuthClientInformationFull,\n OAuthClientMetadata,\n OAuthProtectedResourceMetadata,\n OAuthTokens,\n} from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nimport type { OAuthProviderConfig } from \"../config/config.js\";\nimport { config } from \"../config/env.js\";\nimport { logger } from \"../lib/logger.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\nimport { openBrowser } from \"./browser.js\";\nimport { startCallbackServer } from \"./callbackServer.js\";\nimport { AuthServerDiscoveryError, discoverAuthServer } from \"./discovery.js\";\nimport { isInvalidClientError, isInvalidGrantError } from \"./errors.js\";\nimport { AuthEventType } from \"./events.js\";\nimport type { OAuthCredentialStore, StoredCredentials } from \"./OAuthCredentialStore.js\";\nimport { findProviderForUrl } from \"./providers.js\";\n\n// Maps CLIENT_APP_NAME (set by the scanner installer per agent) to the\n// human-friendly client_name we present during Dynamic Client Registration.\n// Deadline for the well-known probes on the refresh path. Matches the probe's\n// per-server budget (`probe/index.ts` DEFAULT_PER_SERVER_TIMEOUT_MS) so both\n// discovery callers give an unresponsive metadata endpoint the same rope.\nconst REFRESH_DISCOVERY_TIMEOUT_MS = 15_000;\n\nconst DCR_CLIENT_NAME_BY_APP: Record<string, string> = {\n \"claude-code\": \"Claude Code (mcp-gateway)\",\n codex: \"Codex\",\n copilot: \"Visual Studio Code\",\n cursor: \"Cursor\",\n opencode: \"OpenCode\",\n};\n\n// Maps CLIENT_APP_NAME to the client_uri we advertise during DCR, for the\n// small set of agents whose upstream allowlist requires it in addition to\n// client_name. VS Code (copilot) is the case that motivated this: Figma's DCR\n// endpoint only accepts the VS Code client when BOTH client_name ===\n// \"Visual Studio Code\" AND client_uri === \"https://code.visualstudio.com\" are\n// present — sending client_name alone is rejected. The value matches what a\n// real VS Code install sends in its own DCR, so we present its genuine\n// identity rather than a decoy. Agents absent here send no client_uri (their\n// DCR payload is unchanged).\nconst DCR_CLIENT_URI_BY_APP: Record<string, string> = {\n copilot: \"https://code.visualstudio.com\",\n};\n\n// Per-(agent, server-host) decoy redirect_uri used at DCR-registration time only.\n//\n// Some auth servers (notably Figma) gate Dynamic Client Registration on\n// `redirect_uris[0].scheme`. They allowlist schemes owned by registered\n// partners (e.g. `cursor://` for Cursor.app's native OAuth) but reject\n// `http://localhost:*` for those same agents. The gateway can't use\n// `cursor://` for its actual callback (the URL would route to Cursor.app via\n// macOS Launch Services, not to our local HTTP listener), so for affected\n// (agent, server) pairs we register BOTH URIs with the decoy at index 0:\n//\n// redirect_uris: [<decoy>, \"http://localhost:<port>/callback\"]\n//\n// Allowlist clause passes because index 0 carries the registered scheme;\n// the actual OAuth flow uses the localhost URI by selecting it at the\n// authorize step via the `redirect_uri` query parameter (RFC 6749 §3.1.2.3:\n// the auth server must honor any registered URI the client requests).\n//\n// The decoy URI is NEVER used to receive a callback; it exists purely to\n// satisfy the registration-time scheme check.\n//\n// Keyed on (agent, server-host) rather than agent alone so the decoy only\n// fires for known-figma-like servers. Some servers (e.g. asana) actively\n// reject unknown `cursor://` URIs with `invalid_redirect_uri` — applying\n// the decoy to them would silently break cursor for that server. Exact-host\n// match keeps the override surgical.\nconst DECOY_REDIRECT_URI_BY_APP_AND_HOST: Record<string, Record<string, string>> = {\n cursor: {\n \"mcp.figma.com\": \"cursor://anysphere.cursor-mcp/oauth/callback\",\n },\n};\n\nexport interface OAuthFlowOptions {\n /**\n * Custom client metadata for DCR registration.\n * If not provided, default metadata will be used.\n */\n clientMetadata?: Partial<OAuthClientMetadata>;\n /**\n * Credential store for persisting OAuth credentials.\n * If provided, credentials will be loaded from and saved to disk.\n */\n credentialStore?: OAuthCredentialStore;\n /**\n * OAuth scope to request.\n */\n scope?: string;\n /**\n * Timeout in milliseconds for the OAuth callback.\n * Default: 5 minutes\n */\n timeoutMs?: number;\n}\n\nexport interface OAuthFlowResult {\n clientInfo: OAuthClientInformationFull;\n /**\n * Whether the result came from stored credentials (true) or a fresh OAuth flow (false).\n */\n fromCache?: boolean;\n tokens: OAuthTokens;\n}\n\n/**\n * Attempts to detect if a server requires OAuth by making a test request.\n * Returns the resource metadata URL from the WWW-Authenticate header if OAuth is required.\n *\n * @param serverUrl The MCP server URL to check\n * @returns The resource metadata URL if OAuth is required, undefined otherwise\n */\nexport async function detectOAuthRequirement(serverUrl: string | URL): Promise<undefined | URL> {\n // Normalize URL to avoid redirect issues on Windows\n const normalizedUrl = normalizeUrl(serverUrl);\n const serverUrlObj = new URL(normalizedUrl);\n\n try {\n const response = await fetch(serverUrlObj, {\n headers: {\n Accept: \"application/json\",\n },\n method: \"POST\",\n });\n\n if (response.status === 401) {\n // Check for resource metadata URL in response\n const resourceMetadataUrl = extractResourceMetadataUrl(response);\n if (resourceMetadataUrl) {\n logger.debug(\"OAuth required, found resource metadata URL\", {\n resourceMetadataUrl: resourceMetadataUrl.toString(),\n });\n return resourceMetadataUrl;\n }\n\n // Check WWW-Authenticate header for Bearer realm\n const wwwAuth = response.headers.get(\"WWW-Authenticate\");\n if (wwwAuth && wwwAuth.toLowerCase().includes(\"bearer\")) {\n logger.debug(\"OAuth required based on WWW-Authenticate header\", { wwwAuth });\n return undefined; // OAuth required but no metadata URL\n }\n }\n\n return undefined;\n } catch (err) {\n logger.debug(\"Error detecting OAuth requirement\", { error: String(err) });\n return undefined;\n }\n}\n\n/**\n * Returns the DCR `client_name` to advertise based on the current\n * `CLIENT_APP_NAME` env var. Exported so the probe (`src/probe/index.ts`)\n * can use the exact same value the runtime OAuth flow would send — this is\n * how allowlist-style DCR endpoints (e.g., Figma) get the same answer from\n * both paths, instead of probe seeing `\"MCP Gateway\"` (rejected) while the\n * runtime flow sees the agent-specific name (accepted).\n */\nexport function getDcrClientName(): string {\n return resolveDcrClientName(config.clientAppName);\n}\n\n/**\n * Returns the DCR `client_uri` to advertise based on the current\n * `CLIENT_APP_NAME` env var, or `undefined` when the current app has none.\n * Exported so the probe (`src/probe/index.ts`) sends the exact same value the\n * runtime OAuth flow would — allowlist-style DCR endpoints (e.g., Figma) gate\n * on client_uri for some clients (VS Code), so probe and runtime must agree.\n */\nexport function getDcrClientUri(): string | undefined {\n return resolveDcrClientUri(config.clientAppName);\n}\n\n/**\n * Returns the `redirect_uris` array to register at DCR.\n *\n * For (agent, server-host) pairs listed in `DECOY_REDIRECT_URI_BY_APP_AND_HOST`,\n * the decoy URI is prepended at index 0 to satisfy allowlist-style DCR\n * endpoints that gate on `redirect_uris[0].scheme`. The real localhost URI\n * is at index 1 and is what the gateway selects at the authorize step via\n * the `redirect_uri` query parameter.\n *\n * For all other pairs the registered array is just `[localRedirectUrl]`,\n * preserving existing behavior.\n *\n * Exported so the probe (`src/probe/index.ts`) can use the exact same shape\n * the runtime OAuth flow would send — without this, the probe and runtime\n * would register different redirect_uris arrays for the same (agent,\n * server), and allowlist-style endpoints could give divergent verdicts.\n */\nexport function getDcrRedirectUris(serverUrl: string | URL, localRedirectUrl: string): string[] {\n return resolveDcrRedirectUris(config.clientAppName, serverUrl, localRedirectUrl);\n}\n\n/**\n * Performs the complete OAuth 2.0 Authorization Code flow with DCR.\n *\n * This function:\n * 1. Checks for existing valid credentials (if credentialStore provided)\n * 2. Attempts token refresh if access token expired (if credentialStore provided)\n * 3. Discovers OAuth protected resource metadata\n * 4. Discovers authorization server metadata\n * 5. Dynamically registers the client (DCR)\n * 6. Opens browser for user authorization\n * 7. Waits for the callback with authorization code\n * 8. Exchanges the code for tokens\n * 9. Persists credentials (if credentialStore provided)\n *\n * @param serverUrl The MCP server URL that requires OAuth\n * @param options Optional configuration\n * @returns The client info and tokens from successful authorization\n */\nexport async function performOAuthFlow(\n serverUrl: string | URL,\n options: OAuthFlowOptions = {}\n): Promise<OAuthFlowResult> {\n const { credentialStore, scope, timeoutMs } = options;\n // Normalize URL to avoid redirect issues on Windows (POST to GET conversion)\n const serverUrlString = normalizeUrl(serverUrl);\n const serverUrlObj = new URL(serverUrlString);\n\n logger.info(\"Starting OAuth flow\", { serverUrl: serverUrlString });\n\n // Step 0: Check for existing valid credentials\n if (credentialStore) {\n const existingCredentials = await tryLoadExistingCredentials(serverUrlObj, credentialStore);\n if (existingCredentials) {\n logger.info(\"Using existing OAuth credentials\", {\n clientId: existingCredentials.clientInfo.client_id,\n serverUrl: serverUrlString,\n });\n return existingCredentials;\n }\n }\n\n // Step 1: Start the callback server to get a dynamic port. Threading\n // serverUrl in lets the callback's error logs carry it for monitoring.\n logger.debug(\"Starting OAuth callback server...\");\n const callbackServer = await startCallbackServer({ serverUrl: serverUrlString, timeoutMs });\n const redirectUrl = callbackServer.callbackUrl;\n\n logger.info(\"OAuth callback server started\", {\n callbackUrl: redirectUrl,\n port: callbackServer.port,\n });\n\n try {\n // Step 2: Check for pre-configured OAuth provider (non-DCR servers)\n const preConfiguredProvider = findProviderForUrl(serverUrlObj);\n\n if (preConfiguredProvider) {\n logger.info(\"Using pre-configured OAuth provider\", {\n providerId: preConfiguredProvider.id,\n providerName: preConfiguredProvider.name,\n });\n\n // Use pre-configured provider flow - pass the callback server instance\n return await performPreConfiguredProviderFlow(\n serverUrlObj,\n preConfiguredProvider,\n callbackServer,\n credentialStore,\n scope\n );\n }\n\n // Steps 3-5: Discover OAuth protected-resource + authorization-server metadata.\n // The helper emits the same debug/info/warn log lines that used to live inline\n // here — passing `logger` preserves the existing observable log output.\n let authMetadata: Awaited<ReturnType<typeof discoverAuthServer>>[\"authMetadata\"];\n let authServerUrl: URL;\n let resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n try {\n ({ authMetadata, authServerUrl, resourceMetadata } = await discoverAuthServer(serverUrlObj, { logger }));\n } catch (err) {\n // AuthServerDiscoveryError carries the chosen authServerUrl. Other\n // errors (rare — only the resource-metadata fetch swallows its own\n // errors) won't have one to attribute, so omit the field.\n logger.error(\"Auth server metadata discovery failed\", {\n error: String(err),\n eventType: AuthEventType.SERVER_METADATA_FAILED,\n serverUrl: serverUrlString,\n ...(err instanceof AuthServerDiscoveryError ? { authServer: err.authServerUrl.toString() } : {}),\n });\n throw err;\n }\n\n if (!authMetadata) {\n logger.error(\"Auth server metadata absent (server may not support OAuth 2.0)\", {\n authServer: authServerUrl.toString(),\n eventType: AuthEventType.SERVER_METADATA_FAILED,\n reason: \"metadata_absent\",\n serverUrl: serverUrlString,\n });\n throw new Error(\n `Could not discover OAuth metadata from ${authServerUrl}. ` +\n `The server may not support OAuth 2.0 or the metadata endpoint is not accessible.`\n );\n }\n\n // Default the requested scope to what the protected resource advertises —\n // but ONLY for resources that advertise `offline_access`.\n //\n // Sending no `scope` at all leaves the grant to whatever the authorization\n // server treats as its default. On a server that grants scopes only when\n // asked, that default is narrower than the resource needs, and the token is\n // then accepted for some operations and rejected for others. Atlassian\n // answers such calls with `Unauthorized; scope does not match`, delivered\n // INSIDE a 200 tool result rather than as a 401 — so neither the refresh\n // path nor the interactive fallback can observe it, and a refresh could not\n // widen the grant anyway (RFC 6749 §6). The initial authorization request is\n // the only place it is fixable.\n //\n // The `offline_access` condition is what keeps this safe to apply broadly.\n // Asking is not free: a server that currently grants MORE than it advertises\n // would be narrowed by an explicit request. That is not hypothetical —\n // groundcover advertises `access:router` alone yet issues\n // `access:router offline_access`, so requesting its advertised list verbatim\n // would cost us the refresh token on a server that works today.\n //\n // `offline_access` is the canonical must-ask scope, so a resource listing it\n // is signalling that it expects clients to ask for what they need. Resources\n // that omit it are, empirically, the ones that grant broadly by default —\n // for those we keep sending nothing and preserve today's behaviour.\n //\n // An explicit `options.scope` still wins; this is only the fallback.\n const advertised = resourceMetadata?.scopes_supported;\n const resourceExpectsScopeRequest = advertised?.includes(\"offline_access\") ?? false;\n const effectiveScope = scope ?? (resourceExpectsScopeRequest ? advertised?.join(\" \") : undefined);\n if (effectiveScope && !scope) {\n logger.info(\"Defaulting OAuth scope to the resource's advertised scopes_supported\", {\n scope: effectiveScope,\n serverUrl: serverUrlString,\n });\n } else if (!scope && advertised?.length) {\n logger.debug(\"Resource advertises scopes but not offline_access; requesting no scope\", {\n advertised,\n serverUrl: serverUrlString,\n });\n }\n\n // Step 6: Check if we have existing client info that can be reused\n let clientInfo: OAuthClientInformationFull;\n const existingCredentials = credentialStore ? await credentialStore.load(serverUrlString) : undefined;\n\n // Check if existing client registration has a matching redirect_uri\n const existingRedirectUris = existingCredentials?.clientInfo?.redirect_uris || [];\n const canReuseClient = existingCredentials?.clientInfo && existingRedirectUris.includes(redirectUrl);\n\n if (canReuseClient && existingCredentials?.clientInfo) {\n // Reuse existing client registration - redirect_uri matches\n logger.info(\"Reusing existing DCR client registration\", {\n clientId: existingCredentials.clientInfo.client_id,\n clientName: existingCredentials.clientInfo.client_name,\n redirectUri: redirectUrl,\n });\n clientInfo = existingCredentials.clientInfo;\n } else {\n // Need to register new client (either no existing client, or redirect_uri doesn't match)\n if (existingCredentials?.clientInfo) {\n logger.info(\"Existing DCR client has different redirect_uri, re-registering\", {\n existingRedirectUris,\n newRedirectUri: redirectUrl,\n });\n }\n\n // Step 6b: Build client metadata for DCR\n // client_uri is only present for agents that need it (see\n // DCR_CLIENT_URI_BY_APP); when absent we omit the field entirely so\n // other agents' DCR payloads are unchanged.\n const clientUri = options.clientMetadata?.client_uri ?? getDcrClientUri();\n const clientMetadata: OAuthClientMetadata = {\n client_name: options.clientMetadata?.client_name || getDcrClientName(),\n ...(clientUri && { client_uri: clientUri }),\n grant_types: options.clientMetadata?.grant_types || [\"authorization_code\", \"refresh_token\"],\n // For most (agent, server) pairs this is just [redirectUrl]. For\n // pairs in DECOY_REDIRECT_URI_BY_APP_AND_HOST (e.g. cursor + figma)\n // the decoy is prepended at index 0 to pass allowlist-style DCR\n // gates. The actual callback still goes to redirectUrl because the\n // authorize step's `redirect_uri` query param selects it.\n redirect_uris: getDcrRedirectUris(serverUrlObj, redirectUrl),\n response_types: options.clientMetadata?.response_types || [\"code\"],\n // Public client - no client secret\n token_endpoint_auth_method: options.clientMetadata?.token_endpoint_auth_method || \"none\",\n ...(effectiveScope && { scope: effectiveScope }),\n };\n\n logger.debug(\"Client metadata for DCR\", { clientMetadata });\n\n // Step 7: Register client via DCR\n if (!authMetadata.registration_endpoint) {\n logger.error(\"DCR registration unsupported by auth server\", {\n authServer: authServerUrl.toString(),\n eventType: AuthEventType.DCR_FAILED,\n reason: \"no_registration_endpoint\",\n serverUrl: serverUrlString,\n });\n throw new Error(\n `Authorization server does not support Dynamic Client Registration. ` +\n `No registration_endpoint found in metadata.`\n );\n }\n\n logger.info(\"Registering client via DCR...\", {\n registrationEndpoint: authMetadata.registration_endpoint,\n });\n\n try {\n clientInfo = await registerClient(authServerUrl, {\n clientMetadata,\n metadata: authMetadata,\n });\n } catch (err) {\n logger.error(\"DCR registration failed\", {\n authServer: authServerUrl.toString(),\n error: String(err),\n eventType: AuthEventType.DCR_FAILED,\n registrationEndpoint: authMetadata.registration_endpoint,\n serverUrl: serverUrlString,\n });\n throw err;\n }\n\n logger.info(\"Client registered successfully via DCR\", {\n clientId: clientInfo.client_id,\n clientName: clientInfo.client_name,\n clientSecretExpiresAt: clientInfo.client_secret_expires_at,\n });\n\n // Persist the client info immediately\n if (credentialStore) {\n await credentialStore.update(serverUrlString, { clientInfo });\n }\n }\n\n // Step 8: RFC 8707 Resource Indicators\n // NOTE: We intentionally do NOT send the resource parameter. Some OAuth servers (e.g., Notion)\n // ignore the client's requested resource and always issue tokens with their configured default\n // audience. When we then present that token to a different path (e.g., /mcp), the resource\n // server rejects it with \"Token audience does not match resource server\".\n // By omitting the resource parameter entirely, we let the OAuth server use its default\n // behavior, which typically issues tokens that work with the resource server.\n // This matches how Linear works (no protected resource metadata = no resource param sent).\n\n // Step 9: Start authorization flow (generates PKCE challenge)\n // Generate state for CSRF protection — some OAuth servers (e.g., Okta) require it\n const state = crypto.randomUUID();\n logger.debug(\"Starting authorization flow...\");\n const { authorizationUrl, codeVerifier } = await startAuthorization(authServerUrl, {\n clientInformation: clientInfo,\n metadata: authMetadata,\n redirectUrl,\n scope: effectiveScope,\n state,\n });\n\n await openBrowser(authorizationUrl);\n\n // Step 11: Wait for callback with authorization code\n logger.debug(\"Waiting for OAuth callback...\");\n const callbackResult = await callbackServer.waitForCallback();\n\n // Verify state matches to prevent CSRF attacks\n if (callbackResult.state !== state) {\n logger.error(\"OAuth state mismatch — possible CSRF attack\", {\n authServer: authServerUrl.toString(),\n eventType: AuthEventType.STATE_MISMATCH,\n serverUrl: serverUrlString,\n });\n throw new Error(\"OAuth state mismatch - possible CSRF attack\");\n }\n\n logger.info(\"Authorization code received\", {\n codePrefix: `${callbackResult.code.substring(0, 10)}...`,\n });\n\n // Step 12: Exchange authorization code for tokens\n logger.debug(\"Exchanging authorization code for tokens...\");\n let tokens;\n try {\n tokens = await exchangeAuthorization(authServerUrl, {\n authorizationCode: callbackResult.code,\n clientInformation: clientInfo,\n codeVerifier,\n metadata: authMetadata,\n redirectUri: redirectUrl,\n });\n } catch (err) {\n logger.error(\"Token exchange failed\", {\n authServer: authServerUrl.toString(),\n clientId: clientInfo.client_id,\n error: String(err),\n eventType: AuthEventType.TOKEN_EXCHANGE_FAILED,\n grantType: \"authorization_code\",\n serverUrl: serverUrlString,\n });\n throw err;\n }\n\n // Log the scope we asked for alongside the scope we were given. An\n // authorization server may grant less than requested, or more (some grant\n // scopes they never advertise), and the difference is otherwise invisible:\n // an under-scoped token still connects, and only fails later on individual\n // tool calls — which is how a scope problem stays undiagnosed for weeks.\n logger.info(\"OAuth tokens obtained successfully\", {\n expiresIn: tokens.expires_in,\n grantedScope: tokens.scope,\n hasRefreshToken: !!tokens.refresh_token,\n requestedScope: effectiveScope,\n tokenType: tokens.token_type,\n });\n\n // Step 13: Persist tokens\n if (credentialStore) {\n const tokensObtainedAt = new Date().toISOString();\n await credentialStore.update(serverUrlString, {\n tokens,\n tokensObtainedAt,\n });\n logger.info(\"OAuth credentials persisted to disk\", { serverUrl: serverUrlString });\n }\n\n return {\n clientInfo,\n fromCache: false,\n tokens,\n };\n } finally {\n // Always close the callback server\n await callbackServer.close();\n }\n}\n\nexport function resolveDcrClientName(rawAppName: null | string | undefined): string {\n const normalized = rawAppName?.trim().toLowerCase();\n if (!normalized) return \"MCP Gateway\";\n // Own-property check so inherited keys (constructor, toString, __proto__, …)\n // resolve to the fallback rather than an Object.prototype member.\n return Object.hasOwn(DCR_CLIENT_NAME_BY_APP, normalized) ? DCR_CLIENT_NAME_BY_APP[normalized] : \"MCP Gateway\";\n}\n\n/**\n * Pure helper backing `getDcrClientUri`. Returns the client_uri for the given\n * app name, or `undefined` when the app has no mapped uri (most agents) — the\n * caller then omits client_uri from the DCR payload entirely. Normalizes the\n * raw name the same way as `resolveDcrClientName` (trim + lowercase).\n */\nexport function resolveDcrClientUri(rawAppName: null | string | undefined): string | undefined {\n const normalized = rawAppName?.trim().toLowerCase();\n if (!normalized) return undefined;\n // Own-property check so inherited keys (constructor, toString, __proto__, …)\n // resolve to undefined rather than an Object.prototype member.\n return Object.hasOwn(DCR_CLIENT_URI_BY_APP, normalized) ? DCR_CLIENT_URI_BY_APP[normalized] : undefined;\n}\n\n/**\n * Pure helper backing `getDcrRedirectUris`. Exported so unit tests can\n * exercise the decoy-lookup matrix without depending on the\n * `config.clientAppName` module singleton.\n */\nexport function resolveDcrRedirectUris(\n rawAppName: null | string | undefined,\n serverUrl: string | URL,\n localRedirectUrl: string,\n): string[] {\n const normalized = rawAppName?.trim().toLowerCase();\n if (!normalized) return [localRedirectUrl];\n const hostMap = DECOY_REDIRECT_URI_BY_APP_AND_HOST[normalized];\n if (!hostMap) return [localRedirectUrl];\n let hostname: string;\n try {\n // Use hostname (not host) so port-bearing URLs like https://mcp.figma.com:443/...\n // still match the bare-hostname keys in DECOY_REDIRECT_URI_BY_APP_AND_HOST.\n // Lowercased because hostnames are case-insensitive (RFC 3986 §3.2.2).\n hostname = (serverUrl instanceof URL ? serverUrl : new URL(serverUrl)).hostname.toLowerCase();\n } catch {\n return [localRedirectUrl];\n }\n const decoy = hostMap[hostname];\n // Decoy MUST be index 0 — allowlist endpoints check redirect_uris[0].scheme.\n return decoy ? [decoy, localRedirectUrl] : [localRedirectUrl];\n}\n\n/**\n * Refreshes the cached OAuth token for a given server URL using its stored\n * refresh_token. Returns the new tokens on success, or undefined if no\n * refresh_token is on disk or the refresh attempt failed.\n *\n * On transient failures (network / 5xx / timeout) the stored refresh_token is\n * preserved so the next attempt can retry. Two RFC 6749 §5.2 codes are fatal\n * instead: `invalid_grant` clears the tokens (the refresh_token itself is\n * dead) and `invalid_client` clears the client registration as well (the\n * client_id is dead, so an authorization redirect would fail too). An error\n * carrying both codes is ambiguous and clears nothing.\n *\n * Use this from request-time error handlers (e.g., on upstream 401) when you\n * want a silent refresh before falling back to the interactive OAuth flow.\n */\nexport async function tryRefreshCachedToken(\n serverUrl: string | URL,\n credentialStore: OAuthCredentialStore\n): Promise<OAuthFlowResult | undefined> {\n const url = typeof serverUrl === \"string\" ? new URL(serverUrl) : serverUrl;\n const credentials = await credentialStore.load(url.toString());\n if (!credentials?.clientInfo || !credentials.tokens?.refresh_token) {\n return undefined;\n }\n return tryRefreshToken(url, credentials, credentialStore);\n}\n\n/**\n * Exchanges an authorization code for tokens using a pre-configured provider.\n * Handles different token endpoint authentication methods.\n */\nasync function exchangeCodeForTokens(\n provider: OAuthProviderConfig,\n code: string,\n redirectUri: string,\n codeVerifier?: string\n): Promise<OAuthTokens> {\n const tokenEndpoint = provider.tokenEndpoint;\n const authMethod = provider.tokenEndpointAuthMethod || (provider.clientSecret ? \"client_secret_post\" : \"none\");\n\n // Build token request body\n const body = new URLSearchParams();\n body.set(\"grant_type\", \"authorization_code\");\n body.set(\"code\", code);\n body.set(\"redirect_uri\", redirectUri);\n\n // Add PKCE verifier if present\n if (codeVerifier) {\n body.set(\"code_verifier\", codeVerifier);\n }\n\n // Build request headers\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n\n // Add client authentication based on method\n if (authMethod === \"client_secret_basic\" && provider.clientSecret) {\n // HTTP Basic authentication\n const credentials = Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString(\"base64\");\n headers.Authorization = `Basic ${credentials}`;\n } else if (authMethod === \"client_secret_post\" && provider.clientSecret) {\n // Client credentials in POST body\n body.set(\"client_id\", provider.clientId);\n body.set(\"client_secret\", provider.clientSecret);\n } else {\n // Public client - just client_id\n body.set(\"client_id\", provider.clientId);\n }\n\n logger.debug(\"Sending token request\", {\n authMethod,\n providerId: provider.id,\n tokenEndpoint,\n });\n\n const response = await fetch(tokenEndpoint, {\n body: body.toString(),\n headers,\n method: \"POST\",\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\"Token exchange failed\", {\n authMethod,\n error: errorText,\n eventType: AuthEventType.TOKEN_EXCHANGE_FAILED,\n grantType: \"authorization_code\",\n httpStatus: response.status,\n providerId: provider.id,\n tokenEndpoint,\n });\n throw new Error(`Token exchange failed: ${response.status} ${errorText}`);\n }\n\n // GitHub returns tokens as application/x-www-form-urlencoded by default\n // unless Accept: application/json is set\n const contentType = response.headers.get(\"content-type\") || \"\";\n let tokenData: Record<string, unknown>;\n\n if (contentType.includes(\"application/json\")) {\n tokenData = (await response.json()) as Record<string, unknown>;\n } else if (contentType.includes(\"application/x-www-form-urlencoded\") || contentType.includes(\"text/plain\")) {\n // Parse URL-encoded response (GitHub's default format)\n const text = await response.text();\n const params = new URLSearchParams(text);\n tokenData = Object.fromEntries(params.entries()) as Record<string, unknown>;\n } else {\n // Try JSON first, fall back to URL-encoded\n const text = await response.text();\n try {\n tokenData = JSON.parse(text) as Record<string, unknown>;\n } catch {\n const params = new URLSearchParams(text);\n tokenData = Object.fromEntries(params.entries()) as Record<string, unknown>;\n }\n }\n\n // Convert expires_in to number if it's a string\n if (typeof tokenData.expires_in === \"string\") {\n tokenData.expires_in = parseInt(tokenData.expires_in, 10);\n }\n\n return {\n access_token: tokenData.access_token as string,\n expires_in: tokenData.expires_in as number | undefined,\n refresh_token: tokenData.refresh_token as string | undefined,\n scope: tokenData.scope as string | undefined,\n token_type: (tokenData.token_type as string) || \"bearer\",\n };\n}\n\n/**\n * Performs OAuth flow using a pre-configured provider (non-DCR).\n * This is used for OAuth servers that don't support Dynamic Client Registration.\n */\nasync function performPreConfiguredProviderFlow(\n serverUrl: URL,\n provider: OAuthProviderConfig,\n callbackServerInstance: {\n callbackUrl: string;\n close: () => Promise<void>;\n waitForCallback: () => Promise<{ code: string; state?: string }>;\n },\n credentialStore: OAuthCredentialStore | undefined,\n scope: string | undefined\n): Promise<OAuthFlowResult> {\n const serverUrlString = serverUrl.toString();\n const redirectUrl = callbackServerInstance.callbackUrl;\n\n // Build client info from pre-configured provider\n const clientInfo: OAuthClientInformationFull = {\n client_id: provider.clientId,\n client_name: provider.name,\n client_secret: provider.clientSecret,\n redirect_uris: [redirectUrl],\n token_endpoint_auth_method:\n provider.tokenEndpointAuthMethod || (provider.clientSecret ? \"client_secret_post\" : \"none\"),\n };\n\n logger.info(\"Using pre-configured OAuth client\", {\n clientId: provider.clientId,\n hasClientSecret: !!provider.clientSecret,\n providerId: provider.id,\n tokenEndpointAuthMethod: clientInfo.token_endpoint_auth_method,\n });\n\n // Determine scope to use\n const effectiveScope = scope || provider.scopes?.join(\" \");\n\n // Generate PKCE challenge if enabled for this provider\n const usePkce = provider.usePkce !== false; // Default to true unless explicitly disabled\n let codeVerifier: string | undefined;\n let codeChallenge: string | undefined;\n let codeChallengeMethod: string | undefined;\n\n if (usePkce) {\n // Generate PKCE code verifier and challenge\n const { generateCodeChallenge, generateCodeVerifier } = await import(\"./pkce.js\");\n codeVerifier = generateCodeVerifier();\n codeChallenge = await generateCodeChallenge(codeVerifier);\n codeChallengeMethod = \"S256\";\n }\n\n // Build authorization URL manually\n const authUrl = new URL(provider.authorizationEndpoint);\n authUrl.searchParams.set(\"client_id\", provider.clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUrl);\n authUrl.searchParams.set(\"response_type\", \"code\");\n\n if (effectiveScope) {\n authUrl.searchParams.set(\"scope\", effectiveScope);\n }\n\n // Add state for security\n const state = crypto.randomUUID();\n authUrl.searchParams.set(\"state\", state);\n\n // Add PKCE challenge if enabled\n if (codeChallenge && codeChallengeMethod) {\n authUrl.searchParams.set(\"code_challenge\", codeChallenge);\n authUrl.searchParams.set(\"code_challenge_method\", codeChallengeMethod);\n }\n\n await openBrowser(authUrl);\n\n // Wait for callback on the existing callback server\n logger.debug(\"Waiting for OAuth callback...\");\n const callbackResult = await callbackServerInstance.waitForCallback();\n\n // Verify state matches\n if (callbackResult.state !== state) {\n logger.error(\"OAuth state mismatch — possible CSRF attack\", {\n eventType: AuthEventType.STATE_MISMATCH,\n providerId: provider.id,\n serverUrl: serverUrlString,\n });\n throw new Error(\"OAuth state mismatch - possible CSRF attack\");\n }\n\n logger.info(\"Authorization code received\", {\n codePrefix: `${callbackResult.code.substring(0, 10)}...`,\n providerId: provider.id,\n });\n\n // Exchange authorization code for tokens\n logger.debug(\"Exchanging authorization code for tokens...\");\n const tokens = await exchangeCodeForTokens(provider, callbackResult.code, redirectUrl, codeVerifier);\n\n logger.info(\"OAuth tokens obtained successfully\", {\n expiresIn: tokens.expires_in,\n hasRefreshToken: !!tokens.refresh_token,\n providerId: provider.id,\n tokenType: tokens.token_type,\n });\n\n // Persist credentials\n if (credentialStore) {\n const tokensObtainedAt = new Date().toISOString();\n await credentialStore.update(serverUrlString, {\n clientInfo,\n tokens,\n tokensObtainedAt,\n });\n logger.info(\"OAuth credentials persisted to disk\", {\n providerId: provider.id,\n serverUrl: serverUrlString,\n });\n }\n\n return {\n clientInfo,\n fromCache: false,\n tokens,\n };\n}\n\n/**\n * Checks for existing valid credentials and returns them if available.\n * Handles token refresh if the access token is expired but refresh token exists.\n */\nasync function tryLoadExistingCredentials(\n serverUrl: URL,\n credentialStore: OAuthCredentialStore\n): Promise<OAuthFlowResult | undefined> {\n const credentials = await credentialStore.load(serverUrl.toString());\n\n if (!credentials) {\n logger.debug(\"No stored credentials found\", { serverUrl: serverUrl.toString() });\n return undefined;\n }\n\n if (!credentials.clientInfo) {\n logger.debug(\"Stored credentials have no client info\", { serverUrl: serverUrl.toString() });\n return undefined;\n }\n\n // Check if we have a valid access token\n if (credentialStore.hasValidAccessToken(credentials) && credentials.tokens) {\n logger.info(\"Using cached OAuth credentials with valid access token\", {\n clientId: credentials.clientInfo.client_id,\n serverUrl: serverUrl.toString(),\n });\n\n return {\n clientInfo: credentials.clientInfo,\n fromCache: true,\n tokens: credentials.tokens,\n };\n }\n\n // Access token is expired or missing, try to refresh\n if (credentialStore.hasRefreshToken(credentials)) {\n const refreshResult = await tryRefreshToken(serverUrl, credentials, credentialStore);\n if (refreshResult) {\n return refreshResult;\n }\n }\n\n logger.debug(\"Stored credentials are expired and no valid refresh token\", {\n serverUrl: serverUrl.toString(),\n });\n\n return undefined;\n}\n\n/**\n * Attempts to refresh an expired access token using the refresh token.\n * Returns new tokens if successful, undefined if refresh fails.\n */\nasync function tryRefreshToken(\n serverUrl: URL,\n credentials: StoredCredentials,\n credentialStore: OAuthCredentialStore\n): Promise<OAuthFlowResult | undefined> {\n if (!credentials.clientInfo || !credentials.tokens?.refresh_token) {\n return undefined;\n }\n\n logger.info(\"Attempting to refresh expired access token\", {\n clientId: credentials.clientInfo.client_id,\n serverUrl: serverUrl.toString(),\n });\n\n try {\n // Discover through the same shared sequence that minted these tokens,\n // including its origin-root fallback: some servers (e.g. Atlassian's\n // remote MCP) publish auth-server metadata ONLY at the root well-known,\n // and without metadata `refreshAuthorization` guesses `<origin>/token` —\n // an endpoint such servers don't serve, so every refresh would fail.\n //\n // Bounded by a deadline: this runs inside the single-flight mid-session\n // 401 handler, where an unbounded well-known probe would hold every\n // request waiting on that refresh for as long as the socket stays open.\n // Bare `fetch()` has no default timeout, so without a signal a black-holed\n // well-known stalls the whole session rather than failing the refresh.\n const discoveryAbort = new AbortController();\n const discoveryDeadline = setTimeout(() => discoveryAbort.abort(), REFRESH_DISCOVERY_TIMEOUT_MS);\n let authMetadata: Awaited<ReturnType<typeof discoverAuthServer>>[\"authMetadata\"];\n let authServerUrl: URL;\n try {\n ({ authMetadata, authServerUrl } = await discoverAuthServer(serverUrl, {\n logger,\n signal: discoveryAbort.signal,\n }));\n } finally {\n clearTimeout(discoveryDeadline);\n }\n // NOTE: We don't send the resource parameter (same reasoning as in performOAuthFlow)\n\n // Attempt token refresh\n const newTokens = await refreshAuthorization(authServerUrl, {\n clientInformation: credentials.clientInfo,\n metadata: authMetadata,\n refreshToken: credentials.tokens.refresh_token,\n });\n\n // `grantedScope` here is the scope carried forward from the original\n // authorization — a refresh cannot widen it (RFC 6749 §6), so a token that\n // is under-scoped stays under-scoped for its whole life. Logging it makes\n // that visible without waiting for a tool call to fail.\n logger.info(\"Successfully refreshed access token\", {\n clientId: credentials.clientInfo.client_id,\n expiresIn: newTokens.expires_in,\n grantedScope: newTokens.scope,\n hasNewRefreshToken: !!newTokens.refresh_token,\n });\n\n // Per RFC 6749 §6, the server MAY omit `refresh_token` in a refresh\n // response (it keeps the old one valid). Naively persisting `newTokens`\n // as-is would replace our existing refresh_token with `undefined`,\n // re-introducing the silent destructive-wipe failure mode that PRDCT-3666\n // is meant to fix. Carry the existing refresh_token forward when the\n // server didn't send a new one.\n const mergedTokens: OAuthTokens = newTokens.refresh_token\n ? newTokens\n : { ...newTokens, refresh_token: credentials.tokens.refresh_token };\n\n // Persist the new tokens\n const tokensObtainedAt = new Date().toISOString();\n await credentialStore.update(serverUrl.toString(), {\n tokens: mergedTokens,\n tokensObtainedAt,\n });\n\n return {\n clientInfo: credentials.clientInfo,\n fromCache: true,\n tokens: mergedTokens,\n };\n } catch (error) {\n // Both classifiers walk every nested wrapper field independently, so an\n // error carrying `invalid_grant` in one field and `invalid_client` in\n // another sets both. That is not a signal we can act on: the destructive\n // step differs between them, and guessing wrong either discards a live\n // client registration or keeps a dead one. Treat ambiguity as transient\n // and retain everything — the conservative default this module is built\n // around.\n const grantMatch = isInvalidGrantError(error);\n const clientMatch = isInvalidClientError(error);\n const ambiguous = grantMatch && clientMatch;\n const invalidGrant = grantMatch && !ambiguous;\n const invalidClient = clientMatch && !ambiguous;\n logger.warn(\n ambiguous\n ? \"Refresh failed with both invalid_client and invalid_grant; ambiguous, retaining credentials\"\n : invalidClient\n ? \"Client registration rejected by OAuth server (invalid_client); clearing stored client and tokens\"\n : invalidGrant\n ? \"Refresh token rejected by OAuth server (invalid_grant); clearing stored tokens\"\n : \"Token refresh failed transiently; retaining refresh_token for the next attempt\",\n {\n ambiguous,\n clientId: credentials.clientInfo.client_id,\n error: String(error),\n eventType: AuthEventType.REFRESH_FAILED,\n invalidClient,\n invalidGrant,\n serverUrl: serverUrl.toString(),\n }\n );\n\n // Per RFC 6749 §5.2, only two errors mean the stored credentials\n // themselves are dead. invalid_grant: the refresh_token is dead — clear\n // tokens but keep the client registration, which is still valid.\n // invalid_client: the registered client is dead (purged/expired at the\n // auth server) — an authorization redirect with that client_id would fail\n // too, so clear the whole record and let the next interactive flow\n // re-register via DCR. For any other error (network failure, 5xx, timeout,\n // rate limit), keep the refresh_token so a future attempt can use it —\n // wiping it on transient failures is what historically forced users into\n // browser re-auth.\n if (invalidClient) {\n await credentialStore.update(serverUrl.toString(), {\n clientInfo: undefined,\n tokens: undefined,\n tokensObtainedAt: undefined,\n });\n } else if (invalidGrant) {\n await credentialStore.update(serverUrl.toString(), {\n tokens: undefined,\n tokensObtainedAt: undefined,\n });\n }\n\n return undefined;\n }\n}\n","/**\n * MCP Transport Utilities\n *\n * Simple transport creation for remote MCP servers.\n * Transport type is determined by trying Streamable HTTP first,\n * falling back to SSE if the endpoint doesn't support it.\n */\n\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { SSEClientTransport } from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { FetchLike } from \"@modelcontextprotocol/sdk/shared/transport.js\";\n\nimport { logger } from \"./logger.js\";\n\nexport type TransportType = \"sse\" | \"streamable-http\";\n\n/** Order in which transports are tried (per MCP spec: Streamable HTTP first, SSE fallback) */\nconst TRANSPORT_PRIORITY: TransportType[] = [\"streamable-http\", \"sse\"];\n\n/** Result of a successful transport connection */\nexport interface TransportConnectionResult {\n /** The connected client instance */\n client: Client;\n /** The transport type that successfully connected */\n transportType: TransportType;\n}\n\n/**\n * Connects to a remote MCP server, trying transports in order.\n * Per MCP spec: tries Streamable HTTP first, falls back to SSE if not supported (404/405).\n *\n * Creates a fresh client for each transport attempt to avoid state issues.\n * The MCP SDK Client maintains internal connection state that does not properly\n * reset after a failed connect attempt, so reusing the same client instance\n * across transport fallbacks can cause \"already connected\" or similar errors.\n *\n * @param createClient - Factory function to create a new Client instance\n * @param url - The URL of the remote MCP server\n * @param requestInit - Optional request init for the transport (headers, etc.)\n * @returns The connected client and transport type\n * @throws The error from the last transport attempt if none succeed,\n * or immediately if an error is not a \"transport not supported\" error\n */\nexport async function connectWithTransportFallback(\n createClient: () => Client,\n url: string,\n requestInit?: RequestInit,\n customFetch?: FetchLike\n): Promise<TransportConnectionResult> {\n const { result: client, transportType } = await walkTransports(async (transportType) => {\n // Create a fresh client for each transport attempt to avoid state issues\n const client = createClient();\n const transport = createTransport(url, transportType, requestInit, customFetch);\n await client.connect(transport);\n return client;\n });\n return { client, transportType };\n}\n\n/**\n * Creates a transport instance for the given type.\n *\n * Pass `customFetch` to intercept every HTTP request the transport makes — the\n * gateway uses this to inject a dynamic Authorization header and recover from\n * mid-session 401s via silent refresh (PRDCT-3666 Tier 2).\n *\n * Auth-refresh coverage matrix:\n *\n * Streamable HTTP — every JSON-RPC message is a separate POST, all of which\n * go through `customFetch`. A 401 on any request triggers the wrapper's\n * silent-refresh-and-retry. Fully covered.\n *\n * SSE — has two channels:\n * 1. The long-lived `GET /mcp` event-stream. The SDK passes our\n * `customFetch` to the `eventsource` (v4+) polyfill via its `fetch`\n * option, so our wrapper attaches a fresh Authorization header on the\n * initial GET AND on every auto-reconnect after a disconnect.\n * 2. The short `POST /messages/<sessionId>` client→server messages — also\n * routed through `customFetch`.\n * Both channels are covered for the cases where the server closes the\n * stream on token expiry (EventSource auto-reconnects → new fetch call →\n * fresh bearer attached).\n *\n * ====================================================================\n * NOT SUPPORTED — known gap, intentional:\n *\n * SSE \"silent mid-stream drop\" case\n * If the bearer expires while the long-lived GET stream is OPEN and the\n * server chooses to keep the connection open but stop emitting events\n * (or only 401 *new* requests on a different channel), the gateway has no\n * way to detect that the stream has gone stale — EventSource doesn't fire\n * `onerror` because the connection is still alive at the TCP level.\n *\n * Recovering from this would need either:\n * (a) a proactive timer that fires at ~90% of `expires_in`, refreshes\n * the bearer, and tears down + reopens the EventSource before TTL;\n * (b) a heartbeat / liveness probe that detects extended silence and\n * triggers a reconnect.\n *\n * Neither is implemented. Impact today is zero — every OAuth-wrapped MCP\n * server in scope (Notion, Lucid, ClickUp, GitHub Copilot, Sentry, Linear,\n * Asana V2, Atlassian post-2026-06-30) uses Streamable HTTP. The only\n * surviving major SSE+OAuth server is Atlassian, sunsetting on\n * 2026-06-30. None of the surviving SSE+OAuth servers have been observed\n * to use the long-lived GET stream for server-push features (notifications,\n * sampling requests, streaming partial tool results) that would expose\n * this gap — they use it as a generic request/response substrate, in\n * which case any silent-drop manifests as the next POST returning 401,\n * which IS handled.\n * ====================================================================\n */\nexport function createTransport(\n url: string,\n type: TransportType,\n requestInit?: RequestInit,\n customFetch?: FetchLike\n): SSEClientTransport | StreamableHTTPClientTransport {\n const parsedUrl = new URL(url);\n\n if (type === \"streamable-http\") {\n return new StreamableHTTPClientTransport(parsedUrl, { fetch: customFetch, requestInit });\n }\n\n return new SSEClientTransport(parsedUrl, { fetch: customFetch, requestInit });\n}\n\n/**\n * Checks if an error indicates the transport/endpoint is not supported (404/405).\n * This means we should try a different transport type.\n */\nexport function isTransportNotSupportedError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n\n const status = (err as { status?: number }).status;\n const code = (err as { code?: number }).code;\n const message = (err as { message?: string }).message;\n\n // Check status codes that indicate endpoint doesn't support this transport\n if (status === 404 || status === 405 || code === 404 || code === 405) {\n return true;\n }\n\n // Check error message for these status codes\n if (message && /404|not found|405|method not allowed/i.test(String(message))) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Walks transports in priority order, calling `attempt` for each. Returns the\n * first successful result. Continues to the next transport on\n * `isTransportNotSupportedError` (404/405); propagates any other error.\n *\n * Shared primitive for both connect-style attempts (the runtime proxy via\n * `connectWithTransportFallback`) and probe-style attempts (the install-time\n * scan in `src/probe`). Callers supply the per-transport request shape; this\n * helper owns only the priority-order + fallback-on-404/405 logic.\n *\n * When every transport fails with a not-supported error, throws an aggregate\n * error whose message lists each transport's status — so callers can tell\n * \"URL is dead\" from \"URL only speaks one transport\". The original errors are\n * available via `error.cause` (an array of `{ transport, err }`).\n *\n * @param attempt - Per-transport function. Throw an error matching\n * `isTransportNotSupportedError` to trigger fallback; throw anything else\n * (or any one transport's non-fallback error) to propagate.\n * @returns The first transport that succeeded and its result.\n * @throws The propagated non-fallback error, or an aggregate of every\n * transport's not-supported error when all transports fail.\n */\nexport async function walkTransports<T>(\n attempt: (transportType: TransportType) => Promise<T>\n): Promise<{ result: T; transportType: TransportType }> {\n const failures: Array<{ err: unknown; transport: TransportType }> = [];\n\n for (const transportType of TRANSPORT_PRIORITY) {\n try {\n const result = await attempt(transportType);\n return { result, transportType };\n } catch (err) {\n if (isTransportNotSupportedError(err)) {\n logger.info(`${transportType} not supported, trying next transport...`);\n failures.push({ err, transport: transportType });\n continue;\n }\n\n // Non-transport error (e.g., 401, network error) - propagate immediately\n throw err;\n }\n }\n\n // All transports failed with \"not supported\" errors. Build a message that\n // lists every transport's status (or message), so callers don't see only\n // the last transport's failure and assume the others were never tried.\n const summary = failures\n .map(({ err, transport }) => {\n const status = (err as { status?: number } | null)?.status;\n if (status !== undefined) return `${transport}: ${status}`;\n const message = err instanceof Error ? err.message : String(err);\n return `${transport}: ${message}`;\n })\n .join(\", \");\n const aggregate =\n failures.length > 0\n ? new Error(`all transports returned not-supported (${summary})`)\n : new Error(\"No compatible transport found\");\n // Preserve original errors for callers that need to introspect them.\n (aggregate as { cause?: unknown } & Error).cause = failures;\n throw aggregate;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transportDetection-0rgX1zGT.js","names":["command: string","args: string[]","resolveCallback: (result: CallbackServerResult) => void","rejectCallback: (error: Error) => void","timeoutRef: { id?: ReturnType<typeof setTimeout> }","boundPort: number","URL","attemptedPorts: number[]","lastError: Error | undefined","portToTry: number","authServerUrl: URL","cause: unknown","resourceMetadata: OAuthProtectedResourceMetadata | undefined","authMetadata: Awaited<ReturnType<typeof discoverAuthorizationServerMetadata>>","DCR_CLIENT_NAME_BY_APP: Record<string, string>","DCR_CLIENT_URI_BY_APP: Record<string, string>","DECOY_REDIRECT_URI_BY_APP_AND_HOST: Record<string, Record<string, string>>","authMetadata: Awaited<ReturnType<typeof discoverAuthServer>>[\"authMetadata\"]","authServerUrl: URL","resourceMetadata: OAuthProtectedResourceMetadata | undefined","clientInfo: OAuthClientInformationFull","clientMetadata: OAuthClientMetadata","hostname: string","hostname","headers: Record<string, string>","tokenData: Record<string, unknown>","codeVerifier: string | undefined","codeChallenge: string | undefined","codeChallengeMethod: string | undefined","mergedTokens: OAuthTokens","TRANSPORT_PRIORITY: TransportType[]","client","transportType","failures: Array<{ err: unknown; transport: TransportType }>"],"sources":["../src/auth/browser.ts","../src/auth/events.ts","../src/auth/callbackServer.ts","../src/auth/discovery.ts","../src/auth/errors.ts","../src/auth/oauthFlow.ts","../src/lib/transportDetection.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { platform } from \"node:os\";\n\nimport { logger } from \"../lib/logger.js\";\n\n/**\n * Opens the given URL in the user's default browser.\n *\n * Cross-platform support:\n * - macOS: uses `open`\n * - Windows: uses PowerShell Start-Process with Base64-encoded command (injection-safe)\n * - Linux: uses `xdg-open`\n *\n * @param url The URL to open\n * @returns A promise that resolves when the browser command is executed\n */\nexport async function openBrowser(url: string | URL): Promise<void> {\n const urlString = url.toString();\n const os = platform();\n const redactedUrl = redactUrlForLogging(urlString);\n\n return new Promise((resolve, reject) => {\n let command: string;\n let args: string[];\n\n if (os === \"win32\") {\n // Windows - use PowerShell with Base64-encoded command to prevent injection\n // This is the same approach used by the popular 'open' npm package.\n // The command is Base64-encoded (UTF-16LE) so no shell metacharacters can escape.\n const psCommand = `Start-Process ${escapePowerShellArgument(urlString)}`;\n const encodedCommand = encodePowerShellCommand(psCommand);\n\n command = getPowerShellPath();\n args = [\"-NoProfile\", \"-NonInteractive\", \"-ExecutionPolicy\", \"Bypass\", \"-EncodedCommand\", encodedCommand];\n } else if (os === \"darwin\") {\n // macOS\n command = \"open\";\n args = [urlString];\n } else {\n // Linux and others - use xdg-open\n command = \"xdg-open\";\n args = [urlString];\n }\n\n execFile(command, args, (error, _stdout, stderr) => {\n if (error) {\n const errorContext = {\n command,\n os,\n stderr,\n url: redactedUrl,\n };\n\n logger.warn(\"Failed to open browser automatically\", {\n ...errorContext,\n error: error.message,\n });\n\n const launchError = new Error(\n `Failed to open browser: ${error.message} (command: ${command}, os: ${os}, url: ${redactedUrl})`\n );\n launchError.cause = error;\n reject(launchError);\n } else {\n logger.debug(\"Browser opened successfully\", { url: redactedUrl });\n resolve();\n }\n });\n });\n}\n\n/**\n * Encodes a PowerShell command as Base64 UTF-16LE for use with -EncodedCommand.\n * This is the safest way to pass commands to PowerShell as it prevents any\n * shell metacharacter interpretation.\n */\nfunction encodePowerShellCommand(command: string): string {\n // PowerShell's -EncodedCommand expects UTF-16LE encoded Base64\n const buffer = Buffer.from(command, \"utf16le\");\n return buffer.toString(\"base64\");\n}\n\n/**\n * Escapes a string for use as a PowerShell single-quoted string argument.\n * Single quotes inside the string are escaped by doubling them.\n */\nfunction escapePowerShellArgument(value: string): string {\n return `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\n/**\n * Gets the path to Windows PowerShell.\n * PowerShell is included by default since Windows 7 SP1 and Windows Server 2008 R2.\n */\nfunction getPowerShellPath(): string {\n const systemRoot = process.env.SYSTEMROOT || process.env.windir || \"C:\\\\Windows\";\n return `${systemRoot}\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe`;\n}\n\n/**\n * Redacts sensitive query parameters from a URL for safe logging.\n * Preserves the base URL structure while hiding potentially sensitive data.\n *\n * @param url The URL to redact\n * @returns A redacted version of the URL safe for logging\n */\nfunction redactUrlForLogging(url: string): string {\n try {\n const parsed = new URL(url);\n if (parsed.search) {\n return `${parsed.protocol}//${parsed.host}${parsed.pathname}?[REDACTED]`;\n }\n return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n } catch {\n // If URL parsing fails, return a generic redacted string\n return \"[INVALID_URL]\";\n }\n}\n","/**\n * Structured `eventType` values emitted from the OAuth code paths in this\n * package. groundcover monitors filter on these strings, so renames here are\n * breaking changes for the monitoring pipeline — bump the monitor YAMLs in\n * the infrastructure repo when you change anything below.\n *\n * Naming convention: `mcp_auth_*_failed` for unrecoverable errors; the\n * `mcp_auth_performed` happy-path event lives in src/bin/main.ts because it\n * is emitted by the proxy boot code, not by this auth subpackage.\n */\nexport const AuthEventType = {\n /** OAuth callback server received error= or no code, or failed to parse. */\n CALLBACK_FAILED: \"mcp_auth_callback_failed\",\n /** registerClient threw or auth server has no registration_endpoint. */\n DCR_FAILED: \"mcp_auth_dcr_failed\",\n /** Refresh-token grant rejected by the auth server. */\n REFRESH_FAILED: \"mcp_auth_refresh_failed\",\n /**\n * `discoverAuthorizationServerMetadata` threw, or returned an empty\n * document. NOTE: resource-metadata soft-fails are NOT emitted as failed\n * events — the SDK silently falls back to using the server URL as the\n * auth server, which is expected for some MCP servers.\n */\n SERVER_METADATA_FAILED: \"mcp_auth_server_metadata_failed\",\n /** OAuth state parameter mismatch — possible CSRF, definitely a failure. */\n STATE_MISMATCH: \"mcp_auth_state_mismatch\",\n /** Authorization-code or refresh-token POST to /token failed (non-2xx). */\n TOKEN_EXCHANGE_FAILED: \"mcp_auth_token_exchange_failed\",\n} as const;\n\nexport type AuthEventType = (typeof AuthEventType)[keyof typeof AuthEventType];\n","import { createServer, type Server } from \"node:http\";\nimport { URL } from \"node:url\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { AuthEventType } from \"./events.js\";\n\n/**\n * Port range for OAuth callback server (IANA dynamic/private ports)\n */\nconst MIN_PORT = 49152;\nconst MAX_PORT = 65535;\nconst MAX_PORT_ATTEMPTS = 5;\n\nexport interface CallbackServerOptions {\n /**\n * Maximum number of port binding attempts before giving up.\n * Default: 5\n */\n maxAttempts?: number;\n /**\n * Preferred port to use. If unavailable, random ports will be tried.\n */\n preferredPort?: number;\n /**\n * MCP server URL whose OAuth flow this callback is serving. Threaded\n * through to the callback's error logs so groundcover monitors can\n * attribute mcp_auth_callback_failed events to a specific remote.\n * Optional — when omitted, the field is simply absent from the logs.\n */\n serverUrl?: string;\n /**\n * Timeout in milliseconds to wait for the callback.\n * Default: 5 minutes (300000ms)\n */\n timeoutMs?: number;\n}\n\nexport interface CallbackServerResult {\n /**\n * The authorization code received from the OAuth server.\n */\n code: string;\n /**\n * The state parameter, if provided.\n */\n state?: string;\n}\n\n/**\n * Generates a random port number in the dynamic/private port range.\n */\nfunction getRandomPort(): number {\n return Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT;\n}\n\n/**\n * Attempts to start the HTTP server on the given port.\n * Returns a promise that resolves with the server if successful, or rejects if the port is in use.\n */\nfunction tryListenOnPort(server: Server, port: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const onError = (err: NodeJS.ErrnoException) => {\n server.removeListener(\"listening\", onListening);\n reject(err);\n };\n\n const onListening = () => {\n server.removeListener(\"error\", onError);\n resolve();\n };\n\n server.once(\"error\", onError);\n server.once(\"listening\", onListening);\n server.listen(port, \"127.0.0.1\");\n });\n}\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * HTML page shown to the user after successful authorization.\n */\nconst SUCCESS_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Authorization Successful</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n color: #fff;\n }\n .container {\n text-align: center;\n padding: 2rem;\n }\n .checkmark {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: #10b981;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 1.5rem;\n animation: pop 0.3s ease-out;\n }\n .checkmark svg {\n width: 40px;\n height: 40px;\n stroke: white;\n stroke-width: 3;\n }\n h1 {\n margin: 0 0 0.5rem;\n font-size: 1.75rem;\n font-weight: 600;\n }\n p {\n margin: 0;\n color: #a0aec0;\n font-size: 1rem;\n }\n @keyframes pop {\n 0% { transform: scale(0); }\n 50% { transform: scale(1.1); }\n 100% { transform: scale(1); }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"checkmark\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M5 13l4 4L19 7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n </div>\n <script>setTimeout(() => window.close(), 3000);</script>\n</body>\n</html>`;\n\n/**\n * Starts a local HTTP server to receive the OAuth callback.\n * Uses random port selection with retry logic to handle port conflicts.\n *\n * @returns A promise that resolves with the port number and a function to wait for the callback.\n */\nexport async function startCallbackServer(options: CallbackServerOptions = {}): Promise<{\n callbackUrl: string;\n close: () => Promise<void>;\n port: number;\n waitForCallback: () => Promise<CallbackServerResult>;\n}> {\n const { maxAttempts = MAX_PORT_ATTEMPTS, preferredPort, serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n // Strip userinfo + query params before logging — a configured MCP URL\n // can carry tokens or basic-auth credentials, and these errors get\n // persisted to logs/alerts. Keep the protocol+host+path so the log is\n // still useful for triage.\n const safeServerUrl = serverUrl ? sanitizeServerUrl(serverUrl) : undefined;\n\n let resolveCallback: (result: CallbackServerResult) => void;\n let rejectCallback: (error: Error) => void;\n // Using an object to hold the timeout ID so it can be set after server creation\n // but still referenced in the close() function\n const timeoutRef: { id?: ReturnType<typeof setTimeout> } = {};\n\n const callbackPromise = new Promise<CallbackServerResult>((resolve, reject) => {\n resolveCallback = resolve;\n rejectCallback = reject;\n });\n\n // Track the actual bound port for use in request handler\n let boundPort: number;\n\n const server = createServer((req, res) => {\n // Ignore favicon requests\n if (req.url === \"/favicon.ico\") {\n res.writeHead(404);\n res.end();\n return;\n }\n\n // Only handle the callback path\n if (!req.url?.startsWith(\"/callback\")) {\n res.writeHead(404);\n res.end(\"Not Found\");\n return;\n }\n\n try {\n const parsedUrl = new URL(req.url, `http://localhost:${boundPort}`);\n const code = parsedUrl.searchParams.get(\"code\");\n const error = parsedUrl.searchParams.get(\"error\");\n const errorDescription = parsedUrl.searchParams.get(\"error_description\");\n const state = parsedUrl.searchParams.get(\"state\");\n\n if (error) {\n logger.error(\"OAuth authorization error\", {\n error,\n errorDescription,\n eventType: AuthEventType.CALLBACK_FAILED,\n ...(safeServerUrl ? { serverUrl: safeServerUrl } : {}),\n });\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(getErrorHtml(error, errorDescription || undefined));\n clearTimeout(timeoutRef.id);\n rejectCallback(\n new Error(`OAuth authorization failed: ${error}${errorDescription ? ` - ${errorDescription}` : \"\"}`)\n );\n return;\n }\n\n if (!code) {\n logger.error(\"OAuth callback missing authorization code\", {\n eventType: AuthEventType.CALLBACK_FAILED,\n reason: \"missing_code\",\n ...(safeServerUrl ? { serverUrl: safeServerUrl } : {}),\n });\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(getErrorHtml(\"missing_code\", \"No authorization code was provided\"));\n clearTimeout(timeoutRef.id);\n rejectCallback(new Error(\"OAuth callback missing authorization code\"));\n return;\n }\n\n logger.info(\"OAuth authorization code received\", {\n codePrefix: `${code.substring(0, 10)}...`,\n hasState: !!state,\n });\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(SUCCESS_HTML);\n\n clearTimeout(timeoutRef.id);\n resolveCallback({ code, state: state || undefined });\n\n // Close server after a short delay to ensure response is sent\n setTimeout(() => server.close(), 1000);\n } catch (err) {\n logger.error(\"Error processing OAuth callback\", {\n error: String(err),\n eventType: AuthEventType.CALLBACK_FAILED,\n reason: \"callback_processing_error\",\n ...(safeServerUrl ? { serverUrl: safeServerUrl } : {}),\n });\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n clearTimeout(timeoutRef.id);\n rejectCallback(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n // Set up timeout (now that server exists)\n timeoutRef.id = setTimeout(() => {\n rejectCallback(new Error(`OAuth callback timeout after ${timeoutMs}ms`));\n server.close();\n }, timeoutMs);\n\n // Try to bind to a port with retry logic\n const attemptedPorts: number[] = [];\n let lastError: Error | undefined;\n\n while (attemptedPorts.length < maxAttempts) {\n // First attempt uses preferred port if provided, otherwise random\n let portToTry: number;\n if (attemptedPorts.length === 0 && preferredPort !== undefined) {\n portToTry = preferredPort;\n } else {\n // Generate a random port that hasn't been tried yet\n portToTry = getRandomPort();\n while (attemptedPorts.includes(portToTry)) {\n portToTry = getRandomPort();\n }\n }\n attemptedPorts.push(portToTry);\n\n try {\n await tryListenOnPort(server, portToTry);\n boundPort = portToTry;\n const callbackUrl = `http://localhost:${boundPort}/callback`;\n logger.info(\"OAuth callback server started\", { attempt: attemptedPorts.length, callbackUrl, port: boundPort });\n\n return {\n callbackUrl,\n close: () =>\n new Promise<void>((resolve) => {\n clearTimeout(timeoutRef.id);\n server.close(() => resolve());\n // Browsers hold the keep-alive socket open after the OAuth success\n // page loads. Without forcing existing connections closed, Node's\n // server.close() waits indefinitely for them to drain and the\n // OAuth flow hangs after persisting tokens.\n server.closeAllConnections();\n }),\n port: boundPort,\n waitForCallback: () => callbackPromise,\n };\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === \"EADDRINUSE\") {\n logger.debug(\"Port in use, trying another port\", {\n attempt: attemptedPorts.length,\n maxAttempts,\n port: portToTry,\n });\n lastError = new Error(`Port ${portToTry} is already in use`);\n } else {\n // For non-EADDRINUSE errors, clean up and throw immediately\n clearTimeout(timeoutRef.id);\n server.close();\n throw err;\n }\n }\n }\n\n // All attempts failed\n clearTimeout(timeoutRef.id);\n server.close();\n throw new Error(\n `Failed to start OAuth callback server after ${maxAttempts} attempts. ` +\n `Tried ports: ${attemptedPorts.join(\", \")}. Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\n/**\n * HTML page shown to the user when authorization fails.\n */\nfunction getErrorHtml(error: string, description?: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Authorization Failed</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n color: #fff;\n }\n .container {\n text-align: center;\n padding: 2rem;\n max-width: 400px;\n }\n .error-icon {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: #ef4444;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 1.5rem;\n }\n .error-icon svg {\n width: 40px;\n height: 40px;\n stroke: white;\n stroke-width: 3;\n }\n h1 {\n margin: 0 0 0.5rem;\n font-size: 1.75rem;\n font-weight: 600;\n }\n .error-code {\n color: #f87171;\n font-family: monospace;\n margin-bottom: 0.5rem;\n }\n p {\n margin: 0;\n color: #a0aec0;\n font-size: 1rem;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"error-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M6 18L18 6M6 6l12 12\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Authorization Failed</h1>\n <p class=\"error-code\">${escapeHtml(error)}</p>\n ${description ? `<p>${escapeHtml(description)}</p>` : \"\"}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Strip credentials and query params from a URL before logging. Keeps\n * protocol + host + path so the log is still useful for triage. Returns\n * the original string when parsing fails (better to over-log than to\n * silently drop the attribution field).\n */\nfunction sanitizeServerUrl(url: string): string {\n try {\n const u = new URL(url);\n u.username = \"\";\n u.password = \"\";\n u.search = \"\";\n u.hash = \"\";\n return u.toString();\n } catch {\n return url;\n }\n}\n","import {\n discoverAuthorizationServerMetadata,\n discoverOAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type { OAuthProtectedResourceMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nexport interface DiscoverAuthServerOptions {\n /**\n * Optional logger. When provided, emits the same debug/info/warn lines that\n * `performOAuthFlow` used to emit inline. When omitted, discovery runs\n * silently.\n */\n logger?: DiscoveryLogger;\n /**\n * Optional AbortSignal, threaded into both SDK discovery calls via a fetch\n * wrapper. When omitted, the SDK calls are made with no fetch override —\n * preserving the existing `performOAuthFlow` call shape exactly.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal logging surface used by the discovery helper. Matches the shape of\n * `../lib/logger.ts`'s singleton so callers can pass it directly. Omit to run\n * discovery silently (the probe's call pattern).\n */\nexport interface DiscoveryLogger {\n debug(message: string, meta?: Record<string, unknown>): void;\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nexport interface DiscoveryResult {\n /** Auth-server metadata. May be undefined — caller decides how to respond. */\n authMetadata: Awaited<ReturnType<typeof discoverAuthorizationServerMetadata>>;\n /**\n * The chosen auth server URL: the first entry of `resourceMetadata.authorization_servers`\n * when present, otherwise `serverUrl` itself. When metadata was only found\n * via the origin-root fallback, this is the origin root of that URL.\n */\n authServerUrl: URL;\n /** Protected-resource metadata. Undefined when the document is missing or lookup soft-failed. */\n resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n}\n\n/**\n * Thrown when `discoverAuthorizationServerMetadata` fails. Preserves the\n * computed `authServerUrl` so callers can include it in error messages /\n * probe results — the pre-refactor inline code had it in scope naturally.\n */\nexport class AuthServerDiscoveryError extends Error {\n constructor(\n public readonly authServerUrl: URL,\n public readonly cause: unknown\n ) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n super(`Auth server metadata discovery failed for ${authServerUrl}: ${msg}`);\n this.name = \"AuthServerDiscoveryError\";\n }\n}\n\n/**\n * Runs the OAuth metadata discovery sequence shared by the full OAuth flow and\n * the install-time DCR probe:\n *\n * 1. Look up the protected-resource metadata (soft-fails on error).\n * 2. Pick the auth server URL — the first `authorization_servers` entry, or\n * `serverUrl` as a fallback.\n * 3. Look up the auth-server metadata. Throws iff the SDK call itself throws;\n * does NOT throw when the SDK returns undefined (caller decides).\n * 4. If step 3 found nothing and the auth server URL has a path, retry the\n * lookup once at the origin root (see inline comment) — on success,\n * `authServerUrl` in the result is the origin root.\n *\n * No policy decisions baked in — both callers (`performOAuthFlow` and `probeOne`)\n * apply their own handling to the result.\n */\nexport async function discoverAuthServer(\n serverUrl: URL,\n options: DiscoverAuthServerOptions = {}\n): Promise<DiscoveryResult> {\n const { logger, signal } = options;\n const fetchFn = signal ? signaledFetch(signal) : undefined;\n\n logger?.debug(\"Discovering OAuth protected resource metadata...\");\n let resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n try {\n // Preserve the exact SDK call arity used by performOAuthFlow when no\n // signal is provided (single-arg form) — some SDKs treat an explicit\n // `undefined` differently from an omitted arg.\n resourceMetadata = fetchFn\n ? await discoverOAuthProtectedResourceMetadata(serverUrl, undefined, fetchFn)\n : await discoverOAuthProtectedResourceMetadata(serverUrl);\n logger?.info(\"Discovered protected resource metadata\", {\n authorizationServers: resourceMetadata?.authorization_servers,\n resource: resourceMetadata?.resource,\n scopesSupported: resourceMetadata?.scopes_supported,\n });\n } catch (err) {\n logger?.warn(\"Could not discover protected resource metadata, will try direct discovery\", {\n error: String(err),\n });\n }\n\n let authServerUrl: URL;\n if (resourceMetadata?.authorization_servers?.length) {\n authServerUrl = new URL(resourceMetadata.authorization_servers[0]);\n logger?.debug(\"Using authorization server from resource metadata\", {\n authServerUrl: authServerUrl.toString(),\n });\n } else {\n authServerUrl = serverUrl;\n logger?.debug(\"Using server URL as authorization server\", {\n authServerUrl: authServerUrl.toString(),\n });\n }\n\n logger?.debug(\"Discovering authorization server metadata...\");\n let authMetadata: Awaited<ReturnType<typeof discoverAuthorizationServerMetadata>>;\n try {\n authMetadata = fetchFn\n ? await discoverAuthorizationServerMetadata(authServerUrl, { fetchFn })\n : await discoverAuthorizationServerMetadata(authServerUrl);\n } catch (err) {\n throw new AuthServerDiscoveryError(authServerUrl, err);\n }\n\n // The SDK's buildDiscoveryUrls only probes path-aware well-knowns for URLs\n // with a path (e.g. /.well-known/oauth-authorization-server/v1/mcp) — it\n // never tries the origin root. Older SDKs did, and servers like Atlassian's\n // remote MCP publish auth-server metadata ONLY at the root\n // /.well-known/oauth-authorization-server (with no RFC 9728\n // protected-resource metadata to redirect us there). Restore the root\n // fallback: when the path-aware lookup came back absent and the auth server\n // URL has a path, retry once against the origin root. Never fires when\n // metadata was already found.\n if (!authMetadata && authServerUrl.pathname !== \"/\") {\n const rootUrl = new URL(authServerUrl.origin);\n logger?.debug(\"Auth server metadata absent at path-aware well-knowns, retrying at origin root\", {\n authServerUrl: authServerUrl.toString(),\n rootUrl: rootUrl.toString(),\n });\n try {\n authMetadata = fetchFn\n ? await discoverAuthorizationServerMetadata(rootUrl, { fetchFn })\n : await discoverAuthorizationServerMetadata(rootUrl);\n } catch (err) {\n // Caller-initiated cancellation is not evidence about the server: the\n // root check never finished, so reporting \"absent\" here would let a\n // timeout masquerade as a definitive no-OAuth verdict (which the probe\n // caches permanently). Rethrow so cancellation stays a failure.\n // Checked via signal.aborted rather than error-name sniffing — robust\n // across AbortError / TimeoutError / custom abort reasons.\n if (signal?.aborted) {\n throw new AuthServerDiscoveryError(rootUrl, err);\n }\n // Otherwise the path-aware attempt already concluded \"absent\" and this\n // retry is a best-effort extra probe — a transport error here must not\n // upgrade that verdict into a thrown discovery failure (callers would\n // surface it as transient instead of \"no OAuth metadata\"). Swallow and\n // keep the absent outcome, matching pre-fallback behavior exactly.\n logger?.warn(\"Origin-root auth server metadata fallback failed, treating metadata as absent\", {\n error: String(err),\n rootUrl: rootUrl.toString(),\n });\n }\n if (authMetadata) {\n // Callers put authServerUrl in error messages and log fields — point it\n // at the URL that actually served the metadata.\n authServerUrl = rootUrl;\n logger?.info(\"Origin-root auth server metadata fallback succeeded\", {\n authServerUrl: authServerUrl.toString(),\n });\n } else {\n logger?.debug(\"Origin-root auth server metadata fallback found nothing\", {\n rootUrl: rootUrl.toString(),\n });\n }\n }\n\n if (authMetadata) {\n logger?.info(\"Discovered authorization server metadata\", {\n authorizationEndpoint: authMetadata.authorization_endpoint,\n issuer: authMetadata.issuer,\n registrationEndpoint: authMetadata.registration_endpoint,\n tokenEndpoint: authMetadata.token_endpoint,\n });\n }\n\n return { authMetadata, authServerUrl, resourceMetadata };\n}\n\n/** Wraps global fetch so the SDK inherits a caller-supplied AbortSignal. */\nfunction signaledFetch(probeSignal: AbortSignal): typeof fetch {\n return (input, init) => {\n const sdkSignal = init?.signal;\n const combined =\n sdkSignal ? AbortSignal.any([probeSignal, sdkSignal]) : probeSignal;\n return fetch(input, { ...init, signal: combined });\n };\n}\n","import { OAuthError } from \"@modelcontextprotocol/sdk/server/auth/errors.js\";\n\n/**\n * Spec-defined RFC 6749 §5.2 error codes this module classifies. The string\n * comparisons are intentionally case-sensitive lowercase: RFC 6749 §5.2\n * defines `error` as a case-sensitive ASCII token and the spec values are\n * always lowercase. Non-compliant servers that return `INVALID_GRANT` are out\n * of scope — being permissive on case would risk false positives on\n * similarly-cased non-OAuth errors.\n */\ntype FatalOAuthErrorCode = \"invalid_client\" | \"invalid_grant\";\n\n/**\n * Detects the spec-defined `invalid_client` response (RFC 6749 §5.2): client\n * authentication failed — the auth server no longer recognizes the client_id\n * the tokens were minted under (DCR registrations can be purged or expire).\n *\n * A dead client is as fatal as a dead grant, but wider: no token-endpoint call\n * AND no authorization redirect can ever succeed with it, so callers should\n * discard the stored client registration along with the tokens rather than\n * retrying — a retry loop against a purged client never converges.\n *\n * Same conservative-unwrap semantics as `isInvalidGrantError`: returns false\n * on anything ambiguous.\n */\nexport function isInvalidClientError(error: unknown): boolean {\n return checkOAuthErrorCode(error, \"invalid_client\", new WeakSet());\n}\n\n/**\n * Detects whether an error from `refreshAuthorization` (or any OAuth token-endpoint\n * call) represents the spec-defined `invalid_grant` response.\n *\n * Per RFC 6749 §5.2, `invalid_grant` is the only error code that means the\n * refresh_token itself is dead. All other errors (network failures, timeouts,\n * 5xx, rate limits, etc.) are transient or unrelated to the refresh_token's\n * validity and MUST NOT trigger destructive credential cleanup.\n *\n * Returns false on anything ambiguous — the safer default, since wrongly\n * classifying a transient error as `invalid_grant` would wipe a perfectly good\n * refresh_token and force the user through a browser re-auth.\n */\nexport function isInvalidGrantError(error: unknown): boolean {\n return checkOAuthErrorCode(error, \"invalid_grant\", new WeakSet());\n}\n\n/**\n * Recursively unwraps the common shapes a token-endpoint failure can arrive\n * in, testing for one specific RFC 6749 §5.2 error code. The MCP SDK's\n * `refreshAuthorization` typically throws an `OAuthError` subclass when the\n * response was a well-formed RFC 6749 error body, but non-spec-compliant\n * servers, intermediate proxies, and downstream wrappers can produce:\n *\n * - plain `{ error: \"invalid_grant\" }`\n * - `{ response: { body: { error: \"invalid_grant\" }}}` (some SDK wraps)\n * - `{ data: { error: \"invalid_grant\" }}` (other SDK wraps)\n * - an `Error` whose `.cause` carries any of the above (Node.js error chain)\n * - an `Error` whose `.message` mentions the code as last resort\n *\n * The `seen` set prevents infinite recursion on cyclic `.cause` chains\n * (rare but possible).\n */\nfunction checkOAuthErrorCode(\n error: unknown,\n code: FatalOAuthErrorCode,\n seen: WeakSet<object>\n): boolean {\n if (error == null) return false;\n\n // SDK OAuth error class (covers the happy path)\n if (error instanceof OAuthError) {\n return error.errorCode === code;\n }\n\n // Cycle guard for nested .cause / .response / .data traversal\n if (typeof error === \"object\") {\n if (seen.has(error)) return false;\n seen.add(error);\n\n // Plain RFC 6749 error body\n const errField = (error as { error?: unknown }).error;\n if (typeof errField === \"string\" && errField === code) {\n return true;\n }\n\n // Common SDK wrap shapes — each may itself contain the error body\n for (const key of [\"response\", \"data\", \"body\", \"cause\"] as const) {\n const inner = (error as Record<string, unknown>)[key];\n if (inner !== undefined && checkOAuthErrorCode(inner, code, seen)) return true;\n }\n }\n\n // Message-based fallback as the LAST resort. Some SDK wrappers stringify\n // the RFC 6749 error envelope into the Error's `message` instead of\n // preserving structured `.cause` / `.response` / `.data`. Recognize those\n // cases — but ONLY when the message looks like a serialized error envelope,\n // not when the code merely appears as a token somewhere in the prose\n // (e.g., enum listings or negation: `\"expected one of: invalid_grant, ...\"`\n // or `\"not the invalid_grant code\"`). False positives here are expensive:\n // they wipe perfectly good credentials. Require the textual shape of an\n // actual error assignment: a quoted JSON field, an `error=<code>`\n // form-encoded assignment, or an `error: <code>` colon assignment.\n if (error instanceof Error && typeof error.message === \"string\") {\n return (\n new RegExp(`\"error\"\\\\s*:\\\\s*\"${code}\"`).test(error.message) ||\n new RegExp(`\\\\berror\\\\s*=\\\\s*${code}\\\\b`).test(error.message) ||\n new RegExp(`\\\\berror\\\\s*:\\\\s*${code}\\\\b`).test(error.message)\n );\n }\n\n return false;\n}\n","import {\n exchangeAuthorization,\n extractResourceMetadataUrl,\n refreshAuthorization,\n registerClient,\n startAuthorization,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type {\n OAuthClientInformationFull,\n OAuthClientMetadata,\n OAuthProtectedResourceMetadata,\n OAuthTokens,\n} from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nimport type { OAuthProviderConfig } from \"../config/config.js\";\nimport { config } from \"../config/env.js\";\nimport { logger } from \"../lib/logger.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\nimport { openBrowser } from \"./browser.js\";\nimport { startCallbackServer } from \"./callbackServer.js\";\nimport { AuthServerDiscoveryError, discoverAuthServer } from \"./discovery.js\";\nimport { isInvalidClientError, isInvalidGrantError } from \"./errors.js\";\nimport { AuthEventType } from \"./events.js\";\nimport type { OAuthCredentialStore, StoredCredentials } from \"./OAuthCredentialStore.js\";\nimport { findProviderForUrl } from \"./providers.js\";\n\n// Maps CLIENT_APP_NAME (set by the scanner installer per agent) to the\n// human-friendly client_name we present during Dynamic Client Registration.\n// Deadline for the well-known probes on the refresh path. Matches the probe's\n// per-server budget (`probe/index.ts` DEFAULT_PER_SERVER_TIMEOUT_MS) so both\n// discovery callers give an unresponsive metadata endpoint the same rope.\nconst REFRESH_DISCOVERY_TIMEOUT_MS = 15_000;\n\nconst DCR_CLIENT_NAME_BY_APP: Record<string, string> = {\n \"claude-code\": \"Claude Code (mcp-gateway)\",\n codex: \"Codex\",\n copilot: \"Visual Studio Code\",\n cursor: \"Cursor\",\n opencode: \"OpenCode\",\n};\n\n// Maps CLIENT_APP_NAME to the client_uri we advertise during DCR, for the\n// small set of agents whose upstream allowlist requires it in addition to\n// client_name. VS Code (copilot) is the case that motivated this: Figma's DCR\n// endpoint only accepts the VS Code client when BOTH client_name ===\n// \"Visual Studio Code\" AND client_uri === \"https://code.visualstudio.com\" are\n// present — sending client_name alone is rejected. The value matches what a\n// real VS Code install sends in its own DCR, so we present its genuine\n// identity rather than a decoy. Agents absent here send no client_uri (their\n// DCR payload is unchanged).\nconst DCR_CLIENT_URI_BY_APP: Record<string, string> = {\n copilot: \"https://code.visualstudio.com\",\n};\n\n// Per-(agent, server-host) decoy redirect_uri used at DCR-registration time only.\n//\n// Some auth servers (notably Figma) gate Dynamic Client Registration on\n// `redirect_uris[0].scheme`. They allowlist schemes owned by registered\n// partners (e.g. `cursor://` for Cursor.app's native OAuth) but reject\n// `http://localhost:*` for those same agents. The gateway can't use\n// `cursor://` for its actual callback (the URL would route to Cursor.app via\n// macOS Launch Services, not to our local HTTP listener), so for affected\n// (agent, server) pairs we register BOTH URIs with the decoy at index 0:\n//\n// redirect_uris: [<decoy>, \"http://localhost:<port>/callback\"]\n//\n// Allowlist clause passes because index 0 carries the registered scheme;\n// the actual OAuth flow uses the localhost URI by selecting it at the\n// authorize step via the `redirect_uri` query parameter (RFC 6749 §3.1.2.3:\n// the auth server must honor any registered URI the client requests).\n//\n// The decoy URI is NEVER used to receive a callback; it exists purely to\n// satisfy the registration-time scheme check.\n//\n// Keyed on (agent, server-host) rather than agent alone so the decoy only\n// fires for known-figma-like servers. Some servers (e.g. asana) actively\n// reject unknown `cursor://` URIs with `invalid_redirect_uri` — applying\n// the decoy to them would silently break cursor for that server. Exact-host\n// match keeps the override surgical.\nconst DECOY_REDIRECT_URI_BY_APP_AND_HOST: Record<string, Record<string, string>> = {\n cursor: {\n \"mcp.figma.com\": \"cursor://anysphere.cursor-mcp/oauth/callback\",\n },\n};\n\nexport interface OAuthFlowOptions {\n /**\n * Custom client metadata for DCR registration.\n * If not provided, default metadata will be used.\n */\n clientMetadata?: Partial<OAuthClientMetadata>;\n /**\n * Credential store for persisting OAuth credentials.\n * If provided, credentials will be loaded from and saved to disk.\n */\n credentialStore?: OAuthCredentialStore;\n /**\n * OAuth scope to request.\n */\n scope?: string;\n /**\n * Timeout in milliseconds for the OAuth callback.\n * Default: 5 minutes\n */\n timeoutMs?: number;\n}\n\nexport interface OAuthFlowResult {\n clientInfo: OAuthClientInformationFull;\n /**\n * Whether the result came from stored credentials (true) or a fresh OAuth flow (false).\n */\n fromCache?: boolean;\n tokens: OAuthTokens;\n}\n\n/**\n * Attempts to detect if a server requires OAuth by making a test request.\n * Returns the resource metadata URL from the WWW-Authenticate header if OAuth is required.\n *\n * @param serverUrl The MCP server URL to check\n * @returns The resource metadata URL if OAuth is required, undefined otherwise\n */\nexport async function detectOAuthRequirement(serverUrl: string | URL): Promise<undefined | URL> {\n // Normalize URL to avoid redirect issues on Windows\n const normalizedUrl = normalizeUrl(serverUrl);\n const serverUrlObj = new URL(normalizedUrl);\n\n try {\n const response = await fetch(serverUrlObj, {\n headers: {\n Accept: \"application/json\",\n },\n method: \"POST\",\n });\n\n if (response.status === 401) {\n // Check for resource metadata URL in response\n const resourceMetadataUrl = extractResourceMetadataUrl(response);\n if (resourceMetadataUrl) {\n logger.debug(\"OAuth required, found resource metadata URL\", {\n resourceMetadataUrl: resourceMetadataUrl.toString(),\n });\n return resourceMetadataUrl;\n }\n\n // Check WWW-Authenticate header for Bearer realm\n const wwwAuth = response.headers.get(\"WWW-Authenticate\");\n if (wwwAuth && wwwAuth.toLowerCase().includes(\"bearer\")) {\n logger.debug(\"OAuth required based on WWW-Authenticate header\", { wwwAuth });\n return undefined; // OAuth required but no metadata URL\n }\n }\n\n return undefined;\n } catch (err) {\n logger.debug(\"Error detecting OAuth requirement\", { error: String(err) });\n return undefined;\n }\n}\n\n/**\n * Returns the DCR `client_name` to advertise based on the current\n * `CLIENT_APP_NAME` env var. Exported so the probe (`src/probe/index.ts`)\n * can use the exact same value the runtime OAuth flow would send — this is\n * how allowlist-style DCR endpoints (e.g., Figma) get the same answer from\n * both paths, instead of probe seeing `\"MCP Gateway\"` (rejected) while the\n * runtime flow sees the agent-specific name (accepted).\n */\nexport function getDcrClientName(): string {\n return resolveDcrClientName(config.clientAppName);\n}\n\n/**\n * Returns the DCR `client_uri` to advertise based on the current\n * `CLIENT_APP_NAME` env var, or `undefined` when the current app has none.\n * Exported so the probe (`src/probe/index.ts`) sends the exact same value the\n * runtime OAuth flow would — allowlist-style DCR endpoints (e.g., Figma) gate\n * on client_uri for some clients (VS Code), so probe and runtime must agree.\n */\nexport function getDcrClientUri(): string | undefined {\n return resolveDcrClientUri(config.clientAppName);\n}\n\n/**\n * Returns the `redirect_uris` array to register at DCR.\n *\n * For (agent, server-host) pairs listed in `DECOY_REDIRECT_URI_BY_APP_AND_HOST`,\n * the decoy URI is prepended at index 0 to satisfy allowlist-style DCR\n * endpoints that gate on `redirect_uris[0].scheme`. The real localhost URI\n * is at index 1 and is what the gateway selects at the authorize step via\n * the `redirect_uri` query parameter.\n *\n * For all other pairs the registered array is just `[localRedirectUrl]`,\n * preserving existing behavior.\n *\n * Exported so the probe (`src/probe/index.ts`) can use the exact same shape\n * the runtime OAuth flow would send — without this, the probe and runtime\n * would register different redirect_uris arrays for the same (agent,\n * server), and allowlist-style endpoints could give divergent verdicts.\n */\nexport function getDcrRedirectUris(serverUrl: string | URL, localRedirectUrl: string): string[] {\n return resolveDcrRedirectUris(config.clientAppName, serverUrl, localRedirectUrl);\n}\n\n/**\n * Performs the complete OAuth 2.0 Authorization Code flow with DCR.\n *\n * This function:\n * 1. Checks for existing valid credentials (if credentialStore provided)\n * 2. Attempts token refresh if access token expired (if credentialStore provided)\n * 3. Discovers OAuth protected resource metadata\n * 4. Discovers authorization server metadata\n * 5. Dynamically registers the client (DCR)\n * 6. Opens browser for user authorization\n * 7. Waits for the callback with authorization code\n * 8. Exchanges the code for tokens\n * 9. Persists credentials (if credentialStore provided)\n *\n * @param serverUrl The MCP server URL that requires OAuth\n * @param options Optional configuration\n * @returns The client info and tokens from successful authorization\n */\nexport async function performOAuthFlow(\n serverUrl: string | URL,\n options: OAuthFlowOptions = {}\n): Promise<OAuthFlowResult> {\n const { credentialStore, scope, timeoutMs } = options;\n // Normalize URL to avoid redirect issues on Windows (POST to GET conversion)\n const serverUrlString = normalizeUrl(serverUrl);\n const serverUrlObj = new URL(serverUrlString);\n\n logger.info(\"Starting OAuth flow\", { serverUrl: serverUrlString });\n\n // Step 0: Check for existing valid credentials\n if (credentialStore) {\n const existingCredentials = await tryLoadExistingCredentials(serverUrlObj, credentialStore);\n if (existingCredentials) {\n logger.info(\"Using existing OAuth credentials\", {\n clientId: existingCredentials.clientInfo.client_id,\n serverUrl: serverUrlString,\n });\n return existingCredentials;\n }\n }\n\n // Step 1: Start the callback server to get a dynamic port. Threading\n // serverUrl in lets the callback's error logs carry it for monitoring.\n logger.debug(\"Starting OAuth callback server...\");\n const callbackServer = await startCallbackServer({ serverUrl: serverUrlString, timeoutMs });\n const redirectUrl = callbackServer.callbackUrl;\n\n logger.info(\"OAuth callback server started\", {\n callbackUrl: redirectUrl,\n port: callbackServer.port,\n });\n\n try {\n // Step 2: Check for pre-configured OAuth provider (non-DCR servers)\n const preConfiguredProvider = findProviderForUrl(serverUrlObj);\n\n if (preConfiguredProvider) {\n logger.info(\"Using pre-configured OAuth provider\", {\n providerId: preConfiguredProvider.id,\n providerName: preConfiguredProvider.name,\n });\n\n // Use pre-configured provider flow - pass the callback server instance\n return await performPreConfiguredProviderFlow(\n serverUrlObj,\n preConfiguredProvider,\n callbackServer,\n credentialStore,\n scope\n );\n }\n\n // Steps 3-5: Discover OAuth protected-resource + authorization-server metadata.\n // The helper emits the same debug/info/warn log lines that used to live inline\n // here — passing `logger` preserves the existing observable log output.\n let authMetadata: Awaited<ReturnType<typeof discoverAuthServer>>[\"authMetadata\"];\n let authServerUrl: URL;\n let resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n try {\n ({ authMetadata, authServerUrl, resourceMetadata } = await discoverAuthServer(serverUrlObj, { logger }));\n } catch (err) {\n // AuthServerDiscoveryError carries the chosen authServerUrl. Other\n // errors (rare — only the resource-metadata fetch swallows its own\n // errors) won't have one to attribute, so omit the field.\n logger.error(\"Auth server metadata discovery failed\", {\n error: String(err),\n eventType: AuthEventType.SERVER_METADATA_FAILED,\n serverUrl: serverUrlString,\n ...(err instanceof AuthServerDiscoveryError ? { authServer: err.authServerUrl.toString() } : {}),\n });\n throw err;\n }\n\n if (!authMetadata) {\n logger.error(\"Auth server metadata absent (server may not support OAuth 2.0)\", {\n authServer: authServerUrl.toString(),\n eventType: AuthEventType.SERVER_METADATA_FAILED,\n reason: \"metadata_absent\",\n serverUrl: serverUrlString,\n });\n throw new Error(\n `Could not discover OAuth metadata from ${authServerUrl}. ` +\n `The server may not support OAuth 2.0 or the metadata endpoint is not accessible.`\n );\n }\n\n // Default the requested scope to what the protected resource advertises —\n // but ONLY for resources that advertise `offline_access`.\n //\n // Sending no `scope` at all leaves the grant to whatever the authorization\n // server treats as its default. On a server that grants scopes only when\n // asked, that default is narrower than the resource needs, and the token is\n // then accepted for some operations and rejected for others. Atlassian\n // answers such calls with `Unauthorized; scope does not match`, delivered\n // INSIDE a 200 tool result rather than as a 401 — so neither the refresh\n // path nor the interactive fallback can observe it, and a refresh could not\n // widen the grant anyway (RFC 6749 §6). The initial authorization request is\n // the only place it is fixable.\n //\n // The `offline_access` condition is what keeps this safe to apply broadly.\n // Asking is not free: a server that currently grants MORE than it advertises\n // would be narrowed by an explicit request. That is not hypothetical —\n // groundcover advertises `access:router` alone yet issues\n // `access:router offline_access`, so requesting its advertised list verbatim\n // would cost us the refresh token on a server that works today.\n //\n // `offline_access` is the canonical must-ask scope, so a resource listing it\n // is signalling that it expects clients to ask for what they need. Resources\n // that omit it are, empirically, the ones that grant broadly by default —\n // for those we keep sending nothing and preserve today's behaviour.\n //\n // An explicit `options.scope` still wins; this is only the fallback.\n const advertised = resourceMetadata?.scopes_supported;\n const resourceExpectsScopeRequest = advertised?.includes(\"offline_access\") ?? false;\n const effectiveScope = scope ?? (resourceExpectsScopeRequest ? advertised?.join(\" \") : undefined);\n if (effectiveScope && !scope) {\n logger.info(\"Defaulting OAuth scope to the resource's advertised scopes_supported\", {\n scope: effectiveScope,\n serverUrl: serverUrlString,\n });\n } else if (!scope && advertised?.length) {\n logger.debug(\"Resource advertises scopes but not offline_access; requesting no scope\", {\n advertised,\n serverUrl: serverUrlString,\n });\n }\n\n // Step 6: Check if we have existing client info that can be reused\n let clientInfo: OAuthClientInformationFull;\n const existingCredentials = credentialStore ? await credentialStore.load(serverUrlString) : undefined;\n\n // Check if existing client registration has a matching redirect_uri\n const existingRedirectUris = existingCredentials?.clientInfo?.redirect_uris || [];\n const canReuseClient = existingCredentials?.clientInfo && existingRedirectUris.includes(redirectUrl);\n\n if (canReuseClient && existingCredentials?.clientInfo) {\n // Reuse existing client registration - redirect_uri matches\n logger.info(\"Reusing existing DCR client registration\", {\n clientId: existingCredentials.clientInfo.client_id,\n clientName: existingCredentials.clientInfo.client_name,\n redirectUri: redirectUrl,\n });\n clientInfo = existingCredentials.clientInfo;\n } else {\n // Need to register new client (either no existing client, or redirect_uri doesn't match)\n if (existingCredentials?.clientInfo) {\n logger.info(\"Existing DCR client has different redirect_uri, re-registering\", {\n existingRedirectUris,\n newRedirectUri: redirectUrl,\n });\n }\n\n // Step 6b: Build client metadata for DCR\n // client_uri is only present for agents that need it (see\n // DCR_CLIENT_URI_BY_APP); when absent we omit the field entirely so\n // other agents' DCR payloads are unchanged.\n const clientUri = options.clientMetadata?.client_uri ?? getDcrClientUri();\n const clientMetadata: OAuthClientMetadata = {\n client_name: options.clientMetadata?.client_name || getDcrClientName(),\n ...(clientUri && { client_uri: clientUri }),\n grant_types: options.clientMetadata?.grant_types || [\"authorization_code\", \"refresh_token\"],\n // For most (agent, server) pairs this is just [redirectUrl]. For\n // pairs in DECOY_REDIRECT_URI_BY_APP_AND_HOST (e.g. cursor + figma)\n // the decoy is prepended at index 0 to pass allowlist-style DCR\n // gates. The actual callback still goes to redirectUrl because the\n // authorize step's `redirect_uri` query param selects it.\n redirect_uris: getDcrRedirectUris(serverUrlObj, redirectUrl),\n response_types: options.clientMetadata?.response_types || [\"code\"],\n // Public client - no client secret\n token_endpoint_auth_method: options.clientMetadata?.token_endpoint_auth_method || \"none\",\n ...(effectiveScope && { scope: effectiveScope }),\n };\n\n logger.debug(\"Client metadata for DCR\", { clientMetadata });\n\n // Step 7: Register client via DCR\n if (!authMetadata.registration_endpoint) {\n logger.error(\"DCR registration unsupported by auth server\", {\n authServer: authServerUrl.toString(),\n eventType: AuthEventType.DCR_FAILED,\n reason: \"no_registration_endpoint\",\n serverUrl: serverUrlString,\n });\n throw new Error(\n `Authorization server does not support Dynamic Client Registration. ` +\n `No registration_endpoint found in metadata.`\n );\n }\n\n logger.info(\"Registering client via DCR...\", {\n registrationEndpoint: authMetadata.registration_endpoint,\n });\n\n try {\n clientInfo = await registerClient(authServerUrl, {\n clientMetadata,\n metadata: authMetadata,\n });\n } catch (err) {\n logger.error(\"DCR registration failed\", {\n authServer: authServerUrl.toString(),\n error: String(err),\n eventType: AuthEventType.DCR_FAILED,\n registrationEndpoint: authMetadata.registration_endpoint,\n serverUrl: serverUrlString,\n });\n throw err;\n }\n\n logger.info(\"Client registered successfully via DCR\", {\n clientId: clientInfo.client_id,\n clientName: clientInfo.client_name,\n clientSecretExpiresAt: clientInfo.client_secret_expires_at,\n });\n\n // Persist the client info immediately\n if (credentialStore) {\n await credentialStore.update(serverUrlString, { clientInfo });\n }\n }\n\n // Step 8: RFC 8707 Resource Indicators\n // NOTE: We intentionally do NOT send the resource parameter. Some OAuth servers (e.g., Notion)\n // ignore the client's requested resource and always issue tokens with their configured default\n // audience. When we then present that token to a different path (e.g., /mcp), the resource\n // server rejects it with \"Token audience does not match resource server\".\n // By omitting the resource parameter entirely, we let the OAuth server use its default\n // behavior, which typically issues tokens that work with the resource server.\n // This matches how Linear works (no protected resource metadata = no resource param sent).\n\n // Step 9: Start authorization flow (generates PKCE challenge)\n // Generate state for CSRF protection — some OAuth servers (e.g., Okta) require it\n const state = crypto.randomUUID();\n logger.debug(\"Starting authorization flow...\");\n const { authorizationUrl, codeVerifier } = await startAuthorization(authServerUrl, {\n clientInformation: clientInfo,\n metadata: authMetadata,\n redirectUrl,\n scope: effectiveScope,\n state,\n });\n\n await openBrowser(authorizationUrl);\n\n // Step 11: Wait for callback with authorization code\n logger.debug(\"Waiting for OAuth callback...\");\n const callbackResult = await callbackServer.waitForCallback();\n\n // Verify state matches to prevent CSRF attacks\n if (callbackResult.state !== state) {\n logger.error(\"OAuth state mismatch — possible CSRF attack\", {\n authServer: authServerUrl.toString(),\n eventType: AuthEventType.STATE_MISMATCH,\n serverUrl: serverUrlString,\n });\n throw new Error(\"OAuth state mismatch - possible CSRF attack\");\n }\n\n logger.info(\"Authorization code received\", {\n codePrefix: `${callbackResult.code.substring(0, 10)}...`,\n });\n\n // Step 12: Exchange authorization code for tokens\n logger.debug(\"Exchanging authorization code for tokens...\");\n let tokens;\n try {\n tokens = await exchangeAuthorization(authServerUrl, {\n authorizationCode: callbackResult.code,\n clientInformation: clientInfo,\n codeVerifier,\n metadata: authMetadata,\n redirectUri: redirectUrl,\n });\n } catch (err) {\n logger.error(\"Token exchange failed\", {\n authServer: authServerUrl.toString(),\n clientId: clientInfo.client_id,\n error: String(err),\n eventType: AuthEventType.TOKEN_EXCHANGE_FAILED,\n grantType: \"authorization_code\",\n serverUrl: serverUrlString,\n });\n throw err;\n }\n\n // Log the scope we asked for alongside the scope we were given. An\n // authorization server may grant less than requested, or more (some grant\n // scopes they never advertise), and the difference is otherwise invisible:\n // an under-scoped token still connects, and only fails later on individual\n // tool calls — which is how a scope problem stays undiagnosed for weeks.\n logger.info(\"OAuth tokens obtained successfully\", {\n expiresIn: tokens.expires_in,\n grantedScope: tokens.scope,\n hasRefreshToken: !!tokens.refresh_token,\n requestedScope: effectiveScope,\n tokenType: tokens.token_type,\n });\n\n // Step 13: Persist tokens\n if (credentialStore) {\n const tokensObtainedAt = new Date().toISOString();\n await credentialStore.update(serverUrlString, {\n tokens,\n tokensObtainedAt,\n });\n logger.info(\"OAuth credentials persisted to disk\", { serverUrl: serverUrlString });\n }\n\n return {\n clientInfo,\n fromCache: false,\n tokens,\n };\n } finally {\n // Always close the callback server\n await callbackServer.close();\n }\n}\n\nexport function resolveDcrClientName(rawAppName: null | string | undefined): string {\n const normalized = rawAppName?.trim().toLowerCase();\n if (!normalized) return \"MCP Gateway\";\n // Own-property check so inherited keys (constructor, toString, __proto__, …)\n // resolve to the fallback rather than an Object.prototype member.\n return Object.hasOwn(DCR_CLIENT_NAME_BY_APP, normalized) ? DCR_CLIENT_NAME_BY_APP[normalized] : \"MCP Gateway\";\n}\n\n/**\n * Pure helper backing `getDcrClientUri`. Returns the client_uri for the given\n * app name, or `undefined` when the app has no mapped uri (most agents) — the\n * caller then omits client_uri from the DCR payload entirely. Normalizes the\n * raw name the same way as `resolveDcrClientName` (trim + lowercase).\n */\nexport function resolveDcrClientUri(rawAppName: null | string | undefined): string | undefined {\n const normalized = rawAppName?.trim().toLowerCase();\n if (!normalized) return undefined;\n // Own-property check so inherited keys (constructor, toString, __proto__, …)\n // resolve to undefined rather than an Object.prototype member.\n return Object.hasOwn(DCR_CLIENT_URI_BY_APP, normalized) ? DCR_CLIENT_URI_BY_APP[normalized] : undefined;\n}\n\n/**\n * Pure helper backing `getDcrRedirectUris`. Exported so unit tests can\n * exercise the decoy-lookup matrix without depending on the\n * `config.clientAppName` module singleton.\n */\nexport function resolveDcrRedirectUris(\n rawAppName: null | string | undefined,\n serverUrl: string | URL,\n localRedirectUrl: string,\n): string[] {\n const normalized = rawAppName?.trim().toLowerCase();\n if (!normalized) return [localRedirectUrl];\n const hostMap = DECOY_REDIRECT_URI_BY_APP_AND_HOST[normalized];\n if (!hostMap) return [localRedirectUrl];\n let hostname: string;\n try {\n // Use hostname (not host) so port-bearing URLs like https://mcp.figma.com:443/...\n // still match the bare-hostname keys in DECOY_REDIRECT_URI_BY_APP_AND_HOST.\n // Lowercased because hostnames are case-insensitive (RFC 3986 §3.2.2).\n hostname = (serverUrl instanceof URL ? serverUrl : new URL(serverUrl)).hostname.toLowerCase();\n } catch {\n return [localRedirectUrl];\n }\n const decoy = hostMap[hostname];\n // Decoy MUST be index 0 — allowlist endpoints check redirect_uris[0].scheme.\n return decoy ? [decoy, localRedirectUrl] : [localRedirectUrl];\n}\n\n/**\n * Refreshes the cached OAuth token for a given server URL using its stored\n * refresh_token. Returns the new tokens on success, or undefined if no\n * refresh_token is on disk or the refresh attempt failed.\n *\n * On transient failures (network / 5xx / timeout) the stored refresh_token is\n * preserved so the next attempt can retry. Two RFC 6749 §5.2 codes are fatal\n * instead: `invalid_grant` clears the tokens (the refresh_token itself is\n * dead) and `invalid_client` clears the client registration as well (the\n * client_id is dead, so an authorization redirect would fail too). An error\n * carrying both codes is ambiguous and clears nothing.\n *\n * Use this from request-time error handlers (e.g., on upstream 401) when you\n * want a silent refresh before falling back to the interactive OAuth flow.\n */\nexport async function tryRefreshCachedToken(\n serverUrl: string | URL,\n credentialStore: OAuthCredentialStore\n): Promise<OAuthFlowResult | undefined> {\n const url = typeof serverUrl === \"string\" ? new URL(serverUrl) : serverUrl;\n const credentials = await credentialStore.load(url.toString());\n if (!credentials?.clientInfo || !credentials.tokens?.refresh_token) {\n return undefined;\n }\n return tryRefreshToken(url, credentials, credentialStore);\n}\n\n/**\n * Exchanges an authorization code for tokens using a pre-configured provider.\n * Handles different token endpoint authentication methods.\n */\nasync function exchangeCodeForTokens(\n provider: OAuthProviderConfig,\n code: string,\n redirectUri: string,\n codeVerifier?: string\n): Promise<OAuthTokens> {\n const tokenEndpoint = provider.tokenEndpoint;\n const authMethod = provider.tokenEndpointAuthMethod || (provider.clientSecret ? \"client_secret_post\" : \"none\");\n\n // Build token request body\n const body = new URLSearchParams();\n body.set(\"grant_type\", \"authorization_code\");\n body.set(\"code\", code);\n body.set(\"redirect_uri\", redirectUri);\n\n // Add PKCE verifier if present\n if (codeVerifier) {\n body.set(\"code_verifier\", codeVerifier);\n }\n\n // Build request headers\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n\n // Add client authentication based on method\n if (authMethod === \"client_secret_basic\" && provider.clientSecret) {\n // HTTP Basic authentication\n const credentials = Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString(\"base64\");\n headers.Authorization = `Basic ${credentials}`;\n } else if (authMethod === \"client_secret_post\" && provider.clientSecret) {\n // Client credentials in POST body\n body.set(\"client_id\", provider.clientId);\n body.set(\"client_secret\", provider.clientSecret);\n } else {\n // Public client - just client_id\n body.set(\"client_id\", provider.clientId);\n }\n\n logger.debug(\"Sending token request\", {\n authMethod,\n providerId: provider.id,\n tokenEndpoint,\n });\n\n const response = await fetch(tokenEndpoint, {\n body: body.toString(),\n headers,\n method: \"POST\",\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\"Token exchange failed\", {\n authMethod,\n error: errorText,\n eventType: AuthEventType.TOKEN_EXCHANGE_FAILED,\n grantType: \"authorization_code\",\n httpStatus: response.status,\n providerId: provider.id,\n tokenEndpoint,\n });\n throw new Error(`Token exchange failed: ${response.status} ${errorText}`);\n }\n\n // GitHub returns tokens as application/x-www-form-urlencoded by default\n // unless Accept: application/json is set\n const contentType = response.headers.get(\"content-type\") || \"\";\n let tokenData: Record<string, unknown>;\n\n if (contentType.includes(\"application/json\")) {\n tokenData = (await response.json()) as Record<string, unknown>;\n } else if (contentType.includes(\"application/x-www-form-urlencoded\") || contentType.includes(\"text/plain\")) {\n // Parse URL-encoded response (GitHub's default format)\n const text = await response.text();\n const params = new URLSearchParams(text);\n tokenData = Object.fromEntries(params.entries()) as Record<string, unknown>;\n } else {\n // Try JSON first, fall back to URL-encoded\n const text = await response.text();\n try {\n tokenData = JSON.parse(text) as Record<string, unknown>;\n } catch {\n const params = new URLSearchParams(text);\n tokenData = Object.fromEntries(params.entries()) as Record<string, unknown>;\n }\n }\n\n // Convert expires_in to number if it's a string\n if (typeof tokenData.expires_in === \"string\") {\n tokenData.expires_in = parseInt(tokenData.expires_in, 10);\n }\n\n return {\n access_token: tokenData.access_token as string,\n expires_in: tokenData.expires_in as number | undefined,\n refresh_token: tokenData.refresh_token as string | undefined,\n scope: tokenData.scope as string | undefined,\n token_type: (tokenData.token_type as string) || \"bearer\",\n };\n}\n\n/**\n * Performs OAuth flow using a pre-configured provider (non-DCR).\n * This is used for OAuth servers that don't support Dynamic Client Registration.\n */\nasync function performPreConfiguredProviderFlow(\n serverUrl: URL,\n provider: OAuthProviderConfig,\n callbackServerInstance: {\n callbackUrl: string;\n close: () => Promise<void>;\n waitForCallback: () => Promise<{ code: string; state?: string }>;\n },\n credentialStore: OAuthCredentialStore | undefined,\n scope: string | undefined\n): Promise<OAuthFlowResult> {\n const serverUrlString = serverUrl.toString();\n const redirectUrl = callbackServerInstance.callbackUrl;\n\n // Build client info from pre-configured provider\n const clientInfo: OAuthClientInformationFull = {\n client_id: provider.clientId,\n client_name: provider.name,\n client_secret: provider.clientSecret,\n redirect_uris: [redirectUrl],\n token_endpoint_auth_method:\n provider.tokenEndpointAuthMethod || (provider.clientSecret ? \"client_secret_post\" : \"none\"),\n };\n\n logger.info(\"Using pre-configured OAuth client\", {\n clientId: provider.clientId,\n hasClientSecret: !!provider.clientSecret,\n providerId: provider.id,\n tokenEndpointAuthMethod: clientInfo.token_endpoint_auth_method,\n });\n\n // Determine scope to use\n const effectiveScope = scope || provider.scopes?.join(\" \");\n\n // Generate PKCE challenge if enabled for this provider\n const usePkce = provider.usePkce !== false; // Default to true unless explicitly disabled\n let codeVerifier: string | undefined;\n let codeChallenge: string | undefined;\n let codeChallengeMethod: string | undefined;\n\n if (usePkce) {\n // Generate PKCE code verifier and challenge\n const { generateCodeChallenge, generateCodeVerifier } = await import(\"./pkce.js\");\n codeVerifier = generateCodeVerifier();\n codeChallenge = await generateCodeChallenge(codeVerifier);\n codeChallengeMethod = \"S256\";\n }\n\n // Build authorization URL manually\n const authUrl = new URL(provider.authorizationEndpoint);\n authUrl.searchParams.set(\"client_id\", provider.clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUrl);\n authUrl.searchParams.set(\"response_type\", \"code\");\n\n if (effectiveScope) {\n authUrl.searchParams.set(\"scope\", effectiveScope);\n }\n\n // Add state for security\n const state = crypto.randomUUID();\n authUrl.searchParams.set(\"state\", state);\n\n // Add PKCE challenge if enabled\n if (codeChallenge && codeChallengeMethod) {\n authUrl.searchParams.set(\"code_challenge\", codeChallenge);\n authUrl.searchParams.set(\"code_challenge_method\", codeChallengeMethod);\n }\n\n await openBrowser(authUrl);\n\n // Wait for callback on the existing callback server\n logger.debug(\"Waiting for OAuth callback...\");\n const callbackResult = await callbackServerInstance.waitForCallback();\n\n // Verify state matches\n if (callbackResult.state !== state) {\n logger.error(\"OAuth state mismatch — possible CSRF attack\", {\n eventType: AuthEventType.STATE_MISMATCH,\n providerId: provider.id,\n serverUrl: serverUrlString,\n });\n throw new Error(\"OAuth state mismatch - possible CSRF attack\");\n }\n\n logger.info(\"Authorization code received\", {\n codePrefix: `${callbackResult.code.substring(0, 10)}...`,\n providerId: provider.id,\n });\n\n // Exchange authorization code for tokens\n logger.debug(\"Exchanging authorization code for tokens...\");\n const tokens = await exchangeCodeForTokens(provider, callbackResult.code, redirectUrl, codeVerifier);\n\n logger.info(\"OAuth tokens obtained successfully\", {\n expiresIn: tokens.expires_in,\n hasRefreshToken: !!tokens.refresh_token,\n providerId: provider.id,\n tokenType: tokens.token_type,\n });\n\n // Persist credentials\n if (credentialStore) {\n const tokensObtainedAt = new Date().toISOString();\n await credentialStore.update(serverUrlString, {\n clientInfo,\n tokens,\n tokensObtainedAt,\n });\n logger.info(\"OAuth credentials persisted to disk\", {\n providerId: provider.id,\n serverUrl: serverUrlString,\n });\n }\n\n return {\n clientInfo,\n fromCache: false,\n tokens,\n };\n}\n\n/**\n * Checks for existing valid credentials and returns them if available.\n * Handles token refresh if the access token is expired but refresh token exists.\n */\nasync function tryLoadExistingCredentials(\n serverUrl: URL,\n credentialStore: OAuthCredentialStore\n): Promise<OAuthFlowResult | undefined> {\n const credentials = await credentialStore.load(serverUrl.toString());\n\n if (!credentials) {\n logger.debug(\"No stored credentials found\", { serverUrl: serverUrl.toString() });\n return undefined;\n }\n\n if (!credentials.clientInfo) {\n logger.debug(\"Stored credentials have no client info\", { serverUrl: serverUrl.toString() });\n return undefined;\n }\n\n // Check if we have a valid access token\n if (credentialStore.hasValidAccessToken(credentials) && credentials.tokens) {\n logger.info(\"Using cached OAuth credentials with valid access token\", {\n clientId: credentials.clientInfo.client_id,\n serverUrl: serverUrl.toString(),\n });\n\n return {\n clientInfo: credentials.clientInfo,\n fromCache: true,\n tokens: credentials.tokens,\n };\n }\n\n // Access token is expired or missing, try to refresh\n if (credentialStore.hasRefreshToken(credentials)) {\n const refreshResult = await tryRefreshToken(serverUrl, credentials, credentialStore);\n if (refreshResult) {\n return refreshResult;\n }\n }\n\n logger.debug(\"Stored credentials are expired and no valid refresh token\", {\n serverUrl: serverUrl.toString(),\n });\n\n return undefined;\n}\n\n/**\n * Attempts to refresh an expired access token using the refresh token.\n * Returns new tokens if successful, undefined if refresh fails.\n */\nasync function tryRefreshToken(\n serverUrl: URL,\n credentials: StoredCredentials,\n credentialStore: OAuthCredentialStore\n): Promise<OAuthFlowResult | undefined> {\n if (!credentials.clientInfo || !credentials.tokens?.refresh_token) {\n return undefined;\n }\n\n logger.info(\"Attempting to refresh expired access token\", {\n clientId: credentials.clientInfo.client_id,\n serverUrl: serverUrl.toString(),\n });\n\n try {\n // Discover through the same shared sequence that minted these tokens,\n // including its origin-root fallback: some servers (e.g. Atlassian's\n // remote MCP) publish auth-server metadata ONLY at the root well-known,\n // and without metadata `refreshAuthorization` guesses `<origin>/token` —\n // an endpoint such servers don't serve, so every refresh would fail.\n //\n // Bounded by a deadline: this runs inside the single-flight mid-session\n // 401 handler, where an unbounded well-known probe would hold every\n // request waiting on that refresh for as long as the socket stays open.\n // Bare `fetch()` has no default timeout, so without a signal a black-holed\n // well-known stalls the whole session rather than failing the refresh.\n const discoveryAbort = new AbortController();\n const discoveryDeadline = setTimeout(() => discoveryAbort.abort(), REFRESH_DISCOVERY_TIMEOUT_MS);\n let authMetadata: Awaited<ReturnType<typeof discoverAuthServer>>[\"authMetadata\"];\n let authServerUrl: URL;\n try {\n ({ authMetadata, authServerUrl } = await discoverAuthServer(serverUrl, {\n logger,\n signal: discoveryAbort.signal,\n }));\n } finally {\n clearTimeout(discoveryDeadline);\n }\n // NOTE: We don't send the resource parameter (same reasoning as in performOAuthFlow)\n\n // Attempt token refresh\n const newTokens = await refreshAuthorization(authServerUrl, {\n clientInformation: credentials.clientInfo,\n metadata: authMetadata,\n refreshToken: credentials.tokens.refresh_token,\n });\n\n // `grantedScope` here is the scope carried forward from the original\n // authorization — a refresh cannot widen it (RFC 6749 §6), so a token that\n // is under-scoped stays under-scoped for its whole life. Logging it makes\n // that visible without waiting for a tool call to fail.\n logger.info(\"Successfully refreshed access token\", {\n clientId: credentials.clientInfo.client_id,\n expiresIn: newTokens.expires_in,\n grantedScope: newTokens.scope,\n hasNewRefreshToken: !!newTokens.refresh_token,\n });\n\n // Per RFC 6749 §6, the server MAY omit `refresh_token` in a refresh\n // response (it keeps the old one valid). Naively persisting `newTokens`\n // as-is would replace our existing refresh_token with `undefined`,\n // re-introducing the silent destructive-wipe failure mode that PRDCT-3666\n // is meant to fix. Carry the existing refresh_token forward when the\n // server didn't send a new one.\n const mergedTokens: OAuthTokens = newTokens.refresh_token\n ? newTokens\n : { ...newTokens, refresh_token: credentials.tokens.refresh_token };\n\n // Persist the new tokens\n const tokensObtainedAt = new Date().toISOString();\n await credentialStore.update(serverUrl.toString(), {\n tokens: mergedTokens,\n tokensObtainedAt,\n });\n\n return {\n clientInfo: credentials.clientInfo,\n fromCache: true,\n tokens: mergedTokens,\n };\n } catch (error) {\n // Both classifiers walk every nested wrapper field independently, so an\n // error carrying `invalid_grant` in one field and `invalid_client` in\n // another sets both. That is not a signal we can act on: the destructive\n // step differs between them, and guessing wrong either discards a live\n // client registration or keeps a dead one. Treat ambiguity as transient\n // and retain everything — the conservative default this module is built\n // around.\n const grantMatch = isInvalidGrantError(error);\n const clientMatch = isInvalidClientError(error);\n const ambiguous = grantMatch && clientMatch;\n const invalidGrant = grantMatch && !ambiguous;\n const invalidClient = clientMatch && !ambiguous;\n logger.warn(\n ambiguous\n ? \"Refresh failed with both invalid_client and invalid_grant; ambiguous, retaining credentials\"\n : invalidClient\n ? \"Client registration rejected by OAuth server (invalid_client); clearing stored client and tokens\"\n : invalidGrant\n ? \"Refresh token rejected by OAuth server (invalid_grant); clearing stored tokens\"\n : \"Token refresh failed transiently; retaining refresh_token for the next attempt\",\n {\n ambiguous,\n clientId: credentials.clientInfo.client_id,\n error: String(error),\n eventType: AuthEventType.REFRESH_FAILED,\n invalidClient,\n invalidGrant,\n serverUrl: serverUrl.toString(),\n }\n );\n\n // Per RFC 6749 §5.2, only two errors mean the stored credentials\n // themselves are dead. invalid_grant: the refresh_token is dead — clear\n // tokens but keep the client registration, which is still valid.\n // invalid_client: the registered client is dead (purged/expired at the\n // auth server) — an authorization redirect with that client_id would fail\n // too, so clear the whole record and let the next interactive flow\n // re-register via DCR. For any other error (network failure, 5xx, timeout,\n // rate limit), keep the refresh_token so a future attempt can use it —\n // wiping it on transient failures is what historically forced users into\n // browser re-auth.\n if (invalidClient) {\n await credentialStore.update(serverUrl.toString(), {\n clientInfo: undefined,\n tokens: undefined,\n tokensObtainedAt: undefined,\n });\n } else if (invalidGrant) {\n await credentialStore.update(serverUrl.toString(), {\n tokens: undefined,\n tokensObtainedAt: undefined,\n });\n }\n\n return undefined;\n }\n}\n","/**\n * MCP Transport Utilities\n *\n * Simple transport creation for remote MCP servers.\n * Transport type is determined by trying Streamable HTTP first,\n * falling back to SSE if the endpoint doesn't support it.\n */\n\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { SSEClientTransport } from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { FetchLike } from \"@modelcontextprotocol/sdk/shared/transport.js\";\n\nimport { logger } from \"./logger.js\";\n\nexport type TransportType = \"sse\" | \"streamable-http\";\n\n/** Order in which transports are tried (per MCP spec: Streamable HTTP first, SSE fallback) */\nconst TRANSPORT_PRIORITY: TransportType[] = [\"streamable-http\", \"sse\"];\n\n/** Result of a successful transport connection */\nexport interface TransportConnectionResult {\n /** The connected client instance */\n client: Client;\n /** The transport type that successfully connected */\n transportType: TransportType;\n}\n\n/**\n * Connects to a remote MCP server, trying transports in order.\n * Per MCP spec: tries Streamable HTTP first, falls back to SSE if not supported (404/405).\n *\n * Creates a fresh client for each transport attempt to avoid state issues.\n * The MCP SDK Client maintains internal connection state that does not properly\n * reset after a failed connect attempt, so reusing the same client instance\n * across transport fallbacks can cause \"already connected\" or similar errors.\n *\n * @param createClient - Factory function to create a new Client instance\n * @param url - The URL of the remote MCP server\n * @param requestInit - Optional request init for the transport (headers, etc.)\n * @returns The connected client and transport type\n * @throws The error from the last transport attempt if none succeed,\n * or immediately if an error is not a \"transport not supported\" error\n */\nexport async function connectWithTransportFallback(\n createClient: () => Client,\n url: string,\n requestInit?: RequestInit,\n customFetch?: FetchLike\n): Promise<TransportConnectionResult> {\n const { result: client, transportType } = await walkTransports(async (transportType) => {\n // Create a fresh client for each transport attempt to avoid state issues\n const client = createClient();\n const transport = createTransport(url, transportType, requestInit, customFetch);\n await client.connect(transport);\n return client;\n });\n return { client, transportType };\n}\n\n/**\n * Creates a transport instance for the given type.\n *\n * Pass `customFetch` to intercept every HTTP request the transport makes — the\n * gateway uses this to inject a dynamic Authorization header and recover from\n * mid-session 401s via silent refresh (PRDCT-3666 Tier 2).\n *\n * Auth-refresh coverage matrix:\n *\n * Streamable HTTP — every JSON-RPC message is a separate POST, all of which\n * go through `customFetch`. A 401 on any request triggers the wrapper's\n * silent-refresh-and-retry. Fully covered.\n *\n * SSE — has two channels:\n * 1. The long-lived `GET /mcp` event-stream. The SDK passes our\n * `customFetch` to the `eventsource` (v4+) polyfill via its `fetch`\n * option, so our wrapper attaches a fresh Authorization header on the\n * initial GET AND on every auto-reconnect after a disconnect.\n * 2. The short `POST /messages/<sessionId>` client→server messages — also\n * routed through `customFetch`.\n * Both channels are covered for the cases where the server closes the\n * stream on token expiry (EventSource auto-reconnects → new fetch call →\n * fresh bearer attached).\n *\n * ====================================================================\n * NOT SUPPORTED — known gap, intentional:\n *\n * SSE \"silent mid-stream drop\" case\n * If the bearer expires while the long-lived GET stream is OPEN and the\n * server chooses to keep the connection open but stop emitting events\n * (or only 401 *new* requests on a different channel), the gateway has no\n * way to detect that the stream has gone stale — EventSource doesn't fire\n * `onerror` because the connection is still alive at the TCP level.\n *\n * Recovering from this would need either:\n * (a) a proactive timer that fires at ~90% of `expires_in`, refreshes\n * the bearer, and tears down + reopens the EventSource before TTL;\n * (b) a heartbeat / liveness probe that detects extended silence and\n * triggers a reconnect.\n *\n * Neither is implemented. Impact today is zero — every OAuth-wrapped MCP\n * server in scope (Notion, Lucid, ClickUp, GitHub Copilot, Sentry, Linear,\n * Asana V2, Atlassian post-2026-06-30) uses Streamable HTTP. The only\n * surviving major SSE+OAuth server is Atlassian, sunsetting on\n * 2026-06-30. None of the surviving SSE+OAuth servers have been observed\n * to use the long-lived GET stream for server-push features (notifications,\n * sampling requests, streaming partial tool results) that would expose\n * this gap — they use it as a generic request/response substrate, in\n * which case any silent-drop manifests as the next POST returning 401,\n * which IS handled.\n * ====================================================================\n */\nexport function createTransport(\n url: string,\n type: TransportType,\n requestInit?: RequestInit,\n customFetch?: FetchLike\n): SSEClientTransport | StreamableHTTPClientTransport {\n const parsedUrl = new URL(url);\n\n if (type === \"streamable-http\") {\n return new StreamableHTTPClientTransport(parsedUrl, { fetch: customFetch, requestInit });\n }\n\n return new SSEClientTransport(parsedUrl, { fetch: customFetch, requestInit });\n}\n\n/**\n * Checks if an error indicates the transport/endpoint is not supported (404/405).\n * This means we should try a different transport type.\n */\nexport function isTransportNotSupportedError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n\n const status = (err as { status?: number }).status;\n const code = (err as { code?: number }).code;\n const message = (err as { message?: string }).message;\n\n // Check status codes that indicate endpoint doesn't support this transport\n if (status === 404 || status === 405 || code === 404 || code === 405) {\n return true;\n }\n\n // Check error message for these status codes\n if (message && /404|not found|405|method not allowed/i.test(String(message))) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Walks transports in priority order, calling `attempt` for each. Returns the\n * first successful result. Continues to the next transport on\n * `isTransportNotSupportedError` (404/405); propagates any other error.\n *\n * Shared primitive for both connect-style attempts (the runtime proxy via\n * `connectWithTransportFallback`) and probe-style attempts (the install-time\n * scan in `src/probe`). Callers supply the per-transport request shape; this\n * helper owns only the priority-order + fallback-on-404/405 logic.\n *\n * When every transport fails with a not-supported error, throws an aggregate\n * error whose message lists each transport's status — so callers can tell\n * \"URL is dead\" from \"URL only speaks one transport\". The original errors are\n * available via `error.cause` (an array of `{ transport, err }`).\n *\n * @param attempt - Per-transport function. Throw an error matching\n * `isTransportNotSupportedError` to trigger fallback; throw anything else\n * (or any one transport's non-fallback error) to propagate.\n * @returns The first transport that succeeded and its result.\n * @throws The propagated non-fallback error, or an aggregate of every\n * transport's not-supported error when all transports fail.\n */\nexport async function walkTransports<T>(\n attempt: (transportType: TransportType) => Promise<T>\n): Promise<{ result: T; transportType: TransportType }> {\n const failures: Array<{ err: unknown; transport: TransportType }> = [];\n\n for (const transportType of TRANSPORT_PRIORITY) {\n try {\n const result = await attempt(transportType);\n return { result, transportType };\n } catch (err) {\n if (isTransportNotSupportedError(err)) {\n logger.info(`${transportType} not supported, trying next transport...`);\n failures.push({ err, transport: transportType });\n continue;\n }\n\n // Non-transport error (e.g., 401, network error) - propagate immediately\n throw err;\n }\n }\n\n // All transports failed with \"not supported\" errors. Build a message that\n // lists every transport's status (or message), so callers don't see only\n // the last transport's failure and assume the others were never tried.\n const summary = failures\n .map(({ err, transport }) => {\n const status = (err as { status?: number } | null)?.status;\n if (status !== undefined) return `${transport}: ${status}`;\n const message = err instanceof Error ? err.message : String(err);\n return `${transport}: ${message}`;\n })\n .join(\", \");\n const aggregate =\n failures.length > 0\n ? new Error(`all transports returned not-supported (${summary})`)\n : new Error(\"No compatible transport found\");\n // Preserve original errors for callers that need to introspect them.\n (aggregate as { cause?: unknown } & Error).cause = failures;\n throw aggregate;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onyxsecurity/mcp-gateway",
3
- "version": "2.1.21",
3
+ "version": "2.1.22",
4
4
  "packageManager": "bun@1.3.14",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {