Reduce remaining file, LSP, and XAA debug detail

This commit is contained in:
2026-04-04 10:31:29 +08:00
parent 010ded8476
commit 4d506aabf7
6 changed files with 211 additions and 90 deletions

View File

@@ -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`)