Reduce remaining file, LSP, and XAA debug detail
This commit is contained in:
@@ -96,6 +96,24 @@ function redactTokens(raw: unknown): string {
|
||||
return s.replace(SENSITIVE_TOKEN_RE, (_, k) => `"${k}":"[REDACTED]"`)
|
||||
}
|
||||
|
||||
function summarizeXaaPayload(raw: unknown): string {
|
||||
if (typeof raw === 'string') {
|
||||
return `text(${raw.length} chars)`
|
||||
}
|
||||
if (Array.isArray(raw)) {
|
||||
return `array(${raw.length})`
|
||||
}
|
||||
if (raw && typeof raw === 'object') {
|
||||
return jsonStringify({
|
||||
payloadType: 'object',
|
||||
keys: Object.keys(raw as Record<string, unknown>)
|
||||
.sort()
|
||||
.slice(0, 10),
|
||||
})
|
||||
}
|
||||
return typeof raw
|
||||
}
|
||||
|
||||
// ─── Zod Schemas ────────────────────────────────────────────────────────────
|
||||
|
||||
const TokenExchangeResponseSchema = lazySchema(() =>
|
||||
@@ -145,7 +163,7 @@ export async function discoverProtectedResource(
|
||||
)
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`XAA: PRM discovery failed: ${e instanceof Error ? e.message : String(e)}`,
|
||||
`XAA: PRM discovery failed (${e instanceof Error ? e.name : typeof e})`,
|
||||
)
|
||||
}
|
||||
if (!prm.resource || !prm.authorization_servers?.[0]) {
|
||||
@@ -154,9 +172,7 @@ export async function discoverProtectedResource(
|
||||
)
|
||||
}
|
||||
if (normalizeUrl(prm.resource) !== normalizeUrl(serverUrl)) {
|
||||
throw new Error(
|
||||
`XAA: PRM discovery failed: PRM resource mismatch: expected ${serverUrl}, got ${prm.resource}`,
|
||||
)
|
||||
throw new Error('XAA: PRM discovery failed: PRM resource mismatch')
|
||||
}
|
||||
return {
|
||||
resource: prm.resource,
|
||||
@@ -183,22 +199,16 @@ export async function discoverAuthorizationServer(
|
||||
fetchFn: opts?.fetchFn ?? defaultFetch,
|
||||
})
|
||||
if (!meta?.issuer || !meta.token_endpoint) {
|
||||
throw new Error(
|
||||
`XAA: AS metadata discovery failed: no valid metadata at ${asUrl}`,
|
||||
)
|
||||
throw new Error('XAA: AS metadata discovery failed: no valid metadata')
|
||||
}
|
||||
if (normalizeUrl(meta.issuer) !== normalizeUrl(asUrl)) {
|
||||
throw new Error(
|
||||
`XAA: AS metadata discovery failed: issuer mismatch: expected ${asUrl}, got ${meta.issuer}`,
|
||||
)
|
||||
throw new Error('XAA: AS metadata discovery failed: issuer mismatch')
|
||||
}
|
||||
// RFC 8414 §3.3 / RFC 9728 §3 require HTTPS. A PRM-advertised http:// AS
|
||||
// that self-consistently reports an http:// issuer would pass the mismatch
|
||||
// check above, then we'd POST id_token + client_secret over plaintext.
|
||||
if (new URL(meta.token_endpoint).protocol !== 'https:') {
|
||||
throw new Error(
|
||||
`XAA: refusing non-HTTPS token endpoint: ${meta.token_endpoint}`,
|
||||
)
|
||||
throw new Error('XAA: refusing non-HTTPS token endpoint')
|
||||
}
|
||||
return {
|
||||
issuer: meta.issuer,
|
||||
@@ -263,7 +273,7 @@ export async function requestJwtAuthorizationGrant(opts: {
|
||||
body: params,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = redactTokens(await res.text()).slice(0, 200)
|
||||
const body = summarizeXaaPayload(redactTokens(await res.text()))
|
||||
// 4xx → id_token rejected (invalid_grant etc.), clear cache.
|
||||
// 5xx → IdP outage, id_token may still be valid, preserve it.
|
||||
const shouldClear = res.status < 500
|
||||
@@ -278,21 +288,25 @@ export async function requestJwtAuthorizationGrant(opts: {
|
||||
} catch {
|
||||
// Transient network condition (captive portal, proxy) — don't clear id_token.
|
||||
throw new XaaTokenExchangeError(
|
||||
`XAA: token exchange returned non-JSON (captive portal?) at ${opts.tokenEndpoint}`,
|
||||
'XAA: token exchange returned non-JSON response (captive portal?)',
|
||||
false,
|
||||
)
|
||||
}
|
||||
const exchangeParsed = TokenExchangeResponseSchema().safeParse(rawExchange)
|
||||
if (!exchangeParsed.success) {
|
||||
throw new XaaTokenExchangeError(
|
||||
`XAA: token exchange response did not match expected shape: ${redactTokens(rawExchange)}`,
|
||||
`XAA: token exchange response did not match expected shape: ${summarizeXaaPayload(
|
||||
redactTokens(rawExchange),
|
||||
)}`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
const result = exchangeParsed.data
|
||||
if (!result.access_token) {
|
||||
throw new XaaTokenExchangeError(
|
||||
`XAA: token exchange response missing access_token: ${redactTokens(result)}`,
|
||||
`XAA: token exchange response missing access_token: ${summarizeXaaPayload(
|
||||
redactTokens(result),
|
||||
)}`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
@@ -373,7 +387,7 @@ export async function exchangeJwtAuthGrant(opts: {
|
||||
body: params,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = redactTokens(await res.text()).slice(0, 200)
|
||||
const body = summarizeXaaPayload(redactTokens(await res.text()))
|
||||
throw new Error(`XAA: jwt-bearer grant failed: HTTP ${res.status}: ${body}`)
|
||||
}
|
||||
let rawTokens: unknown
|
||||
@@ -381,13 +395,15 @@ export async function exchangeJwtAuthGrant(opts: {
|
||||
rawTokens = await res.json()
|
||||
} catch {
|
||||
throw new Error(
|
||||
`XAA: jwt-bearer grant returned non-JSON (captive portal?) at ${opts.tokenEndpoint}`,
|
||||
'XAA: jwt-bearer grant returned non-JSON response (captive portal?)',
|
||||
)
|
||||
}
|
||||
const tokensParsed = JwtBearerResponseSchema().safeParse(rawTokens)
|
||||
if (!tokensParsed.success) {
|
||||
throw new Error(
|
||||
`XAA: jwt-bearer response did not match expected shape: ${redactTokens(rawTokens)}`,
|
||||
`XAA: jwt-bearer response did not match expected shape: ${summarizeXaaPayload(
|
||||
redactTokens(rawTokens),
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
return tokensParsed.data
|
||||
@@ -431,11 +447,14 @@ export async function performCrossAppAccess(
|
||||
): Promise<XaaResult> {
|
||||
const fetchFn = makeXaaFetch(abortSignal)
|
||||
|
||||
logMCPDebug(serverName, `XAA: discovering PRM for ${serverUrl}`)
|
||||
logMCPDebug(serverName, 'XAA: discovering protected resource metadata')
|
||||
const prm = await discoverProtectedResource(serverUrl, { fetchFn })
|
||||
logMCPDebug(
|
||||
serverName,
|
||||
`XAA: discovered resource=${prm.resource} ASes=[${prm.authorization_servers.join(', ')}]`,
|
||||
`XAA: discovered protected resource metadata ${jsonStringify({
|
||||
hasResource: Boolean(prm.resource),
|
||||
authorizationServerCount: prm.authorization_servers.length,
|
||||
})}`,
|
||||
)
|
||||
|
||||
// Try each advertised AS in order. grant_types_supported is OPTIONAL per
|
||||
@@ -449,16 +468,16 @@ export async function performCrossAppAccess(
|
||||
candidate = await discoverAuthorizationServer(asUrl, { fetchFn })
|
||||
} catch (e) {
|
||||
if (abortSignal?.aborted) throw e
|
||||
asErrors.push(`${asUrl}: ${e instanceof Error ? e.message : String(e)}`)
|
||||
asErrors.push(
|
||||
`authorization server discovery failed (${e instanceof Error ? e.name : typeof e})`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (
|
||||
candidate.grant_types_supported &&
|
||||
!candidate.grant_types_supported.includes(JWT_BEARER_GRANT)
|
||||
) {
|
||||
asErrors.push(
|
||||
`${asUrl}: does not advertise jwt-bearer grant (supported: ${candidate.grant_types_supported.join(', ')})`,
|
||||
)
|
||||
asErrors.push('authorization server does not advertise jwt-bearer grant')
|
||||
continue
|
||||
}
|
||||
asMeta = candidate
|
||||
@@ -466,7 +485,7 @@ export async function performCrossAppAccess(
|
||||
}
|
||||
if (!asMeta) {
|
||||
throw new Error(
|
||||
`XAA: no authorization server supports jwt-bearer. Tried: ${asErrors.join('; ')}`,
|
||||
`XAA: no authorization server supports jwt-bearer (${asErrors.length} candidates tried)`,
|
||||
)
|
||||
}
|
||||
// Pick auth method from what the AS advertises. We handle
|
||||
@@ -481,7 +500,7 @@ export async function performCrossAppAccess(
|
||||
: 'client_secret_basic'
|
||||
logMCPDebug(
|
||||
serverName,
|
||||
`XAA: AS issuer=${asMeta.issuer} token_endpoint=${asMeta.token_endpoint} auth_method=${authMethod}`,
|
||||
`XAA: selected authorization server (auth_method=${authMethod})`,
|
||||
)
|
||||
|
||||
logMCPDebug(serverName, `XAA: exchanging id_token for ID-JAG at IdP`)
|
||||
|
||||
Reference in New Issue
Block a user