Auth hardening CI gaps + SDK consolidation: EMA leg 2 / scopes (#1527) (#1725)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled

Close #1527 acceptance gaps with automated tests (SEP-2468 iss, SEP-2207 offline_access, SEP-2351 discovery shapes, SEP-2352 AS-migration, EraNegotiationFailed→401 unwrap); lean EMA leg 2 and scope helpers on the v2 SDK; drop deprecated TS baseUrl. Reviewed LGTM.
This commit is contained in:
Bob Dickinson 2026-07-19 19:36:21 -07:00 • committed by GitHub
commit a71e6d2012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,3 +1,4 @@
import { UnauthorizedError } from "@modelcontextprotocol/client";
import type { CallbackParams } from "./types.js";
import { ZodError } from "zod";
@ -172,13 +173,50 @@ export const generateOAuthErrorDescription = (
* transport preserves the status on the error object; as a fallback, match
* transport wording `"failed …(401)"` so unrelated `(401)` in messages does
* not trigger OAuth.
*
* Under `protocolEra: auto|modern`, a negotiation-probe 401 can surface as
* `SdkError(EraNegotiationFailed)` with the real {@link UnauthorizedError} at
* `error.data.cause` (and sometimes native `error.cause`). Walk that chain so
* OAuth recovery still starts.
*/
export function isUnauthorizedError(err: unknown): boolean {
if (typeof err === "object" && err !== null) {
const status = (err as { status?: number; code?: number }).status;
const code = (err as { code?: number }).code;
if (status === 401 || code === 401) return true;
}
const message = err instanceof Error ? err.message : String(err);
return /\bfailed\b[^\n]*\(401\)/i.test(message);
return isUnauthorizedErrorDeep(err, new Set());
}
function isUnauthorizedErrorDeep(err: unknown, seen: Set<unknown>): boolean {
if (err == null) return false;
if (typeof err !== "object") {
return /\bfailed\b[^\n]*\(401\)/i.test(String(err));
}
if (seen.has(err)) return false;
seen.add(err);
if (UnauthorizedError.isInstance(err)) return true;
const status = (err as { status?: number }).status;
const code = (err as { code?: unknown }).code;
if (status === 401 || code === 401) return true;
if (err instanceof Error && /\bfailed\b[^\n]*\(401\)/i.test(err.message)) {
return true;
}
if (
"cause" in err &&
isUnauthorizedErrorDeep((err as { cause: unknown }).cause, seen)
) {
return true;
}
const data = (err as { data?: unknown }).data;
if (
data !== null &&
typeof data === "object" &&
"cause" in data &&
isUnauthorizedErrorDeep((data as { cause: unknown }).cause, seen)
) {
return true;
}
return false;
}