///|
/// refresh_flow.mbt — RFC 6749 §6 refresh token grant.
///
/// MCP server credentials refresh at the authorization server's token
/// endpoint; the wire contract mirrors the device-flow token poll, but a
/// server that does not rotate refresh tokens keeps the caller's token.
///|
pub async fn run_refresh_flow(
token_endpoint~ : String,
client_id~ : String,
refresh_token~ : String,
transport~ : &OAuthHttpTransport,
) -> Credential raise OAuthError {
let headers : Map[String, String] = Map([
("Content-Type", "application/x-www-form-urlencoded"),
("Accept", "application/json"),
])
let body = "grant_type=" +
oauth_form_url_encode("refresh_token") +
"&refresh_token=" +
oauth_form_url_encode(refresh_token) +
"&client_id=" +
oauth_form_url_encode(client_id)
let response = transport.post(OAuthHttpRequest::{
url: token_endpoint,
body,
headers,
})
// Server failures are terminal. Do not parse an arbitrary HTML/proxy body
// as OAuth JSON; preserve only safe diagnostics.
if response.status >= 500 {
raise OAuthError::HttpError(
"token HTTP status=" +
response.status.to_string() +
", body_chars=" +
response.body.length().to_string(),
)
}
let json = @json.parse(response.body) catch {
err =>
raise OAuthError::ParseError("refresh token response: " + err.to_string())
}
match json {
Json::Object(map) =>
match map.get("error") {
Some(Json::String(err_code)) =>
match err_code {
"invalid_grant" => raise InvalidGrant
"access_denied" => raise AccessDenied
"expired_token" => raise ExpiredToken
other =>
raise OAuthError::HttpError(
"token error=" +
other +
", status=" +
response.status.to_string(),
)
}
_ => {
if response.status < 200 || response.status >= 300 {
raise OAuthError::HttpError(
"token HTTP status=" + response.status.to_string(),
)
}
parse_refresh_success(map, refresh_token)
}
}
_ => raise OAuthError::ParseError("refresh token response is not an object")
}
}
///|
/// Success-path parse shaped like the device-flow token poll. A rotating
/// server returns a new refresh_token; otherwise the input token carries
/// over into the refreshed credential.
fn parse_refresh_success(
map : Map[String, Json],
input_refresh_token : String,
) -> Credential raise OAuthError {
let access_token = match map.get("access_token") {
Some(Json::String(s)) if s.length() > 0 => s
_ =>
raise OAuthError::ParseError("missing access_token in success response")
}
let refresh_token = match map.get("refresh_token") {
Some(Json::String(s)) if s.length() > 0 => s
_ => input_refresh_token
}
let expires_in = match map.get("expires_in") {
Some(Json::Number(n, ..)) =>
match parse_positive_integer(n) {
Some(value) => value
None =>
raise OAuthError::ParseError("invalid expires_in in success response")
}
_ => raise OAuthError::ParseError("missing expires_in in success response")
}
let token_type = match map.get("token_type") {
Some(Json::String(s)) => if s.length() > 0 { s } else { "Bearer" }
_ => "Bearer"
}
let now_secs = (@async.now() / 1000L).to_int()
Credential::Credential(
access_token~,
refresh_token~,
expires_at=now_secs + expires_in,
token_type~,
)
}