Watch
0
0
Fork
You've already forked hyperhive
0

swarm UI: delete linked accounts — R2 fixes

Addresses argus review comment 90297 on PR #4899:

- swarm-controller/README.md: list the three DELETE routes (including
  matrix's ?revoke=true) beside the PUT/GET ones already documented.
- LinkedAccounts.tsx: a delete answering 404 means the account is
  already gone, so treat it as the delete's end state — re-fetch and
  close the dialog instead of showing an error.
- matrix_account.rs: matrix_logout treats a 401 M_UNKNOWN_TOKEN as the
  token already being revoked and proceeds with the delete; every
  other logout failure still keeps the account. Adds unit tests and
  updates docs/swarm/ui.md to match.
This commit is contained in:
atlas 2026-10-03 00:56:36 +02:00
commit b90be9e65e
4 changed files with 72 additions and 11 deletions

View file

@ -34,6 +34,9 @@ The swarm's control plane. The swarm UI and `swarmctl` are its clients.
`.../forge-accounts/{label}`, `.../github-account`); distinct from the agent's own swarm-minted
accounts above. `GET .../linked-accounts` lists them, plus the agent's own
`main` matrix account, by kind, name and host, never with a credential.
Each of the three also takes a `DELETE`; matrix's also takes
`?revoke=true`, which logs the stored token out at its homeserver first and
leaves the account in place if that fails.
- **Config PR status** — each agent's open config-repo PR, cached from forge
webhooks (`GET /api/config-prs`, `/api/agents/{name}/config-pr`).
- **Swarm-wide forge objects and webhooks** →

View file

@ -359,6 +359,10 @@ async fn matrix_password_login(
/// POST `<homeserver>/_matrix/client/v3/logout` with `token`, which
/// invalidates that token at the homeserver.
///
/// A 401 `M_UNKNOWN_TOKEN` means the homeserver already does not recognise
/// the token — the state a logout wants — so that one answer counts as
/// success rather than failure. Every other non-2xx is a failure.
///
/// The error names the status and the homeserver's `error` text, never the
/// token.
pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), String> {
@ -377,10 +381,17 @@ pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), S
if status.is_success() {
return Ok(());
}
let err = resp
.json::<serde_json::Value>()
.await
.ok()
let body = resp.json::<serde_json::Value>().await.ok();
if status == StatusCode::UNAUTHORIZED
&& body
.as_ref()
.and_then(|j| j.get("errcode"))
.and_then(serde_json::Value::as_str)
== Some("M_UNKNOWN_TOKEN")
{
return Ok(());
}
let err = body
.and_then(|j| {
j.get("error")
.and_then(serde_json::Value::as_str)
@ -394,7 +405,7 @@ pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), S
mod tests {
use super::{
PutMatrixAccountRequest, homeserver_or_configured_default, is_reserved_account,
password_fields, resolve_credential, token_credential,
matrix_logout, password_fields, resolve_credential, token_credential,
};
fn request(mode: &str) -> PutMatrixAccountRequest {
@ -600,4 +611,46 @@ mod tests {
"{problem:?}"
);
}
/// A stand-in homeserver on a loopback port, answering every
/// `/_matrix/client/v3/logout` with `status` and `body`.
async fn stub_homeserver(status: u16, body: &'static str) -> String {
let app = axum::Router::new().route(
"/_matrix/client/v3/logout",
axum::routing::post(move || async move {
(
axum::http::StatusCode::from_u16(status).unwrap(),
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
}),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await });
format!("http://{addr}")
}
/// A revoke whose token the homeserver already does not recognise has
/// reached logout's end state, not failed it.
#[tokio::test]
async fn logout_treats_401_m_unknown_token_as_already_revoked() {
let homeserver = stub_homeserver(
401,
r#"{"errcode":"M_UNKNOWN_TOKEN","error":"Access token invalid"}"#,
)
.await;
assert!(matrix_logout(&homeserver, "t0k3n").await.is_ok());
}
/// The control: a 401 with a different errcode is still a failure, so
/// the fold above is specific to `M_UNKNOWN_TOKEN` and not any 401.
#[tokio::test]
async fn logout_fails_on_other_401s() {
let homeserver = stub_homeserver(401, r#"{"errcode":"M_FORBIDDEN","error":"nope"}"#).await;
let err = matrix_logout(&homeserver, "t0k3n")
.await
.expect_err("not M_UNKNOWN_TOKEN");
assert!(err.contains("401"), "{err}");
}
}