mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(mcp): record why a client fails to resolve at /authorize
CIMD client resolution swallowed every fetch failure as ssrf_blocked and /authorize logged a bare invalid event, so an unreachable metadata host was indistinguishable from a genuine SSRF block and the consent flow returned invalid_client with no diagnosable cause. Split the resolution failure into ssrf_blocked (a real private-host refusal) and fetch_failed (timeout / connect error), carrying the concrete SafeFetchError kind and cause as a short non-secret detail. Thread the resolution reason and detail into the authorize.invalid annotation behind a pinned meta shape, and cache a resolved CIMD document for up to 24h so the consent flow does not re-fetch on every hit.
This commit is contained in:
1 parent
324df70071
commit
3c6cb8b08d
3 files changed
+228
-24
No files matched your search
@@ -118,16 +118,42 @@ interface ValidatedRequest {
|
||||
resource: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The gate a request failed, pinned to a stable enum so the
|
||||
* `mcp.oauth.authorize.invalid` annotation stays dashboard-friendly. The
|
||||
* swallowed-reason gap (a bare `invalid` event) is exactly why the prod CIMD
|
||||
* failure was undiagnosable.
|
||||
*/
|
||||
type ValidateFailure =
|
||||
| "invalid_params"
|
||||
| "client_unresolved"
|
||||
| "redirect_mismatch"
|
||||
| "bad_response_type"
|
||||
| "pkce_required"
|
||||
| "audience_mismatch";
|
||||
|
||||
type ValidateMeta = {
|
||||
reason: ValidateFailure;
|
||||
/** For `client_unresolved`: the `resolveClient` reason. */
|
||||
clientReason?: string;
|
||||
/** For `client_unresolved`: a short, non-secret transport diagnostic. */
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
/** Shared validation for GET + POST. Returns a ready error Response on failure. */
|
||||
async function validate(
|
||||
params: Record<string, string | undefined>,
|
||||
requestUrl: string,
|
||||
): Promise<{ ok: true; v: ValidatedRequest } | { ok: false; res: Response }> {
|
||||
): Promise<
|
||||
| { ok: true; v: ValidatedRequest }
|
||||
| { ok: false; res: Response; meta: ValidateMeta }
|
||||
> {
|
||||
const parsed = z.object(baseParams).safeParse(params);
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
ok: false,
|
||||
res: badRequest("invalid_request", "Missing or malformed parameters"),
|
||||
meta: { reason: "invalid_params" },
|
||||
};
|
||||
}
|
||||
const p = parsed.data;
|
||||
@@ -139,6 +165,11 @@ async function validate(
|
||||
return {
|
||||
ok: false,
|
||||
res: badRequest("invalid_client", "Unknown or unresolvable client"),
|
||||
meta: {
|
||||
reason: "client_unresolved",
|
||||
clientReason: resolved.reason,
|
||||
...(resolved.detail ? { detail: resolved.detail } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +181,7 @@ async function validate(
|
||||
"invalid_request",
|
||||
"redirect_uri does not match the registered client",
|
||||
),
|
||||
meta: { reason: "redirect_mismatch" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,6 +193,7 @@ async function validate(
|
||||
"unsupported_response_type",
|
||||
"Only response_type=code is supported",
|
||||
),
|
||||
meta: { reason: "bad_response_type" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,6 +208,7 @@ async function validate(
|
||||
"invalid_request",
|
||||
"PKCE with code_challenge_method=S256 is required",
|
||||
),
|
||||
meta: { reason: "pkce_required" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -186,6 +220,7 @@ async function validate(
|
||||
"invalid_target",
|
||||
"resource must equal this server's MCP endpoint",
|
||||
),
|
||||
meta: { reason: "audience_mismatch" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,7 +315,10 @@ export async function GET(request: NextRequest): Promise<Response> {
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const result = await validate(params, request.url);
|
||||
if (!result.ok) {
|
||||
annotate({ action: { name: "mcp.oauth.authorize.invalid" } });
|
||||
annotate({
|
||||
action: { name: "mcp.oauth.authorize.invalid" },
|
||||
meta: result.meta,
|
||||
});
|
||||
return result.res;
|
||||
}
|
||||
const { v } = result;
|
||||
@@ -409,7 +447,10 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
|
||||
const result = await validate(params, request.url);
|
||||
if (!result.ok) {
|
||||
annotate({ action: { name: "mcp.oauth.authorize.invalid" } });
|
||||
annotate({
|
||||
action: { name: "mcp.oauth.authorize.invalid" },
|
||||
meta: result.meta,
|
||||
});
|
||||
return result.res;
|
||||
}
|
||||
const { v } = result;
|
||||
|
||||
Reference in new issue
Block a user