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
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:
parent
ec185f1053
commit
a71e6d2012
1 changed files with 1513 additions and 852 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue