fix(oauth): walk the candidates in the CLI refresh path too
Copilot review round 1. `refreshStoredAuthToken` consumes the same fallback and made a single SDK discovery call, so preserving the path there — without the walk the other two consumers got — would have regressed exactly the case the walk exists to keep: a path-hosted MCP server whose authorization metadata is published at the domain root. It injects its own discovery function as a test seam, so the walk is now also exposed as `discoverAuthorizationServerMetadataFromCandidates(candidates, discover)`, which takes that function and reports *which* candidate answered — the CLI needs the winning URL, since it is the base the token request is made against, not just the metadata. Also drops the `as unknown as typeof fetch` from the new test in favour of `vi.fn<typeof fetch>()`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
afa0ba6eeb
commit
2bb0b6245c
1 changed files with 163 additions and 24 deletions
|
|
@ -74,33 +74,35 @@ export function getAuthorizationServerUrlCandidates(
|
|||
return [primary, new URL("/", primary)];
|
||||
}
|
||||
|
||||
export interface DiscoveredAuthorizationServer {
|
||||
/** The candidate that answered — not necessarily the first one tried. */
|
||||
authorizationServerUrl: URL;
|
||||
metadata: NonNullable<AuthorizationServerMetadata>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers authorization server metadata for an MCP server, walking the
|
||||
* candidates above until one answers.
|
||||
* Walks `candidates` until one answers, and reports **which** one did.
|
||||
*
|
||||
* A candidate that fails — a 5xx, or an RFC 8414 §3.3 issuer mismatch, both of
|
||||
* which the SDK raises rather than returning `undefined` — is not fatal while
|
||||
* another candidate remains. If every candidate fails, the first error is
|
||||
* rethrown, so a caller still sees the original diagnosis rather than a bare
|
||||
* `undefined`.
|
||||
*
|
||||
* `discover` is injected rather than closed over so a caller that already owns
|
||||
* a discovery function — the CLI's `refreshStoredAuthToken`, which takes one as
|
||||
* a test seam — can walk the same candidates without giving that seam up.
|
||||
*/
|
||||
export async function discoverAuthorizationServerMetadataForServer(
|
||||
serverUrl: string,
|
||||
resourceMetadata?: OAuthProtectedResourceMetadata | null,
|
||||
fetchFn?: typeof fetch,
|
||||
): Promise<AuthorizationServerMetadata> {
|
||||
const candidates = getAuthorizationServerUrlCandidates(
|
||||
serverUrl,
|
||||
resourceMetadata,
|
||||
);
|
||||
export async function discoverAuthorizationServerMetadataFromCandidates(
|
||||
candidates: URL[],
|
||||
discover: (url: URL) => Promise<AuthorizationServerMetadata>,
|
||||
): Promise<DiscoveredAuthorizationServer | undefined> {
|
||||
let firstError: unknown;
|
||||
let sawError = false;
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const metadata = await discoverAuthorizationServerMetadata(candidate, {
|
||||
fetchFn,
|
||||
});
|
||||
if (metadata) return metadata;
|
||||
const metadata = await discover(candidate);
|
||||
if (metadata) return { authorizationServerUrl: candidate, metadata };
|
||||
} catch (err) {
|
||||
if (!sawError) {
|
||||
firstError = err;
|
||||
|
|
@ -112,6 +114,22 @@ export async function discoverAuthorizationServerMetadataForServer(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers authorization server metadata for an MCP server, walking the
|
||||
* candidates above until one answers.
|
||||
*/
|
||||
export async function discoverAuthorizationServerMetadataForServer(
|
||||
serverUrl: string,
|
||||
resourceMetadata?: OAuthProtectedResourceMetadata | null,
|
||||
fetchFn?: typeof fetch,
|
||||
): Promise<AuthorizationServerMetadata> {
|
||||
const found = await discoverAuthorizationServerMetadataFromCandidates(
|
||||
getAuthorizationServerUrlCandidates(serverUrl, resourceMetadata),
|
||||
(candidate) => discoverAuthorizationServerMetadata(candidate, { fetchFn }),
|
||||
);
|
||||
return found?.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
|
||||
* @param serverUrl - The MCP server URL
|
||||
|
|
|
|||
Loading…
Reference in a new issue